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