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