]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos.ts
Add concept of video state, and add ability to wait transcoding before
[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 { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, toValueOrNull } from '../../helpers/custom-validators/misc'
6 import {
7 isVideoAbuseReasonValid,
8 isVideoCategoryValid,
9 isVideoChannelOfAccountExist,
10 isVideoDescriptionValid,
11 isVideoExist,
12 isVideoFile,
13 isVideoImage,
14 isVideoLanguageValid,
15 isVideoLicenceValid,
16 isVideoNameValid,
17 isVideoPrivacyValid,
18 isVideoRatingTypeValid,
19 isVideoSupportValid,
20 isVideoTagsValid
21 } from '../../helpers/custom-validators/videos'
22 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
23 import { logger } from '../../helpers/logger'
24 import { CONSTRAINTS_FIELDS } from '../../initializers'
25 import { UserModel } from '../../models/account/user'
26 import { VideoModel } from '../../models/video/video'
27 import { VideoShareModel } from '../../models/video/video-share'
28 import { authenticate } from '../oauth'
29 import { areValidationErrors } from './utils'
30
31 const videosAddValidator = [
32 body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
33 'This file is not supported. Please, make sure it is of the following type : '
34 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
35 ),
36 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
37 'This thumbnail file is not supported. Please, make sure it is of the following type : '
38 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
39 ),
40 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
41 'This preview file is not supported. Please, make sure it is of the following type : '
42 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
43 ),
44 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
45 body('category')
46 .optional()
47 .customSanitizer(toIntOrNull)
48 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
49 body('licence')
50 .optional()
51 .customSanitizer(toIntOrNull)
52 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
53 body('language')
54 .optional()
55 .customSanitizer(toValueOrNull)
56 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
57 body('nsfw')
58 .optional()
59 .toBoolean()
60 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
61 body('waitTranscoding')
62 .optional()
63 .toBoolean()
64 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
65 body('description')
66 .optional()
67 .customSanitizer(toValueOrNull)
68 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
69 body('support')
70 .optional()
71 .customSanitizer(toValueOrNull)
72 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
73 body('tags')
74 .optional()
75 .customSanitizer(toValueOrNull)
76 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
77 body('commentsEnabled')
78 .optional()
79 .toBoolean()
80 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
81 body('privacy')
82 .optional()
83 .toInt()
84 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
85 body('channelId')
86 .toInt()
87 .custom(isIdValid)
88 .withMessage('Should have correct video channel id'),
89
90 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
91 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
92
93 if (areValidationErrors(req, res)) return
94 if (areErrorsInVideoImageFiles(req, res)) return
95
96 const videoFile: Express.Multer.File = req.files['videofile'][0]
97 const user = res.locals.oauth.token.User
98
99 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
100
101 const isAble = await user.isAbleToUploadVideo(videoFile)
102 if (isAble === false) {
103 res.status(403)
104 .json({ error: 'The user video quota is exceeded with this video.' })
105 .end()
106
107 return
108 }
109
110 let duration: number
111
112 try {
113 duration = await getDurationFromVideoFile(videoFile.path)
114 } catch (err) {
115 logger.error('Invalid input file in videosAddValidator.', { err })
116 res.status(400)
117 .json({ error: 'Invalid input file.' })
118 .end()
119
120 return
121 }
122
123 videoFile['duration'] = duration
124
125 return next()
126 }
127 ]
128
129 const videosUpdateValidator = [
130 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
131 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
132 'This thumbnail file is not supported. Please, make sure it is of the following type : '
133 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
134 ),
135 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
136 'This preview file is not supported. Please, make sure it is of the following type : '
137 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
138 ),
139 body('name')
140 .optional()
141 .custom(isVideoNameValid).withMessage('Should have a valid name'),
142 body('category')
143 .optional()
144 .customSanitizer(toIntOrNull)
145 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
146 body('licence')
147 .optional()
148 .customSanitizer(toIntOrNull)
149 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
150 body('language')
151 .optional()
152 .customSanitizer(toValueOrNull)
153 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
154 body('nsfw')
155 .optional()
156 .toBoolean()
157 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
158 body('waitTranscoding')
159 .optional()
160 .toBoolean()
161 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
162 body('privacy')
163 .optional()
164 .toInt()
165 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
166 body('description')
167 .optional()
168 .customSanitizer(toValueOrNull)
169 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
170 body('support')
171 .optional()
172 .customSanitizer(toValueOrNull)
173 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
174 body('tags')
175 .optional()
176 .customSanitizer(toValueOrNull)
177 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
178 body('commentsEnabled')
179 .optional()
180 .toBoolean()
181 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
182 body('channelId')
183 .optional()
184 .toInt()
185 .custom(isIdValid).withMessage('Should have correct video channel id'),
186
187 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
188 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
189
190 if (areValidationErrors(req, res)) return
191 if (areErrorsInVideoImageFiles(req, res)) return
192 if (!await isVideoExist(req.params.id, res)) return
193
194 const video = res.locals.video
195
196 // Check if the user who did the request is able to update the video
197 const user = res.locals.oauth.token.User
198 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
199
200 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
201 return res.status(409)
202 .json({ error: 'Cannot set "private" a video that was not private anymore.' })
203 .end()
204 }
205
206 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
207
208 return next()
209 }
210 ]
211
212 const videosGetValidator = [
213 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
214
215 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
216 logger.debug('Checking videosGet parameters', { parameters: req.params })
217
218 if (areValidationErrors(req, res)) return
219 if (!await isVideoExist(req.params.id, res)) return
220
221 const video = res.locals.video
222
223 // Video is public, anyone can access it
224 if (video.privacy === VideoPrivacy.PUBLIC) return next()
225
226 // Video is unlisted, check we used the uuid to fetch it
227 if (video.privacy === VideoPrivacy.UNLISTED) {
228 if (isUUIDValid(req.params.id)) return next()
229
230 // Don't leak this unlisted video
231 return res.status(404).end()
232 }
233
234 // Video is private, check the user
235 authenticate(req, res, () => {
236 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
237 return res.status(403)
238 .json({ error: 'Cannot get this private video of another user' })
239 .end()
240 }
241
242 return next()
243 })
244 }
245 ]
246
247 const videosRemoveValidator = [
248 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
249
250 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
251 logger.debug('Checking videosRemove parameters', { parameters: req.params })
252
253 if (areValidationErrors(req, res)) return
254 if (!await isVideoExist(req.params.id, res)) return
255
256 // Check if the user who did the request is able to delete the video
257 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
258
259 return next()
260 }
261 ]
262
263 const videosSearchValidator = [
264 query('search').not().isEmpty().withMessage('Should have a valid search'),
265
266 (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 logger.debug('Checking videosSearch parameters', { parameters: req.params })
268
269 if (areValidationErrors(req, res)) return
270
271 return next()
272 }
273 ]
274
275 const videoAbuseReportValidator = [
276 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
277 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
278
279 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
280 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
281
282 if (areValidationErrors(req, res)) return
283 if (!await isVideoExist(req.params.id, res)) return
284
285 return next()
286 }
287 ]
288
289 const videoRateValidator = [
290 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
291 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
292
293 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
294 logger.debug('Checking videoRate parameters', { parameters: req.body })
295
296 if (areValidationErrors(req, res)) return
297 if (!await isVideoExist(req.params.id, res)) return
298
299 return next()
300 }
301 ]
302
303 const videosShareValidator = [
304 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
305 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
306
307 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
308 logger.debug('Checking videoShare parameters', { parameters: req.params })
309
310 if (areValidationErrors(req, res)) return
311 if (!await isVideoExist(req.params.id, res)) return
312
313 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
314 if (!share) {
315 return res.status(404)
316 .end()
317 }
318
319 res.locals.videoShare = share
320 return next()
321 }
322 ]
323
324 // ---------------------------------------------------------------------------
325
326 export {
327 videosAddValidator,
328 videosUpdateValidator,
329 videosGetValidator,
330 videosRemoveValidator,
331 videosSearchValidator,
332 videosShareValidator,
333
334 videoAbuseReportValidator,
335
336 videoRateValidator
337 }
338
339 // ---------------------------------------------------------------------------
340
341 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
342 // Retrieve the user who did the request
343 if (video.isOwned() === false) {
344 res.status(403)
345 .json({ error: 'Cannot manage a video of another server.' })
346 .end()
347 return false
348 }
349
350 // Check if the user can delete the video
351 // The user can delete it if he has the right
352 // Or if s/he is the video's account
353 const account = video.VideoChannel.Account
354 if (user.hasRight(right) === false && account.userId !== user.id) {
355 res.status(403)
356 .json({ error: 'Cannot manage a video of another user.' })
357 .end()
358 return false
359 }
360
361 return true
362 }
363
364 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
365 // Files are optional
366 if (!req.files) return false
367
368 for (const imageField of [ 'thumbnail', 'preview' ]) {
369 if (!req.files[ imageField ]) continue
370
371 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
372 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
373 res.status(400)
374 .send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
375 .end()
376 return true
377 }
378 }
379
380 return false
381 }