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 doesVideoChannelOfAccountExist,
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 = 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'),
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 doesVideoChannelOfAccountExist(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 = getCommonVideoEditAttributes().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 doesVideoExist(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 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: 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 doesVideoExist(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 doesVideoExist(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 doesVideoExist(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 doesVideoChannelOfAccountExist(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 getCommonVideoEditAttributes () {
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('downloadEnabled')
347 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
348 body('originallyPublishedAt')
350 .customSanitizer(toValueOrNull)
351 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
352 body('scheduleUpdate')
354 .customSanitizer(toValueOrNull),
355 body('scheduleUpdate.updateAt')
357 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
358 body('scheduleUpdate.privacy')
361 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
362 ] as (ValidationChain | express.Handler)[]
365 const commonVideosFiltersValidator = [
366 query('categoryOneOf')
368 .customSanitizer(toArray)
369 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
370 query('licenceOneOf')
372 .customSanitizer(toArray)
373 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
374 query('languageOneOf')
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid one of language array'),
380 .customSanitizer(toArray)
381 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
384 .customSanitizer(toArray)
385 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
388 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
391 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 logger.debug('Checking commons video filters query', { parameters: req.query })
396 if (areValidationErrors(req, res)) return
398 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
399 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
401 .json({ error: 'You are not allowed to see all local videos.' })
410 // ---------------------------------------------------------------------------
414 videosUpdateValidator,
416 checkVideoFollowConstraints,
417 videosCustomGetValidator,
418 videosRemoveValidator,
420 videosChangeOwnershipValidator,
421 videosTerminateChangeOwnershipValidator,
422 videosAcceptChangeOwnershipValidator,
424 getCommonVideoEditAttributes,
426 commonVideosFiltersValidator
429 // ---------------------------------------------------------------------------
431 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
432 if (req.body.scheduleUpdate) {
433 if (!req.body.scheduleUpdate.updateAt) {
434 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
437 .json({ error: 'Schedule update at is mandatory.' })