]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos.ts
Ability to programmatically control embeds (#776)
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos.ts
CommitLineData
69818c93 1import * as express from 'express'
3fd3ab2d 2import 'express-validator'
8d468a16
C
3import { body, param, query } from 'express-validator/check'
4import { UserRight, VideoPrivacy } from '../../../shared'
b60e5f38 5import {
2baea0c7
C
6 isBooleanValid,
7 isDateValid,
8 isIdOrUUIDValid,
9 isIdValid,
10 isUUIDValid,
11 toIntOrNull,
12 toValueOrNull
13} from '../../helpers/custom-validators/misc'
14import {
15 isScheduleVideoUpdatePrivacyValid,
ac81d1a0
C
16 isVideoAbuseReasonValid,
17 isVideoCategoryValid,
0f320037 18 isVideoChannelOfAccountExist,
ac81d1a0
C
19 isVideoDescriptionValid,
20 isVideoExist,
21 isVideoFile,
22 isVideoImage,
23 isVideoLanguageValid,
24 isVideoLicenceValid,
25 isVideoNameValid,
26 isVideoPrivacyValid,
360329cc
C
27 isVideoRatingTypeValid,
28 isVideoSupportValid,
ac81d1a0 29 isVideoTagsValid
8d468a16 30} from '../../helpers/custom-validators/videos'
da854ddd
C
31import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
32import { logger } from '../../helpers/logger'
f3aaa9a9 33import { CONSTRAINTS_FIELDS } from '../../initializers'
3fd3ab2d
C
34import { UserModel } from '../../models/account/user'
35import { VideoModel } from '../../models/video/video'
3fd3ab2d 36import { VideoShareModel } from '../../models/video/video-share'
11474c3c 37import { authenticate } from '../oauth'
a2431b7d 38import { areValidationErrors } from './utils'
34ca3b52 39
b60e5f38 40const videosAddValidator = [
0c237b19
C
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 ),
b60e5f38 56 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
360329cc
C
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()
2efd32f6 67 .customSanitizer(toValueOrNull)
360329cc
C
68 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
69 body('nsfw')
2186386c 70 .optional()
360329cc
C
71 .toBoolean()
72 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
2186386c
C
73 body('waitTranscoding')
74 .optional()
75 .toBoolean()
76 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
360329cc
C
77 body('description')
78 .optional()
2efd32f6 79 .customSanitizer(toValueOrNull)
360329cc
C
80 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
81 body('support')
82 .optional()
2efd32f6 83 .customSanitizer(toValueOrNull)
360329cc
C
84 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
85 body('tags')
86 .optional()
2efd32f6 87 .customSanitizer(toValueOrNull)
360329cc
C
88 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
89 body('commentsEnabled')
2186386c 90 .optional()
360329cc
C
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'),
0f320037
C
97 body('channelId')
98 .toInt()
2baea0c7 99 .custom(isIdValid).withMessage('Should have correct video channel id'),
e94fc297
C
100 body('scheduleUpdate')
101 .optional()
102 .customSanitizer(toValueOrNull),
2baea0c7
C
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'),
b60e5f38 110
a2431b7d 111 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
112 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
113
a2431b7d 114 if (areValidationErrors(req, res)) return
ac81d1a0 115 if (areErrorsInVideoImageFiles(req, res)) return
2baea0c7 116 if (areErrorsInScheduleUpdate(req, res)) return
a2431b7d
C
117
118 const videoFile: Express.Multer.File = req.files['videofile'][0]
119 const user = res.locals.oauth.token.User
b60e5f38 120
6200d8d9 121 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
a2431b7d
C
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) {
d5b7d911 137 logger.error('Invalid input file in videosAddValidator.', { err })
a2431b7d
C
138 res.status(400)
139 .json({ error: 'Invalid input file.' })
140 .end()
141
142 return
143 }
144
a2431b7d
C
145 videoFile['duration'] = duration
146
147 return next()
b60e5f38
C
148 }
149]
150
151const videosUpdateValidator = [
72c7248b 152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
0c237b19
C
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 ),
360329cc
C
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()
2efd32f6 176 .customSanitizer(toValueOrNull)
360329cc
C
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'),
2186386c
C
182 body('waitTranscoding')
183 .optional()
184 .toBoolean()
185 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
360329cc
C
186 body('privacy')
187 .optional()
188 .toInt()
189 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
190 body('description')
191 .optional()
2efd32f6 192 .customSanitizer(toValueOrNull)
360329cc
C
193 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
194 body('support')
195 .optional()
2efd32f6 196 .customSanitizer(toValueOrNull)
360329cc
C
197 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
198 body('tags')
199 .optional()
2efd32f6 200 .customSanitizer(toValueOrNull)
360329cc
C
201 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
202 body('commentsEnabled')
203 .optional()
204 .toBoolean()
205 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
0f320037
C
206 body('channelId')
207 .optional()
208 .toInt()
209 .custom(isIdValid).withMessage('Should have correct video channel id'),
e94fc297
C
210 body('scheduleUpdate')
211 .optional()
212 .customSanitizer(toValueOrNull),
2baea0c7
C
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'),
b60e5f38 220
a2431b7d 221 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
222 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
223
a2431b7d 224 if (areValidationErrors(req, res)) return
ac81d1a0 225 if (areErrorsInVideoImageFiles(req, res)) return
2baea0c7 226 if (areErrorsInScheduleUpdate(req, res)) return
a2431b7d
C
227 if (!await isVideoExist(req.params.id, res)) return
228
229 const video = res.locals.video
230
6221f311 231 // Check if the user who did the request is able to update the video
0f320037
C
232 const user = res.locals.oauth.token.User
233 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
a2431b7d
C
234
235 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
236 return res.status(409)
bbe0f064 237 .json({ error: 'Cannot set "private" a video that was not private.' })
a2431b7d
C
238 .end()
239 }
240
6200d8d9 241 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
0f320037 242
a2431b7d 243 return next()
b60e5f38
C
244 }
245]
c173e565 246
b60e5f38 247const videosGetValidator = [
72c7248b 248 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 249
a2431b7d 250 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 251 logger.debug('Checking videosGet parameters', { parameters: req.params })
7b1f49de 252
a2431b7d
C
253 if (areValidationErrors(req, res)) return
254 if (!await isVideoExist(req.params.id, res)) return
11474c3c 255
a2431b7d 256 const video = res.locals.video
11474c3c 257
81ebea48
C
258 // Video is public, anyone can access it
259 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 260
81ebea48
C
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
a2431b7d
C
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()
b60e5f38
C
278 })
279 }
280]
34ca3b52 281
b60e5f38 282const videosRemoveValidator = [
72c7248b 283 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 284
a2431b7d 285 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 286 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 287
a2431b7d
C
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
6221f311 292 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
293
294 return next()
b60e5f38
C
295 }
296]
34ca3b52 297
b60e5f38 298const videosSearchValidator = [
f3aaa9a9 299 query('search').not().isEmpty().withMessage('Should have a valid search'),
c45f7f84 300
b60e5f38
C
301 (req: express.Request, res: express.Response, next: express.NextFunction) => {
302 logger.debug('Checking videosSearch parameters', { parameters: req.params })
c45f7f84 303
a2431b7d
C
304 if (areValidationErrors(req, res)) return
305
306 return next()
b60e5f38
C
307 }
308]
c45f7f84 309
b60e5f38 310const videoAbuseReportValidator = [
72c7248b 311 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 312 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
55fa55a9 313
a2431b7d 314 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 315 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
55fa55a9 316
a2431b7d
C
317 if (areValidationErrors(req, res)) return
318 if (!await isVideoExist(req.params.id, res)) return
319
320 return next()
b60e5f38
C
321 }
322]
55fa55a9 323
b60e5f38 324const videoRateValidator = [
72c7248b 325 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 326 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
d38b8281 327
a2431b7d 328 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 329 logger.debug('Checking videoRate parameters', { parameters: req.body })
d38b8281 330
a2431b7d
C
331 if (areValidationErrors(req, res)) return
332 if (!await isVideoExist(req.params.id, res)) return
333
334 return next()
b60e5f38
C
335 }
336]
d38b8281 337
4e50b6a1
C
338const 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
a2431b7d 346 if (!await isVideoExist(req.params.id, res)) return
4e50b6a1 347
3fd3ab2d 348 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
4e50b6a1
C
349 if (!share) {
350 return res.status(404)
351 .end()
352 }
353
354 res.locals.videoShare = share
4e50b6a1
C
355 return next()
356 }
357]
358
9f10b292 359// ---------------------------------------------------------------------------
c45f7f84 360
65fcc311
C
361export {
362 videosAddValidator,
363 videosUpdateValidator,
364 videosGetValidator,
365 videosRemoveValidator,
366 videosSearchValidator,
4e50b6a1 367 videosShareValidator,
65fcc311
C
368
369 videoAbuseReportValidator,
370
35bf0c83 371 videoRateValidator
65fcc311 372}
7b1f49de
C
373
374// ---------------------------------------------------------------------------
375
6221f311 376function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
198b205c 377 // Retrieve the user who did the request
a2431b7d
C
378 if (video.isOwned() === false) {
379 res.status(403)
6200d8d9 380 .json({ error: 'Cannot manage a video of another server.' })
11474c3c 381 .end()
a2431b7d 382 return false
11474c3c
C
383 }
384
385 // Check if the user can delete the video
4cb6d457 386 // The user can delete it if he has the right
38fa2065 387 // Or if s/he is the video's account
a2431b7d 388 const account = video.VideoChannel.Account
6221f311 389 if (user.hasRight(right) === false && account.userId !== user.id) {
a2431b7d 390 res.status(403)
6200d8d9 391 .json({ error: 'Cannot manage a video of another user.' })
11474c3c 392 .end()
a2431b7d 393 return false
11474c3c
C
394 }
395
a2431b7d 396 return true
198b205c 397}
ac81d1a0
C
398
399function 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)
2baea0c7 409 .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
ac81d1a0
C
410 .end()
411 return true
412 }
413 }
414
415 return false
416}
2baea0c7
C
417
418function 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}