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