1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { isAbleToUploadVideo } from '@server/lib/user'
4 import { getServerActor } from '@server/models/application/application'
5 import { ExpressPromiseHandler } from '@server/types/express'
6 import { MVideoFullLight } from '@server/types/models'
7 import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
8 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
9 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
22 } from '../../../helpers/custom-validators/misc'
23 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
24 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
26 isScheduleVideoUpdatePrivacyValid,
28 isVideoDescriptionValid,
29 isVideoFileMimeTypeValid,
36 isVideoOriginallyPublishedAtValid,
40 } from '../../../helpers/custom-validators/videos'
41 import { cleanUpReqFiles } from '../../../helpers/express-utils'
42 import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
43 import { logger } from '../../../helpers/logger'
45 checkUserCanManageVideo,
46 doesVideoChannelOfAccountExist,
48 doesVideoFileOfVideoExist
49 } from '../../../helpers/middlewares'
50 import { getVideoWithAttributes } from '../../../helpers/video'
51 import { CONFIG } from '../../../initializers/config'
52 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
53 import { isLocalVideoAccepted } from '../../../lib/moderation'
54 import { Hooks } from '../../../lib/plugins/hooks'
55 import { AccountModel } from '../../../models/account/account'
56 import { VideoModel } from '../../../models/video/video'
57 import { authenticatePromiseIfNeeded } from '../../oauth'
58 import { areValidationErrors } from '../utils'
60 const videosAddValidator = getCommonVideoEditAttributes().concat([
62 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
63 .withMessage('Should have a file'),
66 .custom(isVideoNameValid)
67 .withMessage('Should have a valid name'),
69 .customSanitizer(toIntOrNull)
70 .custom(isIdValid).withMessage('Should have correct video channel id'),
72 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
73 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
75 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
76 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
78 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
79 const user = res.locals.oauth.token.User
81 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
83 if (!isVideoFileMimeTypeValid(req.files)) {
84 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
86 error: 'This file is not supported. Please, make sure it is of the following type: ' +
87 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
90 return cleanUpReqFiles(req)
93 if (!isVideoFileSizeValid(videoFile.size.toString())) {
94 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
96 error: 'This file is too large.'
99 return cleanUpReqFiles(req)
102 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
103 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
104 .json({ error: 'The user video quota is exceeded with this video.' })
106 return cleanUpReqFiles(req)
112 duration = await getDurationFromVideoFile(videoFile.path)
114 logger.error('Invalid input file in videosAddValidator.', { err })
115 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
116 .json({ error: 'Video file unreadable.' })
118 return cleanUpReqFiles(req)
121 videoFile.duration = duration
123 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
129 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
130 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
134 .custom(isVideoNameValid).withMessage('Should have a valid name'),
137 .customSanitizer(toIntOrNull)
138 .custom(isIdValid).withMessage('Should have correct video channel id'),
140 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
141 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
143 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
144 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
145 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
147 // Check if the user who did the request is able to update the video
148 const user = res.locals.oauth.token.User
149 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
151 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
157 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
158 const video = getVideoWithAttributes(res)
160 // Anybody can watch local videos
161 if (video.isOwned() === true) return next()
164 if (res.locals.oauth) {
165 // Users can search or watch remote videos
166 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
169 // Anybody can search or watch remote videos
170 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
172 // Check our instance follows an actor that shared this video
173 const serverActor = await getServerActor()
174 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
176 return res.status(HttpStatusCode.FORBIDDEN_403)
178 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
179 error: 'Cannot get this video regarding follow constraints.',
184 const videosCustomGetValidator = (
185 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
186 authenticateInQuery = false
189 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
191 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
192 logger.debug('Checking videosGet parameters', { parameters: req.params })
194 if (areValidationErrors(req, res)) return
195 if (!await doesVideoExist(req.params.id, res, fetchType)) return
197 // Controllers does not need to check video rights
198 if (fetchType === 'only-immutable-attributes') return next()
200 const video = getVideoWithAttributes(res)
201 const videoAll = video as MVideoFullLight
203 // Video private or blacklisted
204 if (videoAll.requiresAuth()) {
205 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
207 const user = res.locals.oauth ? res.locals.oauth.token.User : null
209 // Only the owner or a user that have blacklist rights can see the video
210 if (!user || !user.canGetVideo(videoAll)) {
211 return res.status(HttpStatusCode.FORBIDDEN_403)
212 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
218 // Video is public, anyone can access it
219 if (video.privacy === VideoPrivacy.PUBLIC) return next()
221 // Video is unlisted, check we used the uuid to fetch it
222 if (video.privacy === VideoPrivacy.UNLISTED) {
223 if (isUUIDValid(req.params.id)) return next()
225 // Don't leak this unlisted video
226 return res.status(HttpStatusCode.NOT_FOUND_404).end()
232 const videosGetValidator = videosCustomGetValidator('all')
233 const videosDownloadValidator = videosCustomGetValidator('all', true)
235 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
236 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
237 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
239 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
240 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
242 if (areValidationErrors(req, res)) return
243 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
249 const videosRemoveValidator = [
250 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
252 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
253 logger.debug('Checking videosRemove parameters', { parameters: req.params })
255 if (areValidationErrors(req, res)) return
256 if (!await doesVideoExist(req.params.id, res)) return
258 // Check if the user who did the request is able to delete the video
259 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
265 const videosChangeOwnershipValidator = [
266 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
268 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
269 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
271 if (areValidationErrors(req, res)) return
272 if (!await doesVideoExist(req.params.videoId, res)) return
274 // Check if the user who did the request is able to change the ownership of the video
275 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
277 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
279 res.status(HttpStatusCode.BAD_REQUEST_400)
280 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
284 res.locals.nextOwner = nextOwner
290 const videosTerminateChangeOwnershipValidator = [
291 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
293 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
294 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
296 if (areValidationErrors(req, res)) return
297 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
299 // Check if the user who did the request is able to change the ownership of the video
300 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
302 const videoChangeOwnership = res.locals.videoChangeOwnership
304 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
305 res.status(HttpStatusCode.FORBIDDEN_403)
306 .json({ error: 'Ownership already accepted or refused' })
314 const videosAcceptChangeOwnershipValidator = [
315 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
316 const body = req.body as VideoChangeOwnershipAccept
317 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
319 const user = res.locals.oauth.token.User
320 const videoChangeOwnership = res.locals.videoChangeOwnership
321 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
322 if (isAble === false) {
323 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
324 .json({ error: 'The user video quota is exceeded with this video.' })
333 const videosOverviewValidator = [
336 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
337 .withMessage('Should have a valid pagination'),
339 (req: express.Request, res: express.Response, next: express.NextFunction) => {
340 if (areValidationErrors(req, res)) return
346 function getCommonVideoEditAttributes () {
348 body('thumbnailfile')
349 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
350 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
351 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
354 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
355 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
356 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
361 .customSanitizer(toIntOrNull)
362 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
365 .customSanitizer(toIntOrNull)
366 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
369 .customSanitizer(toValueOrNull)
370 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
373 .customSanitizer(toBooleanOrNull)
374 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
375 body('waitTranscoding')
377 .customSanitizer(toBooleanOrNull)
378 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
381 .customSanitizer(toValueOrNull)
382 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
385 .customSanitizer(toValueOrNull)
386 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
389 .customSanitizer(toValueOrNull)
390 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
393 .customSanitizer(toValueOrNull)
394 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
395 body('commentsEnabled')
397 .customSanitizer(toBooleanOrNull)
398 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
399 body('downloadEnabled')
401 .customSanitizer(toBooleanOrNull)
402 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
403 body('originallyPublishedAt')
405 .customSanitizer(toValueOrNull)
406 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
407 body('scheduleUpdate')
409 .customSanitizer(toValueOrNull),
410 body('scheduleUpdate.updateAt')
412 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
413 body('scheduleUpdate.privacy')
415 .customSanitizer(toIntOrNull)
416 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
417 ] as (ValidationChain | ExpressPromiseHandler)[]
420 const commonVideosFiltersValidator = [
421 query('categoryOneOf')
423 .customSanitizer(toArray)
424 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
425 query('licenceOneOf')
427 .customSanitizer(toArray)
428 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
429 query('languageOneOf')
431 .customSanitizer(toArray)
432 .custom(isStringArray).withMessage('Should have a valid one of language array'),
435 .customSanitizer(toArray)
436 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
439 .customSanitizer(toArray)
440 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
443 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
446 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
449 .customSanitizer(toBooleanOrNull)
450 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
453 .custom(exists).withMessage('Should have a valid search'),
455 (req: express.Request, res: express.Response, next: express.NextFunction) => {
456 logger.debug('Checking commons video filters query', { parameters: req.query })
458 if (areValidationErrors(req, res)) return
460 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
462 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
463 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
465 res.status(HttpStatusCode.UNAUTHORIZED_401)
466 .json({ error: 'You are not allowed to see all local videos.' })
475 // ---------------------------------------------------------------------------
479 videosUpdateValidator,
481 videoFileMetadataGetValidator,
482 videosDownloadValidator,
483 checkVideoFollowConstraints,
484 videosCustomGetValidator,
485 videosRemoveValidator,
487 videosChangeOwnershipValidator,
488 videosTerminateChangeOwnershipValidator,
489 videosAcceptChangeOwnershipValidator,
491 getCommonVideoEditAttributes,
493 commonVideosFiltersValidator,
495 videosOverviewValidator
498 // ---------------------------------------------------------------------------
500 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
501 if (req.body.scheduleUpdate) {
502 if (!req.body.scheduleUpdate.updateAt) {
503 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
505 res.status(HttpStatusCode.BAD_REQUEST_400)
506 .json({ error: 'Schedule update at is mandatory.' })
515 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
516 // Check we accept this video
517 const acceptParameters = {
520 user: res.locals.oauth.token.User
522 const acceptedResult = await Hooks.wrapFun(
523 isLocalVideoAccepted,
525 'filter:api.video.upload.accept.result'
528 if (!acceptedResult || acceptedResult.accepted !== true) {
529 logger.info('Refused local video.', { acceptedResult, acceptParameters })
530 res.status(HttpStatusCode.FORBIDDEN_403)
531 .json({ error: acceptedResult.errorMessage || 'Refused local video' })