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