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 } 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'
45 import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares'
46 import { MVideoFullLight } from '@server/typings/models'
47 import { getVideoWithAttributes } from '../../../helpers/video'
49 const videosAddValidator = getCommonVideoEditAttributes().concat([
51 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
52 'This file is not supported or too large. Please, make sure it is of the following type: ' +
53 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
57 .customSanitizer(toIntOrNull)
58 .custom(isIdValid).withMessage('Should have correct video channel id'),
60 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
61 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
63 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
64 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
66 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
69 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
71 if (await user.isAbleToUploadVideo(videoFile) === false) {
73 .json({ error: 'The user video quota is exceeded with this video.' })
75 return cleanUpReqFiles(req)
81 duration = await getDurationFromVideoFile(videoFile.path)
83 logger.error('Invalid input file in videosAddValidator.', { err })
85 .json({ error: 'Invalid input file.' })
87 return cleanUpReqFiles(req)
90 videoFile.duration = duration
92 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
98 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
99 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
102 .custom(isVideoNameValid).withMessage('Should have a valid name'),
105 .customSanitizer(toIntOrNull)
106 .custom(isIdValid).withMessage('Should have correct video channel id'),
108 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
109 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
111 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
112 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
113 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
115 // Check if the user who did the request is able to update the video
116 const user = res.locals.oauth.token.User
117 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
119 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
125 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
126 const video = getVideoWithAttributes(res)
128 // Anybody can watch local videos
129 if (video.isOwned() === true) return next()
132 if (res.locals.oauth) {
133 // Users can search or watch remote videos
134 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
137 // Anybody can search or watch remote videos
138 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
140 // Check our instance follows an actor that shared this video
141 const serverActor = await getServerActor()
142 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
144 return res.status(403)
146 error: 'Cannot get this video regarding follow constraints.'
150 const videosCustomGetValidator = (
151 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
152 authenticateInQuery = false
155 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
157 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
158 logger.debug('Checking videosGet parameters', { parameters: req.params })
160 if (areValidationErrors(req, res)) return
161 if (!await doesVideoExist(req.params.id, res, fetchType)) return
163 // Controllers does not need to check video rights
164 if (fetchType === 'only-immutable-attributes') return next()
166 const video = getVideoWithAttributes(res)
167 const videoAll = video as MVideoFullLight
169 // Video private or blacklisted
170 if (videoAll.requiresAuth()) {
171 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
173 const user = res.locals.oauth ? res.locals.oauth.token.User : null
175 // Only the owner or a user that have blacklist rights can see the video
176 if (!user || !user.canGetVideo(videoAll)) {
177 return res.status(403)
178 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
184 // Video is public, anyone can access it
185 if (video.privacy === VideoPrivacy.PUBLIC) return next()
187 // Video is unlisted, check we used the uuid to fetch it
188 if (video.privacy === VideoPrivacy.UNLISTED) {
189 if (isUUIDValid(req.params.id)) return next()
191 // Don't leak this unlisted video
192 return res.status(404).end()
198 const videosGetValidator = videosCustomGetValidator('all')
199 const videosDownloadValidator = videosCustomGetValidator('all', true)
201 const videosRemoveValidator = [
202 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
204 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
205 logger.debug('Checking videosRemove parameters', { parameters: req.params })
207 if (areValidationErrors(req, res)) return
208 if (!await doesVideoExist(req.params.id, res)) return
210 // Check if the user who did the request is able to delete the video
211 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
217 const videosChangeOwnershipValidator = [
218 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
220 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
221 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
223 if (areValidationErrors(req, res)) return
224 if (!await doesVideoExist(req.params.videoId, res)) return
226 // Check if the user who did the request is able to change the ownership of the video
227 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
229 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
232 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
236 res.locals.nextOwner = nextOwner
242 const videosTerminateChangeOwnershipValidator = [
243 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
245 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
246 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
248 if (areValidationErrors(req, res)) return
249 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
251 // Check if the user who did the request is able to change the ownership of the video
252 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
254 const videoChangeOwnership = res.locals.videoChangeOwnership
256 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
258 .json({ error: 'Ownership already accepted or refused' })
266 const videosAcceptChangeOwnershipValidator = [
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 const body = req.body as VideoChangeOwnershipAccept
269 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
271 const user = res.locals.oauth.token.User
272 const videoChangeOwnership = res.locals.videoChangeOwnership
273 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
274 if (isAble === false) {
276 .json({ error: 'The user video quota is exceeded with this video.' })
285 function getCommonVideoEditAttributes () {
287 body('thumbnailfile')
288 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
289 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
290 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
293 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
294 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
295 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
300 .customSanitizer(toIntOrNull)
301 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
304 .customSanitizer(toIntOrNull)
305 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
308 .customSanitizer(toValueOrNull)
309 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
312 .customSanitizer(toBooleanOrNull)
313 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
314 body('waitTranscoding')
316 .customSanitizer(toBooleanOrNull)
317 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
320 .customSanitizer(toValueOrNull)
321 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
324 .customSanitizer(toValueOrNull)
325 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
328 .customSanitizer(toValueOrNull)
329 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
332 .customSanitizer(toValueOrNull)
333 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
334 body('commentsEnabled')
336 .customSanitizer(toBooleanOrNull)
337 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
338 body('downloadEnabled')
340 .customSanitizer(toBooleanOrNull)
341 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
342 body('originallyPublishedAt')
344 .customSanitizer(toValueOrNull)
345 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
346 body('scheduleUpdate')
348 .customSanitizer(toValueOrNull),
349 body('scheduleUpdate.updateAt')
351 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
352 body('scheduleUpdate.privacy')
354 .customSanitizer(toIntOrNull)
355 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
356 ] as (ValidationChain | express.Handler)[]
359 const commonVideosFiltersValidator = [
360 query('categoryOneOf')
362 .customSanitizer(toArray)
363 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
364 query('licenceOneOf')
366 .customSanitizer(toArray)
367 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
368 query('languageOneOf')
370 .customSanitizer(toArray)
371 .custom(isStringArray).withMessage('Should have a valid one of language array'),
374 .customSanitizer(toArray)
375 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
378 .customSanitizer(toArray)
379 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
382 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
385 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
388 .customSanitizer(toBooleanOrNull)
389 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
391 (req: express.Request, res: express.Response, next: express.NextFunction) => {
392 logger.debug('Checking commons video filters query', { parameters: req.query })
394 if (areValidationErrors(req, res)) return
396 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
397 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
399 .json({ error: 'You are not allowed to see all local videos.' })
408 // ---------------------------------------------------------------------------
412 videosUpdateValidator,
414 videosDownloadValidator,
415 checkVideoFollowConstraints,
416 videosCustomGetValidator,
417 videosRemoveValidator,
419 videosChangeOwnershipValidator,
420 videosTerminateChangeOwnershipValidator,
421 videosAcceptChangeOwnershipValidator,
423 getCommonVideoEditAttributes,
425 commonVideosFiltersValidator
428 // ---------------------------------------------------------------------------
430 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
431 if (req.body.scheduleUpdate) {
432 if (!req.body.scheduleUpdate.updateAt) {
433 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
436 .json({ error: 'Schedule update at is mandatory.' })
445 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
446 // Check we accept this video
447 const acceptParameters = {
450 user: res.locals.oauth.token.User
452 const acceptedResult = await Hooks.wrapFun(
453 isLocalVideoAccepted,
455 'filter:api.video.upload.accept.result'
458 if (!acceptedResult || acceptedResult.accepted !== true) {
459 logger.info('Refused local video.', { acceptedResult, acceptParameters })
461 .json({ error: acceptedResult.errorMessage || 'Refused local video' })