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 doesVideoChannelOfAccountExist,
19 isScheduleVideoUpdatePrivacyValid,
21 isVideoDescriptionValid,
28 isVideoOriginallyPublishedAtValid,
32 } from '../../../helpers/custom-validators/videos'
33 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34 import { logger } from '../../../helpers/logger'
35 import { 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 { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42 import { AccountModel } from '../../../models/account/account'
43 import { VideoFetchType } from '../../../helpers/video'
44 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
45 import { getServerActor } from '../../../helpers/utils'
46 import { CONFIG } from '../../../initializers/config'
48 const videosAddValidator = getCommonVideoEditAttributes().concat([
50 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
51 'This file is not supported or too large. Please, make sure it is of the following type: '
52 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
54 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
57 .custom(isIdValid).withMessage('Should have correct video channel id'),
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
62 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
63 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
65 const videoFile: Express.Multer.File = req.files['videofile'][0]
66 const user = res.locals.oauth.token.User
68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
70 const isAble = await user.isAbleToUploadVideo(videoFile)
71 if (isAble === 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
96 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
97 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
100 .custom(isVideoNameValid).withMessage('Should have a valid name'),
104 .custom(isIdValid).withMessage('Should have correct video channel id'),
106 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
107 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
109 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
110 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
111 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
113 const video = res.locals.video
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.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
119 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
121 return res.status(409)
122 .json({ error: 'Cannot set "private" a video that was not private.' })
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 = res.locals.video
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 error: 'Cannot get this video regarding follow constraints.'
156 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
158 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
160 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
161 logger.debug('Checking videosGet parameters', { parameters: req.params })
163 if (areValidationErrors(req, res)) return
164 if (!await doesVideoExist(req.params.id, res, fetchType)) return
166 const video = res.locals.video
168 // Video private or blacklisted
169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
170 await authenticatePromiseIfNeeded(req, res)
172 const user = res.locals.oauth ? res.locals.oauth.token.User : null
174 // Only the owner or a user that have blacklist rights can see the video
177 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
179 return res.status(403)
180 .json({ error: 'Cannot get this private or blacklisted video.' })
186 // Video is public, anyone can access it
187 if (video.privacy === VideoPrivacy.PUBLIC) return next()
189 // Video is unlisted, check we used the uuid to fetch it
190 if (video.privacy === VideoPrivacy.UNLISTED) {
191 if (isUUIDValid(req.params.id)) return next()
193 // Don't leak this unlisted video
194 return res.status(404).end()
200 const videosGetValidator = videosCustomGetValidator('all')
202 const videosRemoveValidator = [
203 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
206 logger.debug('Checking videosRemove parameters', { parameters: req.params })
208 if (areValidationErrors(req, res)) return
209 if (!await doesVideoExist(req.params.id, res)) return
211 // Check if the user who did the request is able to delete the video
212 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
218 const videosChangeOwnershipValidator = [
219 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
221 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
222 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
224 if (areValidationErrors(req, res)) return
225 if (!await doesVideoExist(req.params.videoId, res)) return
227 // Check if the user who did the request is able to change the ownership of the video
228 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
230 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
233 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
237 res.locals.nextOwner = nextOwner
243 const videosTerminateChangeOwnershipValidator = [
244 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
246 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
247 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
249 if (areValidationErrors(req, res)) return
250 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
252 // Check if the user who did the request is able to change the ownership of the video
253 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
257 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
258 const videoChangeOwnership = res.locals.videoChangeOwnership
260 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
264 .json({ error: 'Ownership already accepted or refused' })
271 const videosAcceptChangeOwnershipValidator = [
272 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
273 const body = req.body as VideoChangeOwnershipAccept
274 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
276 const user = res.locals.oauth.token.User
277 const videoChangeOwnership = res.locals.videoChangeOwnership
278 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
279 if (isAble === false) {
281 .json({ error: 'The user video quota is exceeded with this video.' })
290 function getCommonVideoEditAttributes () {
292 body('thumbnailfile')
293 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
294 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
295 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
298 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
299 'This preview file is not supported or too large. Please, make sure it is of the following type: '
300 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
305 .customSanitizer(toIntOrNull)
306 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
309 .customSanitizer(toIntOrNull)
310 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
313 .customSanitizer(toValueOrNull)
314 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
318 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
319 body('waitTranscoding')
322 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
326 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
329 .customSanitizer(toValueOrNull)
330 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
333 .customSanitizer(toValueOrNull)
334 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
337 .customSanitizer(toValueOrNull)
338 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
339 body('commentsEnabled')
342 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
343 body('downloadEnabled')
346 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
347 body('originallyPublishedAt')
349 .customSanitizer(toValueOrNull)
350 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
351 body('scheduleUpdate')
353 .customSanitizer(toValueOrNull),
354 body('scheduleUpdate.updateAt')
356 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
357 body('scheduleUpdate.privacy')
360 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
361 ] as (ValidationChain | express.Handler)[]
364 const commonVideosFiltersValidator = [
365 query('categoryOneOf')
367 .customSanitizer(toArray)
368 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
369 query('licenceOneOf')
371 .customSanitizer(toArray)
372 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
373 query('languageOneOf')
375 .customSanitizer(toArray)
376 .custom(isStringArray).withMessage('Should have a valid one of language array'),
379 .customSanitizer(toArray)
380 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
383 .customSanitizer(toArray)
384 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
387 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
390 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
392 (req: express.Request, res: express.Response, next: express.NextFunction) => {
393 logger.debug('Checking commons video filters query', { parameters: req.query })
395 if (areValidationErrors(req, res)) return
397 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
398 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
400 .json({ error: 'You are not allowed to see all local videos.' })
409 // ---------------------------------------------------------------------------
413 videosUpdateValidator,
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.' })