Refractor validators
[oweals/peertube.git] / server / middlewares / validators / videos.ts
1 import * as express from 'express'
2 import { body, param, query } from 'express-validator/check'
3 import { UserRight, VideoPrivacy } from '../../../shared'
4 import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
5 import {
6   isVideoAbuseReasonValid,
7   isVideoCategoryValid,
8   isVideoDescriptionValid,
9   isVideoDurationValid,
10   isVideoExist,
11   isVideoFile,
12   isVideoLanguageValid,
13   isVideoLicenceValid,
14   isVideoNameValid,
15   isVideoNSFWValid,
16   isVideoPrivacyValid,
17   isVideoRatingTypeValid,
18   isVideoTagsValid
19 } from '../../helpers/custom-validators/videos'
20 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
21 import { logger } from '../../helpers/logger'
22 import { CONSTRAINTS_FIELDS, SEARCHABLE_COLUMNS } from '../../initializers'
23 import { database as db } from '../../initializers/database'
24 import { UserInstance } from '../../models/account/user-interface'
25 import { VideoInstance } from '../../models/video/video-interface'
26 import { authenticate } from '../oauth'
27 import { areValidationErrors } from './utils'
28
29 const videosAddValidator = [
30   body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
31     'This file is not supported. Please, make sure it is of the following type : '
32     + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
33   ),
34   body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
35   body('category').custom(isVideoCategoryValid).withMessage('Should have a valid category'),
36   body('licence').custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
37   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
38   body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
39   body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
40   body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
41   body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
42   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
43
44   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
45     logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
46
47     if (areValidationErrors(req, res)) return
48
49     const videoFile: Express.Multer.File = req.files['videofile'][0]
50     const user = res.locals.oauth.token.User
51
52     const videoChannel = await db.VideoChannel.loadByIdAndAccount(req.body.channelId, user.Account.id)
53     if (!videoChannel) {
54       res.status(400)
55         .json({ error: 'Unknown video video channel for this account.' })
56         .end()
57
58       return
59     }
60
61     res.locals.videoChannel = videoChannel
62
63     const isAble = await user.isAbleToUploadVideo(videoFile)
64     if (isAble === false) {
65       res.status(403)
66          .json({ error: 'The user video quota is exceeded with this video.' })
67          .end()
68
69       return
70     }
71
72     let duration: number
73
74     try {
75       duration = await getDurationFromVideoFile(videoFile.path)
76     } catch (err) {
77       logger.error('Invalid input file in videosAddValidator.', err)
78       res.status(400)
79          .json({ error: 'Invalid input file.' })
80          .end()
81
82       return
83     }
84
85     if (!isVideoDurationValid('' + duration)) {
86       return res.status(400)
87                 .json({
88                   error: 'Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).'
89                 })
90                 .end()
91     }
92
93     videoFile['duration'] = duration
94
95     return next()
96   }
97 ]
98
99 const videosUpdateValidator = [
100   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
101   body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'),
102   body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
103   body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
104   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
105   body('nsfw').optional().custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'),
106   body('privacy').optional().custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
107   body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
108   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
109
110   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111     logger.debug('Checking videosUpdate parameters', { parameters: req.body })
112
113     if (areValidationErrors(req, res)) return
114     if (!await isVideoExist(req.params.id, res)) return
115
116     const video = res.locals.video
117
118     // We need to make additional checks
119     if (video.isOwned() === false) {
120       return res.status(403)
121                 .json({ error: 'Cannot update video of another server' })
122                 .end()
123     }
124
125     if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
126       return res.status(403)
127                 .json({ error: 'Cannot update video of another user' })
128                 .end()
129     }
130
131     if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
132       return res.status(409)
133         .json({ error: 'Cannot set "private" a video that was not private anymore.' })
134         .end()
135     }
136
137     return next()
138   }
139 ]
140
141 const videosGetValidator = [
142   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
143
144   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
145     logger.debug('Checking videosGet parameters', { parameters: req.params })
146
147     if (areValidationErrors(req, res)) return
148     if (!await isVideoExist(req.params.id, res)) return
149
150     const video = res.locals.video
151
152     // Video is not private, anyone can access it
153     if (video.privacy !== VideoPrivacy.PRIVATE) return next()
154
155     authenticate(req, res, () => {
156       if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
157         return res.status(403)
158           .json({ error: 'Cannot get this private video of another user' })
159           .end()
160       }
161
162       return next()
163     })
164   }
165 ]
166
167 const videosRemoveValidator = [
168   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
169
170   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
171     logger.debug('Checking videosRemove parameters', { parameters: req.params })
172
173     if (areValidationErrors(req, res)) return
174     if (!await isVideoExist(req.params.id, res)) return
175
176     // Check if the user who did the request is able to delete the video
177     if (!checkUserCanDeleteVideo(res.locals.oauth.token.User, res.locals.video, res)) return
178
179     return next()
180   }
181 ]
182
183 const videosSearchValidator = [
184   param('value').not().isEmpty().withMessage('Should have a valid search'),
185   query('field').optional().isIn(SEARCHABLE_COLUMNS.VIDEOS).withMessage('Should have correct searchable column'),
186
187   (req: express.Request, res: express.Response, next: express.NextFunction) => {
188     logger.debug('Checking videosSearch parameters', { parameters: req.params })
189
190     if (areValidationErrors(req, res)) return
191
192     return next()
193   }
194 ]
195
196 const videoAbuseReportValidator = [
197   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
198   body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
199
200   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
201     logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
202
203     if (areValidationErrors(req, res)) return
204     if (!await isVideoExist(req.params.id, res)) return
205
206     return next()
207   }
208 ]
209
210 const videoRateValidator = [
211   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
212   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
213
214   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
215     logger.debug('Checking videoRate parameters', { parameters: req.body })
216
217     if (areValidationErrors(req, res)) return
218     if (!await isVideoExist(req.params.id, res)) return
219
220     return next()
221   }
222 ]
223
224 const videosShareValidator = [
225   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
226   param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
227
228   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
229     logger.debug('Checking videoShare parameters', { parameters: req.params })
230
231     if (areValidationErrors(req, res)) return
232     if (!await isVideoExist(req.params.id, res)) return
233
234     const share = await db.VideoShare.load(req.params.accountId, res.locals.video.id)
235     if (!share) {
236       return res.status(404)
237         .end()
238     }
239
240     res.locals.videoShare = share
241     return next()
242   }
243 ]
244
245 // ---------------------------------------------------------------------------
246
247 export {
248   videosAddValidator,
249   videosUpdateValidator,
250   videosGetValidator,
251   videosRemoveValidator,
252   videosSearchValidator,
253   videosShareValidator,
254
255   videoAbuseReportValidator,
256
257   videoRateValidator
258 }
259
260 // ---------------------------------------------------------------------------
261
262 function checkUserCanDeleteVideo (user: UserInstance, video: VideoInstance, res: express.Response) {
263   // Retrieve the user who did the request
264   if (video.isOwned() === false) {
265     res.status(403)
266               .json({ error: 'Cannot remove video of another server, blacklist it' })
267               .end()
268     return false
269   }
270
271   // Check if the user can delete the video
272   // The user can delete it if s/he is an admin
273   // Or if s/he is the video's account
274   const account = video.VideoChannel.Account
275   if (user.hasRight(UserRight.REMOVE_ANY_VIDEO) === false && account.userId !== user.id) {
276     res.status(403)
277               .json({ error: 'Cannot remove video of another user' })
278               .end()
279     return false
280   }
281
282   return true
283 }