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