1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query } from 'express-validator/check'
4 import { UserRight, VideoPrivacy } from '../../../shared'
5 import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, toStringOrNull } from '../../helpers/custom-validators/misc'
7 isVideoAbuseReasonValid,
9 isVideoChannelOfAccountExist,
10 isVideoDescriptionValid,
18 isVideoRatingTypeValid,
21 } from '../../helpers/custom-validators/videos'
22 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
23 import { logger } from '../../helpers/logger'
24 import { CONSTRAINTS_FIELDS } from '../../initializers'
25 import { UserModel } from '../../models/account/user'
26 import { VideoModel } from '../../models/video/video'
27 import { VideoShareModel } from '../../models/video/video-share'
28 import { authenticate } from '../oauth'
29 import { areValidationErrors } from './utils'
31 const videosAddValidator = [
32 body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
33 'This file is not supported. Please, make sure it is of the following type : '
34 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
36 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
37 'This thumbnail file is not supported. Please, make sure it is of the following type : '
38 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
40 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
41 'This preview file is not supported. Please, make sure it is of the following type : '
42 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
44 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
47 .customSanitizer(toIntOrNull)
48 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
51 .customSanitizer(toIntOrNull)
52 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
55 .customSanitizer(toStringOrNull)
56 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
59 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
62 .customSanitizer(toStringOrNull)
63 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
66 .customSanitizer(toStringOrNull)
67 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
70 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
71 body('commentsEnabled')
73 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
77 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
81 .withMessage('Should have correct video channel id'),
83 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
84 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
86 if (areValidationErrors(req, res)) return
87 if (areErrorsInVideoImageFiles(req, res)) return
89 const videoFile: Express.Multer.File = req.files['videofile'][0]
90 const user = res.locals.oauth.token.User
92 if (!await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
94 const isAble = await user.isAbleToUploadVideo(videoFile)
95 if (isAble === false) {
97 .json({ error: 'The user video quota is exceeded with this video.' })
106 duration = await getDurationFromVideoFile(videoFile.path)
108 logger.error('Invalid input file in videosAddValidator.', { err })
110 .json({ error: 'Invalid input file.' })
116 videoFile['duration'] = duration
122 const videosUpdateValidator = [
123 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
124 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
125 'This thumbnail file is not supported. Please, make sure it is of the following type : '
126 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
128 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
129 'This preview file is not supported. Please, make sure it is of the following type : '
130 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
134 .custom(isVideoNameValid).withMessage('Should have a valid name'),
137 .customSanitizer(toIntOrNull)
138 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
141 .customSanitizer(toIntOrNull)
142 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
145 .customSanitizer(toStringOrNull)
146 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
150 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
154 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
157 .customSanitizer(toStringOrNull)
158 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
161 .customSanitizer(toStringOrNull)
162 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
165 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
166 body('commentsEnabled')
169 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
173 .custom(isIdValid).withMessage('Should have correct video channel id'),
175 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
176 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
178 if (areValidationErrors(req, res)) return
179 if (areErrorsInVideoImageFiles(req, res)) return
180 if (!await isVideoExist(req.params.id, res)) return
182 const video = res.locals.video
184 // Check if the user who did the request is able to update the video
185 const user = res.locals.oauth.token.User
186 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
188 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
189 return res.status(409)
190 .json({ error: 'Cannot set "private" a video that was not private anymore.' })
194 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
200 const videosGetValidator = [
201 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
203 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
204 logger.debug('Checking videosGet parameters', { parameters: req.params })
206 if (areValidationErrors(req, res)) return
207 if (!await isVideoExist(req.params.id, res)) return
209 const video = res.locals.video
211 // Video is public, anyone can access it
212 if (video.privacy === VideoPrivacy.PUBLIC) return next()
214 // Video is unlisted, check we used the uuid to fetch it
215 if (video.privacy === VideoPrivacy.UNLISTED) {
216 if (isUUIDValid(req.params.id)) return next()
218 // Don't leak this unlisted video
219 return res.status(404).end()
222 // Video is private, check the user
223 authenticate(req, res, () => {
224 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
225 return res.status(403)
226 .json({ error: 'Cannot get this private video of another user' })
235 const videosRemoveValidator = [
236 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
238 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
239 logger.debug('Checking videosRemove parameters', { parameters: req.params })
241 if (areValidationErrors(req, res)) return
242 if (!await isVideoExist(req.params.id, res)) return
244 // Check if the user who did the request is able to delete the video
245 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
251 const videosSearchValidator = [
252 query('search').not().isEmpty().withMessage('Should have a valid search'),
254 (req: express.Request, res: express.Response, next: express.NextFunction) => {
255 logger.debug('Checking videosSearch parameters', { parameters: req.params })
257 if (areValidationErrors(req, res)) return
263 const videoAbuseReportValidator = [
264 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
265 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
270 if (areValidationErrors(req, res)) return
271 if (!await isVideoExist(req.params.id, res)) return
277 const videoRateValidator = [
278 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
279 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
281 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
282 logger.debug('Checking videoRate parameters', { parameters: req.body })
284 if (areValidationErrors(req, res)) return
285 if (!await isVideoExist(req.params.id, res)) return
291 const videosShareValidator = [
292 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
293 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
295 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
296 logger.debug('Checking videoShare parameters', { parameters: req.params })
298 if (areValidationErrors(req, res)) return
299 if (!await isVideoExist(req.params.id, res)) return
301 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
303 return res.status(404)
307 res.locals.videoShare = share
312 // ---------------------------------------------------------------------------
316 videosUpdateValidator,
318 videosRemoveValidator,
319 videosSearchValidator,
320 videosShareValidator,
322 videoAbuseReportValidator,
327 // ---------------------------------------------------------------------------
329 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
330 // Retrieve the user who did the request
331 if (video.isOwned() === false) {
333 .json({ error: 'Cannot remove video of another server, blacklist it' })
338 // Check if the user can delete the video
339 // The user can delete it if he has the right
340 // Or if s/he is the video's account
341 const account = video.VideoChannel.Account
342 if (user.hasRight(right) === false && account.userId !== user.id) {
344 .json({ error: 'Cannot remove video of another user' })
352 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
353 // Files are optional
354 if (!req.files) return false
356 for (const imageField of [ 'thumbnail', 'preview' ]) {
357 if (!req.files[ imageField ]) continue
359 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
360 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
362 .send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })