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 isScheduleVideoUpdatePrivacyValid,
16 isVideoAbuseReasonValid,
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 { UserModel } from '../../models/account/user'
35 import { VideoModel } from '../../models/video/video'
36 import { VideoShareModel } from '../../models/video/video-share'
37 import { authenticate } from '../oauth'
38 import { areValidationErrors } from './utils'
40 const videosAddValidator = [
41 body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
42 'This file is not supported. Please, make sure it is of the following type : '
43 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
45 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
46 'This thumbnail file is not supported. Please, make sure it is of the following type : '
47 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
49 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
50 'This preview file is not supported. Please, make sure it is of the following type : '
51 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
53 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
56 .customSanitizer(toIntOrNull)
57 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
60 .customSanitizer(toIntOrNull)
61 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
64 .customSanitizer(toValueOrNull)
65 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
69 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
70 body('waitTranscoding')
73 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
76 .customSanitizer(toValueOrNull)
77 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
80 .customSanitizer(toValueOrNull)
81 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
84 .customSanitizer(toValueOrNull)
85 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
86 body('commentsEnabled')
89 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
93 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
96 .custom(isIdValid).withMessage('Should have correct video channel id'),
97 body('scheduleUpdate.updateAt')
99 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
100 body('scheduleUpdate.privacy')
103 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
105 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
106 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
108 if (areValidationErrors(req, res)) return
109 if (areErrorsInVideoImageFiles(req, res)) return
110 if (areErrorsInScheduleUpdate(req, res)) return
112 const videoFile: Express.Multer.File = req.files['videofile'][0]
113 const user = res.locals.oauth.token.User
115 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
117 const isAble = await user.isAbleToUploadVideo(videoFile)
118 if (isAble === false) {
120 .json({ error: 'The user video quota is exceeded with this video.' })
129 duration = await getDurationFromVideoFile(videoFile.path)
131 logger.error('Invalid input file in videosAddValidator.', { err })
133 .json({ error: 'Invalid input file.' })
139 videoFile['duration'] = duration
145 const videosUpdateValidator = [
146 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
147 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
148 'This thumbnail file is not supported. Please, make sure it is of the following type : '
149 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
151 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
152 'This preview file is not supported. Please, make sure it is of the following type : '
153 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
157 .custom(isVideoNameValid).withMessage('Should have a valid name'),
160 .customSanitizer(toIntOrNull)
161 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
164 .customSanitizer(toIntOrNull)
165 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
168 .customSanitizer(toValueOrNull)
169 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
173 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
174 body('waitTranscoding')
177 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
181 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
184 .customSanitizer(toValueOrNull)
185 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
188 .customSanitizer(toValueOrNull)
189 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
192 .customSanitizer(toValueOrNull)
193 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
194 body('commentsEnabled')
197 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
201 .custom(isIdValid).withMessage('Should have correct video channel id'),
202 body('scheduleUpdate.updateAt')
204 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
205 body('scheduleUpdate.privacy')
208 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
213 if (areValidationErrors(req, res)) return
214 if (areErrorsInVideoImageFiles(req, res)) return
215 if (areErrorsInScheduleUpdate(req, res)) return
216 if (!await isVideoExist(req.params.id, res)) return
218 const video = res.locals.video
220 // Check if the user who did the request is able to update the video
221 const user = res.locals.oauth.token.User
222 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
224 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
225 return res.status(409)
226 .json({ error: 'Cannot set "private" a video that was not private.' })
230 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
236 const videosGetValidator = [
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 videosGet parameters', { parameters: req.params })
242 if (areValidationErrors(req, res)) return
243 if (!await isVideoExist(req.params.id, res)) return
245 const video = res.locals.video
247 // Video is public, anyone can access it
248 if (video.privacy === VideoPrivacy.PUBLIC) return next()
250 // Video is unlisted, check we used the uuid to fetch it
251 if (video.privacy === VideoPrivacy.UNLISTED) {
252 if (isUUIDValid(req.params.id)) return next()
254 // Don't leak this unlisted video
255 return res.status(404).end()
258 // Video is private, check the user
259 authenticate(req, res, () => {
260 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
261 return res.status(403)
262 .json({ error: 'Cannot get this private video of another user' })
271 const videosRemoveValidator = [
272 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
274 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
275 logger.debug('Checking videosRemove parameters', { parameters: req.params })
277 if (areValidationErrors(req, res)) return
278 if (!await isVideoExist(req.params.id, res)) return
280 // Check if the user who did the request is able to delete the video
281 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
287 const videosSearchValidator = [
288 query('search').not().isEmpty().withMessage('Should have a valid search'),
290 (req: express.Request, res: express.Response, next: express.NextFunction) => {
291 logger.debug('Checking videosSearch parameters', { parameters: req.params })
293 if (areValidationErrors(req, res)) return
299 const videoAbuseReportValidator = [
300 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
301 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
303 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
304 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
306 if (areValidationErrors(req, res)) return
307 if (!await isVideoExist(req.params.id, res)) return
313 const videoRateValidator = [
314 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
315 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
317 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
318 logger.debug('Checking videoRate parameters', { parameters: req.body })
320 if (areValidationErrors(req, res)) return
321 if (!await isVideoExist(req.params.id, res)) return
327 const videosShareValidator = [
328 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
329 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
331 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
332 logger.debug('Checking videoShare parameters', { parameters: req.params })
334 if (areValidationErrors(req, res)) return
335 if (!await isVideoExist(req.params.id, res)) return
337 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
339 return res.status(404)
343 res.locals.videoShare = share
348 // ---------------------------------------------------------------------------
352 videosUpdateValidator,
354 videosRemoveValidator,
355 videosSearchValidator,
356 videosShareValidator,
358 videoAbuseReportValidator,
363 // ---------------------------------------------------------------------------
365 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
366 // Retrieve the user who did the request
367 if (video.isOwned() === false) {
369 .json({ error: 'Cannot manage a video of another server.' })
374 // Check if the user can delete the video
375 // The user can delete it if he has the right
376 // Or if s/he is the video's account
377 const account = video.VideoChannel.Account
378 if (user.hasRight(right) === false && account.userId !== user.id) {
380 .json({ error: 'Cannot manage a video of another user.' })
388 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
389 // Files are optional
390 if (!req.files) return false
392 for (const imageField of [ 'thumbnail', 'preview' ]) {
393 if (!req.files[ imageField ]) continue
395 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
396 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
398 .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
407 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
408 if (req.body.scheduleUpdate) {
409 if (!req.body.scheduleUpdate.updateAt) {
411 .json({ error: 'Schedule update at is mandatory.' })