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 = [
42 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
43 'This file is not supported or too large. Please, make sure it is of the following type : '
44 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
47 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
48 'This thumbnail file is not supported or too large. Please, make sure it is of the following type : '
49 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
52 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
53 'This preview file is not supported or too large. Please, make sure it is of the following type : '
54 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
56 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
59 .customSanitizer(toIntOrNull)
60 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
63 .customSanitizer(toIntOrNull)
64 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
67 .customSanitizer(toValueOrNull)
68 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
72 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
73 body('waitTranscoding')
76 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
79 .customSanitizer(toValueOrNull)
80 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
83 .customSanitizer(toValueOrNull)
84 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
87 .customSanitizer(toValueOrNull)
88 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
89 body('commentsEnabled')
92 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
96 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
99 .custom(isIdValid).withMessage('Should have correct video channel id'),
100 body('scheduleUpdate')
102 .customSanitizer(toValueOrNull),
103 body('scheduleUpdate.updateAt')
105 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
106 body('scheduleUpdate.privacy')
109 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
111 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
112 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
114 if (areValidationErrors(req, res)) return
115 if (areErrorsInVideoImageFiles(req, res)) return
116 if (areErrorsInScheduleUpdate(req, res)) return
118 const videoFile: Express.Multer.File = req.files['videofile'][0]
119 const user = res.locals.oauth.token.User
121 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
123 const isAble = await user.isAbleToUploadVideo(videoFile)
124 if (isAble === false) {
126 .json({ error: 'The user video quota is exceeded with this video.' })
135 duration = await getDurationFromVideoFile(videoFile.path)
137 logger.error('Invalid input file in videosAddValidator.', { err })
139 .json({ error: 'Invalid input file.' })
145 videoFile['duration'] = duration
151 const videosUpdateValidator = [
152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
153 body('thumbnailfile')
154 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
155 'This thumbnail file is not supported or too large. Please, make sure it is of the following type : '
156 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
159 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
160 'This preview file is not supported or too large. Please, make sure it is of the following type : '
161 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
165 .custom(isVideoNameValid).withMessage('Should have a valid name'),
168 .customSanitizer(toIntOrNull)
169 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
172 .customSanitizer(toIntOrNull)
173 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
176 .customSanitizer(toValueOrNull)
177 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
181 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
182 body('waitTranscoding')
185 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
189 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
192 .customSanitizer(toValueOrNull)
193 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
196 .customSanitizer(toValueOrNull)
197 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
200 .customSanitizer(toValueOrNull)
201 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
202 body('commentsEnabled')
205 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
209 .custom(isIdValid).withMessage('Should have correct video channel id'),
210 body('scheduleUpdate')
212 .customSanitizer(toValueOrNull),
213 body('scheduleUpdate.updateAt')
215 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
216 body('scheduleUpdate.privacy')
219 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
221 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
222 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
224 if (areValidationErrors(req, res)) return
225 if (areErrorsInVideoImageFiles(req, res)) return
226 if (areErrorsInScheduleUpdate(req, res)) return
227 if (!await isVideoExist(req.params.id, res)) return
229 const video = res.locals.video
231 // Check if the user who did the request is able to update the video
232 const user = res.locals.oauth.token.User
233 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
235 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
236 return res.status(409)
237 .json({ error: 'Cannot set "private" a video that was not private.' })
241 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
247 const videosGetValidator = [
248 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
250 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
251 logger.debug('Checking videosGet parameters', { parameters: req.params })
253 if (areValidationErrors(req, res)) return
254 if (!await isVideoExist(req.params.id, res)) return
256 const video = res.locals.video
258 // Video is public, anyone can access it
259 if (video.privacy === VideoPrivacy.PUBLIC) return next()
261 // Video is unlisted, check we used the uuid to fetch it
262 if (video.privacy === VideoPrivacy.UNLISTED) {
263 if (isUUIDValid(req.params.id)) return next()
265 // Don't leak this unlisted video
266 return res.status(404).end()
269 // Video is private, check the user
270 authenticate(req, res, () => {
271 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
272 return res.status(403)
273 .json({ error: 'Cannot get this private video of another user' })
282 const videosRemoveValidator = [
283 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
285 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
286 logger.debug('Checking videosRemove parameters', { parameters: req.params })
288 if (areValidationErrors(req, res)) return
289 if (!await isVideoExist(req.params.id, res)) return
291 // Check if the user who did the request is able to delete the video
292 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
298 const videosSearchValidator = [
299 query('search').not().isEmpty().withMessage('Should have a valid search'),
301 (req: express.Request, res: express.Response, next: express.NextFunction) => {
302 logger.debug('Checking videosSearch parameters', { parameters: req.params })
304 if (areValidationErrors(req, res)) return
310 const videoAbuseReportValidator = [
311 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
312 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
314 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
315 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
317 if (areValidationErrors(req, res)) return
318 if (!await isVideoExist(req.params.id, res)) return
324 const videoRateValidator = [
325 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
326 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
328 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
329 logger.debug('Checking videoRate parameters', { parameters: req.body })
331 if (areValidationErrors(req, res)) return
332 if (!await isVideoExist(req.params.id, res)) return
338 const videosShareValidator = [
339 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
340 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
342 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
343 logger.debug('Checking videoShare parameters', { parameters: req.params })
345 if (areValidationErrors(req, res)) return
346 if (!await isVideoExist(req.params.id, res)) return
348 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
350 return res.status(404)
354 res.locals.videoShare = share
359 // ---------------------------------------------------------------------------
363 videosUpdateValidator,
365 videosRemoveValidator,
366 videosSearchValidator,
367 videosShareValidator,
369 videoAbuseReportValidator,
374 // ---------------------------------------------------------------------------
376 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
377 // Retrieve the user who did the request
378 if (video.isOwned() === false) {
380 .json({ error: 'Cannot manage a video of another server.' })
385 // Check if the user can delete the video
386 // The user can delete it if he has the right
387 // Or if s/he is the video's account
388 const account = video.VideoChannel.Account
389 if (user.hasRight(right) === false && account.userId !== user.id) {
391 .json({ error: 'Cannot manage a video of another user.' })
399 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
400 // Files are optional
401 if (!req.files) return false
403 for (const imageField of [ 'thumbnail', 'preview' ]) {
404 if (!req.files[ imageField ]) continue
406 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
407 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
409 .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
418 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
419 if (req.body.scheduleUpdate) {
420 if (!req.body.scheduleUpdate.updateAt) {
422 .json({ error: 'Schedule update at is mandatory.' })