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