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 { MVideoFullLight } from '@server/types/models'
6 import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
7 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
18 } from '../../../helpers/custom-validators/misc'
19 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
20 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
22 isScheduleVideoUpdatePrivacyValid,
24 isVideoDescriptionValid,
31 isVideoOriginallyPublishedAtValid,
35 } from '../../../helpers/custom-validators/videos'
36 import { cleanUpReqFiles } from '../../../helpers/express-utils'
37 import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
38 import { logger } from '../../../helpers/logger'
40 checkUserCanManageVideo,
41 doesVideoChannelOfAccountExist,
43 doesVideoFileOfVideoExist
44 } from '../../../helpers/middlewares'
45 import { getVideoWithAttributes } from '../../../helpers/video'
46 import { CONFIG } from '../../../initializers/config'
47 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
48 import { isLocalVideoAccepted } from '../../../lib/moderation'
49 import { Hooks } from '../../../lib/plugins/hooks'
50 import { AccountModel } from '../../../models/account/account'
51 import { VideoModel } from '../../../models/video/video'
52 import { authenticatePromiseIfNeeded } from '../../oauth'
53 import { areValidationErrors } from '../utils'
54 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
56 const videosAddValidator = getCommonVideoEditAttributes().concat([
58 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
59 'This file is not supported or too large. Please, make sure it is of the following type: ' +
60 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
62 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
64 .customSanitizer(toIntOrNull)
65 .custom(isIdValid).withMessage('Should have correct video channel id'),
67 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
68 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
70 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
71 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
73 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
74 const user = res.locals.oauth.token.User
76 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
78 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
79 res.status(HttpStatusCode.FORBIDDEN_403)
80 .json({ error: 'The user video quota is exceeded with this video.' })
82 return cleanUpReqFiles(req)
88 duration = await getDurationFromVideoFile(videoFile.path)
90 logger.error('Invalid input file in videosAddValidator.', { err })
91 res.status(HttpStatusCode.BAD_REQUEST_400)
92 .json({ error: 'Invalid input file.' })
94 return cleanUpReqFiles(req)
97 videoFile.duration = duration
99 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
105 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
106 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
109 .custom(isVideoNameValid).withMessage('Should have a valid name'),
112 .customSanitizer(toIntOrNull)
113 .custom(isIdValid).withMessage('Should have correct video channel id'),
115 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
116 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
118 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
119 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
120 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
122 // Check if the user who did the request is able to update the video
123 const user = res.locals.oauth.token.User
124 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
126 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
132 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
133 const video = getVideoWithAttributes(res)
135 // Anybody can watch local videos
136 if (video.isOwned() === true) return next()
139 if (res.locals.oauth) {
140 // Users can search or watch remote videos
141 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
144 // Anybody can search or watch remote videos
145 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
147 // Check our instance follows an actor that shared this video
148 const serverActor = await getServerActor()
149 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
151 return res.status(HttpStatusCode.FORBIDDEN_403)
153 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
154 error: 'Cannot get this video regarding follow constraints.',
159 const videosCustomGetValidator = (
160 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
161 authenticateInQuery = false
164 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
166 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
167 logger.debug('Checking videosGet parameters', { parameters: req.params })
169 if (areValidationErrors(req, res)) return
170 if (!await doesVideoExist(req.params.id, res, fetchType)) return
172 // Controllers does not need to check video rights
173 if (fetchType === 'only-immutable-attributes') return next()
175 const video = getVideoWithAttributes(res)
176 const videoAll = video as MVideoFullLight
178 // Video private or blacklisted
179 if (videoAll.requiresAuth()) {
180 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
182 const user = res.locals.oauth ? res.locals.oauth.token.User : null
184 // Only the owner or a user that have blacklist rights can see the video
185 if (!user || !user.canGetVideo(videoAll)) {
186 return res.status(HttpStatusCode.FORBIDDEN_403)
187 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
193 // Video is public, anyone can access it
194 if (video.privacy === VideoPrivacy.PUBLIC) return next()
196 // Video is unlisted, check we used the uuid to fetch it
197 if (video.privacy === VideoPrivacy.UNLISTED) {
198 if (isUUIDValid(req.params.id)) return next()
200 // Don't leak this unlisted video
201 return res.status(HttpStatusCode.NOT_FOUND_404).end()
207 const videosGetValidator = videosCustomGetValidator('all')
208 const videosDownloadValidator = videosCustomGetValidator('all', true)
210 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
211 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
212 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
214 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
215 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
217 if (areValidationErrors(req, res)) return
218 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
224 const videosRemoveValidator = [
225 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
227 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
228 logger.debug('Checking videosRemove parameters', { parameters: req.params })
230 if (areValidationErrors(req, res)) return
231 if (!await doesVideoExist(req.params.id, res)) return
233 // Check if the user who did the request is able to delete the video
234 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
240 const videosChangeOwnershipValidator = [
241 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
243 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
244 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
246 if (areValidationErrors(req, res)) return
247 if (!await doesVideoExist(req.params.videoId, res)) return
249 // Check if the user who did the request is able to change the ownership of the video
250 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
252 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
254 res.status(HttpStatusCode.BAD_REQUEST_400)
255 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
259 res.locals.nextOwner = nextOwner
265 const videosTerminateChangeOwnershipValidator = [
266 param('id').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 doesChangeVideoOwnershipExist(req.params.id, res)) return
274 // Check if the user who did the request is able to change the ownership of the video
275 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
277 const videoChangeOwnership = res.locals.videoChangeOwnership
279 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
280 res.status(HttpStatusCode.FORBIDDEN_403)
281 .json({ error: 'Ownership already accepted or refused' })
289 const videosAcceptChangeOwnershipValidator = [
290 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
291 const body = req.body as VideoChangeOwnershipAccept
292 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
294 const user = res.locals.oauth.token.User
295 const videoChangeOwnership = res.locals.videoChangeOwnership
296 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
297 if (isAble === false) {
298 res.status(HttpStatusCode.FORBIDDEN_403)
299 .json({ error: 'The user video quota is exceeded with this video.' })
308 const videosOverviewValidator = [
311 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
312 .withMessage('Should have a valid pagination'),
314 (req: express.Request, res: express.Response, next: express.NextFunction) => {
315 if (areValidationErrors(req, res)) return
321 function getCommonVideoEditAttributes () {
323 body('thumbnailfile')
324 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
325 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
326 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
329 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
330 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
331 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
336 .customSanitizer(toIntOrNull)
337 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
340 .customSanitizer(toIntOrNull)
341 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
344 .customSanitizer(toValueOrNull)
345 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
348 .customSanitizer(toBooleanOrNull)
349 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
350 body('waitTranscoding')
352 .customSanitizer(toBooleanOrNull)
353 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
356 .customSanitizer(toValueOrNull)
357 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
360 .customSanitizer(toValueOrNull)
361 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
364 .customSanitizer(toValueOrNull)
365 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
368 .customSanitizer(toValueOrNull)
369 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
370 body('commentsEnabled')
372 .customSanitizer(toBooleanOrNull)
373 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
374 body('downloadEnabled')
376 .customSanitizer(toBooleanOrNull)
377 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
378 body('originallyPublishedAt')
380 .customSanitizer(toValueOrNull)
381 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
382 body('scheduleUpdate')
384 .customSanitizer(toValueOrNull),
385 body('scheduleUpdate.updateAt')
387 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
388 body('scheduleUpdate.privacy')
390 .customSanitizer(toIntOrNull)
391 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
392 ] as (ValidationChain | express.Handler)[]
395 const commonVideosFiltersValidator = [
396 query('categoryOneOf')
398 .customSanitizer(toArray)
399 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
400 query('licenceOneOf')
402 .customSanitizer(toArray)
403 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
404 query('languageOneOf')
406 .customSanitizer(toArray)
407 .custom(isStringArray).withMessage('Should have a valid one of language array'),
410 .customSanitizer(toArray)
411 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
414 .customSanitizer(toArray)
415 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
418 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
421 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
424 .customSanitizer(toBooleanOrNull)
425 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
427 (req: express.Request, res: express.Response, next: express.NextFunction) => {
428 logger.debug('Checking commons video filters query', { parameters: req.query })
430 if (areValidationErrors(req, res)) return
432 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
434 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
435 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
437 res.status(HttpStatusCode.UNAUTHORIZED_401)
438 .json({ error: 'You are not allowed to see all local videos.' })
447 // ---------------------------------------------------------------------------
451 videosUpdateValidator,
453 videoFileMetadataGetValidator,
454 videosDownloadValidator,
455 checkVideoFollowConstraints,
456 videosCustomGetValidator,
457 videosRemoveValidator,
459 videosChangeOwnershipValidator,
460 videosTerminateChangeOwnershipValidator,
461 videosAcceptChangeOwnershipValidator,
463 getCommonVideoEditAttributes,
465 commonVideosFiltersValidator,
467 videosOverviewValidator
470 // ---------------------------------------------------------------------------
472 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
473 if (req.body.scheduleUpdate) {
474 if (!req.body.scheduleUpdate.updateAt) {
475 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
477 res.status(HttpStatusCode.BAD_REQUEST_400)
478 .json({ error: 'Schedule update at is mandatory.' })
487 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
488 // Check we accept this video
489 const acceptParameters = {
492 user: res.locals.oauth.token.User
494 const acceptedResult = await Hooks.wrapFun(
495 isLocalVideoAccepted,
497 'filter:api.video.upload.accept.result'
500 if (!acceptedResult || acceptedResult.accepted !== true) {
501 logger.info('Refused local video.', { acceptedResult, acceptParameters })
502 res.status(HttpStatusCode.FORBIDDEN_403)
503 .json({ error: acceptedResult.errorMessage || 'Refused local video' })