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