1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, ValidationChain } from 'express-validator/check'
4 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared'
13 } from '../../helpers/custom-validators/misc'
15 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid,
18 isVideoChannelOfAccountExist,
19 isVideoDescriptionValid,
27 isVideoRatingTypeValid,
30 } from '../../helpers/custom-validators/videos'
31 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
32 import { logger } from '../../helpers/logger'
33 import { CONSTRAINTS_FIELDS } from '../../initializers'
34 import { VideoShareModel } from '../../models/video/video-share'
35 import { authenticate } from '../oauth'
36 import { areValidationErrors } from './utils'
37 import { cleanUpReqFiles } from '../../helpers/express-utils'
38 import { VideoModel } from '../../models/video/video'
39 import { UserModel } from '../../models/account/user'
40 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership'
41 import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model'
42 import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership'
43 import { AccountModel } from '../../models/account/account'
44 import { VideoFetchType } from '../../helpers/video'
46 const videosAddValidator = getCommonVideoAttributes().concat([
48 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
49 'This file is not supported or too large. Please, make sure it is of the following type: '
50 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
52 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
55 .custom(isIdValid).withMessage('Should have correct video channel id'),
57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
58 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
60 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
61 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
63 const videoFile: Express.Multer.File = req.files['videofile'][0]
64 const user = res.locals.oauth.token.User
66 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
68 const isAble = await user.isAbleToUploadVideo(videoFile)
69 if (isAble === false) {
71 .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.' })
87 return cleanUpReqFiles(req)
90 videoFile['duration'] = duration
96 const videosUpdateValidator = getCommonVideoAttributes().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 isVideoExist(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.' })
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
132 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
134 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
136 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
137 logger.debug('Checking videosGet parameters', { parameters: req.params })
139 if (areValidationErrors(req, res)) return
140 if (!await isVideoExist(req.params.id, res, fetchType)) return
142 const video: VideoModel = res.locals.video
144 // Video private or blacklisted
145 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
146 return authenticate(req, res, () => {
147 const user: UserModel = res.locals.oauth.token.User
149 // Only the owner or a user that have blacklist rights can see the video
150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
151 return res.status(403)
152 .json({ error: 'Cannot get this private or blacklisted video.' })
160 // Video is public, anyone can access it
161 if (video.privacy === VideoPrivacy.PUBLIC) return next()
163 // Video is unlisted, check we used the uuid to fetch it
164 if (video.privacy === VideoPrivacy.UNLISTED) {
165 if (isUUIDValid(req.params.id)) return next()
167 // Don't leak this unlisted video
168 return res.status(404).end()
174 const videosGetValidator = videosCustomGetValidator('all')
176 const videosRemoveValidator = [
177 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
179 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
180 logger.debug('Checking videosRemove parameters', { parameters: req.params })
182 if (areValidationErrors(req, res)) return
183 if (!await isVideoExist(req.params.id, res)) return
185 // Check if the user who did the request is able to delete the video
186 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
192 const videoRateValidator = [
193 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
194 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
196 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
197 logger.debug('Checking videoRate parameters', { parameters: req.body })
199 if (areValidationErrors(req, res)) return
200 if (!await isVideoExist(req.params.id, res)) return
206 const videosShareValidator = [
207 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
208 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videoShare parameters', { parameters: req.params })
213 if (areValidationErrors(req, res)) return
214 if (!await isVideoExist(req.params.id, res)) return
216 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
218 return res.status(404)
222 res.locals.videoShare = share
227 const videosChangeOwnershipValidator = [
228 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
230 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
231 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
233 if (areValidationErrors(req, res)) return
234 if (!await isVideoExist(req.params.videoId, res)) return
236 // Check if the user who did the request is able to change the ownership of the video
237 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
239 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
246 res.locals.nextOwner = nextOwner
252 const videosTerminateChangeOwnershipValidator = [
253 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
255 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
256 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
258 if (areValidationErrors(req, res)) return
259 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
261 // Check if the user who did the request is able to change the ownership of the video
262 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
269 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
273 .json({ error: 'Ownership already accepted or refused' })
280 const videosAcceptChangeOwnershipValidator = [
281 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
282 const body = req.body as VideoChangeOwnershipAccept
283 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
285 const user = res.locals.oauth.token.User
286 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
287 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
288 if (isAble === false) {
290 .json({ error: 'The user video quota is exceeded with this video.' })
299 function getCommonVideoAttributes () {
301 body('thumbnailfile')
302 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
303 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
304 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
307 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
308 'This preview file is not supported or too large. Please, make sure it is of the following type: '
309 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
314 .customSanitizer(toIntOrNull)
315 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
318 .customSanitizer(toIntOrNull)
319 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
322 .customSanitizer(toValueOrNull)
323 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
327 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
328 body('waitTranscoding')
331 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
335 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
338 .customSanitizer(toValueOrNull)
339 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
346 .customSanitizer(toValueOrNull)
347 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
348 body('commentsEnabled')
351 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
353 body('scheduleUpdate')
355 .customSanitizer(toValueOrNull),
356 body('scheduleUpdate.updateAt')
358 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
359 body('scheduleUpdate.privacy')
362 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
363 ] as (ValidationChain | express.Handler)[]
366 // ---------------------------------------------------------------------------
370 videosUpdateValidator,
372 videosCustomGetValidator,
373 videosRemoveValidator,
374 videosShareValidator,
378 videosChangeOwnershipValidator,
379 videosTerminateChangeOwnershipValidator,
380 videosAcceptChangeOwnershipValidator,
382 getCommonVideoAttributes
385 // ---------------------------------------------------------------------------
387 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
388 if (req.body.scheduleUpdate) {
389 if (!req.body.scheduleUpdate.updateAt) {
391 .json({ error: 'Schedule update at is mandatory.' })