]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Speed up plugins loading
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
CommitLineData
69818c93 1import * as express from 'express'
f6d6e7f8 2import { body, header, param, query, ValidationChain } from 'express-validator'
3import { getResumableUploadPath } from '@server/helpers/upload'
fb719404 4import { isAbleToUploadVideo } from '@server/lib/user'
e6abf95e 5import { getServerActor } from '@server/models/application/application'
ba5a8d89 6import { ExpressPromiseHandler } from '@server/types/express'
f6d6e7f8 7import { MUserAccountId, MVideoWithRights } from '@server/types/models'
e6abf95e 8import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
ba5a8d89 9import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
2b02c520 10import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/change-ownership/video-change-ownership-accept.model'
b60e5f38 11import {
37024082 12 exists,
2baea0c7
C
13 isBooleanValid,
14 isDateValid,
f2eb23cd 15 isFileFieldValid,
2baea0c7
C
16 isIdOrUUIDValid,
17 isIdValid,
18 isUUIDValid,
1cd3facc 19 toArray,
c8861d5d 20 toBooleanOrNull,
2baea0c7
C
21 toIntOrNull,
22 toValueOrNull
6e46de09 23} from '../../../helpers/custom-validators/misc'
1fd61899 24import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
10363c74 25import { checkUserCanTerminateOwnershipChange } from '../../../helpers/custom-validators/video-ownership'
2baea0c7
C
26import {
27 isScheduleVideoUpdatePrivacyValid,
ac81d1a0
C
28 isVideoCategoryValid,
29 isVideoDescriptionValid,
f2eb23cd
RK
30 isVideoFileMimeTypeValid,
31 isVideoFileSizeValid,
1cd3facc 32 isVideoFilterValid,
ac81d1a0
C
33 isVideoImage,
34 isVideoLanguageValid,
35 isVideoLicenceValid,
36 isVideoNameValid,
fd8710b8 37 isVideoOriginallyPublishedAtValid,
ac81d1a0 38 isVideoPrivacyValid,
360329cc 39 isVideoSupportValid,
4157cdb1 40 isVideoTagsValid
6e46de09 41} from '../../../helpers/custom-validators/videos'
e6abf95e 42import { cleanUpReqFiles } from '../../../helpers/express-utils'
daf6e480 43import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
6e46de09 44import { logger } from '../../../helpers/logger'
f6d6e7f8 45import { deleteFileAndCatch } from '../../../helpers/utils'
0283eaac 46import { getVideoWithAttributes } from '../../../helpers/video'
e6abf95e
C
47import { CONFIG } from '../../../initializers/config'
48import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
49import { isLocalVideoAccepted } from '../../../lib/moderation'
50import { Hooks } from '../../../lib/plugins/hooks'
51import { AccountModel } from '../../../models/account/account'
52import { VideoModel } from '../../../models/video/video'
f43db2f4 53import { authenticatePromiseIfNeeded } from '../../auth'
10363c74
C
54import {
55 areValidationErrors,
56 checkUserCanManageVideo,
57 doesChangeVideoOwnershipExist,
58 doesVideoChannelOfAccountExist,
59 doesVideoExist,
60 doesVideoFileOfVideoExist
61} from '../shared'
34ca3b52 62
f6d6e7f8 63const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
0c237b19 64 body('videofile')
f2eb23cd
RK
65 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
66 .withMessage('Should have a file'),
67 body('name')
0221f8c9 68 .trim()
7dab0bd6
RK
69 .custom(isVideoNameValid).withMessage(
70 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
71 ),
0f320037 72 body('channelId')
c8861d5d 73 .customSanitizer(toIntOrNull)
2baea0c7 74 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 75
a2431b7d 76 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
77 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
78
cf7a61b5 79 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
a2431b7d 80
f6d6e7f8 81 const videoFile: express.VideoUploadFile = req.files['videofile'][0]
a2431b7d 82 const user = res.locals.oauth.token.User
b60e5f38 83
f6d6e7f8 84 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
f2eb23cd
RK
85 return cleanUpReqFiles(req)
86 }
87
f6d6e7f8 88 try {
89 if (!videoFile.duration) await addDurationToVideo(videoFile)
90 } catch (err) {
91 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
f2eb23cd 92
76148b27
RK
93 res.fail({
94 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
95 message: 'Video file unreadable.'
96 })
f2eb23cd
RK
97 return cleanUpReqFiles(req)
98 }
99
f6d6e7f8 100 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
a2431b7d 101
f6d6e7f8 102 return next()
103 }
104])
105
106/**
107 * Gets called after the last PUT request
108 */
109const videosAddResumableValidator = [
110 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111 const user = res.locals.oauth.token.User
112
113 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
114 const file = { ...body, duration: undefined, path: getResumableUploadPath(body.id), filename: body.metadata.filename }
115
116 const cleanup = () => deleteFileAndCatch(file.path)
a2431b7d 117
f6d6e7f8 118 if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
a2431b7d
C
119
120 try {
f6d6e7f8 121 if (!file.duration) await addDurationToVideo(file)
a2431b7d 122 } catch (err) {
f6d6e7f8 123 logger.error('Invalid input file in videosAddResumableValidator.', { err })
a2431b7d 124
76148b27
RK
125 res.fail({
126 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
127 message: 'Video file unreadable.'
128 })
f6d6e7f8 129 return cleanup()
a2431b7d
C
130 }
131
f6d6e7f8 132 if (!await isVideoAccepted(req, res, file)) return cleanup()
b4055e1c 133
f6d6e7f8 134 res.locals.videoFileResumable = file
135
136 return next()
137 }
138]
139
140/**
141 * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
142 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
143 *
144 * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
145 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
146 *
147 */
148const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
149 body('filename')
150 .isString()
151 .exists()
152 .withMessage('Should have a valid filename'),
153 body('name')
154 .trim()
7dab0bd6
RK
155 .custom(isVideoNameValid).withMessage(
156 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
157 ),
f6d6e7f8 158 body('channelId')
159 .customSanitizer(toIntOrNull)
160 .custom(isIdValid).withMessage('Should have correct video channel id'),
161
162 header('x-upload-content-length')
163 .isNumeric()
164 .exists()
165 .withMessage('Should specify the file length'),
166 header('x-upload-content-type')
167 .isString()
168 .exists()
169 .withMessage('Should specify the file mimetype'),
170
171 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
172 const videoFileMetadata = {
173 mimetype: req.headers['x-upload-content-type'] as string,
174 size: +req.headers['x-upload-content-length'],
175 originalname: req.body.name
176 }
177
178 const user = res.locals.oauth.token.User
179 const cleanup = () => cleanUpReqFiles(req)
180
181 logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
182 parameters: req.body,
183 headers: req.headers,
184 files: req.files
185 })
186
187 if (areValidationErrors(req, res)) return cleanup()
188
189 const files = { videofile: [ videoFileMetadata ] }
190 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup()
191
192 // multer required unsetting the Content-Type, now we can set it for node-uploadx
193 req.headers['content-type'] = 'application/json; charset=utf-8'
194 // place previewfile in metadata so that uploadx saves it in .META
195 if (req.files['previewfile']) req.body.previewfile = req.files['previewfile']
a2431b7d
C
196
197 return next()
b60e5f38 198 }
a920fef1 199])
b60e5f38 200
418d092a 201const videosUpdateValidator = getCommonVideoEditAttributes().concat([
72c7248b 202 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
203 body('name')
204 .optional()
0221f8c9 205 .trim()
7dab0bd6
RK
206 .custom(isVideoNameValid).withMessage(
207 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
208 ),
0f320037
C
209 body('channelId')
210 .optional()
c8861d5d 211 .customSanitizer(toIntOrNull)
0f320037 212 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 213
a2431b7d 214 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
215 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
216
cf7a61b5
C
217 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
218 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
0f6acda1 219 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
a2431b7d 220
6221f311 221 // Check if the user who did the request is able to update the video
0f320037 222 const user = res.locals.oauth.token.User
453e83ea 223 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d 224
0f6acda1 225 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 226
a2431b7d 227 return next()
b60e5f38 228 }
a920fef1 229])
c173e565 230
8d427346 231async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
0283eaac 232 const video = getVideoWithAttributes(res)
8d427346
C
233
234 // Anybody can watch local videos
235 if (video.isOwned() === true) return next()
236
237 // Logged user
238 if (res.locals.oauth) {
239 // Users can search or watch remote videos
240 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
241 }
242
243 // Anybody can search or watch remote videos
244 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
245
246 // Check our instance follows an actor that shared this video
247 const serverActor = await getServerActor()
248 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
249
76148b27
RK
250 return res.fail({
251 status: HttpStatusCode.FORBIDDEN_403,
81628e50 252 message: 'Cannot get this video regarding follow constraints',
3866ea02 253 type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
76148b27
RK
254 data: {
255 originUrl: video.url
256 }
257 })
8d427346
C
258}
259
7eba5e1f
C
260const videosCustomGetValidator = (
261 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
262 authenticateInQuery = false
263) => {
96f29c0f
C
264 return [
265 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 266
96f29c0f
C
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 269
96f29c0f 270 if (areValidationErrors(req, res)) return
0f6acda1 271 if (!await doesVideoExist(req.params.id, res, fetchType)) return
191764f3 272
943e5193
C
273 // Controllers does not need to check video rights
274 if (fetchType === 'only-immutable-attributes') return next()
275
d7df188f 276 const video = getVideoWithAttributes(res) as MVideoWithRights
191764f3 277
96f29c0f 278 // Video private or blacklisted
d7df188f 279 if (video.requiresAuth()) {
eccf70f0 280 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
8d427346 281
dae86118 282 const user = res.locals.oauth ? res.locals.oauth.token.User : null
191764f3 283
81628e50 284 // Only the owner or a user that have blocklist rights can see the video
d7df188f 285 if (!user || !user.canGetVideo(video)) {
76148b27
RK
286 return res.fail({
287 status: HttpStatusCode.FORBIDDEN_403,
81628e50 288 message: 'Cannot get this private/internal or blocklisted video'
76148b27 289 })
8d427346 290 }
191764f3 291
8d427346 292 return next()
96f29c0f 293 }
11474c3c 294
96f29c0f
C
295 // Video is public, anyone can access it
296 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 297
96f29c0f
C
298 // Video is unlisted, check we used the uuid to fetch it
299 if (video.privacy === VideoPrivacy.UNLISTED) {
300 if (isUUIDValid(req.params.id)) return next()
81ebea48 301
96f29c0f 302 // Don't leak this unlisted video
76148b27
RK
303 return res.fail({
304 status: HttpStatusCode.NOT_FOUND_404,
305 message: 'Video not found'
306 })
96f29c0f 307 }
81ebea48 308 }
96f29c0f
C
309 ]
310}
311
312const videosGetValidator = videosCustomGetValidator('all')
eccf70f0 313const videosDownloadValidator = videosCustomGetValidator('all', true)
34ca3b52 314
8319d6ae
RK
315const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
316 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
317 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
318
319 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
320 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
321
322 if (areValidationErrors(req, res)) return
323 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
324
325 return next()
326 }
327])
328
b60e5f38 329const videosRemoveValidator = [
72c7248b 330 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 331
a2431b7d 332 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 333 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 334
a2431b7d 335 if (areValidationErrors(req, res)) return
0f6acda1 336 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
337
338 // Check if the user who did the request is able to delete the video
453e83ea 339 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
340
341 return next()
b60e5f38
C
342 }
343]
34ca3b52 344
74d63469
GR
345const videosChangeOwnershipValidator = [
346 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
347
348 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
349 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
350
351 if (areValidationErrors(req, res)) return
0f6acda1 352 if (!await doesVideoExist(req.params.videoId, res)) return
74d63469
GR
353
354 // Check if the user who did the request is able to change the ownership of the video
453e83ea 355 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
74d63469
GR
356
357 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
358 if (!nextOwner) {
76148b27 359 res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
74d63469
GR
360 return
361 }
74d63469 362
76148b27 363 res.locals.nextOwner = nextOwner
74d63469
GR
364 return next()
365 }
366]
367
368const videosTerminateChangeOwnershipValidator = [
369 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
370
371 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
372 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
373
374 if (areValidationErrors(req, res)) return
375 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
376
377 // Check if the user who did the request is able to change the ownership of the video
378 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
379
dae86118 380 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469 381
a1587156 382 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
76148b27
RK
383 res.fail({
384 status: HttpStatusCode.FORBIDDEN_403,
385 message: 'Ownership already accepted or refused'
386 })
74d63469
GR
387 return
388 }
a1587156
C
389
390 return next()
74d63469
GR
391 }
392]
393
394const videosAcceptChangeOwnershipValidator = [
395 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
396 const body = req.body as VideoChangeOwnershipAccept
0f6acda1 397 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
74d63469
GR
398
399 const user = res.locals.oauth.token.User
dae86118 400 const videoChangeOwnership = res.locals.videoChangeOwnership
fb719404 401 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
74d63469 402 if (isAble === false) {
76148b27
RK
403 res.fail({
404 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
405 message: 'The user video quota is exceeded with this video.'
406 })
74d63469
GR
407 return
408 }
409
410 return next()
411 }
412]
413
764a9657
C
414const videosOverviewValidator = [
415 query('page')
416 .optional()
417 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
418 .withMessage('Should have a valid pagination'),
419
420 (req: express.Request, res: express.Response, next: express.NextFunction) => {
421 if (areValidationErrors(req, res)) return
422
423 return next()
424 }
425]
426
418d092a 427function getCommonVideoEditAttributes () {
a920fef1
C
428 return [
429 body('thumbnailfile')
430 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
a1587156
C
431 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
432 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
433 ),
a920fef1
C
434 body('previewfile')
435 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
a1587156
C
436 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
437 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
438 ),
a920fef1
C
439
440 body('category')
441 .optional()
442 .customSanitizer(toIntOrNull)
443 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
444 body('licence')
445 .optional()
446 .customSanitizer(toIntOrNull)
447 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
448 body('language')
449 .optional()
450 .customSanitizer(toValueOrNull)
451 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
452 body('nsfw')
453 .optional()
c8861d5d 454 .customSanitizer(toBooleanOrNull)
a920fef1
C
455 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
456 body('waitTranscoding')
457 .optional()
c8861d5d 458 .customSanitizer(toBooleanOrNull)
a920fef1
C
459 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
460 body('privacy')
461 .optional()
c8861d5d 462 .customSanitizer(toValueOrNull)
a920fef1
C
463 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
464 body('description')
465 .optional()
466 .customSanitizer(toValueOrNull)
467 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
468 body('support')
469 .optional()
470 .customSanitizer(toValueOrNull)
471 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
472 body('tags')
473 .optional()
474 .customSanitizer(toValueOrNull)
7dab0bd6
RK
475 .custom(isVideoTagsValid)
476 .withMessage(
477 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
478 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
479 ),
a920fef1
C
480 body('commentsEnabled')
481 .optional()
c8861d5d 482 .customSanitizer(toBooleanOrNull)
a920fef1 483 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
7f2cfe3a 484 body('downloadEnabled')
1e74f19a 485 .optional()
c8861d5d 486 .customSanitizer(toBooleanOrNull)
156c50af 487 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22 488 body('originallyPublishedAt')
c8861d5d
C
489 .optional()
490 .customSanitizer(toValueOrNull)
491 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
492 body('scheduleUpdate')
493 .optional()
494 .customSanitizer(toValueOrNull),
495 body('scheduleUpdate.updateAt')
496 .optional()
70330f63 497 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
a920fef1
C
498 body('scheduleUpdate.privacy')
499 .optional()
2b65c4e5 500 .customSanitizer(toIntOrNull)
a920fef1 501 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
ba5a8d89 502 ] as (ValidationChain | ExpressPromiseHandler)[]
a920fef1 503}
fbad87b0 504
1cd3facc
C
505const commonVideosFiltersValidator = [
506 query('categoryOneOf')
507 .optional()
508 .customSanitizer(toArray)
509 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
510 query('licenceOneOf')
511 .optional()
512 .customSanitizer(toArray)
513 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
514 query('languageOneOf')
515 .optional()
516 .customSanitizer(toArray)
517 .custom(isStringArray).withMessage('Should have a valid one of language array'),
518 query('tagsOneOf')
519 .optional()
520 .customSanitizer(toArray)
521 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
522 query('tagsAllOf')
523 .optional()
524 .customSanitizer(toArray)
525 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
526 query('nsfw')
527 .optional()
1fd61899
C
528 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
529 query('isLive')
530 .optional()
531 .customSanitizer(toBooleanOrNull)
532 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
1cd3facc
C
533 query('filter')
534 .optional()
535 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
fe987656
C
536 query('skipCount')
537 .optional()
538 .customSanitizer(toBooleanOrNull)
539 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
37024082
RK
540 query('search')
541 .optional()
542 .custom(exists).withMessage('Should have a valid search'),
1cd3facc
C
543
544 (req: express.Request, res: express.Response, next: express.NextFunction) => {
545 logger.debug('Checking commons video filters query', { parameters: req.query })
546
547 if (areValidationErrors(req, res)) return
548
dae86118 549 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
0aa52e17
C
550 if (
551 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
552 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
553 ) {
76148b27
RK
554 res.fail({
555 status: HttpStatusCode.UNAUTHORIZED_401,
556 message: 'You are not allowed to see all local videos.'
557 })
1cd3facc
C
558 return
559 }
560
561 return next()
562 }
563]
564
fbad87b0
C
565// ---------------------------------------------------------------------------
566
567export {
f6d6e7f8 568 videosAddLegacyValidator,
569 videosAddResumableValidator,
570 videosAddResumableInitValidator,
571
fbad87b0
C
572 videosUpdateValidator,
573 videosGetValidator,
8319d6ae 574 videoFileMetadataGetValidator,
eccf70f0 575 videosDownloadValidator,
8d427346 576 checkVideoFollowConstraints,
96f29c0f 577 videosCustomGetValidator,
fbad87b0 578 videosRemoveValidator,
fbad87b0 579
74d63469
GR
580 videosChangeOwnershipValidator,
581 videosTerminateChangeOwnershipValidator,
582 videosAcceptChangeOwnershipValidator,
583
418d092a 584 getCommonVideoEditAttributes,
1cd3facc 585
764a9657
C
586 commonVideosFiltersValidator,
587
588 videosOverviewValidator
fbad87b0
C
589}
590
591// ---------------------------------------------------------------------------
592
593function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
594 if (req.body.scheduleUpdate) {
595 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
596 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
597
76148b27 598 res.fail({ message: 'Schedule update at is mandatory.' })
fbad87b0
C
599 return true
600 }
601 }
602
603 return false
604}
b4055e1c 605
f6d6e7f8 606async function commonVideoChecksPass (parameters: {
607 req: express.Request
608 res: express.Response
609 user: MUserAccountId
610 videoFileSize: number
611 files: express.UploadFilesForCheck
612}): Promise<boolean> {
613 const { req, res, user, videoFileSize, files } = parameters
614
615 if (areErrorsInScheduleUpdate(req, res)) return false
616
617 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
618
619 if (!isVideoFileMimeTypeValid(files)) {
76148b27
RK
620 res.fail({
621 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
622 message: 'This file is not supported. Please, make sure it is of the following type: ' +
623 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
624 })
f6d6e7f8 625 return false
626 }
627
628 if (!isVideoFileSizeValid(videoFileSize.toString())) {
76148b27
RK
629 res.fail({
630 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
631 message: 'This file is too large. It exceeds the maximum file size authorized.'
632 })
f6d6e7f8 633 return false
634 }
635
636 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
76148b27
RK
637 res.fail({
638 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
639 message: 'The user video quota is exceeded with this video.'
640 })
f6d6e7f8 641 return false
642 }
643
644 return true
645}
646
647export async function isVideoAccepted (
648 req: express.Request,
649 res: express.Response,
650 videoFile: express.VideoUploadFile
651) {
b4055e1c
C
652 // Check we accept this video
653 const acceptParameters = {
654 videoBody: req.body,
655 videoFile,
656 user: res.locals.oauth.token.User
657 }
89cd1275
C
658 const acceptedResult = await Hooks.wrapFun(
659 isLocalVideoAccepted,
660 acceptParameters,
b4055e1c
C
661 'filter:api.video.upload.accept.result'
662 )
663
664 if (!acceptedResult || acceptedResult.accepted !== true) {
665 logger.info('Refused local video.', { acceptedResult, acceptParameters })
76148b27
RK
666 res.fail({
667 status: HttpStatusCode.FORBIDDEN_403,
668 message: acceptedResult.errorMessage || 'Refused local video'
669 })
b4055e1c
C
670 return false
671 }
672
673 return true
674}
f6d6e7f8 675
676async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
677 const duration: number = await getDurationFromVideoFile(videoFile.path)
678
679 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
680
681 videoFile.duration = duration
682}