1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
14 } from '../../../helpers/custom-validators/misc'
16 isScheduleVideoUpdatePrivacyValid,
18 isVideoDescriptionValid,
25 isVideoOriginallyPublishedAtValid,
29 } from '../../../helpers/custom-validators/videos'
30 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
31 import { logger } from '../../../helpers/logger'
32 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
33 import { authenticatePromiseIfNeeded } from '../../oauth'
34 import { areValidationErrors } from '../utils'
35 import { cleanUpReqFiles } from '../../../helpers/express-utils'
36 import { VideoModel } from '../../../models/video/video'
37 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
38 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
39 import { AccountModel } from '../../../models/account/account'
40 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
41 import { getServerActor } from '../../../helpers/utils'
42 import { CONFIG } from '../../../initializers/config'
43 import { isLocalVideoAccepted } from '../../../lib/moderation'
44 import { Hooks } from '../../../lib/plugins/hooks'
46 checkUserCanManageVideo,
47 doesVideoChannelOfAccountExist,
49 doesVideoFileOfVideoExist
50 } from '../../../helpers/middlewares'
51 import { MVideoFullLight } from '@server/typings/models'
52 import { getVideoWithAttributes } from '../../../helpers/video'
54 const videosAddValidator = getCommonVideoEditAttributes().concat([
56 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
57 'This file is not supported or too large. Please, make sure it is of the following type: ' +
58 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
60 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
62 .customSanitizer(toIntOrNull)
63 .custom(isIdValid).withMessage('Should have correct video channel id'),
65 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
66 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
68 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
69 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
71 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
72 const user = res.locals.oauth.token.User
74 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
76 if (await user.isAbleToUploadVideo(videoFile) === false) {
78 .json({ error: 'The user video quota is exceeded with this video.' })
80 return cleanUpReqFiles(req)
86 duration = await getDurationFromVideoFile(videoFile.path)
88 logger.error('Invalid input file in videosAddValidator.', { err })
90 .json({ error: 'Invalid input file.' })
92 return cleanUpReqFiles(req)
95 videoFile.duration = duration
97 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
103 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
104 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
107 .custom(isVideoNameValid).withMessage('Should have a valid name'),
110 .customSanitizer(toIntOrNull)
111 .custom(isIdValid).withMessage('Should have correct video channel id'),
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
116 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
117 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
118 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
120 // Check if the user who did the request is able to update the video
121 const user = res.locals.oauth.token.User
122 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
124 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
130 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
131 const video = getVideoWithAttributes(res)
133 // Anybody can watch local videos
134 if (video.isOwned() === true) return next()
137 if (res.locals.oauth) {
138 // Users can search or watch remote videos
139 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
142 // Anybody can search or watch remote videos
143 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
145 // Check our instance follows an actor that shared this video
146 const serverActor = await getServerActor()
147 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
149 return res.status(403)
151 error: 'Cannot get this video regarding follow constraints.'
155 const videosCustomGetValidator = (
156 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
157 authenticateInQuery = false
160 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
162 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
163 logger.debug('Checking videosGet parameters', { parameters: req.params })
165 if (areValidationErrors(req, res)) return
166 if (!await doesVideoExist(req.params.id, res, fetchType)) return
168 // Controllers does not need to check video rights
169 if (fetchType === 'only-immutable-attributes') return next()
171 const video = getVideoWithAttributes(res)
172 const videoAll = video as MVideoFullLight
174 // Video private or blacklisted
175 if (videoAll.requiresAuth()) {
176 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
178 const user = res.locals.oauth ? res.locals.oauth.token.User : null
180 // Only the owner or a user that have blacklist rights can see the video
181 if (!user || !user.canGetVideo(videoAll)) {
182 return res.status(403)
183 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
189 // Video is public, anyone can access it
190 if (video.privacy === VideoPrivacy.PUBLIC) return next()
192 // Video is unlisted, check we used the uuid to fetch it
193 if (video.privacy === VideoPrivacy.UNLISTED) {
194 if (isUUIDValid(req.params.id)) return next()
196 // Don't leak this unlisted video
197 return res.status(404).end()
203 const videosGetValidator = videosCustomGetValidator('all')
204 const videosDownloadValidator = videosCustomGetValidator('all', true)
206 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
207 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
208 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
213 if (areValidationErrors(req, res)) return
214 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
220 const videosRemoveValidator = [
221 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
223 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
224 logger.debug('Checking videosRemove parameters', { parameters: req.params })
226 if (areValidationErrors(req, res)) return
227 if (!await doesVideoExist(req.params.id, res)) return
229 // Check if the user who did the request is able to delete the video
230 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
236 const videosChangeOwnershipValidator = [
237 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
239 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
240 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
242 if (areValidationErrors(req, res)) return
243 if (!await doesVideoExist(req.params.videoId, res)) return
245 // Check if the user who did the request is able to change the ownership of the video
246 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
248 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
251 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
255 res.locals.nextOwner = nextOwner
261 const videosTerminateChangeOwnershipValidator = [
262 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
264 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
265 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
267 if (areValidationErrors(req, res)) return
268 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
270 // Check if the user who did the request is able to change the ownership of the video
271 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
273 const videoChangeOwnership = res.locals.videoChangeOwnership
275 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
277 .json({ error: 'Ownership already accepted or refused' })
285 const videosAcceptChangeOwnershipValidator = [
286 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
287 const body = req.body as VideoChangeOwnershipAccept
288 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
290 const user = res.locals.oauth.token.User
291 const videoChangeOwnership = res.locals.videoChangeOwnership
292 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
293 if (isAble === false) {
295 .json({ error: 'The user video quota is exceeded with this video.' })
304 const videosOverviewValidator = [
307 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
308 .withMessage('Should have a valid pagination'),
310 (req: express.Request, res: express.Response, next: express.NextFunction) => {
311 if (areValidationErrors(req, res)) return
317 function getCommonVideoEditAttributes () {
319 body('thumbnailfile')
320 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
321 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
322 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
325 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
326 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
327 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
332 .customSanitizer(toIntOrNull)
333 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
336 .customSanitizer(toIntOrNull)
337 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
340 .customSanitizer(toValueOrNull)
341 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
344 .customSanitizer(toBooleanOrNull)
345 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
346 body('waitTranscoding')
348 .customSanitizer(toBooleanOrNull)
349 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
352 .customSanitizer(toValueOrNull)
353 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
356 .customSanitizer(toValueOrNull)
357 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
360 .customSanitizer(toValueOrNull)
361 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
364 .customSanitizer(toValueOrNull)
365 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
366 body('commentsEnabled')
368 .customSanitizer(toBooleanOrNull)
369 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
370 body('downloadEnabled')
372 .customSanitizer(toBooleanOrNull)
373 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
374 body('originallyPublishedAt')
376 .customSanitizer(toValueOrNull)
377 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
378 body('scheduleUpdate')
380 .customSanitizer(toValueOrNull),
381 body('scheduleUpdate.updateAt')
383 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
384 body('scheduleUpdate.privacy')
386 .customSanitizer(toIntOrNull)
387 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
388 ] as (ValidationChain | express.Handler)[]
391 const commonVideosFiltersValidator = [
392 query('categoryOneOf')
394 .customSanitizer(toArray)
395 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
396 query('licenceOneOf')
398 .customSanitizer(toArray)
399 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
400 query('languageOneOf')
402 .customSanitizer(toArray)
403 .custom(isStringArray).withMessage('Should have a valid one of language array'),
406 .customSanitizer(toArray)
407 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
410 .customSanitizer(toArray)
411 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
414 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
417 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
420 .customSanitizer(toBooleanOrNull)
421 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
423 (req: express.Request, res: express.Response, next: express.NextFunction) => {
424 logger.debug('Checking commons video filters query', { parameters: req.query })
426 if (areValidationErrors(req, res)) return
428 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
429 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
431 .json({ error: 'You are not allowed to see all local videos.' })
440 // ---------------------------------------------------------------------------
444 videosUpdateValidator,
446 videoFileMetadataGetValidator,
447 videosDownloadValidator,
448 checkVideoFollowConstraints,
449 videosCustomGetValidator,
450 videosRemoveValidator,
452 videosChangeOwnershipValidator,
453 videosTerminateChangeOwnershipValidator,
454 videosAcceptChangeOwnershipValidator,
456 getCommonVideoEditAttributes,
458 commonVideosFiltersValidator,
460 videosOverviewValidator
463 // ---------------------------------------------------------------------------
465 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
466 if (req.body.scheduleUpdate) {
467 if (!req.body.scheduleUpdate.updateAt) {
468 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
471 .json({ error: 'Schedule update at is mandatory.' })
480 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
481 // Check we accept this video
482 const acceptParameters = {
485 user: res.locals.oauth.token.User
487 const acceptedResult = await Hooks.wrapFun(
488 isLocalVideoAccepted,
490 'filter:api.video.upload.accept.result'
493 if (!acceptedResult || acceptedResult.accepted !== true) {
494 logger.info('Refused local video.', { acceptedResult, acceptParameters })
496 .json({ error: acceptedResult.errorMessage || 'Refused local video' })