]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos.ts
Ability to programmatically control embeds (#776)
[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')
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 }