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