]>
Commit | Line | Data |
---|---|---|
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 | } |