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'
13 } from '../../helpers/custom-validators/misc'
15 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid,
17 isVideoAbuseReasonValid,
19 isVideoChannelOfAccountExist,
20 isVideoDescriptionValid,
28 isVideoRatingTypeValid,
31 } from '../../helpers/custom-validators/videos'
32 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
33 import { logger } from '../../helpers/logger'
34 import { CONSTRAINTS_FIELDS } from '../../initializers'
35 import { VideoShareModel } from '../../models/video/video-share'
36 import { authenticate } from '../oauth'
37 import { areValidationErrors } from './utils'
39 const videosAddValidator = [
41 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
42 'This file is not supported or too large. Please, make sure it is of the following type: '
43 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
46 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
47 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
48 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
51 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
52 'This preview file is not supported or too large. Please, make sure it is of the following type: '
53 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
58 .customSanitizer(toIntOrNull)
59 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
62 .customSanitizer(toIntOrNull)
63 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
66 .customSanitizer(toValueOrNull)
67 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
71 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
72 body('waitTranscoding')
75 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
78 .customSanitizer(toValueOrNull)
79 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
82 .customSanitizer(toValueOrNull)
83 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
86 .customSanitizer(toValueOrNull)
87 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
88 body('commentsEnabled')
91 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
95 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
98 .custom(isIdValid).withMessage('Should have correct video channel id'),
99 body('scheduleUpdate')
101 .customSanitizer(toValueOrNull),
102 body('scheduleUpdate.updateAt')
104 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
105 body('scheduleUpdate.privacy')
108 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
110 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
113 if (areValidationErrors(req, res)) return
114 if (areErrorsInVideoImageFiles(req, res)) return
115 if (areErrorsInScheduleUpdate(req, res)) return
117 const videoFile: Express.Multer.File = req.files['videofile'][0]
118 const user = res.locals.oauth.token.User
120 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
122 const isAble = await user.isAbleToUploadVideo(videoFile)
123 if (isAble === false) {
125 .json({ error: 'The user video quota is exceeded with this video.' })
134 duration = await getDurationFromVideoFile(videoFile.path)
136 logger.error('Invalid input file in videosAddValidator.', { err })
138 .json({ error: 'Invalid input file.' })
144 videoFile['duration'] = duration
150 const videosUpdateValidator = [
151 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
152 body('thumbnailfile')
153 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
154 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
155 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
158 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
159 'This preview file is not supported or too large. Please, make sure it is of the following type: '
160 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
164 .custom(isVideoNameValid).withMessage('Should have a valid name'),
167 .customSanitizer(toIntOrNull)
168 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
171 .customSanitizer(toIntOrNull)
172 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
175 .customSanitizer(toValueOrNull)
176 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
180 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
181 body('waitTranscoding')
184 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
188 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
191 .customSanitizer(toValueOrNull)
192 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
195 .customSanitizer(toValueOrNull)
196 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
199 .customSanitizer(toValueOrNull)
200 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
201 body('commentsEnabled')
204 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
208 .custom(isIdValid).withMessage('Should have correct video channel id'),
209 body('scheduleUpdate')
211 .customSanitizer(toValueOrNull),
212 body('scheduleUpdate.updateAt')
214 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
215 body('scheduleUpdate.privacy')
218 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
220 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
221 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
223 if (areValidationErrors(req, res)) return
224 if (areErrorsInVideoImageFiles(req, res)) return
225 if (areErrorsInScheduleUpdate(req, res)) return
226 if (!await isVideoExist(req.params.id, res)) return
228 const video = res.locals.video
230 // Check if the user who did the request is able to update the video
231 const user = res.locals.oauth.token.User
232 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
234 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
235 return res.status(409)
236 .json({ error: 'Cannot set "private" a video that was not private.' })
240 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
246 const videosGetValidator = [
247 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
249 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
250 logger.debug('Checking videosGet parameters', { parameters: req.params })
252 if (areValidationErrors(req, res)) return
253 if (!await isVideoExist(req.params.id, res)) return
255 const video = res.locals.video
257 // Video is public, anyone can access it
258 if (video.privacy === VideoPrivacy.PUBLIC) return next()
260 // Video is unlisted, check we used the uuid to fetch it
261 if (video.privacy === VideoPrivacy.UNLISTED) {
262 if (isUUIDValid(req.params.id)) return next()
264 // Don't leak this unlisted video
265 return res.status(404).end()
268 // Video is private, check the user
269 authenticate(req, res, () => {
270 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
271 return res.status(403)
272 .json({ error: 'Cannot get this private video of another user' })
281 const videosRemoveValidator = [
282 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
284 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
285 logger.debug('Checking videosRemove parameters', { parameters: req.params })
287 if (areValidationErrors(req, res)) return
288 if (!await isVideoExist(req.params.id, res)) return
290 // Check if the user who did the request is able to delete the video
291 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
297 const videosSearchValidator = [
298 query('search').not().isEmpty().withMessage('Should have a valid search'),
300 (req: express.Request, res: express.Response, next: express.NextFunction) => {
301 logger.debug('Checking videosSearch parameters', { parameters: req.params })
303 if (areValidationErrors(req, res)) return
309 const videoAbuseReportValidator = [
310 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
311 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
313 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
314 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
316 if (areValidationErrors(req, res)) return
317 if (!await isVideoExist(req.params.id, res)) return
323 const videoRateValidator = [
324 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
325 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
327 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
328 logger.debug('Checking videoRate parameters', { parameters: req.body })
330 if (areValidationErrors(req, res)) return
331 if (!await isVideoExist(req.params.id, res)) return
337 const videosShareValidator = [
338 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
339 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
341 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
342 logger.debug('Checking videoShare parameters', { parameters: req.params })
344 if (areValidationErrors(req, res)) return
345 if (!await isVideoExist(req.params.id, res)) return
347 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
349 return res.status(404)
353 res.locals.videoShare = share
358 // ---------------------------------------------------------------------------
362 videosUpdateValidator,
364 videosRemoveValidator,
365 videosSearchValidator,
366 videosShareValidator,
368 videoAbuseReportValidator,
373 // ---------------------------------------------------------------------------
375 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
376 // Files are optional
377 if (!req.files) return false
379 for (const imageField of [ 'thumbnail', 'preview' ]) {
380 if (!req.files[ imageField ]) continue
382 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
383 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
385 .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
394 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
395 if (req.body.scheduleUpdate) {
396 if (!req.body.scheduleUpdate.updateAt) {
398 .json({ error: 'Schedule update at is mandatory.' })