1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query, ValidationChain } from 'express-validator/check'
4 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
14 } from '../../../helpers/custom-validators/misc'
16 checkUserCanManageVideo,
17 isVideoOriginallyPublishedAtValid,
18 isScheduleVideoUpdatePrivacyValid,
20 isVideoChannelOfAccountExist,
21 isVideoDescriptionValid,
32 } from '../../../helpers/custom-validators/videos'
33 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34 import { logger } from '../../../helpers/logger'
35 import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
36 import { authenticatePromiseIfNeeded } from '../../oauth'
37 import { areValidationErrors } from '../utils'
38 import { cleanUpReqFiles } from '../../../helpers/express-utils'
39 import { VideoModel } from '../../../models/video/video'
40 import { UserModel } from '../../../models/account/user'
41 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
42 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
43 import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
44 import { AccountModel } from '../../../models/account/account'
45 import { VideoFetchType } from '../../../helpers/video'
46 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
47 import { getServerActor } from '../../../helpers/utils'
49 const videosAddValidator = getCommonVideoAttributes().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'),
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 = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
69 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
71 const isAble = await user.isAbleToUploadVideo(videoFile)
72 if (isAble === false) {
74 .json({ error: 'The user video quota is exceeded with this video.' })
76 return cleanUpReqFiles(req)
82 duration = await getDurationFromVideoFile(videoFile.path)
84 logger.error('Invalid input file in videosAddValidator.', { err })
86 .json({ error: 'Invalid input file.' })
88 return cleanUpReqFiles(req)
91 videoFile['duration'] = duration
97 const videosUpdateValidator = getCommonVideoAttributes().concat([
98 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
101 .custom(isVideoNameValid).withMessage('Should have a valid name'),
105 .custom(isIdValid).withMessage('Should have correct video channel id'),
107 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
108 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
110 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
111 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
112 if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
114 const video = res.locals.video
116 // Check if the user who did the request is able to update the video
117 const user = res.locals.oauth.token.User
118 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
120 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
122 return res.status(409)
123 .json({ error: 'Cannot set "private" a video that was not private.' })
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
132 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
133 const video: VideoModel = res.locals.video
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(403)
153 error: 'Cannot get this video regarding follow constraints.'
157 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
159 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
161 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
162 logger.debug('Checking videosGet parameters', { parameters: req.params })
164 if (areValidationErrors(req, res)) return
165 if (!await isVideoExist(req.params.id, res, fetchType)) return
167 const video: VideoModel = res.locals.video
169 // Video private or blacklisted
170 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
171 await authenticatePromiseIfNeeded(req, res)
173 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
175 // Only the owner or a user that have blacklist rights can see the video
178 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
180 return res.status(403)
181 .json({ error: 'Cannot get this private or blacklisted video.' })
187 // Video is public, anyone can access it
188 if (video.privacy === VideoPrivacy.PUBLIC) return next()
190 // Video is unlisted, check we used the uuid to fetch it
191 if (video.privacy === VideoPrivacy.UNLISTED) {
192 if (isUUIDValid(req.params.id)) return next()
194 // Don't leak this unlisted video
195 return res.status(404).end()
201 const videosGetValidator = videosCustomGetValidator('all')
203 const videosRemoveValidator = [
204 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
206 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
207 logger.debug('Checking videosRemove parameters', { parameters: req.params })
209 if (areValidationErrors(req, res)) return
210 if (!await isVideoExist(req.params.id, res)) return
212 // Check if the user who did the request is able to delete the video
213 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
219 const videosChangeOwnershipValidator = [
220 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
222 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
223 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
225 if (areValidationErrors(req, res)) return
226 if (!await isVideoExist(req.params.videoId, res)) return
228 // Check if the user who did the request is able to change the ownership of the video
229 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
231 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
234 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
238 res.locals.nextOwner = nextOwner
244 const videosTerminateChangeOwnershipValidator = [
245 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
247 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
248 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
250 if (areValidationErrors(req, res)) return
251 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
253 // Check if the user who did the request is able to change the ownership of the video
254 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
258 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
259 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
261 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
265 .json({ error: 'Ownership already accepted or refused' })
272 const videosAcceptChangeOwnershipValidator = [
273 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
274 const body = req.body as VideoChangeOwnershipAccept
275 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
277 const user = res.locals.oauth.token.User
278 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
279 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
280 if (isAble === false) {
282 .json({ error: 'The user video quota is exceeded with this video.' })
291 function getCommonVideoAttributes () {
293 body('thumbnailfile')
294 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
295 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
296 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
299 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
300 'This preview file is not supported or too large. Please, make sure it is of the following type: '
301 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
306 .customSanitizer(toIntOrNull)
307 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
310 .customSanitizer(toIntOrNull)
311 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
314 .customSanitizer(toValueOrNull)
315 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
319 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
320 body('waitTranscoding')
323 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
327 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
330 .customSanitizer(toValueOrNull)
331 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
334 .customSanitizer(toValueOrNull)
335 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
338 .customSanitizer(toValueOrNull)
339 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
340 body('commentsEnabled')
343 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
344 body('originallyPublishedAt')
346 .customSanitizer(toValueOrNull)
347 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
349 body('scheduleUpdate')
351 .customSanitizer(toValueOrNull),
352 body('scheduleUpdate.updateAt')
354 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
355 body('scheduleUpdate.privacy')
358 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
359 ] as (ValidationChain | express.Handler)[]
362 const commonVideosFiltersValidator = [
363 query('categoryOneOf')
365 .customSanitizer(toArray)
366 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
367 query('licenceOneOf')
369 .customSanitizer(toArray)
370 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
371 query('languageOneOf')
373 .customSanitizer(toArray)
374 .custom(isStringArray).withMessage('Should have a valid one of language array'),
377 .customSanitizer(toArray)
378 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
381 .customSanitizer(toArray)
382 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
385 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
388 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
390 (req: express.Request, res: express.Response, next: express.NextFunction) => {
391 logger.debug('Checking commons video filters query', { parameters: req.query })
393 if (areValidationErrors(req, res)) return
395 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
396 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
398 .json({ error: 'You are not allowed to see all local videos.' })
407 // ---------------------------------------------------------------------------
411 videosUpdateValidator,
413 checkVideoFollowConstraints,
414 videosCustomGetValidator,
415 videosRemoveValidator,
417 videosChangeOwnershipValidator,
418 videosTerminateChangeOwnershipValidator,
419 videosAcceptChangeOwnershipValidator,
421 getCommonVideoAttributes,
423 commonVideosFiltersValidator
426 // ---------------------------------------------------------------------------
428 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
429 if (req.body.scheduleUpdate) {
430 if (!req.body.scheduleUpdate.updateAt) {
431 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
434 .json({ error: 'Schedule update at is mandatory.' })