]>
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 = [ |
8376734e | 41 | body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( |
10db166b C |
42 | 'This file is not supported. Please, make sure it is of the following type : ' |
43 | + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | |
8376734e | 44 | ), |
ac81d1a0 C |
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 | ), | |
b60e5f38 | 53 | body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), |
360329cc C |
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() | |
2efd32f6 | 64 | .customSanitizer(toValueOrNull) |
360329cc C |
65 | .custom(isVideoLanguageValid).withMessage('Should have a valid language'), |
66 | body('nsfw') | |
2186386c | 67 | .optional() |
360329cc C |
68 | .toBoolean() |
69 | .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), | |
2186386c C |
70 | body('waitTranscoding') |
71 | .optional() | |
72 | .toBoolean() | |
73 | .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'), | |
360329cc C |
74 | body('description') |
75 | .optional() | |
2efd32f6 | 76 | .customSanitizer(toValueOrNull) |
360329cc C |
77 | .custom(isVideoDescriptionValid).withMessage('Should have a valid description'), |
78 | body('support') | |
79 | .optional() | |
2efd32f6 | 80 | .customSanitizer(toValueOrNull) |
360329cc C |
81 | .custom(isVideoSupportValid).withMessage('Should have a valid support text'), |
82 | body('tags') | |
83 | .optional() | |
2efd32f6 | 84 | .customSanitizer(toValueOrNull) |
360329cc C |
85 | .custom(isVideoTagsValid).withMessage('Should have correct tags'), |
86 | body('commentsEnabled') | |
2186386c | 87 | .optional() |
360329cc C |
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'), | |
0f320037 C |
94 | body('channelId') |
95 | .toInt() | |
2baea0c7 C |
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'), | |
b60e5f38 | 104 | |
a2431b7d | 105 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
b60e5f38 C |
106 | logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) |
107 | ||
a2431b7d | 108 | if (areValidationErrors(req, res)) return |
ac81d1a0 | 109 | if (areErrorsInVideoImageFiles(req, res)) return |
2baea0c7 | 110 | if (areErrorsInScheduleUpdate(req, res)) return |
a2431b7d C |
111 | |
112 | const videoFile: Express.Multer.File = req.files['videofile'][0] | |
113 | const user = res.locals.oauth.token.User | |
b60e5f38 | 114 | |
6200d8d9 | 115 | if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return |
a2431b7d C |
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) { | |
d5b7d911 | 131 | logger.error('Invalid input file in videosAddValidator.', { err }) |
a2431b7d C |
132 | res.status(400) |
133 | .json({ error: 'Invalid input file.' }) | |
134 | .end() | |
135 | ||
136 | return | |
137 | } | |
138 | ||
a2431b7d C |
139 | videoFile['duration'] = duration |
140 | ||
141 | return next() | |
b60e5f38 C |
142 | } |
143 | ] | |
144 | ||
145 | const videosUpdateValidator = [ | |
72c7248b | 146 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
ac81d1a0 C |
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 | ), | |
360329cc C |
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() | |
2efd32f6 | 168 | .customSanitizer(toValueOrNull) |
360329cc C |
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'), | |
2186386c C |
174 | body('waitTranscoding') |
175 | .optional() | |
176 | .toBoolean() | |
177 | .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'), | |
360329cc C |
178 | body('privacy') |
179 | .optional() | |
180 | .toInt() | |
181 | .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), | |
182 | body('description') | |
183 | .optional() | |
2efd32f6 | 184 | .customSanitizer(toValueOrNull) |
360329cc C |
185 | .custom(isVideoDescriptionValid).withMessage('Should have a valid description'), |
186 | body('support') | |
187 | .optional() | |
2efd32f6 | 188 | .customSanitizer(toValueOrNull) |
360329cc C |
189 | .custom(isVideoSupportValid).withMessage('Should have a valid support text'), |
190 | body('tags') | |
191 | .optional() | |
2efd32f6 | 192 | .customSanitizer(toValueOrNull) |
360329cc C |
193 | .custom(isVideoTagsValid).withMessage('Should have correct tags'), |
194 | body('commentsEnabled') | |
195 | .optional() | |
196 | .toBoolean() | |
197 | .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), | |
0f320037 C |
198 | body('channelId') |
199 | .optional() | |
200 | .toInt() | |
201 | .custom(isIdValid).withMessage('Should have correct video channel id'), | |
2baea0c7 C |
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'), | |
b60e5f38 | 209 | |
a2431b7d | 210 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
b60e5f38 C |
211 | logger.debug('Checking videosUpdate parameters', { parameters: req.body }) |
212 | ||
a2431b7d | 213 | if (areValidationErrors(req, res)) return |
ac81d1a0 | 214 | if (areErrorsInVideoImageFiles(req, res)) return |
2baea0c7 | 215 | if (areErrorsInScheduleUpdate(req, res)) return |
a2431b7d C |
216 | if (!await isVideoExist(req.params.id, res)) return |
217 | ||
218 | const video = res.locals.video | |
219 | ||
6221f311 | 220 | // Check if the user who did the request is able to update the video |
0f320037 C |
221 | const user = res.locals.oauth.token.User |
222 | if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return | |
a2431b7d C |
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 anymore.' }) | |
227 | .end() | |
228 | } | |
229 | ||
6200d8d9 | 230 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return |
0f320037 | 231 | |
a2431b7d | 232 | return next() |
b60e5f38 C |
233 | } |
234 | ] | |
c173e565 | 235 | |
b60e5f38 | 236 | const videosGetValidator = [ |
72c7248b | 237 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
34ca3b52 | 238 | |
a2431b7d | 239 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
b60e5f38 | 240 | logger.debug('Checking videosGet parameters', { parameters: req.params }) |
7b1f49de | 241 | |
a2431b7d C |
242 | if (areValidationErrors(req, res)) return |
243 | if (!await isVideoExist(req.params.id, res)) return | |
11474c3c | 244 | |
a2431b7d | 245 | const video = res.locals.video |
11474c3c | 246 | |
81ebea48 C |
247 | // Video is public, anyone can access it |
248 | if (video.privacy === VideoPrivacy.PUBLIC) return next() | |
11474c3c | 249 | |
81ebea48 C |
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 | |
a2431b7d C |
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() | |
b60e5f38 C |
267 | }) |
268 | } | |
269 | ] | |
34ca3b52 | 270 | |
b60e5f38 | 271 | const videosRemoveValidator = [ |
72c7248b | 272 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
34ca3b52 | 273 | |
a2431b7d | 274 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
b60e5f38 | 275 | logger.debug('Checking videosRemove parameters', { parameters: req.params }) |
34ca3b52 | 276 | |
a2431b7d C |
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 | |
6221f311 | 281 | if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return |
a2431b7d C |
282 | |
283 | return next() | |
b60e5f38 C |
284 | } |
285 | ] | |
34ca3b52 | 286 | |
b60e5f38 | 287 | const videosSearchValidator = [ |
f3aaa9a9 | 288 | query('search').not().isEmpty().withMessage('Should have a valid search'), |
c45f7f84 | 289 | |
b60e5f38 C |
290 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
291 | logger.debug('Checking videosSearch parameters', { parameters: req.params }) | |
c45f7f84 | 292 | |
a2431b7d C |
293 | if (areValidationErrors(req, res)) return |
294 | ||
295 | return next() | |
b60e5f38 C |
296 | } |
297 | ] | |
c45f7f84 | 298 | |
b60e5f38 | 299 | const videoAbuseReportValidator = [ |
72c7248b | 300 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
b60e5f38 | 301 | body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), |
55fa55a9 | 302 | |
a2431b7d | 303 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
b60e5f38 | 304 | logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) |
55fa55a9 | 305 | |
a2431b7d C |
306 | if (areValidationErrors(req, res)) return |
307 | if (!await isVideoExist(req.params.id, res)) return | |
308 | ||
309 | return next() | |
b60e5f38 C |
310 | } |
311 | ] | |
55fa55a9 | 312 | |
b60e5f38 | 313 | const videoRateValidator = [ |
72c7248b | 314 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
b60e5f38 | 315 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), |
d38b8281 | 316 | |
a2431b7d | 317 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
b60e5f38 | 318 | logger.debug('Checking videoRate parameters', { parameters: req.body }) |
d38b8281 | 319 | |
a2431b7d C |
320 | if (areValidationErrors(req, res)) return |
321 | if (!await isVideoExist(req.params.id, res)) return | |
322 | ||
323 | return next() | |
b60e5f38 C |
324 | } |
325 | ] | |
d38b8281 | 326 | |
4e50b6a1 C |
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 | |
a2431b7d | 335 | if (!await isVideoExist(req.params.id, res)) return |
4e50b6a1 | 336 | |
3fd3ab2d | 337 | const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined) |
4e50b6a1 C |
338 | if (!share) { |
339 | return res.status(404) | |
340 | .end() | |
341 | } | |
342 | ||
343 | res.locals.videoShare = share | |
4e50b6a1 C |
344 | return next() |
345 | } | |
346 | ] | |
347 | ||
9f10b292 | 348 | // --------------------------------------------------------------------------- |
c45f7f84 | 349 | |
65fcc311 C |
350 | export { |
351 | videosAddValidator, | |
352 | videosUpdateValidator, | |
353 | videosGetValidator, | |
354 | videosRemoveValidator, | |
355 | videosSearchValidator, | |
4e50b6a1 | 356 | videosShareValidator, |
65fcc311 C |
357 | |
358 | videoAbuseReportValidator, | |
359 | ||
35bf0c83 | 360 | videoRateValidator |
65fcc311 | 361 | } |
7b1f49de C |
362 | |
363 | // --------------------------------------------------------------------------- | |
364 | ||
6221f311 | 365 | function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) { |
198b205c | 366 | // Retrieve the user who did the request |
a2431b7d C |
367 | if (video.isOwned() === false) { |
368 | res.status(403) | |
6200d8d9 | 369 | .json({ error: 'Cannot manage a video of another server.' }) |
11474c3c | 370 | .end() |
a2431b7d | 371 | return false |
11474c3c C |
372 | } |
373 | ||
374 | // Check if the user can delete the video | |
4cb6d457 | 375 | // The user can delete it if he has the right |
38fa2065 | 376 | // Or if s/he is the video's account |
a2431b7d | 377 | const account = video.VideoChannel.Account |
6221f311 | 378 | if (user.hasRight(right) === false && account.userId !== user.id) { |
a2431b7d | 379 | res.status(403) |
6200d8d9 | 380 | .json({ error: 'Cannot manage a video of another user.' }) |
11474c3c | 381 | .end() |
a2431b7d | 382 | return false |
11474c3c C |
383 | } |
384 | ||
a2431b7d | 385 | return true |
198b205c | 386 | } |
ac81d1a0 C |
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) | |
2baea0c7 | 398 | .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` }) |
ac81d1a0 C |
399 | .end() |
400 | return true | |
401 | } | |
402 | } | |
403 | ||
404 | return false | |
405 | } | |
2baea0c7 C |
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 | } |