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/constants'
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)
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 // Check if the user who did the request is able to update the video
115 const user = res.locals.oauth.token.User
116 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
118 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
124 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
125 const video = res.locals.video
127 // Anybody can watch local videos
128 if (video.isOwned() === true) return next()
131 if (res.locals.oauth) {
132 // Users can search or watch remote videos
133 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
136 // Anybody can search or watch remote videos
137 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
139 // Check our instance follows an actor that shared this video
140 const serverActor = await getServerActor()
141 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
143 return res.status(403)
145 error: 'Cannot get this video regarding follow constraints.'
149 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
151 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
153 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
154 logger.debug('Checking videosGet parameters', { parameters: req.params })
156 if (areValidationErrors(req, res)) return
157 if (!await doesVideoExist(req.params.id, res, fetchType)) return
159 const video = res.locals.video
161 // Video private or blacklisted
162 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
163 await authenticatePromiseIfNeeded(req, res)
165 const user = res.locals.oauth ? res.locals.oauth.token.User : null
167 // Only the owner or a user that have blacklist rights can see the video
170 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
172 return res.status(403)
173 .json({ error: 'Cannot get this private or blacklisted video.' })
179 // Video is public, anyone can access it
180 if (video.privacy === VideoPrivacy.PUBLIC) return next()
182 // Video is unlisted, check we used the uuid to fetch it
183 if (video.privacy === VideoPrivacy.UNLISTED) {
184 if (isUUIDValid(req.params.id)) return next()
186 // Don't leak this unlisted video
187 return res.status(404).end()
193 const videosGetValidator = videosCustomGetValidator('all')
195 const videosRemoveValidator = [
196 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
198 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
199 logger.debug('Checking videosRemove parameters', { parameters: req.params })
201 if (areValidationErrors(req, res)) return
202 if (!await doesVideoExist(req.params.id, res)) return
204 // Check if the user who did the request is able to delete the video
205 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
211 const videosChangeOwnershipValidator = [
212 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
214 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
215 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
217 if (areValidationErrors(req, res)) return
218 if (!await doesVideoExist(req.params.videoId, res)) return
220 // Check if the user who did the request is able to change the ownership of the video
221 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
223 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
226 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
230 res.locals.nextOwner = nextOwner
236 const videosTerminateChangeOwnershipValidator = [
237 param('id').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 doesChangeVideoOwnershipExist(req.params.id, res)) return
245 // Check if the user who did the request is able to change the ownership of the video
246 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
250 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
251 const videoChangeOwnership = res.locals.videoChangeOwnership
253 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
257 .json({ error: 'Ownership already accepted or refused' })
264 const videosAcceptChangeOwnershipValidator = [
265 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
266 const body = req.body as VideoChangeOwnershipAccept
267 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
269 const user = res.locals.oauth.token.User
270 const videoChangeOwnership = res.locals.videoChangeOwnership
271 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
272 if (isAble === false) {
274 .json({ error: 'The user video quota is exceeded with this video.' })
283 function getCommonVideoEditAttributes () {
285 body('thumbnailfile')
286 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
287 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
288 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
291 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
292 'This preview file is not supported or too large. Please, make sure it is of the following type: '
293 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
298 .customSanitizer(toIntOrNull)
299 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
302 .customSanitizer(toIntOrNull)
303 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
306 .customSanitizer(toValueOrNull)
307 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
311 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
312 body('waitTranscoding')
315 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
319 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
322 .customSanitizer(toValueOrNull)
323 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
326 .customSanitizer(toValueOrNull)
327 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
330 .customSanitizer(toValueOrNull)
331 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
332 body('commentsEnabled')
335 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
336 body('downloadEnabled')
339 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
340 body('originallyPublishedAt')
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
344 body('scheduleUpdate')
346 .customSanitizer(toValueOrNull),
347 body('scheduleUpdate.updateAt')
349 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
350 body('scheduleUpdate.privacy')
353 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
354 ] as (ValidationChain | express.Handler)[]
357 const commonVideosFiltersValidator = [
358 query('categoryOneOf')
360 .customSanitizer(toArray)
361 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
362 query('licenceOneOf')
364 .customSanitizer(toArray)
365 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
366 query('languageOneOf')
368 .customSanitizer(toArray)
369 .custom(isStringArray).withMessage('Should have a valid one of language array'),
372 .customSanitizer(toArray)
373 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
380 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
383 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
385 (req: express.Request, res: express.Response, next: express.NextFunction) => {
386 logger.debug('Checking commons video filters query', { parameters: req.query })
388 if (areValidationErrors(req, res)) return
390 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
391 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
393 .json({ error: 'You are not allowed to see all local videos.' })
402 // ---------------------------------------------------------------------------
406 videosUpdateValidator,
408 checkVideoFollowConstraints,
409 videosCustomGetValidator,
410 videosRemoveValidator,
412 videosChangeOwnershipValidator,
413 videosTerminateChangeOwnershipValidator,
414 videosAcceptChangeOwnershipValidator,
416 getCommonVideoEditAttributes,
418 commonVideosFiltersValidator
421 // ---------------------------------------------------------------------------
423 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
424 if (req.body.scheduleUpdate) {
425 if (!req.body.scheduleUpdate.updateAt) {
426 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
429 .json({ error: 'Schedule update at is mandatory.' })