]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos.ts
899def6fccacacce85e19af1c193b0399a70c0b9
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos.ts
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 {
6 isBooleanValid,
7 isDateValid,
8 isIdOrUUIDValid,
9 isIdValid,
10 isUUIDValid,
11 toIntOrNull,
12 toValueOrNull
13 } from '../../helpers/custom-validators/misc'
14 import {
15 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid,
17 isVideoAbuseReasonValid,
18 isVideoCategoryValid,
19 isVideoChannelOfAccountExist,
20 isVideoDescriptionValid,
21 isVideoExist,
22 isVideoFile,
23 isVideoImage,
24 isVideoLanguageValid,
25 isVideoLicenceValid,
26 isVideoNameValid,
27 isVideoPrivacyValid,
28 isVideoRatingTypeValid,
29 isVideoSupportValid,
30 isVideoTagsValid
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'
38
39 const videosAddValidator = [
40 body('videofile')
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(', ')
44 ),
45 body('thumbnailfile')
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(', ')
49 ),
50 body('previewfile')
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(', ')
54 ),
55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
56 body('category')
57 .optional()
58 .customSanitizer(toIntOrNull)
59 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
60 body('licence')
61 .optional()
62 .customSanitizer(toIntOrNull)
63 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
64 body('language')
65 .optional()
66 .customSanitizer(toValueOrNull)
67 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
68 body('nsfw')
69 .optional()
70 .toBoolean()
71 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
72 body('waitTranscoding')
73 .optional()
74 .toBoolean()
75 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
76 body('description')
77 .optional()
78 .customSanitizer(toValueOrNull)
79 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
80 body('support')
81 .optional()
82 .customSanitizer(toValueOrNull)
83 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
84 body('tags')
85 .optional()
86 .customSanitizer(toValueOrNull)
87 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
88 body('commentsEnabled')
89 .optional()
90 .toBoolean()
91 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
92 body('privacy')
93 .optional()
94 .toInt()
95 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
96 body('channelId')
97 .toInt()
98 .custom(isIdValid).withMessage('Should have correct video channel id'),
99 body('scheduleUpdate')
100 .optional()
101 .customSanitizer(toValueOrNull),
102 body('scheduleUpdate.updateAt')
103 .optional()
104 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
105 body('scheduleUpdate.privacy')
106 .optional()
107 .toInt()
108 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
109
110 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
112
113 if (areValidationErrors(req, res)) return
114 if (areErrorsInVideoImageFiles(req, res)) return
115 if (areErrorsInScheduleUpdate(req, res)) return
116
117 const videoFile: Express.Multer.File = req.files['videofile'][0]
118 const user = res.locals.oauth.token.User
119
120 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
121
122 const isAble = await user.isAbleToUploadVideo(videoFile)
123 if (isAble === false) {
124 res.status(403)
125 .json({ error: 'The user video quota is exceeded with this video.' })
126 .end()
127
128 return
129 }
130
131 let duration: number
132
133 try {
134 duration = await getDurationFromVideoFile(videoFile.path)
135 } catch (err) {
136 logger.error('Invalid input file in videosAddValidator.', { err })
137 res.status(400)
138 .json({ error: 'Invalid input file.' })
139 .end()
140
141 return
142 }
143
144 videoFile['duration'] = duration
145
146 return next()
147 }
148 ]
149
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(', ')
156 ),
157 body('previewfile')
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(', ')
161 ),
162 body('name')
163 .optional()
164 .custom(isVideoNameValid).withMessage('Should have a valid name'),
165 body('category')
166 .optional()
167 .customSanitizer(toIntOrNull)
168 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
169 body('licence')
170 .optional()
171 .customSanitizer(toIntOrNull)
172 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
173 body('language')
174 .optional()
175 .customSanitizer(toValueOrNull)
176 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
177 body('nsfw')
178 .optional()
179 .toBoolean()
180 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
181 body('waitTranscoding')
182 .optional()
183 .toBoolean()
184 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
185 body('privacy')
186 .optional()
187 .toInt()
188 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
189 body('description')
190 .optional()
191 .customSanitizer(toValueOrNull)
192 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
193 body('support')
194 .optional()
195 .customSanitizer(toValueOrNull)
196 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
197 body('tags')
198 .optional()
199 .customSanitizer(toValueOrNull)
200 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
201 body('commentsEnabled')
202 .optional()
203 .toBoolean()
204 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
205 body('channelId')
206 .optional()
207 .toInt()
208 .custom(isIdValid).withMessage('Should have correct video channel id'),
209 body('scheduleUpdate')
210 .optional()
211 .customSanitizer(toValueOrNull),
212 body('scheduleUpdate.updateAt')
213 .optional()
214 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
215 body('scheduleUpdate.privacy')
216 .optional()
217 .toInt()
218 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy'),
219
220 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
221 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
222
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
227
228 const video = res.locals.video
229
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
233
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.' })
237 .end()
238 }
239
240 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
241
242 return next()
243 }
244 ]
245
246 const videosGetValidator = [
247 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
248
249 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
250 logger.debug('Checking videosGet parameters', { parameters: req.params })
251
252 if (areValidationErrors(req, res)) return
253 if (!await isVideoExist(req.params.id, res)) return
254
255 const video = res.locals.video
256
257 // Video is public, anyone can access it
258 if (video.privacy === VideoPrivacy.PUBLIC) return next()
259
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()
263
264 // Don't leak this unlisted video
265 return res.status(404).end()
266 }
267
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' })
273 .end()
274 }
275
276 return next()
277 })
278 }
279 ]
280
281 const videosRemoveValidator = [
282 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
283
284 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
285 logger.debug('Checking videosRemove parameters', { parameters: req.params })
286
287 if (areValidationErrors(req, res)) return
288 if (!await isVideoExist(req.params.id, res)) return
289
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
292
293 return next()
294 }
295 ]
296
297 const videosSearchValidator = [
298 query('search').not().isEmpty().withMessage('Should have a valid search'),
299
300 (req: express.Request, res: express.Response, next: express.NextFunction) => {
301 logger.debug('Checking videosSearch parameters', { parameters: req.params })
302
303 if (areValidationErrors(req, res)) return
304
305 return next()
306 }
307 ]
308
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'),
312
313 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
314 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
315
316 if (areValidationErrors(req, res)) return
317 if (!await isVideoExist(req.params.id, res)) return
318
319 return next()
320 }
321 ]
322
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'),
326
327 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
328 logger.debug('Checking videoRate parameters', { parameters: req.body })
329
330 if (areValidationErrors(req, res)) return
331 if (!await isVideoExist(req.params.id, res)) return
332
333 return next()
334 }
335 ]
336
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'),
340
341 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
342 logger.debug('Checking videoShare parameters', { parameters: req.params })
343
344 if (areValidationErrors(req, res)) return
345 if (!await isVideoExist(req.params.id, res)) return
346
347 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
348 if (!share) {
349 return res.status(404)
350 .end()
351 }
352
353 res.locals.videoShare = share
354 return next()
355 }
356 ]
357
358 // ---------------------------------------------------------------------------
359
360 export {
361 videosAddValidator,
362 videosUpdateValidator,
363 videosGetValidator,
364 videosRemoveValidator,
365 videosSearchValidator,
366 videosShareValidator,
367
368 videoAbuseReportValidator,
369
370 videoRateValidator
371 }
372
373 // ---------------------------------------------------------------------------
374
375 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
376 // Files are optional
377 if (!req.files) return false
378
379 for (const imageField of [ 'thumbnail', 'preview' ]) {
380 if (!req.files[ imageField ]) continue
381
382 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
383 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
384 res.status(400)
385 .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
386 .end()
387 return true
388 }
389 }
390
391 return false
392 }
393
394 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
395 if (req.body.scheduleUpdate) {
396 if (!req.body.scheduleUpdate.updateAt) {
397 res.status(400)
398 .json({ error: 'Schedule update at is mandatory.' })
399 .end()
400
401 return true
402 }
403 }
404
405 return false
406 }