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 { MVideoWithRights } 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 '../../auth'
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) as MVideoWithRights
202 // Video private or blacklisted
203 if (video.requiresAuth()) {
204 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
206 const user = res.locals.oauth ? res.locals.oauth.token.User : null
208 // Only the owner or a user that have blacklist rights can see the video
209 if (!user || !user.canGetVideo(video)) {
210 return res.status(HttpStatusCode.FORBIDDEN_403)
211 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
217 // Video is public, anyone can access it
218 if (video.privacy === VideoPrivacy.PUBLIC) return next()
220 // Video is unlisted, check we used the uuid to fetch it
221 if (video.privacy === VideoPrivacy.UNLISTED) {
222 if (isUUIDValid(req.params.id)) return next()
224 // Don't leak this unlisted video
225 return res.status(HttpStatusCode.NOT_FOUND_404).end()
231 const videosGetValidator = videosCustomGetValidator('all')
232 const videosDownloadValidator = videosCustomGetValidator('all', true)
234 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
235 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
236 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
238 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
239 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
241 if (areValidationErrors(req, res)) return
242 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
248 const videosRemoveValidator = [
249 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
251 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
252 logger.debug('Checking videosRemove parameters', { parameters: req.params })
254 if (areValidationErrors(req, res)) return
255 if (!await doesVideoExist(req.params.id, res)) return
257 // Check if the user who did the request is able to delete the video
258 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
264 const videosChangeOwnershipValidator = [
265 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
270 if (areValidationErrors(req, res)) return
271 if (!await doesVideoExist(req.params.videoId, res)) return
273 // Check if the user who did the request is able to change the ownership of the video
274 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
276 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
278 res.status(HttpStatusCode.BAD_REQUEST_400)
279 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
283 res.locals.nextOwner = nextOwner
289 const videosTerminateChangeOwnershipValidator = [
290 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
292 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
293 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
295 if (areValidationErrors(req, res)) return
296 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
298 // Check if the user who did the request is able to change the ownership of the video
299 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
301 const videoChangeOwnership = res.locals.videoChangeOwnership
303 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
304 res.status(HttpStatusCode.FORBIDDEN_403)
305 .json({ error: 'Ownership already accepted or refused' })
313 const videosAcceptChangeOwnershipValidator = [
314 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
315 const body = req.body as VideoChangeOwnershipAccept
316 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
318 const user = res.locals.oauth.token.User
319 const videoChangeOwnership = res.locals.videoChangeOwnership
320 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
321 if (isAble === false) {
322 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
323 .json({ error: 'The user video quota is exceeded with this video.' })
332 const videosOverviewValidator = [
335 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
336 .withMessage('Should have a valid pagination'),
338 (req: express.Request, res: express.Response, next: express.NextFunction) => {
339 if (areValidationErrors(req, res)) return
345 function getCommonVideoEditAttributes () {
347 body('thumbnailfile')
348 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
349 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
350 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
353 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
354 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
355 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
360 .customSanitizer(toIntOrNull)
361 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
364 .customSanitizer(toIntOrNull)
365 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
368 .customSanitizer(toValueOrNull)
369 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
372 .customSanitizer(toBooleanOrNull)
373 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
374 body('waitTranscoding')
376 .customSanitizer(toBooleanOrNull)
377 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
380 .customSanitizer(toValueOrNull)
381 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
384 .customSanitizer(toValueOrNull)
385 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
388 .customSanitizer(toValueOrNull)
389 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
392 .customSanitizer(toValueOrNull)
393 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
394 body('commentsEnabled')
396 .customSanitizer(toBooleanOrNull)
397 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
398 body('downloadEnabled')
400 .customSanitizer(toBooleanOrNull)
401 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
402 body('originallyPublishedAt')
404 .customSanitizer(toValueOrNull)
405 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
406 body('scheduleUpdate')
408 .customSanitizer(toValueOrNull),
409 body('scheduleUpdate.updateAt')
411 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
412 body('scheduleUpdate.privacy')
414 .customSanitizer(toIntOrNull)
415 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
416 ] as (ValidationChain | ExpressPromiseHandler)[]
419 const commonVideosFiltersValidator = [
420 query('categoryOneOf')
422 .customSanitizer(toArray)
423 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
424 query('licenceOneOf')
426 .customSanitizer(toArray)
427 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
428 query('languageOneOf')
430 .customSanitizer(toArray)
431 .custom(isStringArray).withMessage('Should have a valid one of language array'),
434 .customSanitizer(toArray)
435 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
438 .customSanitizer(toArray)
439 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
442 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
445 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
448 .customSanitizer(toBooleanOrNull)
449 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
452 .custom(exists).withMessage('Should have a valid search'),
454 (req: express.Request, res: express.Response, next: express.NextFunction) => {
455 logger.debug('Checking commons video filters query', { parameters: req.query })
457 if (areValidationErrors(req, res)) return
459 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
461 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
462 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
464 res.status(HttpStatusCode.UNAUTHORIZED_401)
465 .json({ error: 'You are not allowed to see all local videos.' })
474 // ---------------------------------------------------------------------------
478 videosUpdateValidator,
480 videoFileMetadataGetValidator,
481 videosDownloadValidator,
482 checkVideoFollowConstraints,
483 videosCustomGetValidator,
484 videosRemoveValidator,
486 videosChangeOwnershipValidator,
487 videosTerminateChangeOwnershipValidator,
488 videosAcceptChangeOwnershipValidator,
490 getCommonVideoEditAttributes,
492 commonVideosFiltersValidator,
494 videosOverviewValidator
497 // ---------------------------------------------------------------------------
499 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
500 if (req.body.scheduleUpdate) {
501 if (!req.body.scheduleUpdate.updateAt) {
502 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
504 res.status(HttpStatusCode.BAD_REQUEST_400)
505 .json({ error: 'Schedule update at is mandatory.' })
514 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
515 // Check we accept this video
516 const acceptParameters = {
519 user: res.locals.oauth.token.User
521 const acceptedResult = await Hooks.wrapFun(
522 isLocalVideoAccepted,
524 'filter:api.video.upload.accept.result'
527 if (!acceptedResult || acceptedResult.accepted !== true) {
528 logger.info('Refused local video.', { acceptedResult, acceptParameters })
529 res.status(HttpStatusCode.FORBIDDEN_403)
530 .json({ error: acceptedResult.errorMessage || 'Refused local video' })