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