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