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