]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Add HTTP signature check before linked signature
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
CommitLineData
69818c93 1import * as express from 'express'
3fd3ab2d 2import 'express-validator'
1cd3facc 3import { body, param, query, ValidationChain } from 'express-validator/check'
6e46de09 4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
b60e5f38 5import {
2baea0c7
C
6 isBooleanValid,
7 isDateValid,
8 isIdOrUUIDValid,
9 isIdValid,
10 isUUIDValid,
1cd3facc 11 toArray,
2baea0c7
C
12 toIntOrNull,
13 toValueOrNull
6e46de09 14} from '../../../helpers/custom-validators/misc'
2baea0c7 15import {
40e87e9e 16 checkUserCanManageVideo,
2baea0c7 17 isScheduleVideoUpdatePrivacyValid,
ac81d1a0 18 isVideoCategoryValid,
0f320037 19 isVideoChannelOfAccountExist,
ac81d1a0
C
20 isVideoDescriptionValid,
21 isVideoExist,
22 isVideoFile,
1cd3facc 23 isVideoFilterValid,
ac81d1a0
C
24 isVideoImage,
25 isVideoLanguageValid,
26 isVideoLicenceValid,
27 isVideoNameValid,
28 isVideoPrivacyValid,
360329cc
C
29 isVideoRatingTypeValid,
30 isVideoSupportValid,
4157cdb1 31 isVideoTagsValid
6e46de09
C
32} from '../../../helpers/custom-validators/videos'
33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34import { logger } from '../../../helpers/logger'
35import { CONSTRAINTS_FIELDS } from '../../../initializers'
36import { VideoShareModel } from '../../../models/video/video-share'
37import { authenticate } from '../../oauth'
38import { areValidationErrors } from '../utils'
39import { cleanUpReqFiles } from '../../../helpers/express-utils'
40import { VideoModel } from '../../../models/video/video'
41import { UserModel } from '../../../models/account/user'
42import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
43import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
44import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
45import { AccountModel } from '../../../models/account/account'
46import { VideoFetchType } from '../../../helpers/video'
1cd3facc 47import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
34ca3b52 48
a920fef1 49const videosAddValidator = getCommonVideoAttributes().concat([
0c237b19
C
50 body('videofile')
51 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
40e87e9e 52 'This file is not supported or too large. Please, make sure it is of the following type: '
0c237b19
C
53 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
54 ),
b60e5f38 55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
56 body('channelId')
57 .toInt()
2baea0c7 58 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 59
a2431b7d 60 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
61 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
62
cf7a61b5
C
63 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
64 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
a2431b7d
C
65
66 const videoFile: Express.Multer.File = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
b60e5f38 68
cf7a61b5 69 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
a2431b7d
C
70
71 const isAble = await user.isAbleToUploadVideo(videoFile)
72 if (isAble === false) {
73 res.status(403)
74 .json({ error: 'The user video quota is exceeded with this video.' })
a2431b7d 75
cf7a61b5 76 return cleanUpReqFiles(req)
a2431b7d
C
77 }
78
79 let duration: number
80
81 try {
82 duration = await getDurationFromVideoFile(videoFile.path)
83 } catch (err) {
d5b7d911 84 logger.error('Invalid input file in videosAddValidator.', { err })
a2431b7d
C
85 res.status(400)
86 .json({ error: 'Invalid input file.' })
a2431b7d 87
cf7a61b5 88 return cleanUpReqFiles(req)
a2431b7d
C
89 }
90
a2431b7d
C
91 videoFile['duration'] = duration
92
93 return next()
b60e5f38 94 }
a920fef1 95])
b60e5f38 96
a920fef1 97const videosUpdateValidator = getCommonVideoAttributes().concat([
72c7248b 98 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
99 body('name')
100 .optional()
101 .custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
102 body('channelId')
103 .optional()
104 .toInt()
105 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 106
a2431b7d 107 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
108 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
109
cf7a61b5
C
110 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
111 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
112 if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
a2431b7d
C
113
114 const video = res.locals.video
115
6221f311 116 // Check if the user who did the request is able to update the video
0f320037 117 const user = res.locals.oauth.token.User
cf7a61b5 118 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d
C
119
120 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
cf7a61b5 121 cleanUpReqFiles(req)
a2431b7d 122 return res.status(409)
bbe0f064 123 .json({ error: 'Cannot set "private" a video that was not private.' })
a2431b7d
C
124 }
125
cf7a61b5 126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 127
a2431b7d 128 return next()
b60e5f38 129 }
a920fef1 130])
c173e565 131
96f29c0f
C
132const videosCustomGetValidator = (fetchType: VideoFetchType) => {
133 return [
134 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 135
96f29c0f
C
136 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
137 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 138
96f29c0f
C
139 if (areValidationErrors(req, res)) return
140 if (!await isVideoExist(req.params.id, res, fetchType)) return
191764f3 141
96f29c0f 142 const video: VideoModel = res.locals.video
191764f3 143
96f29c0f
C
144 // Video private or blacklisted
145 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
146 return authenticate(req, res, () => {
147 const user: UserModel = res.locals.oauth.token.User
191764f3 148
96f29c0f
C
149 // Only the owner or a user that have blacklist rights can see the video
150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
151 return res.status(403)
152 .json({ error: 'Cannot get this private or blacklisted video.' })
96f29c0f 153 }
191764f3 154
96f29c0f
C
155 return next()
156 })
157 }
11474c3c 158
96f29c0f
C
159 // Video is public, anyone can access it
160 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 161
96f29c0f
C
162 // Video is unlisted, check we used the uuid to fetch it
163 if (video.privacy === VideoPrivacy.UNLISTED) {
164 if (isUUIDValid(req.params.id)) return next()
81ebea48 165
96f29c0f
C
166 // Don't leak this unlisted video
167 return res.status(404).end()
168 }
81ebea48 169 }
96f29c0f
C
170 ]
171}
172
173const videosGetValidator = videosCustomGetValidator('all')
34ca3b52 174
b60e5f38 175const videosRemoveValidator = [
72c7248b 176 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 177
a2431b7d 178 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 179 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 180
a2431b7d
C
181 if (areValidationErrors(req, res)) return
182 if (!await isVideoExist(req.params.id, res)) return
183
184 // Check if the user who did the request is able to delete the video
6221f311 185 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
186
187 return next()
b60e5f38
C
188 }
189]
34ca3b52 190
b60e5f38 191const videoRateValidator = [
72c7248b 192 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 193 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
d38b8281 194
a2431b7d 195 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 196 logger.debug('Checking videoRate parameters', { parameters: req.body })
d38b8281 197
a2431b7d
C
198 if (areValidationErrors(req, res)) return
199 if (!await isVideoExist(req.params.id, res)) return
200
201 return next()
b60e5f38
C
202 }
203]
d38b8281 204
4e50b6a1
C
205const videosShareValidator = [
206 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
207 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
208
209 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
210 logger.debug('Checking videoShare parameters', { parameters: req.params })
211
212 if (areValidationErrors(req, res)) return
a2431b7d 213 if (!await isVideoExist(req.params.id, res)) return
4e50b6a1 214
3fd3ab2d 215 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
4e50b6a1
C
216 if (!share) {
217 return res.status(404)
218 .end()
219 }
220
221 res.locals.videoShare = share
4e50b6a1
C
222 return next()
223 }
224]
225
74d63469
GR
226const videosChangeOwnershipValidator = [
227 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
228
229 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
230 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
231
232 if (areValidationErrors(req, res)) return
233 if (!await isVideoExist(req.params.videoId, res)) return
234
235 // Check if the user who did the request is able to change the ownership of the video
236 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
237
238 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
239 if (!nextOwner) {
240 res.status(400)
9ccff238
LD
241 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
242
74d63469
GR
243 return
244 }
245 res.locals.nextOwner = nextOwner
246
247 return next()
248 }
249]
250
251const videosTerminateChangeOwnershipValidator = [
252 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
253
254 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
255 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
256
257 if (areValidationErrors(req, res)) return
258 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
259
260 // Check if the user who did the request is able to change the ownership of the video
261 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
262
263 return next()
264 },
265 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
266 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
267
268 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
269 return next()
270 } else {
271 res.status(403)
272 .json({ error: 'Ownership already accepted or refused' })
9ccff238 273
74d63469
GR
274 return
275 }
276 }
277]
278
279const videosAcceptChangeOwnershipValidator = [
280 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
281 const body = req.body as VideoChangeOwnershipAccept
282 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
283
284 const user = res.locals.oauth.token.User
285 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
286 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
287 if (isAble === false) {
288 res.status(403)
289 .json({ error: 'The user video quota is exceeded with this video.' })
9ccff238 290
74d63469
GR
291 return
292 }
293
294 return next()
295 }
296]
297
a920fef1
C
298function getCommonVideoAttributes () {
299 return [
300 body('thumbnailfile')
301 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
302 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
303 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
304 ),
305 body('previewfile')
306 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
307 'This preview file is not supported or too large. Please, make sure it is of the following type: '
308 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
309 ),
310
311 body('category')
312 .optional()
313 .customSanitizer(toIntOrNull)
314 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
315 body('licence')
316 .optional()
317 .customSanitizer(toIntOrNull)
318 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
319 body('language')
320 .optional()
321 .customSanitizer(toValueOrNull)
322 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
323 body('nsfw')
324 .optional()
325 .toBoolean()
326 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
327 body('waitTranscoding')
328 .optional()
329 .toBoolean()
330 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
331 body('privacy')
332 .optional()
333 .toInt()
334 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
335 body('description')
336 .optional()
337 .customSanitizer(toValueOrNull)
338 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
339 body('support')
340 .optional()
341 .customSanitizer(toValueOrNull)
342 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
343 body('tags')
344 .optional()
345 .customSanitizer(toValueOrNull)
346 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
347 body('commentsEnabled')
348 .optional()
349 .toBoolean()
350 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
351
352 body('scheduleUpdate')
353 .optional()
354 .customSanitizer(toValueOrNull),
355 body('scheduleUpdate.updateAt')
356 .optional()
357 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
358 body('scheduleUpdate.privacy')
359 .optional()
360 .toInt()
361 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
362 ] as (ValidationChain | express.Handler)[]
363}
fbad87b0 364
1cd3facc
C
365const commonVideosFiltersValidator = [
366 query('categoryOneOf')
367 .optional()
368 .customSanitizer(toArray)
369 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
370 query('licenceOneOf')
371 .optional()
372 .customSanitizer(toArray)
373 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
374 query('languageOneOf')
375 .optional()
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid one of language array'),
378 query('tagsOneOf')
379 .optional()
380 .customSanitizer(toArray)
381 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
382 query('tagsAllOf')
383 .optional()
384 .customSanitizer(toArray)
385 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
386 query('nsfw')
387 .optional()
388 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
389 query('filter')
390 .optional()
391 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
392
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 logger.debug('Checking commons video filters query', { parameters: req.query })
395
396 if (areValidationErrors(req, res)) return
397
398 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
399 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
400 res.status(401)
401 .json({ error: 'You are not allowed to see all local videos.' })
402
403 return
404 }
405
406 return next()
407 }
408]
409
fbad87b0
C
410// ---------------------------------------------------------------------------
411
412export {
413 videosAddValidator,
414 videosUpdateValidator,
415 videosGetValidator,
96f29c0f 416 videosCustomGetValidator,
fbad87b0
C
417 videosRemoveValidator,
418 videosShareValidator,
419
fbad87b0
C
420 videoRateValidator,
421
74d63469
GR
422 videosChangeOwnershipValidator,
423 videosTerminateChangeOwnershipValidator,
424 videosAcceptChangeOwnershipValidator,
425
1cd3facc
C
426 getCommonVideoAttributes,
427
428 commonVideosFiltersValidator
fbad87b0
C
429}
430
431// ---------------------------------------------------------------------------
432
433function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
434 if (req.body.scheduleUpdate) {
435 if (!req.body.scheduleUpdate.updateAt) {
436 res.status(400)
437 .json({ error: 'Schedule update at is mandatory.' })
fbad87b0
C
438
439 return true
440 }
441 }
442
443 return false
444}