]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Fix redis connection timeout
[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'
ba5a8d89 8import { ExpressPromiseHandler } from '@server/types/express'
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'
f43db2f4 52import { authenticatePromiseIfNeeded } from '../../auth'
10363c74
C
53import {
54 areValidationErrors,
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()) {
eccf70f0 318 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
8d427346 319
dae86118 320 const user = res.locals.oauth ? res.locals.oauth.token.User : null
191764f3 321
81628e50 322 // Only the owner or a user that have blocklist rights can see the video
d7df188f 323 if (!user || !user.canGetVideo(video)) {
76148b27
RK
324 return res.fail({
325 status: HttpStatusCode.FORBIDDEN_403,
81628e50 326 message: 'Cannot get this private/internal or blocklisted video'
76148b27 327 })
8d427346 328 }
191764f3 329
8d427346 330 return next()
96f29c0f 331 }
11474c3c 332
96f29c0f
C
333 // Video is public, anyone can access it
334 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 335
96f29c0f
C
336 // Video is unlisted, check we used the uuid to fetch it
337 if (video.privacy === VideoPrivacy.UNLISTED) {
338 if (isUUIDValid(req.params.id)) return next()
81ebea48 339
96f29c0f 340 // Don't leak this unlisted video
76148b27
RK
341 return res.fail({
342 status: HttpStatusCode.NOT_FOUND_404,
343 message: 'Video not found'
344 })
96f29c0f 345 }
81ebea48 346 }
96f29c0f
C
347 ]
348}
349
350const videosGetValidator = videosCustomGetValidator('all')
eccf70f0 351const videosDownloadValidator = videosCustomGetValidator('all', true)
34ca3b52 352
8319d6ae 353const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
d4a8e7a6
C
354 isValidVideoIdParam('id'),
355
356 param('videoFileId')
357 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
8319d6ae
RK
358
359 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
360 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
361
362 if (areValidationErrors(req, res)) return
363 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
364
365 return next()
366 }
367])
368
b60e5f38 369const videosRemoveValidator = [
d4a8e7a6 370 isValidVideoIdParam('id'),
34ca3b52 371
a2431b7d 372 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 373 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 374
a2431b7d 375 if (areValidationErrors(req, res)) return
0f6acda1 376 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
377
378 // Check if the user who did the request is able to delete the video
453e83ea 379 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
380
381 return next()
b60e5f38
C
382 }
383]
34ca3b52 384
764a9657
C
385const videosOverviewValidator = [
386 query('page')
387 .optional()
388 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
389 .withMessage('Should have a valid pagination'),
390
391 (req: express.Request, res: express.Response, next: express.NextFunction) => {
392 if (areValidationErrors(req, res)) return
393
394 return next()
395 }
396]
397
418d092a 398function getCommonVideoEditAttributes () {
a920fef1
C
399 return [
400 body('thumbnailfile')
401 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
a1587156
C
402 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
403 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
404 ),
a920fef1
C
405 body('previewfile')
406 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
a1587156
C
407 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
408 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
409 ),
a920fef1
C
410
411 body('category')
412 .optional()
413 .customSanitizer(toIntOrNull)
414 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
415 body('licence')
416 .optional()
417 .customSanitizer(toIntOrNull)
418 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
419 body('language')
420 .optional()
421 .customSanitizer(toValueOrNull)
422 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
423 body('nsfw')
424 .optional()
c8861d5d 425 .customSanitizer(toBooleanOrNull)
a920fef1
C
426 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
427 body('waitTranscoding')
428 .optional()
c8861d5d 429 .customSanitizer(toBooleanOrNull)
a920fef1
C
430 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
431 body('privacy')
432 .optional()
c8861d5d 433 .customSanitizer(toValueOrNull)
a920fef1
C
434 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
435 body('description')
436 .optional()
437 .customSanitizer(toValueOrNull)
438 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
439 body('support')
440 .optional()
441 .customSanitizer(toValueOrNull)
442 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
443 body('tags')
444 .optional()
445 .customSanitizer(toValueOrNull)
7dab0bd6
RK
446 .custom(isVideoTagsValid)
447 .withMessage(
448 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
449 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
450 ),
a920fef1
C
451 body('commentsEnabled')
452 .optional()
c8861d5d 453 .customSanitizer(toBooleanOrNull)
a920fef1 454 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
7f2cfe3a 455 body('downloadEnabled')
1e74f19a 456 .optional()
c8861d5d 457 .customSanitizer(toBooleanOrNull)
156c50af 458 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22 459 body('originallyPublishedAt')
c8861d5d
C
460 .optional()
461 .customSanitizer(toValueOrNull)
462 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
463 body('scheduleUpdate')
464 .optional()
465 .customSanitizer(toValueOrNull),
466 body('scheduleUpdate.updateAt')
467 .optional()
70330f63 468 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
a920fef1
C
469 body('scheduleUpdate.privacy')
470 .optional()
2b65c4e5 471 .customSanitizer(toIntOrNull)
a920fef1 472 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
ba5a8d89 473 ] as (ValidationChain | ExpressPromiseHandler)[]
a920fef1 474}
fbad87b0 475
1cd3facc
C
476const commonVideosFiltersValidator = [
477 query('categoryOneOf')
478 .optional()
479 .customSanitizer(toArray)
480 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
481 query('licenceOneOf')
482 .optional()
483 .customSanitizer(toArray)
484 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
485 query('languageOneOf')
486 .optional()
487 .customSanitizer(toArray)
488 .custom(isStringArray).withMessage('Should have a valid one of language array'),
527a52ac
C
489 query('privacyOneOf')
490 .optional()
491 .customSanitizer(toArray)
492 .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
1cd3facc
C
493 query('tagsOneOf')
494 .optional()
495 .customSanitizer(toArray)
496 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
497 query('tagsAllOf')
498 .optional()
499 .customSanitizer(toArray)
500 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
501 query('nsfw')
502 .optional()
1fd61899
C
503 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
504 query('isLive')
505 .optional()
506 .customSanitizer(toBooleanOrNull)
507 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
1cd3facc
C
508 query('filter')
509 .optional()
510 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
2760b454
C
511 query('include')
512 .optional()
513 .custom(isVideoIncludeValid).withMessage('Should have a valid include attribute'),
514 query('isLocal')
515 .optional()
516 .customSanitizer(toBooleanOrNull)
517 .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
d324756e
C
518 query('hasHLSFiles')
519 .optional()
520 .customSanitizer(toBooleanOrNull)
521 .custom(isBooleanValid).withMessage('Should have a valid has hls boolean'),
522 query('hasWebtorrentFiles')
523 .optional()
524 .customSanitizer(toBooleanOrNull)
525 .custom(isBooleanValid).withMessage('Should have a valid has webtorrent boolean'),
fe987656
C
526 query('skipCount')
527 .optional()
528 .customSanitizer(toBooleanOrNull)
529 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
37024082
RK
530 query('search')
531 .optional()
532 .custom(exists).withMessage('Should have a valid search'),
1cd3facc
C
533
534 (req: express.Request, res: express.Response, next: express.NextFunction) => {
535 logger.debug('Checking commons video filters query', { parameters: req.query })
536
537 if (areValidationErrors(req, res)) return
538
2760b454
C
539 // FIXME: deprecated in 4.0, to remove
540 {
541 if (req.query.filter === 'all-local') {
527a52ac 542 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
2760b454 543 req.query.isLocal = true
527a52ac 544 req.query.privacyOneOf = getAllPrivacies()
2760b454 545 } else if (req.query.filter === 'all') {
527a52ac
C
546 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
547 req.query.privacyOneOf = getAllPrivacies()
2760b454
C
548 } else if (req.query.filter === 'local') {
549 req.query.isLocal = true
550 }
551
552 req.query.filter = undefined
553 }
554
555 const user = res.locals.oauth?.token.User
556
d324756e 557 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
527a52ac 558 if (req.query.include || req.query.privacyOneOf) {
d324756e
C
559 return res.fail({
560 status: HttpStatusCode.UNAUTHORIZED_401,
561 message: 'You are not allowed to see all videos.'
562 })
563 }
1cd3facc
C
564 }
565
566 return next()
567 }
568]
569
fbad87b0
C
570// ---------------------------------------------------------------------------
571
572export {
f6d6e7f8 573 videosAddLegacyValidator,
574 videosAddResumableValidator,
575 videosAddResumableInitValidator,
020d3d3d 576 videosResumableUploadIdValidator,
f6d6e7f8 577
fbad87b0
C
578 videosUpdateValidator,
579 videosGetValidator,
8319d6ae 580 videoFileMetadataGetValidator,
eccf70f0 581 videosDownloadValidator,
8d427346 582 checkVideoFollowConstraints,
96f29c0f 583 videosCustomGetValidator,
fbad87b0 584 videosRemoveValidator,
fbad87b0 585
418d092a 586 getCommonVideoEditAttributes,
1cd3facc 587
764a9657
C
588 commonVideosFiltersValidator,
589
590 videosOverviewValidator
fbad87b0
C
591}
592
593// ---------------------------------------------------------------------------
594
595function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
596 if (req.body.scheduleUpdate) {
597 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
598 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
599
76148b27 600 res.fail({ message: 'Schedule update at is mandatory.' })
fbad87b0
C
601 return true
602 }
603 }
604
605 return false
606}
b4055e1c 607
f6d6e7f8 608async function commonVideoChecksPass (parameters: {
609 req: express.Request
610 res: express.Response
611 user: MUserAccountId
612 videoFileSize: number
613 files: express.UploadFilesForCheck
614}): Promise<boolean> {
615 const { req, res, user, videoFileSize, files } = parameters
616
617 if (areErrorsInScheduleUpdate(req, res)) return false
618
619 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
620
621 if (!isVideoFileMimeTypeValid(files)) {
76148b27
RK
622 res.fail({
623 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
624 message: 'This file is not supported. Please, make sure it is of the following type: ' +
625 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
626 })
f6d6e7f8 627 return false
628 }
629
630 if (!isVideoFileSizeValid(videoFileSize.toString())) {
76148b27
RK
631 res.fail({
632 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
c756bae0
RK
633 message: 'This file is too large. It exceeds the maximum file size authorized.',
634 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
76148b27 635 })
f6d6e7f8 636 return false
637 }
638
639 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
76148b27
RK
640 res.fail({
641 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
c756bae0
RK
642 message: 'The user video quota is exceeded with this video.',
643 type: ServerErrorCode.QUOTA_REACHED
76148b27 644 })
f6d6e7f8 645 return false
646 }
647
648 return true
649}
650
651export async function isVideoAccepted (
652 req: express.Request,
653 res: express.Response,
654 videoFile: express.VideoUploadFile
655) {
b4055e1c
C
656 // Check we accept this video
657 const acceptParameters = {
658 videoBody: req.body,
659 videoFile,
660 user: res.locals.oauth.token.User
661 }
89cd1275
C
662 const acceptedResult = await Hooks.wrapFun(
663 isLocalVideoAccepted,
664 acceptParameters,
b4055e1c
C
665 'filter:api.video.upload.accept.result'
666 )
667
668 if (!acceptedResult || acceptedResult.accepted !== true) {
669 logger.info('Refused local video.', { acceptedResult, acceptParameters })
76148b27
RK
670 res.fail({
671 status: HttpStatusCode.FORBIDDEN_403,
672 message: acceptedResult.errorMessage || 'Refused local video'
673 })
b4055e1c
C
674 return false
675 }
676
677 return true
678}
f6d6e7f8 679
680async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
681 const duration: number = await getDurationFromVideoFile(videoFile.path)
682
683 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
684
685 videoFile.duration = duration
686}