aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares/validators/videos/videos.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/middlewares/validators/videos/videos.ts')
-rw-r--r--server/middlewares/validators/videos/videos.ts575
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 @@
1import express from 'express'
2import { body, header, param, query, ValidationChain } from 'express-validator'
3import { isTestInstance } from '@server/helpers/core-utils'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { Redis } from '@server/lib/redis'
6import { uploadx } from '@server/lib/uploadx'
7import { getServerActor } from '@server/models/application/application'
8import { ExpressPromiseHandler } from '@server/types/express-handler'
9import { MUserAccountId, MVideoFullLight } from '@server/types/models'
10import { arrayify } from '@shared/core-utils'
11import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models'
12import {
13 exists,
14 isBooleanValid,
15 isDateValid,
16 isFileValid,
17 isIdValid,
18 toBooleanOrNull,
19 toIntOrNull,
20 toValueOrNull
21} from '../../../helpers/custom-validators/misc'
22import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
23import {
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'
38import { cleanUpReqFiles } from '../../../helpers/express-utils'
39import { logger } from '../../../helpers/logger'
40import { getVideoWithAttributes } from '../../../helpers/video'
41import { CONFIG } from '../../../initializers/config'
42import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
43import { VideoModel } from '../../../models/video/video'
44import {
45 areValidationErrors,
46 checkCanAccessVideoStaticFiles,
47 checkCanSeeVideo,
48 checkUserCanManageVideo,
49 doesVideoChannelOfAccountExist,
50 doesVideoExist,
51 doesVideoFileOfVideoExist,
52 isValidVideoIdParam,
53 isValidVideoPasswordHeader
54} from '../shared'
55import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared'
56
57const 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 */
96const 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 */
149const 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
208const 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
248async 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
277const 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
299const videosGetValidator = videosCustomGetValidator('all')
300
301const 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
315const 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
330const 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
344const 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
356function 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
434const 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
524export {
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
546function 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
559async 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}