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