]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
add video upload types, add doc middleware to more routes
[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,
c756bae0
RK
405 message: 'The user video quota is exceeded with this video.',
406 type: ServerErrorCode.QUOTA_REACHED
76148b27 407 })
74d63469
GR
408 return
409 }
410
411 return next()
412 }
413]
414
764a9657
C
415const videosOverviewValidator = [
416 query('page')
417 .optional()
418 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
419 .withMessage('Should have a valid pagination'),
420
421 (req: express.Request, res: express.Response, next: express.NextFunction) => {
422 if (areValidationErrors(req, res)) return
423
424 return next()
425 }
426]
427
418d092a 428function getCommonVideoEditAttributes () {
a920fef1
C
429 return [
430 body('thumbnailfile')
431 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
a1587156
C
432 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
433 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
434 ),
a920fef1
C
435 body('previewfile')
436 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
a1587156
C
437 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
438 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
439 ),
a920fef1
C
440
441 body('category')
442 .optional()
443 .customSanitizer(toIntOrNull)
444 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
445 body('licence')
446 .optional()
447 .customSanitizer(toIntOrNull)
448 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
449 body('language')
450 .optional()
451 .customSanitizer(toValueOrNull)
452 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
453 body('nsfw')
454 .optional()
c8861d5d 455 .customSanitizer(toBooleanOrNull)
a920fef1
C
456 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
457 body('waitTranscoding')
458 .optional()
c8861d5d 459 .customSanitizer(toBooleanOrNull)
a920fef1
C
460 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
461 body('privacy')
462 .optional()
c8861d5d 463 .customSanitizer(toValueOrNull)
a920fef1
C
464 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
465 body('description')
466 .optional()
467 .customSanitizer(toValueOrNull)
468 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
469 body('support')
470 .optional()
471 .customSanitizer(toValueOrNull)
472 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
473 body('tags')
474 .optional()
475 .customSanitizer(toValueOrNull)
7dab0bd6
RK
476 .custom(isVideoTagsValid)
477 .withMessage(
478 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
479 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
480 ),
a920fef1
C
481 body('commentsEnabled')
482 .optional()
c8861d5d 483 .customSanitizer(toBooleanOrNull)
a920fef1 484 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
7f2cfe3a 485 body('downloadEnabled')
1e74f19a 486 .optional()
c8861d5d 487 .customSanitizer(toBooleanOrNull)
156c50af 488 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22 489 body('originallyPublishedAt')
c8861d5d
C
490 .optional()
491 .customSanitizer(toValueOrNull)
492 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
493 body('scheduleUpdate')
494 .optional()
495 .customSanitizer(toValueOrNull),
496 body('scheduleUpdate.updateAt')
497 .optional()
70330f63 498 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
a920fef1
C
499 body('scheduleUpdate.privacy')
500 .optional()
2b65c4e5 501 .customSanitizer(toIntOrNull)
a920fef1 502 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
ba5a8d89 503 ] as (ValidationChain | ExpressPromiseHandler)[]
a920fef1 504}
fbad87b0 505
1cd3facc
C
506const commonVideosFiltersValidator = [
507 query('categoryOneOf')
508 .optional()
509 .customSanitizer(toArray)
510 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
511 query('licenceOneOf')
512 .optional()
513 .customSanitizer(toArray)
514 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
515 query('languageOneOf')
516 .optional()
517 .customSanitizer(toArray)
518 .custom(isStringArray).withMessage('Should have a valid one of language array'),
519 query('tagsOneOf')
520 .optional()
521 .customSanitizer(toArray)
522 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
523 query('tagsAllOf')
524 .optional()
525 .customSanitizer(toArray)
526 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
527 query('nsfw')
528 .optional()
1fd61899
C
529 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
530 query('isLive')
531 .optional()
532 .customSanitizer(toBooleanOrNull)
533 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
1cd3facc
C
534 query('filter')
535 .optional()
536 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
fe987656
C
537 query('skipCount')
538 .optional()
539 .customSanitizer(toBooleanOrNull)
540 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
37024082
RK
541 query('search')
542 .optional()
543 .custom(exists).withMessage('Should have a valid search'),
1cd3facc
C
544
545 (req: express.Request, res: express.Response, next: express.NextFunction) => {
546 logger.debug('Checking commons video filters query', { parameters: req.query })
547
548 if (areValidationErrors(req, res)) return
549
dae86118 550 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
0aa52e17
C
551 if (
552 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
553 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
554 ) {
76148b27
RK
555 res.fail({
556 status: HttpStatusCode.UNAUTHORIZED_401,
557 message: 'You are not allowed to see all local videos.'
558 })
1cd3facc
C
559 return
560 }
561
562 return next()
563 }
564]
565
fbad87b0
C
566// ---------------------------------------------------------------------------
567
568export {
f6d6e7f8 569 videosAddLegacyValidator,
570 videosAddResumableValidator,
571 videosAddResumableInitValidator,
572
fbad87b0
C
573 videosUpdateValidator,
574 videosGetValidator,
8319d6ae 575 videoFileMetadataGetValidator,
eccf70f0 576 videosDownloadValidator,
8d427346 577 checkVideoFollowConstraints,
96f29c0f 578 videosCustomGetValidator,
fbad87b0 579 videosRemoveValidator,
fbad87b0 580
74d63469
GR
581 videosChangeOwnershipValidator,
582 videosTerminateChangeOwnershipValidator,
583 videosAcceptChangeOwnershipValidator,
584
418d092a 585 getCommonVideoEditAttributes,
1cd3facc 586
764a9657
C
587 commonVideosFiltersValidator,
588
589 videosOverviewValidator
fbad87b0
C
590}
591
592// ---------------------------------------------------------------------------
593
594function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
595 if (req.body.scheduleUpdate) {
596 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
597 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
598
76148b27 599 res.fail({ message: 'Schedule update at is mandatory.' })
fbad87b0
C
600 return true
601 }
602 }
603
604 return false
605}
b4055e1c 606
f6d6e7f8 607async function commonVideoChecksPass (parameters: {
608 req: express.Request
609 res: express.Response
610 user: MUserAccountId
611 videoFileSize: number
612 files: express.UploadFilesForCheck
613}): Promise<boolean> {
614 const { req, res, user, videoFileSize, files } = parameters
615
616 if (areErrorsInScheduleUpdate(req, res)) return false
617
618 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
619
620 if (!isVideoFileMimeTypeValid(files)) {
76148b27
RK
621 res.fail({
622 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
623 message: 'This file is not supported. Please, make sure it is of the following type: ' +
624 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
625 })
f6d6e7f8 626 return false
627 }
628
629 if (!isVideoFileSizeValid(videoFileSize.toString())) {
76148b27
RK
630 res.fail({
631 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
c756bae0
RK
632 message: 'This file is too large. It exceeds the maximum file size authorized.',
633 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
76148b27 634 })
f6d6e7f8 635 return false
636 }
637
638 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
76148b27
RK
639 res.fail({
640 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
c756bae0
RK
641 message: 'The user video quota is exceeded with this video.',
642 type: ServerErrorCode.QUOTA_REACHED
76148b27 643 })
f6d6e7f8 644 return false
645 }
646
647 return true
648}
649
650export async function isVideoAccepted (
651 req: express.Request,
652 res: express.Response,
653 videoFile: express.VideoUploadFile
654) {
b4055e1c
C
655 // Check we accept this video
656 const acceptParameters = {
657 videoBody: req.body,
658 videoFile,
659 user: res.locals.oauth.token.User
660 }
89cd1275
C
661 const acceptedResult = await Hooks.wrapFun(
662 isLocalVideoAccepted,
663 acceptParameters,
b4055e1c
C
664 'filter:api.video.upload.accept.result'
665 )
666
667 if (!acceptedResult || acceptedResult.accepted !== true) {
668 logger.info('Refused local video.', { acceptedResult, acceptParameters })
76148b27
RK
669 res.fail({
670 status: HttpStatusCode.FORBIDDEN_403,
671 message: acceptedResult.errorMessage || 'Refused local video'
672 })
b4055e1c
C
673 return false
674 }
675
676 return true
677}
f6d6e7f8 678
679async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
680 const duration: number = await getDurationFromVideoFile(videoFile.path)
681
682 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
683
684 videoFile.duration = duration
685}