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