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