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