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 { 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 { 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'
47 const videosAddValidator = getCommonVideoEditAttributes().concat([
49 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
50 'This file is not supported or too large. Please, make sure it is of the following type: '
51 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
53 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
56 .custom(isIdValid).withMessage('Should have correct video channel id'),
58 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
61 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
62 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
64 const videoFile: Express.Multer.File = req.files['videofile'][0]
65 const user = res.locals.oauth.token.User
67 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
69 const isAble = await user.isAbleToUploadVideo(videoFile)
70 if (isAble === false) {
72 .json({ error: 'The user video quota is exceeded with this video.' })
74 return cleanUpReqFiles(req)
80 duration = await getDurationFromVideoFile(videoFile.path)
82 logger.error('Invalid input file in videosAddValidator.', { err })
84 .json({ error: 'Invalid input file.' })
86 return cleanUpReqFiles(req)
89 videoFile['duration'] = duration
95 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
96 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
99 .custom(isVideoNameValid).withMessage('Should have a valid name'),
103 .custom(isIdValid).withMessage('Should have correct video channel id'),
105 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
106 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
108 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
109 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
110 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
112 const video = res.locals.video
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 (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
120 return res.status(409)
121 .json({ error: 'Cannot set "private" a video that was not private.' })
124 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
130 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
131 const video = res.locals.video
133 // Anybody can watch local videos
134 if (video.isOwned() === true) return next()
137 if (res.locals.oauth) {
138 // Users can search or watch remote videos
139 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
142 // Anybody can search or watch remote videos
143 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
145 // Check our instance follows an actor that shared this video
146 const serverActor = await getServerActor()
147 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
149 return res.status(403)
151 error: 'Cannot get this video regarding follow constraints.'
155 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
157 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
159 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
160 logger.debug('Checking videosGet parameters', { parameters: req.params })
162 if (areValidationErrors(req, res)) return
163 if (!await doesVideoExist(req.params.id, res, fetchType)) return
165 const video = res.locals.video
167 // Video private or blacklisted
168 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
169 await authenticatePromiseIfNeeded(req, res)
171 const user = res.locals.oauth ? res.locals.oauth.token.User : null
173 // Only the owner or a user that have blacklist rights can see the video
176 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
178 return res.status(403)
179 .json({ error: 'Cannot get this private or blacklisted video.' })
185 // Video is public, anyone can access it
186 if (video.privacy === VideoPrivacy.PUBLIC) return next()
188 // Video is unlisted, check we used the uuid to fetch it
189 if (video.privacy === VideoPrivacy.UNLISTED) {
190 if (isUUIDValid(req.params.id)) return next()
192 // Don't leak this unlisted video
193 return res.status(404).end()
199 const videosGetValidator = videosCustomGetValidator('all')
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.video, 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.video, 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
256 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
257 const videoChangeOwnership = res.locals.videoChangeOwnership
259 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
263 .json({ error: 'Ownership already accepted or refused' })
270 const videosAcceptChangeOwnershipValidator = [
271 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
272 const body = req.body as VideoChangeOwnershipAccept
273 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
275 const user = res.locals.oauth.token.User
276 const videoChangeOwnership = res.locals.videoChangeOwnership
277 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
278 if (isAble === false) {
280 .json({ error: 'The user video quota is exceeded with this video.' })
289 function getCommonVideoEditAttributes () {
291 body('thumbnailfile')
292 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
293 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
294 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
297 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
298 'This preview file is not supported or too large. Please, make sure it is of the following type: '
299 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
304 .customSanitizer(toIntOrNull)
305 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
308 .customSanitizer(toIntOrNull)
309 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
312 .customSanitizer(toValueOrNull)
313 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
317 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
318 body('waitTranscoding')
321 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
325 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
328 .customSanitizer(toValueOrNull)
329 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
332 .customSanitizer(toValueOrNull)
333 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
336 .customSanitizer(toValueOrNull)
337 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
338 body('commentsEnabled')
341 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
342 body('downloadEnabled')
345 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
346 body('originallyPublishedAt')
348 .customSanitizer(toValueOrNull)
349 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
350 body('scheduleUpdate')
352 .customSanitizer(toValueOrNull),
353 body('scheduleUpdate.updateAt')
355 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
356 body('scheduleUpdate.privacy')
359 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
360 ] as (ValidationChain | express.Handler)[]
363 const commonVideosFiltersValidator = [
364 query('categoryOneOf')
366 .customSanitizer(toArray)
367 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
368 query('licenceOneOf')
370 .customSanitizer(toArray)
371 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
372 query('languageOneOf')
374 .customSanitizer(toArray)
375 .custom(isStringArray).withMessage('Should have a valid one of language array'),
378 .customSanitizer(toArray)
379 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
382 .customSanitizer(toArray)
383 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
386 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
389 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
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 checkVideoFollowConstraints,
415 videosCustomGetValidator,
416 videosRemoveValidator,
418 videosChangeOwnershipValidator,
419 videosTerminateChangeOwnershipValidator,
420 videosAcceptChangeOwnershipValidator,
422 getCommonVideoEditAttributes,
424 commonVideosFiltersValidator
427 // ---------------------------------------------------------------------------
429 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
430 if (req.body.scheduleUpdate) {
431 if (!req.body.scheduleUpdate.updateAt) {
432 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
435 .json({ error: 'Schedule update at is mandatory.' })