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