]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Live streaming implementation first step
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
CommitLineData
69818c93 1import * as express from 'express'
c8861d5d 2import { body, param, query, ValidationChain } from 'express-validator'
e6abf95e
C
3import { getServerActor } from '@server/models/application/application'
4import { MVideoFullLight } from '@server/types/models'
5import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
6import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
b60e5f38 7import {
2baea0c7
C
8 isBooleanValid,
9 isDateValid,
10 isIdOrUUIDValid,
11 isIdValid,
12 isUUIDValid,
1cd3facc 13 toArray,
c8861d5d 14 toBooleanOrNull,
2baea0c7
C
15 toIntOrNull,
16 toValueOrNull
6e46de09 17} from '../../../helpers/custom-validators/misc'
e6abf95e
C
18import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
19import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
2baea0c7
C
20import {
21 isScheduleVideoUpdatePrivacyValid,
ac81d1a0
C
22 isVideoCategoryValid,
23 isVideoDescriptionValid,
ac81d1a0 24 isVideoFile,
1cd3facc 25 isVideoFilterValid,
ac81d1a0
C
26 isVideoImage,
27 isVideoLanguageValid,
28 isVideoLicenceValid,
29 isVideoNameValid,
fd8710b8 30 isVideoOriginallyPublishedAtValid,
ac81d1a0 31 isVideoPrivacyValid,
360329cc 32 isVideoSupportValid,
4157cdb1 33 isVideoTagsValid
6e46de09 34} from '../../../helpers/custom-validators/videos'
e6abf95e 35import { cleanUpReqFiles } from '../../../helpers/express-utils'
6e46de09
C
36import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
37import { logger } from '../../../helpers/logger'
8319d6ae
RK
38import {
39 checkUserCanManageVideo,
40 doesVideoChannelOfAccountExist,
41 doesVideoExist,
42 doesVideoFileOfVideoExist
43} from '../../../helpers/middlewares'
0283eaac 44import { getVideoWithAttributes } from '../../../helpers/video'
e6abf95e
C
45import { CONFIG } from '../../../initializers/config'
46import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
47import { isLocalVideoAccepted } from '../../../lib/moderation'
48import { Hooks } from '../../../lib/plugins/hooks'
49import { AccountModel } from '../../../models/account/account'
50import { VideoModel } from '../../../models/video/video'
51import { authenticatePromiseIfNeeded } from '../../oauth'
52import { areValidationErrors } from '../utils'
34ca3b52 53
418d092a 54const videosAddValidator = getCommonVideoEditAttributes().concat([
0c237b19
C
55 body('videofile')
56 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
a1587156
C
57 'This file is not supported or too large. Please, make sure it is of the following type: ' +
58 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
0c237b19 59 ),
b60e5f38 60 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037 61 body('channelId')
c8861d5d 62 .customSanitizer(toIntOrNull)
2baea0c7 63 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 64
a2431b7d 65 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
66 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
67
cf7a61b5
C
68 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
69 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
a2431b7d 70
b4055e1c 71 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
a2431b7d 72 const user = res.locals.oauth.token.User
b60e5f38 73
0f6acda1 74 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
a2431b7d 75
b4055e1c 76 if (await user.isAbleToUploadVideo(videoFile) === false) {
a2431b7d
C
77 res.status(403)
78 .json({ error: 'The user video quota is exceeded with this video.' })
a2431b7d 79
cf7a61b5 80 return cleanUpReqFiles(req)
a2431b7d
C
81 }
82
83 let duration: number
84
85 try {
86 duration = await getDurationFromVideoFile(videoFile.path)
87 } catch (err) {
d5b7d911 88 logger.error('Invalid input file in videosAddValidator.', { err })
215304ea 89 res.status(400)
a2431b7d 90 .json({ error: 'Invalid input file.' })
a2431b7d 91
cf7a61b5 92 return cleanUpReqFiles(req)
a2431b7d
C
93 }
94
b4055e1c
C
95 videoFile.duration = duration
96
97 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
a2431b7d
C
98
99 return next()
b60e5f38 100 }
a920fef1 101])
b60e5f38 102
418d092a 103const videosUpdateValidator = getCommonVideoEditAttributes().concat([
72c7248b 104 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
105 body('name')
106 .optional()
107 .custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
108 body('channelId')
109 .optional()
c8861d5d 110 .customSanitizer(toIntOrNull)
0f320037 111 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 112
a2431b7d 113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
114 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
115
cf7a61b5
C
116 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
117 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
0f6acda1 118 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
a2431b7d 119
6221f311 120 // Check if the user who did the request is able to update the video
0f320037 121 const user = res.locals.oauth.token.User
453e83ea 122 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d 123
0f6acda1 124 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 125
a2431b7d 126 return next()
b60e5f38 127 }
a920fef1 128])
c173e565 129
8d427346 130async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
0283eaac 131 const video = getVideoWithAttributes(res)
8d427346
C
132
133 // Anybody can watch local videos
134 if (video.isOwned() === true) return next()
135
136 // Logged user
137 if (res.locals.oauth) {
138 // Users can search or watch remote videos
139 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
140 }
141
142 // Anybody can search or watch remote videos
143 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
144
145 // Check our instance follows an actor that shared this video
146 const serverActor = await getServerActor()
147 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
148
149 return res.status(403)
150 .json({
e6abf95e
C
151 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
152 error: 'Cannot get this video regarding follow constraints.',
153 originUrl: video.url
8d427346
C
154 })
155}
156
7eba5e1f
C
157const videosCustomGetValidator = (
158 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
159 authenticateInQuery = false
160) => {
96f29c0f
C
161 return [
162 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 163
96f29c0f
C
164 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
165 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 166
96f29c0f 167 if (areValidationErrors(req, res)) return
0f6acda1 168 if (!await doesVideoExist(req.params.id, res, fetchType)) return
191764f3 169
943e5193
C
170 // Controllers does not need to check video rights
171 if (fetchType === 'only-immutable-attributes') return next()
172
0283eaac 173 const video = getVideoWithAttributes(res)
453e83ea 174 const videoAll = video as MVideoFullLight
191764f3 175
96f29c0f 176 // Video private or blacklisted
22a73cb8 177 if (videoAll.requiresAuth()) {
eccf70f0 178 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
8d427346 179
dae86118 180 const user = res.locals.oauth ? res.locals.oauth.token.User : null
191764f3 181
8d427346 182 // Only the owner or a user that have blacklist rights can see the video
22a73cb8 183 if (!user || !user.canGetVideo(videoAll)) {
8d427346 184 return res.status(403)
22a73cb8 185 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
8d427346 186 }
191764f3 187
8d427346 188 return next()
96f29c0f 189 }
11474c3c 190
96f29c0f
C
191 // Video is public, anyone can access it
192 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 193
96f29c0f
C
194 // Video is unlisted, check we used the uuid to fetch it
195 if (video.privacy === VideoPrivacy.UNLISTED) {
196 if (isUUIDValid(req.params.id)) return next()
81ebea48 197
96f29c0f
C
198 // Don't leak this unlisted video
199 return res.status(404).end()
200 }
81ebea48 201 }
96f29c0f
C
202 ]
203}
204
205const videosGetValidator = videosCustomGetValidator('all')
eccf70f0 206const videosDownloadValidator = videosCustomGetValidator('all', true)
34ca3b52 207
8319d6ae
RK
208const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
209 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
210 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
211
212 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
213 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
214
215 if (areValidationErrors(req, res)) return
216 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
217
218 return next()
219 }
220])
221
b60e5f38 222const videosRemoveValidator = [
72c7248b 223 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 224
a2431b7d 225 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 226 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 227
a2431b7d 228 if (areValidationErrors(req, res)) return
0f6acda1 229 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
230
231 // Check if the user who did the request is able to delete the video
453e83ea 232 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
233
234 return next()
b60e5f38
C
235 }
236]
34ca3b52 237
74d63469
GR
238const videosChangeOwnershipValidator = [
239 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
240
241 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
242 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
243
244 if (areValidationErrors(req, res)) return
0f6acda1 245 if (!await doesVideoExist(req.params.videoId, res)) return
74d63469
GR
246
247 // Check if the user who did the request is able to change the ownership of the video
453e83ea 248 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
74d63469
GR
249
250 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
251 if (!nextOwner) {
252 res.status(400)
9ccff238
LD
253 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
254
74d63469
GR
255 return
256 }
257 res.locals.nextOwner = nextOwner
258
259 return next()
260 }
261]
262
263const videosTerminateChangeOwnershipValidator = [
264 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
265
266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
268
269 if (areValidationErrors(req, res)) return
270 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
271
272 // Check if the user who did the request is able to change the ownership of the video
273 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
274
dae86118 275 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469 276
a1587156 277 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
74d63469 278 res.status(403)
a1587156 279 .json({ error: 'Ownership already accepted or refused' })
74d63469
GR
280 return
281 }
a1587156
C
282
283 return next()
74d63469
GR
284 }
285]
286
287const videosAcceptChangeOwnershipValidator = [
288 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
289 const body = req.body as VideoChangeOwnershipAccept
0f6acda1 290 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
74d63469
GR
291
292 const user = res.locals.oauth.token.User
dae86118 293 const videoChangeOwnership = res.locals.videoChangeOwnership
d7a25329 294 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
74d63469
GR
295 if (isAble === false) {
296 res.status(403)
297 .json({ error: 'The user video quota is exceeded with this video.' })
9ccff238 298
74d63469
GR
299 return
300 }
301
302 return next()
303 }
304]
305
764a9657
C
306const videosOverviewValidator = [
307 query('page')
308 .optional()
309 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
310 .withMessage('Should have a valid pagination'),
311
312 (req: express.Request, res: express.Response, next: express.NextFunction) => {
313 if (areValidationErrors(req, res)) return
314
315 return next()
316 }
317]
318
418d092a 319function getCommonVideoEditAttributes () {
a920fef1
C
320 return [
321 body('thumbnailfile')
322 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
a1587156
C
323 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
324 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
325 ),
a920fef1
C
326 body('previewfile')
327 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
a1587156
C
328 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
329 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
330 ),
a920fef1
C
331
332 body('category')
333 .optional()
334 .customSanitizer(toIntOrNull)
335 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
336 body('licence')
337 .optional()
338 .customSanitizer(toIntOrNull)
339 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
340 body('language')
341 .optional()
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
344 body('nsfw')
345 .optional()
c8861d5d 346 .customSanitizer(toBooleanOrNull)
a920fef1
C
347 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
348 body('waitTranscoding')
349 .optional()
c8861d5d 350 .customSanitizer(toBooleanOrNull)
a920fef1
C
351 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
352 body('privacy')
353 .optional()
c8861d5d 354 .customSanitizer(toValueOrNull)
a920fef1
C
355 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
356 body('description')
357 .optional()
358 .customSanitizer(toValueOrNull)
359 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
360 body('support')
361 .optional()
362 .customSanitizer(toValueOrNull)
363 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
364 body('tags')
365 .optional()
366 .customSanitizer(toValueOrNull)
367 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
368 body('commentsEnabled')
369 .optional()
c8861d5d 370 .customSanitizer(toBooleanOrNull)
a920fef1 371 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
7f2cfe3a 372 body('downloadEnabled')
1e74f19a 373 .optional()
c8861d5d 374 .customSanitizer(toBooleanOrNull)
156c50af 375 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22 376 body('originallyPublishedAt')
c8861d5d
C
377 .optional()
378 .customSanitizer(toValueOrNull)
379 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
380 body('scheduleUpdate')
381 .optional()
382 .customSanitizer(toValueOrNull),
383 body('scheduleUpdate.updateAt')
384 .optional()
385 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
386 body('scheduleUpdate.privacy')
387 .optional()
2b65c4e5 388 .customSanitizer(toIntOrNull)
a920fef1
C
389 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
390 ] as (ValidationChain | express.Handler)[]
391}
fbad87b0 392
1cd3facc
C
393const commonVideosFiltersValidator = [
394 query('categoryOneOf')
395 .optional()
396 .customSanitizer(toArray)
397 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
398 query('licenceOneOf')
399 .optional()
400 .customSanitizer(toArray)
401 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
402 query('languageOneOf')
403 .optional()
404 .customSanitizer(toArray)
405 .custom(isStringArray).withMessage('Should have a valid one of language array'),
406 query('tagsOneOf')
407 .optional()
408 .customSanitizer(toArray)
409 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
410 query('tagsAllOf')
411 .optional()
412 .customSanitizer(toArray)
413 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
414 query('nsfw')
415 .optional()
416 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
417 query('filter')
418 .optional()
419 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
fe987656
C
420 query('skipCount')
421 .optional()
422 .customSanitizer(toBooleanOrNull)
423 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
1cd3facc
C
424
425 (req: express.Request, res: express.Response, next: express.NextFunction) => {
426 logger.debug('Checking commons video filters query', { parameters: req.query })
427
428 if (areValidationErrors(req, res)) return
429
dae86118 430 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
1cd3facc
C
431 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
432 res.status(401)
433 .json({ error: 'You are not allowed to see all local videos.' })
434
435 return
436 }
437
438 return next()
439 }
440]
441
fbad87b0
C
442// ---------------------------------------------------------------------------
443
444export {
445 videosAddValidator,
446 videosUpdateValidator,
447 videosGetValidator,
8319d6ae 448 videoFileMetadataGetValidator,
eccf70f0 449 videosDownloadValidator,
8d427346 450 checkVideoFollowConstraints,
96f29c0f 451 videosCustomGetValidator,
fbad87b0 452 videosRemoveValidator,
fbad87b0 453
74d63469
GR
454 videosChangeOwnershipValidator,
455 videosTerminateChangeOwnershipValidator,
456 videosAcceptChangeOwnershipValidator,
457
418d092a 458 getCommonVideoEditAttributes,
1cd3facc 459
764a9657
C
460 commonVideosFiltersValidator,
461
462 videosOverviewValidator
fbad87b0
C
463}
464
465// ---------------------------------------------------------------------------
466
467function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
468 if (req.body.scheduleUpdate) {
469 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
470 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
471
fbad87b0
C
472 res.status(400)
473 .json({ error: 'Schedule update at is mandatory.' })
fbad87b0
C
474
475 return true
476 }
477 }
478
479 return false
480}
b4055e1c
C
481
482async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
483 // Check we accept this video
484 const acceptParameters = {
485 videoBody: req.body,
486 videoFile,
487 user: res.locals.oauth.token.User
488 }
89cd1275
C
489 const acceptedResult = await Hooks.wrapFun(
490 isLocalVideoAccepted,
491 acceptParameters,
b4055e1c
C
492 'filter:api.video.upload.accept.result'
493 )
494
495 if (!acceptedResult || acceptedResult.accepted !== true) {
496 logger.info('Refused local video.', { acceptedResult, acceptParameters })
497 res.status(403)
498 .json({ error: acceptedResult.errorMessage || 'Refused local video' })
499
500 return false
501 }
502
503 return true
504}