]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos.ts
5595edf1795aa5838ceb9ef93bd25028f862b0bc
[github/Chocobozzz/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 }