Fix images size limit
[oweals/peertube.git] / server / middlewares / validators / videos.ts
1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query } from 'express-validator/check'
4 import { UserRight, VideoPrivacy } from '../../../shared'
5 import {
6   isBooleanValid,
7   isDateValid,
8   isIdOrUUIDValid,
9   isIdValid,
10   isUUIDValid,
11   toIntOrNull,
12   toValueOrNull
13 } from '../../helpers/custom-validators/misc'
14 import {
15   isScheduleVideoUpdatePrivacyValid,
16   isVideoAbuseReasonValid,
17   isVideoCategoryValid,
18   isVideoChannelOfAccountExist,
19   isVideoDescriptionValid,
20   isVideoExist,
21   isVideoFile,
22   isVideoImage,
23   isVideoLanguageValid,
24   isVideoLicenceValid,
25   isVideoNameValid,
26   isVideoPrivacyValid,
27   isVideoRatingTypeValid,
28   isVideoSupportValid,
29   isVideoTagsValid
30 } from '../../helpers/custom-validators/videos'
31 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
32 import { logger } from '../../helpers/logger'
33 import { CONSTRAINTS_FIELDS } from '../../initializers'
34 import { UserModel } from '../../models/account/user'
35 import { VideoModel } from '../../models/video/video'
36 import { VideoShareModel } from '../../models/video/video-share'
37 import { authenticate } from '../oauth'
38 import { areValidationErrors } from './utils'
39
40 const videosAddValidator = [
41   body('videofile')
42     .custom((value, { req }) => isVideoFile(req.files)).withMessage(
43       'This file is not supported or too large. Please, make sure it is of the following type : '
44       + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
45     ),
46   body('thumbnailfile')
47     .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
48       'This thumbnail file is not supported or too large. Please, make sure it is of the following type : '
49       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
50     ),
51   body('previewfile')
52     .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
53       'This preview file is not supported or too large. Please, make sure it is of the following type : '
54       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
55     ),
56   body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
57   body('category')
58     .optional()
59     .customSanitizer(toIntOrNull)
60     .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
61   body('licence')
62     .optional()
63     .customSanitizer(toIntOrNull)
64     .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
65   body('language')
66     .optional()
67     .customSanitizer(toValueOrNull)
68     .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
69   body('nsfw')
70     .optional()
71     .toBoolean()
72     .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
73   body('waitTranscoding')
74     .optional()
75     .toBoolean()
76     .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
77   body('description')
78     .optional()
79     .customSanitizer(toValueOrNull)
80     .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
81   body('support')
82     .optional()
83     .customSanitizer(toValueOrNull)
84     .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
85   body('tags')
86     .optional()
87     .customSanitizer(toValueOrNull)
88     .custom(isVideoTagsValid).withMessage('Should have correct tags'),
89   body('commentsEnabled')
90     .optional()
91     .toBoolean()
92     .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
93   body('privacy')
94     .optional()
95     .toInt()
96     .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
97   body('channelId')
98     .toInt()
99     .custom(isIdValid).withMessage('Should have correct video channel id'),
100   body('scheduleUpdate')
101     .optional()
102     .customSanitizer(toValueOrNull),
103   body('scheduleUpdate.updateAt')
104     .optional()
105     .custom(isDateValid).withMessage('Should have a valid schedule update date'),
106   body('scheduleUpdate.privacy')
107     .optional()
108     .toInt()
109     .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
110
111   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
112     logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
113
114     if (areValidationErrors(req, res)) return
115     if (areErrorsInVideoImageFiles(req, res)) return
116     if (areErrorsInScheduleUpdate(req, res)) return
117
118     const videoFile: Express.Multer.File = req.files['videofile'][0]
119     const user = res.locals.oauth.token.User
120
121     if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
122
123     const isAble = await user.isAbleToUploadVideo(videoFile)
124     if (isAble === false) {
125       res.status(403)
126          .json({ error: 'The user video quota is exceeded with this video.' })
127          .end()
128
129       return
130     }
131
132     let duration: number
133
134     try {
135       duration = await getDurationFromVideoFile(videoFile.path)
136     } catch (err) {
137       logger.error('Invalid input file in videosAddValidator.', { err })
138       res.status(400)
139          .json({ error: 'Invalid input file.' })
140          .end()
141
142       return
143     }
144
145     videoFile['duration'] = duration
146
147     return next()
148   }
149 ]
150
151 const videosUpdateValidator = [
152   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
153   body('thumbnailfile')
154     .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
155       'This thumbnail file is not supported or too large. Please, make sure it is of the following type : '
156       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
157     ),
158   body('previewfile')
159     .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
160       'This preview file is not supported or too large. Please, make sure it is of the following type : '
161       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
162     ),
163   body('name')
164     .optional()
165     .custom(isVideoNameValid).withMessage('Should have a valid name'),
166   body('category')
167     .optional()
168     .customSanitizer(toIntOrNull)
169     .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
170   body('licence')
171     .optional()
172     .customSanitizer(toIntOrNull)
173     .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
174   body('language')
175     .optional()
176     .customSanitizer(toValueOrNull)
177     .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
178   body('nsfw')
179     .optional()
180     .toBoolean()
181     .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
182   body('waitTranscoding')
183     .optional()
184     .toBoolean()
185     .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
186   body('privacy')
187     .optional()
188     .toInt()
189     .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
190   body('description')
191     .optional()
192     .customSanitizer(toValueOrNull)
193     .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
194   body('support')
195     .optional()
196     .customSanitizer(toValueOrNull)
197     .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
198   body('tags')
199     .optional()
200     .customSanitizer(toValueOrNull)
201     .custom(isVideoTagsValid).withMessage('Should have correct tags'),
202   body('commentsEnabled')
203     .optional()
204     .toBoolean()
205     .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
206   body('channelId')
207     .optional()
208     .toInt()
209     .custom(isIdValid).withMessage('Should have correct video channel id'),
210   body('scheduleUpdate')
211     .optional()
212     .customSanitizer(toValueOrNull),
213   body('scheduleUpdate.updateAt')
214     .optional()
215     .custom(isDateValid).withMessage('Should have a valid schedule update date'),
216   body('scheduleUpdate.privacy')
217     .optional()
218     .toInt()
219     .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
220
221   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
222     logger.debug('Checking videosUpdate parameters', { parameters: req.body })
223
224     if (areValidationErrors(req, res)) return
225     if (areErrorsInVideoImageFiles(req, res)) return
226     if (areErrorsInScheduleUpdate(req, res)) return
227     if (!await isVideoExist(req.params.id, res)) return
228
229     const video = res.locals.video
230
231     // Check if the user who did the request is able to update the video
232     const user = res.locals.oauth.token.User
233     if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
234
235     if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
236       return res.status(409)
237         .json({ error: 'Cannot set "private" a video that was not private.' })
238         .end()
239     }
240
241     if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
242
243     return next()
244   }
245 ]
246
247 const videosGetValidator = [
248   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
249
250   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
251     logger.debug('Checking videosGet parameters', { parameters: req.params })
252
253     if (areValidationErrors(req, res)) return
254     if (!await isVideoExist(req.params.id, res)) return
255
256     const video = res.locals.video
257
258     // Video is public, anyone can access it
259     if (video.privacy === VideoPrivacy.PUBLIC) return next()
260
261     // Video is unlisted, check we used the uuid to fetch it
262     if (video.privacy === VideoPrivacy.UNLISTED) {
263       if (isUUIDValid(req.params.id)) return next()
264
265       // Don't leak this unlisted video
266       return res.status(404).end()
267     }
268
269     // Video is private, check the user
270     authenticate(req, res, () => {
271       if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
272         return res.status(403)
273           .json({ error: 'Cannot get this private video of another user' })
274           .end()
275       }
276
277       return next()
278     })
279   }
280 ]
281
282 const videosRemoveValidator = [
283   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
284
285   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
286     logger.debug('Checking videosRemove parameters', { parameters: req.params })
287
288     if (areValidationErrors(req, res)) return
289     if (!await isVideoExist(req.params.id, res)) return
290
291     // Check if the user who did the request is able to delete the video
292     if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
293
294     return next()
295   }
296 ]
297
298 const videosSearchValidator = [
299   query('search').not().isEmpty().withMessage('Should have a valid search'),
300
301   (req: express.Request, res: express.Response, next: express.NextFunction) => {
302     logger.debug('Checking videosSearch parameters', { parameters: req.params })
303
304     if (areValidationErrors(req, res)) return
305
306     return next()
307   }
308 ]
309
310 const videoAbuseReportValidator = [
311   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
312   body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
313
314   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
315     logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
316
317     if (areValidationErrors(req, res)) return
318     if (!await isVideoExist(req.params.id, res)) return
319
320     return next()
321   }
322 ]
323
324 const videoRateValidator = [
325   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
326   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
327
328   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
329     logger.debug('Checking videoRate parameters', { parameters: req.body })
330
331     if (areValidationErrors(req, res)) return
332     if (!await isVideoExist(req.params.id, res)) return
333
334     return next()
335   }
336 ]
337
338 const videosShareValidator = [
339   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
340   param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
341
342   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
343     logger.debug('Checking videoShare parameters', { parameters: req.params })
344
345     if (areValidationErrors(req, res)) return
346     if (!await isVideoExist(req.params.id, res)) return
347
348     const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
349     if (!share) {
350       return res.status(404)
351         .end()
352     }
353
354     res.locals.videoShare = share
355     return next()
356   }
357 ]
358
359 // ---------------------------------------------------------------------------
360
361 export {
362   videosAddValidator,
363   videosUpdateValidator,
364   videosGetValidator,
365   videosRemoveValidator,
366   videosSearchValidator,
367   videosShareValidator,
368
369   videoAbuseReportValidator,
370
371   videoRateValidator
372 }
373
374 // ---------------------------------------------------------------------------
375
376 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
377   // Retrieve the user who did the request
378   if (video.isOwned() === false) {
379     res.status(403)
380               .json({ error: 'Cannot manage a video of another server.' })
381               .end()
382     return false
383   }
384
385   // Check if the user can delete the video
386   // The user can delete it if he has the right
387   // Or if s/he is the video's account
388   const account = video.VideoChannel.Account
389   if (user.hasRight(right) === false && account.userId !== user.id) {
390     res.status(403)
391               .json({ error: 'Cannot manage a video of another user.' })
392               .end()
393     return false
394   }
395
396   return true
397 }
398
399 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
400   // Files are optional
401   if (!req.files) return false
402
403   for (const imageField of [ 'thumbnail', 'preview' ]) {
404     if (!req.files[ imageField ]) continue
405
406     const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
407     if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
408       res.status(400)
409         .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
410         .end()
411       return true
412     }
413   }
414
415   return false
416 }
417
418 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
419   if (req.body.scheduleUpdate) {
420     if (!req.body.scheduleUpdate.updateAt) {
421       res.status(400)
422          .json({ error: 'Schedule update at is mandatory.' })
423          .end()
424
425       return true
426     }
427   }
428
429   return false
430 }