diff options
Diffstat (limited to 'server/middlewares/validators/videos/videos.ts')
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 575 |
1 files changed, 0 insertions, 575 deletions
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts deleted file mode 100644 index 5a49779ed..000000000 --- a/server/middlewares/validators/videos/videos.ts +++ /dev/null | |||
@@ -1,575 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { body, header, param, query, ValidationChain } from 'express-validator' | ||
3 | import { isTestInstance } from '@server/helpers/core-utils' | ||
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { Redis } from '@server/lib/redis' | ||
6 | import { uploadx } from '@server/lib/uploadx' | ||
7 | import { getServerActor } from '@server/models/application/application' | ||
8 | import { ExpressPromiseHandler } from '@server/types/express-handler' | ||
9 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | ||
10 | import { arrayify } from '@shared/core-utils' | ||
11 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models' | ||
12 | import { | ||
13 | exists, | ||
14 | isBooleanValid, | ||
15 | isDateValid, | ||
16 | isFileValid, | ||
17 | isIdValid, | ||
18 | toBooleanOrNull, | ||
19 | toIntOrNull, | ||
20 | toValueOrNull | ||
21 | } from '../../../helpers/custom-validators/misc' | ||
22 | import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | ||
23 | import { | ||
24 | areVideoTagsValid, | ||
25 | isScheduleVideoUpdatePrivacyValid, | ||
26 | isValidPasswordProtectedPrivacy, | ||
27 | isVideoCategoryValid, | ||
28 | isVideoDescriptionValid, | ||
29 | isVideoImageValid, | ||
30 | isVideoIncludeValid, | ||
31 | isVideoLanguageValid, | ||
32 | isVideoLicenceValid, | ||
33 | isVideoNameValid, | ||
34 | isVideoOriginallyPublishedAtValid, | ||
35 | isVideoPrivacyValid, | ||
36 | isVideoSupportValid | ||
37 | } from '../../../helpers/custom-validators/videos' | ||
38 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | ||
39 | import { logger } from '../../../helpers/logger' | ||
40 | import { getVideoWithAttributes } from '../../../helpers/video' | ||
41 | import { CONFIG } from '../../../initializers/config' | ||
42 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | ||
43 | import { VideoModel } from '../../../models/video/video' | ||
44 | import { | ||
45 | areValidationErrors, | ||
46 | checkCanAccessVideoStaticFiles, | ||
47 | checkCanSeeVideo, | ||
48 | checkUserCanManageVideo, | ||
49 | doesVideoChannelOfAccountExist, | ||
50 | doesVideoExist, | ||
51 | doesVideoFileOfVideoExist, | ||
52 | isValidVideoIdParam, | ||
53 | isValidVideoPasswordHeader | ||
54 | } from '../shared' | ||
55 | import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared' | ||
56 | |||
57 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | ||
58 | body('videofile') | ||
59 | .custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null })) | ||
60 | .withMessage('Should have a file'), | ||
61 | body('name') | ||
62 | .trim() | ||
63 | .custom(isVideoNameValid).withMessage( | ||
64 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
65 | ), | ||
66 | body('channelId') | ||
67 | .customSanitizer(toIntOrNull) | ||
68 | .custom(isIdValid), | ||
69 | body('videoPasswords') | ||
70 | .optional() | ||
71 | .isArray() | ||
72 | .withMessage('Video passwords should be an array.'), | ||
73 | |||
74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
76 | |||
77 | const videoFile: express.VideoUploadFile = req.files['videofile'][0] | ||
78 | const user = res.locals.oauth.token.User | ||
79 | |||
80 | if ( | ||
81 | !await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) || | ||
82 | !isValidPasswordProtectedPrivacy(req, res) || | ||
83 | !await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) || | ||
84 | !await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' }) | ||
85 | ) { | ||
86 | return cleanUpReqFiles(req) | ||
87 | } | ||
88 | |||
89 | return next() | ||
90 | } | ||
91 | ]) | ||
92 | |||
93 | /** | ||
94 | * Gets called after the last PUT request | ||
95 | */ | ||
96 | const videosAddResumableValidator = [ | ||
97 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
98 | const user = res.locals.oauth.token.User | ||
99 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | ||
100 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } | ||
101 | const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err })) | ||
102 | |||
103 | const uploadId = req.query.upload_id | ||
104 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) | ||
105 | |||
106 | if (sessionExists) { | ||
107 | const sessionResponse = await Redis.Instance.getUploadSession(uploadId) | ||
108 | |||
109 | if (!sessionResponse) { | ||
110 | res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion | ||
111 | |||
112 | return res.fail({ | ||
113 | status: HttpStatusCode.SERVICE_UNAVAILABLE_503, | ||
114 | message: 'The upload is already being processed' | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | const videoStillExists = await VideoModel.load(sessionResponse.video.id) | ||
119 | |||
120 | if (videoStillExists) { | ||
121 | if (isTestInstance()) { | ||
122 | res.setHeader('x-resumable-upload-cached', 'true') | ||
123 | } | ||
124 | |||
125 | return res.json(sessionResponse) | ||
126 | } | ||
127 | } | ||
128 | |||
129 | await Redis.Instance.setUploadSession(uploadId) | ||
130 | |||
131 | if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup() | ||
132 | if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'videosAddResumableValidator' })) return cleanup() | ||
133 | if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.upload.accept.result' })) return cleanup() | ||
134 | |||
135 | res.locals.uploadVideoFileResumable = { ...file, originalname: file.filename } | ||
136 | |||
137 | return next() | ||
138 | } | ||
139 | ] | ||
140 | |||
141 | /** | ||
142 | * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use. | ||
143 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts | ||
144 | * | ||
145 | * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx | ||
146 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts | ||
147 | * | ||
148 | */ | ||
149 | const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | ||
150 | body('filename') | ||
151 | .exists(), | ||
152 | body('name') | ||
153 | .trim() | ||
154 | .custom(isVideoNameValid).withMessage( | ||
155 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
156 | ), | ||
157 | body('channelId') | ||
158 | .customSanitizer(toIntOrNull) | ||
159 | .custom(isIdValid), | ||
160 | body('videoPasswords') | ||
161 | .optional() | ||
162 | .isArray() | ||
163 | .withMessage('Video passwords should be an array.'), | ||
164 | |||
165 | header('x-upload-content-length') | ||
166 | .isNumeric() | ||
167 | .exists() | ||
168 | .withMessage('Should specify the file length'), | ||
169 | header('x-upload-content-type') | ||
170 | .isString() | ||
171 | .exists() | ||
172 | .withMessage('Should specify the file mimetype'), | ||
173 | |||
174 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
175 | const videoFileMetadata = { | ||
176 | mimetype: req.headers['x-upload-content-type'] as string, | ||
177 | size: +req.headers['x-upload-content-length'], | ||
178 | originalname: req.body.filename | ||
179 | } | ||
180 | |||
181 | const user = res.locals.oauth.token.User | ||
182 | const cleanup = () => cleanUpReqFiles(req) | ||
183 | |||
184 | logger.debug('Checking videosAddResumableInitValidator parameters and headers', { | ||
185 | parameters: req.body, | ||
186 | headers: req.headers, | ||
187 | files: req.files | ||
188 | }) | ||
189 | |||
190 | if (areValidationErrors(req, res, { omitLog: true })) return cleanup() | ||
191 | |||
192 | const files = { videofile: [ videoFileMetadata ] } | ||
193 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | ||
194 | |||
195 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup() | ||
196 | |||
197 | // Multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
198 | req.headers['content-type'] = 'application/json; charset=utf-8' | ||
199 | |||
200 | // Place thumbnail/previewfile in metadata so that uploadx saves it in .META | ||
201 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] | ||
202 | if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile'] | ||
203 | |||
204 | return next() | ||
205 | } | ||
206 | ]) | ||
207 | |||
208 | const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | ||
209 | isValidVideoIdParam('id'), | ||
210 | |||
211 | body('name') | ||
212 | .optional() | ||
213 | .trim() | ||
214 | .custom(isVideoNameValid).withMessage( | ||
215 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
216 | ), | ||
217 | body('channelId') | ||
218 | .optional() | ||
219 | .customSanitizer(toIntOrNull) | ||
220 | .custom(isIdValid), | ||
221 | body('videoPasswords') | ||
222 | .optional() | ||
223 | .isArray() | ||
224 | .withMessage('Video passwords should be an array.'), | ||
225 | |||
226 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
227 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
228 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | ||
229 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | ||
230 | |||
231 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
232 | |||
233 | const video = getVideoWithAttributes(res) | ||
234 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { | ||
235 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | ||
236 | } | ||
237 | |||
238 | // Check if the user who did the request is able to update the video | ||
239 | const user = res.locals.oauth.token.User | ||
240 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) | ||
241 | |||
242 | if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | ||
243 | |||
244 | return next() | ||
245 | } | ||
246 | ]) | ||
247 | |||
248 | async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
249 | const video = getVideoWithAttributes(res) | ||
250 | |||
251 | // Anybody can watch local videos | ||
252 | if (video.isOwned() === true) return next() | ||
253 | |||
254 | // Logged user | ||
255 | if (res.locals.oauth) { | ||
256 | // Users can search or watch remote videos | ||
257 | if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next() | ||
258 | } | ||
259 | |||
260 | // Anybody can search or watch remote videos | ||
261 | if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next() | ||
262 | |||
263 | // Check our instance follows an actor that shared this video | ||
264 | const serverActor = await getServerActor() | ||
265 | if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() | ||
266 | |||
267 | return res.fail({ | ||
268 | status: HttpStatusCode.FORBIDDEN_403, | ||
269 | message: 'Cannot get this video regarding follow constraints', | ||
270 | type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, | ||
271 | data: { | ||
272 | originUrl: video.url | ||
273 | } | ||
274 | }) | ||
275 | } | ||
276 | |||
277 | const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes') => { | ||
278 | return [ | ||
279 | isValidVideoIdParam('id'), | ||
280 | |||
281 | isValidVideoPasswordHeader(), | ||
282 | |||
283 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
284 | if (areValidationErrors(req, res)) return | ||
285 | if (!await doesVideoExist(req.params.id, res, fetchType)) return | ||
286 | |||
287 | // Controllers does not need to check video rights | ||
288 | if (fetchType === 'only-immutable-attributes') return next() | ||
289 | |||
290 | const video = getVideoWithAttributes(res) as MVideoFullLight | ||
291 | |||
292 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id })) return | ||
293 | |||
294 | return next() | ||
295 | } | ||
296 | ] | ||
297 | } | ||
298 | |||
299 | const videosGetValidator = videosCustomGetValidator('all') | ||
300 | |||
301 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | ||
302 | isValidVideoIdParam('id'), | ||
303 | |||
304 | param('videoFileId') | ||
305 | .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), | ||
306 | |||
307 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
308 | if (areValidationErrors(req, res)) return | ||
309 | if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return | ||
310 | |||
311 | return next() | ||
312 | } | ||
313 | ]) | ||
314 | |||
315 | const videosDownloadValidator = [ | ||
316 | isValidVideoIdParam('id'), | ||
317 | |||
318 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
319 | if (areValidationErrors(req, res)) return | ||
320 | if (!await doesVideoExist(req.params.id, res, 'all')) return | ||
321 | |||
322 | const video = getVideoWithAttributes(res) | ||
323 | |||
324 | if (!await checkCanAccessVideoStaticFiles({ req, res, video, paramId: req.params.id })) return | ||
325 | |||
326 | return next() | ||
327 | } | ||
328 | ] | ||
329 | |||
330 | const videosRemoveValidator = [ | ||
331 | isValidVideoIdParam('id'), | ||
332 | |||
333 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
334 | if (areValidationErrors(req, res)) return | ||
335 | if (!await doesVideoExist(req.params.id, res)) return | ||
336 | |||
337 | // Check if the user who did the request is able to delete the video | ||
338 | if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return | ||
339 | |||
340 | return next() | ||
341 | } | ||
342 | ] | ||
343 | |||
344 | const videosOverviewValidator = [ | ||
345 | query('page') | ||
346 | .optional() | ||
347 | .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT }), | ||
348 | |||
349 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
350 | if (areValidationErrors(req, res)) return | ||
351 | |||
352 | return next() | ||
353 | } | ||
354 | ] | ||
355 | |||
356 | function getCommonVideoEditAttributes () { | ||
357 | return [ | ||
358 | body('thumbnailfile') | ||
359 | .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage( | ||
360 | 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + | ||
361 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') | ||
362 | ), | ||
363 | body('previewfile') | ||
364 | .custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage( | ||
365 | 'This preview file is not supported or too large. Please, make sure it is of the following type: ' + | ||
366 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') | ||
367 | ), | ||
368 | |||
369 | body('category') | ||
370 | .optional() | ||
371 | .customSanitizer(toIntOrNull) | ||
372 | .custom(isVideoCategoryValid), | ||
373 | body('licence') | ||
374 | .optional() | ||
375 | .customSanitizer(toIntOrNull) | ||
376 | .custom(isVideoLicenceValid), | ||
377 | body('language') | ||
378 | .optional() | ||
379 | .customSanitizer(toValueOrNull) | ||
380 | .custom(isVideoLanguageValid), | ||
381 | body('nsfw') | ||
382 | .optional() | ||
383 | .customSanitizer(toBooleanOrNull) | ||
384 | .custom(isBooleanValid).withMessage('Should have a valid nsfw boolean'), | ||
385 | body('waitTranscoding') | ||
386 | .optional() | ||
387 | .customSanitizer(toBooleanOrNull) | ||
388 | .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'), | ||
389 | body('privacy') | ||
390 | .optional() | ||
391 | .customSanitizer(toIntOrNull) | ||
392 | .custom(isVideoPrivacyValid), | ||
393 | body('description') | ||
394 | .optional() | ||
395 | .customSanitizer(toValueOrNull) | ||
396 | .custom(isVideoDescriptionValid), | ||
397 | body('support') | ||
398 | .optional() | ||
399 | .customSanitizer(toValueOrNull) | ||
400 | .custom(isVideoSupportValid), | ||
401 | body('tags') | ||
402 | .optional() | ||
403 | .customSanitizer(toValueOrNull) | ||
404 | .custom(areVideoTagsValid) | ||
405 | .withMessage( | ||
406 | `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` + | ||
407 | `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each` | ||
408 | ), | ||
409 | body('commentsEnabled') | ||
410 | .optional() | ||
411 | .customSanitizer(toBooleanOrNull) | ||
412 | .custom(isBooleanValid).withMessage('Should have commentsEnabled boolean'), | ||
413 | body('downloadEnabled') | ||
414 | .optional() | ||
415 | .customSanitizer(toBooleanOrNull) | ||
416 | .custom(isBooleanValid).withMessage('Should have downloadEnabled boolean'), | ||
417 | body('originallyPublishedAt') | ||
418 | .optional() | ||
419 | .customSanitizer(toValueOrNull) | ||
420 | .custom(isVideoOriginallyPublishedAtValid), | ||
421 | body('scheduleUpdate') | ||
422 | .optional() | ||
423 | .customSanitizer(toValueOrNull), | ||
424 | body('scheduleUpdate.updateAt') | ||
425 | .optional() | ||
426 | .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'), | ||
427 | body('scheduleUpdate.privacy') | ||
428 | .optional() | ||
429 | .customSanitizer(toIntOrNull) | ||
430 | .custom(isScheduleVideoUpdatePrivacyValid) | ||
431 | ] as (ValidationChain | ExpressPromiseHandler)[] | ||
432 | } | ||
433 | |||
434 | const commonVideosFiltersValidator = [ | ||
435 | query('categoryOneOf') | ||
436 | .optional() | ||
437 | .customSanitizer(arrayify) | ||
438 | .custom(isNumberArray).withMessage('Should have a valid categoryOneOf array'), | ||
439 | query('licenceOneOf') | ||
440 | .optional() | ||
441 | .customSanitizer(arrayify) | ||
442 | .custom(isNumberArray).withMessage('Should have a valid licenceOneOf array'), | ||
443 | query('languageOneOf') | ||
444 | .optional() | ||
445 | .customSanitizer(arrayify) | ||
446 | .custom(isStringArray).withMessage('Should have a valid languageOneOf array'), | ||
447 | query('privacyOneOf') | ||
448 | .optional() | ||
449 | .customSanitizer(arrayify) | ||
450 | .custom(isNumberArray).withMessage('Should have a valid privacyOneOf array'), | ||
451 | query('tagsOneOf') | ||
452 | .optional() | ||
453 | .customSanitizer(arrayify) | ||
454 | .custom(isStringArray).withMessage('Should have a valid tagsOneOf array'), | ||
455 | query('tagsAllOf') | ||
456 | .optional() | ||
457 | .customSanitizer(arrayify) | ||
458 | .custom(isStringArray).withMessage('Should have a valid tagsAllOf array'), | ||
459 | query('nsfw') | ||
460 | .optional() | ||
461 | .custom(isBooleanBothQueryValid), | ||
462 | query('isLive') | ||
463 | .optional() | ||
464 | .customSanitizer(toBooleanOrNull) | ||
465 | .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'), | ||
466 | query('include') | ||
467 | .optional() | ||
468 | .custom(isVideoIncludeValid), | ||
469 | query('isLocal') | ||
470 | .optional() | ||
471 | .customSanitizer(toBooleanOrNull) | ||
472 | .custom(isBooleanValid).withMessage('Should have a valid isLocal boolean'), | ||
473 | query('hasHLSFiles') | ||
474 | .optional() | ||
475 | .customSanitizer(toBooleanOrNull) | ||
476 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), | ||
477 | query('hasWebtorrentFiles') // TODO: remove in v7 | ||
478 | .optional() | ||
479 | .customSanitizer(toBooleanOrNull) | ||
480 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), | ||
481 | query('hasWebVideoFiles') | ||
482 | .optional() | ||
483 | .customSanitizer(toBooleanOrNull) | ||
484 | .custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'), | ||
485 | query('skipCount') | ||
486 | .optional() | ||
487 | .customSanitizer(toBooleanOrNull) | ||
488 | .custom(isBooleanValid).withMessage('Should have a valid skipCount boolean'), | ||
489 | query('search') | ||
490 | .optional() | ||
491 | .custom(exists), | ||
492 | query('excludeAlreadyWatched') | ||
493 | .optional() | ||
494 | .customSanitizer(toBooleanOrNull) | ||
495 | .isBoolean().withMessage('Should be a valid excludeAlreadyWatched boolean'), | ||
496 | |||
497 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
498 | if (areValidationErrors(req, res)) return | ||
499 | |||
500 | const user = res.locals.oauth?.token.User | ||
501 | |||
502 | if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { | ||
503 | if (req.query.include || req.query.privacyOneOf) { | ||
504 | return res.fail({ | ||
505 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
506 | message: 'You are not allowed to see all videos.' | ||
507 | }) | ||
508 | } | ||
509 | } | ||
510 | |||
511 | if (!user && exists(req.query.excludeAlreadyWatched)) { | ||
512 | res.fail({ | ||
513 | status: HttpStatusCode.BAD_REQUEST_400, | ||
514 | message: 'Cannot use excludeAlreadyWatched parameter when auth token is not provided' | ||
515 | }) | ||
516 | return false | ||
517 | } | ||
518 | return next() | ||
519 | } | ||
520 | ] | ||
521 | |||
522 | // --------------------------------------------------------------------------- | ||
523 | |||
524 | export { | ||
525 | videosAddLegacyValidator, | ||
526 | videosAddResumableValidator, | ||
527 | videosAddResumableInitValidator, | ||
528 | |||
529 | videosUpdateValidator, | ||
530 | videosGetValidator, | ||
531 | videoFileMetadataGetValidator, | ||
532 | videosDownloadValidator, | ||
533 | checkVideoFollowConstraints, | ||
534 | videosCustomGetValidator, | ||
535 | videosRemoveValidator, | ||
536 | |||
537 | getCommonVideoEditAttributes, | ||
538 | |||
539 | commonVideosFiltersValidator, | ||
540 | |||
541 | videosOverviewValidator | ||
542 | } | ||
543 | |||
544 | // --------------------------------------------------------------------------- | ||
545 | |||
546 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { | ||
547 | if (req.body.scheduleUpdate) { | ||
548 | if (!req.body.scheduleUpdate.updateAt) { | ||
549 | logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') | ||
550 | |||
551 | res.fail({ message: 'Schedule update at is mandatory.' }) | ||
552 | return true | ||
553 | } | ||
554 | } | ||
555 | |||
556 | return false | ||
557 | } | ||
558 | |||
559 | async function commonVideoChecksPass (options: { | ||
560 | req: express.Request | ||
561 | res: express.Response | ||
562 | user: MUserAccountId | ||
563 | videoFileSize: number | ||
564 | files: express.UploadFilesForCheck | ||
565 | }): Promise<boolean> { | ||
566 | const { req, res, user } = options | ||
567 | |||
568 | if (areErrorsInScheduleUpdate(req, res)) return false | ||
569 | |||
570 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false | ||
571 | |||
572 | if (!await commonVideoFileChecks(options)) return false | ||
573 | |||
574 | return true | ||
575 | } | ||