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'
55 const videosAddValidator = getCommonVideoEditAttributes().concat([
57 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
58 'This file is not supported or too large. Please, make sure it is of the following type: ' +
59 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
61 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
63 .customSanitizer(toIntOrNull)
64 .custom(isIdValid).withMessage('Should have correct video channel id'),
66 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
67 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
69 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
70 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
72 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
73 const user = res.locals.oauth.token.User
75 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
77 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
79 .json({ error: 'The user video quota is exceeded with this video.' })
81 return cleanUpReqFiles(req)
87 duration = await getDurationFromVideoFile(videoFile.path)
89 logger.error('Invalid input file in videosAddValidator.', { err })
91 .json({ error: 'Invalid input file.' })
93 return cleanUpReqFiles(req)
96 videoFile.duration = duration
98 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
104 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
105 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
108 .custom(isVideoNameValid).withMessage('Should have a valid name'),
111 .customSanitizer(toIntOrNull)
112 .custom(isIdValid).withMessage('Should have correct video channel id'),
114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
117 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
118 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
119 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
121 // Check if the user who did the request is able to update the video
122 const user = res.locals.oauth.token.User
123 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
125 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
131 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video = getVideoWithAttributes(res)
134 // Anybody can watch local videos
135 if (video.isOwned() === true) return next()
138 if (res.locals.oauth) {
139 // Users can search or watch remote videos
140 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
143 // Anybody can search or watch remote videos
144 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
146 // Check our instance follows an actor that shared this video
147 const serverActor = await getServerActor()
148 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
150 return res.status(403)
152 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
153 error: 'Cannot get this video regarding follow constraints.',
158 const videosCustomGetValidator = (
159 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
160 authenticateInQuery = false
163 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
165 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
166 logger.debug('Checking videosGet parameters', { parameters: req.params })
168 if (areValidationErrors(req, res)) return
169 if (!await doesVideoExist(req.params.id, res, fetchType)) return
171 // Controllers does not need to check video rights
172 if (fetchType === 'only-immutable-attributes') return next()
174 const video = getVideoWithAttributes(res)
175 const videoAll = video as MVideoFullLight
177 // Video private or blacklisted
178 if (videoAll.requiresAuth()) {
179 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
181 const user = res.locals.oauth ? res.locals.oauth.token.User : null
183 // Only the owner or a user that have blacklist rights can see the video
184 if (!user || !user.canGetVideo(videoAll)) {
185 return res.status(403)
186 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
192 // Video is public, anyone can access it
193 if (video.privacy === VideoPrivacy.PUBLIC) return next()
195 // Video is unlisted, check we used the uuid to fetch it
196 if (video.privacy === VideoPrivacy.UNLISTED) {
197 if (isUUIDValid(req.params.id)) return next()
199 // Don't leak this unlisted video
200 return res.status(404).end()
206 const videosGetValidator = videosCustomGetValidator('all')
207 const videosDownloadValidator = videosCustomGetValidator('all', true)
209 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
210 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
211 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
213 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
214 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
216 if (areValidationErrors(req, res)) return
217 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
223 const videosRemoveValidator = [
224 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
226 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
227 logger.debug('Checking videosRemove parameters', { parameters: req.params })
229 if (areValidationErrors(req, res)) return
230 if (!await doesVideoExist(req.params.id, res)) return
232 // Check if the user who did the request is able to delete the video
233 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
239 const videosChangeOwnershipValidator = [
240 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
242 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
243 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
245 if (areValidationErrors(req, res)) return
246 if (!await doesVideoExist(req.params.videoId, res)) return
248 // Check if the user who did the request is able to change the ownership of the video
249 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
251 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
254 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
258 res.locals.nextOwner = nextOwner
264 const videosTerminateChangeOwnershipValidator = [
265 param('id').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 doesChangeVideoOwnershipExist(req.params.id, res)) return
273 // Check if the user who did the request is able to change the ownership of the video
274 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
276 const videoChangeOwnership = res.locals.videoChangeOwnership
278 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
280 .json({ error: 'Ownership already accepted or refused' })
288 const videosAcceptChangeOwnershipValidator = [
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 const body = req.body as VideoChangeOwnershipAccept
291 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
293 const user = res.locals.oauth.token.User
294 const videoChangeOwnership = res.locals.videoChangeOwnership
295 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
296 if (isAble === false) {
298 .json({ error: 'The user video quota is exceeded with this video.' })
307 const videosOverviewValidator = [
310 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
311 .withMessage('Should have a valid pagination'),
313 (req: express.Request, res: express.Response, next: express.NextFunction) => {
314 if (areValidationErrors(req, res)) return
320 function getCommonVideoEditAttributes () {
322 body('thumbnailfile')
323 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
324 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
325 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
328 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
329 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
330 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
335 .customSanitizer(toIntOrNull)
336 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
339 .customSanitizer(toIntOrNull)
340 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
343 .customSanitizer(toValueOrNull)
344 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
347 .customSanitizer(toBooleanOrNull)
348 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
349 body('waitTranscoding')
351 .customSanitizer(toBooleanOrNull)
352 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
355 .customSanitizer(toValueOrNull)
356 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
359 .customSanitizer(toValueOrNull)
360 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
363 .customSanitizer(toValueOrNull)
364 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
367 .customSanitizer(toValueOrNull)
368 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
369 body('commentsEnabled')
371 .customSanitizer(toBooleanOrNull)
372 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
373 body('downloadEnabled')
375 .customSanitizer(toBooleanOrNull)
376 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
377 body('originallyPublishedAt')
379 .customSanitizer(toValueOrNull)
380 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
381 body('scheduleUpdate')
383 .customSanitizer(toValueOrNull),
384 body('scheduleUpdate.updateAt')
386 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
387 body('scheduleUpdate.privacy')
389 .customSanitizer(toIntOrNull)
390 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
391 ] as (ValidationChain | express.Handler)[]
394 const commonVideosFiltersValidator = [
395 query('categoryOneOf')
397 .customSanitizer(toArray)
398 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
399 query('licenceOneOf')
401 .customSanitizer(toArray)
402 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
403 query('languageOneOf')
405 .customSanitizer(toArray)
406 .custom(isStringArray).withMessage('Should have a valid one of language array'),
409 .customSanitizer(toArray)
410 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
413 .customSanitizer(toArray)
414 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
417 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
420 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
423 .customSanitizer(toBooleanOrNull)
424 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
426 (req: express.Request, res: express.Response, next: express.NextFunction) => {
427 logger.debug('Checking commons video filters query', { parameters: req.query })
429 if (areValidationErrors(req, res)) return
431 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
433 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
434 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
437 .json({ error: 'You are not allowed to see all local videos.' })
446 // ---------------------------------------------------------------------------
450 videosUpdateValidator,
452 videoFileMetadataGetValidator,
453 videosDownloadValidator,
454 checkVideoFollowConstraints,
455 videosCustomGetValidator,
456 videosRemoveValidator,
458 videosChangeOwnershipValidator,
459 videosTerminateChangeOwnershipValidator,
460 videosAcceptChangeOwnershipValidator,
462 getCommonVideoEditAttributes,
464 commonVideosFiltersValidator,
466 videosOverviewValidator
469 // ---------------------------------------------------------------------------
471 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
472 if (req.body.scheduleUpdate) {
473 if (!req.body.scheduleUpdate.updateAt) {
474 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
477 .json({ error: 'Schedule update at is mandatory.' })
486 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
487 // Check we accept this video
488 const acceptParameters = {
491 user: res.locals.oauth.token.User
493 const acceptedResult = await Hooks.wrapFun(
494 isLocalVideoAccepted,
496 'filter:api.video.upload.accept.result'
499 if (!acceptedResult || acceptedResult.accepted !== true) {
500 logger.info('Refused local video.', { acceptedResult, acceptParameters })
502 .json({ error: acceptedResult.errorMessage || 'Refused local video' })