aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares/validators/videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/middlewares/validators/videos
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/middlewares/validators/videos')
-rw-r--r--server/middlewares/validators/videos/index.ts19
-rw-r--r--server/middlewares/validators/videos/shared/index.ts2
-rw-r--r--server/middlewares/validators/videos/shared/upload.ts39
-rw-r--r--server/middlewares/validators/videos/shared/video-validators.ts100
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts87
-rw-r--r--server/middlewares/validators/videos/video-captions.ts83
-rw-r--r--server/middlewares/validators/videos/video-channel-sync.ts56
-rw-r--r--server/middlewares/validators/videos/video-channels.ts194
-rw-r--r--server/middlewares/validators/videos/video-comments.ts249
-rw-r--r--server/middlewares/validators/videos/video-files.ts163
-rw-r--r--server/middlewares/validators/videos/video-imports.ts209
-rw-r--r--server/middlewares/validators/videos/video-live.ts342
-rw-r--r--server/middlewares/validators/videos/video-ownership-changes.ts107
-rw-r--r--server/middlewares/validators/videos/video-passwords.ts77
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts419
-rw-r--r--server/middlewares/validators/videos/video-rates.ts72
-rw-r--r--server/middlewares/validators/videos/video-shares.ts35
-rw-r--r--server/middlewares/validators/videos/video-source.ts130
-rw-r--r--server/middlewares/validators/videos/video-stats.ts108
-rw-r--r--server/middlewares/validators/videos/video-studio.ts105
-rw-r--r--server/middlewares/validators/videos/video-token.ts24
-rw-r--r--server/middlewares/validators/videos/video-transcoding.ts61
-rw-r--r--server/middlewares/validators/videos/video-view.ts61
-rw-r--r--server/middlewares/validators/videos/videos.ts575
24 files changed, 0 insertions, 3317 deletions
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
deleted file mode 100644
index 8c6fc49b1..000000000
--- a/server/middlewares/validators/videos/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1export * from './video-blacklist'
2export * from './video-captions'
3export * from './video-channel-sync'
4export * from './video-channels'
5export * from './video-comments'
6export * from './video-files'
7export * from './video-imports'
8export * from './video-live'
9export * from './video-ownership-changes'
10export * from './video-passwords'
11export * from './video-rates'
12export * from './video-shares'
13export * from './video-source'
14export * from './video-stats'
15export * from './video-studio'
16export * from './video-token'
17export * from './video-transcoding'
18export * from './video-view'
19export * from './videos'
diff --git a/server/middlewares/validators/videos/shared/index.ts b/server/middlewares/validators/videos/shared/index.ts
deleted file mode 100644
index eb11dcc6a..000000000
--- a/server/middlewares/validators/videos/shared/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './upload'
2export * from './video-validators'
diff --git a/server/middlewares/validators/videos/shared/upload.ts b/server/middlewares/validators/videos/shared/upload.ts
deleted file mode 100644
index ea0dddc3c..000000000
--- a/server/middlewares/validators/videos/shared/upload.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import express from 'express'
2import { logger } from '@server/helpers/logger'
3import { getVideoStreamDuration } from '@shared/ffmpeg'
4import { HttpStatusCode } from '@shared/models'
5
6export async function addDurationToVideoFileIfNeeded (options: {
7 res: express.Response
8 videoFile: { path: string, duration?: number }
9 middlewareName: string
10}) {
11 const { res, middlewareName, videoFile } = options
12
13 try {
14 if (!videoFile.duration) await addDurationToVideo(videoFile)
15 } catch (err) {
16 logger.error('Invalid input file in ' + middlewareName, { err })
17
18 res.fail({
19 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
20 message: 'Video file unreadable.'
21 })
22 return false
23 }
24
25 return true
26}
27
28// ---------------------------------------------------------------------------
29// Private
30// ---------------------------------------------------------------------------
31
32async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
33 const duration = await getVideoStreamDuration(videoFile.path)
34
35 // FFmpeg may not be able to guess video duration
36 // For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
37 if (isNaN(duration)) videoFile.duration = 0
38 else videoFile.duration = duration
39}
diff --git a/server/middlewares/validators/videos/shared/video-validators.ts b/server/middlewares/validators/videos/shared/video-validators.ts
deleted file mode 100644
index 95e4fef11..000000000
--- a/server/middlewares/validators/videos/shared/video-validators.ts
+++ /dev/null
@@ -1,100 +0,0 @@
1import express from 'express'
2import { isVideoFileMimeTypeValid, isVideoFileSizeValid } from '@server/helpers/custom-validators/videos'
3import { logger } from '@server/helpers/logger'
4import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
5import { isLocalVideoFileAccepted } from '@server/lib/moderation'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { MUserAccountId, MVideo } from '@server/types/models'
8import { HttpStatusCode, ServerErrorCode, ServerFilterHookName, VideoState } from '@shared/models'
9import { checkUserQuota } from '../../shared'
10
11export async function commonVideoFileChecks (options: {
12 res: express.Response
13 user: MUserAccountId
14 videoFileSize: number
15 files: express.UploadFilesForCheck
16}): Promise<boolean> {
17 const { res, user, videoFileSize, files } = options
18
19 if (!isVideoFileMimeTypeValid(files)) {
20 res.fail({
21 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
22 message: 'This file is not supported. Please, make sure it is of the following type: ' +
23 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
24 })
25 return false
26 }
27
28 if (!isVideoFileSizeValid(videoFileSize.toString())) {
29 res.fail({
30 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
31 message: 'This file is too large. It exceeds the maximum file size authorized.',
32 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
33 })
34 return false
35 }
36
37 if (await checkUserQuota(user, videoFileSize, res) === false) return false
38
39 return true
40}
41
42export async function isVideoFileAccepted (options: {
43 req: express.Request
44 res: express.Response
45 videoFile: express.VideoUploadFile
46 hook: Extract<ServerFilterHookName, 'filter:api.video.upload.accept.result' | 'filter:api.video.update-file.accept.result'>
47}) {
48 const { req, res, videoFile, hook } = options
49
50 // Check we accept this video
51 const acceptParameters = {
52 videoBody: req.body,
53 videoFile,
54 user: res.locals.oauth.token.User
55 }
56 const acceptedResult = await Hooks.wrapFun(isLocalVideoFileAccepted, acceptParameters, hook)
57
58 if (!acceptedResult || acceptedResult.accepted !== true) {
59 logger.info('Refused local video file.', { acceptedResult, acceptParameters })
60 res.fail({
61 status: HttpStatusCode.FORBIDDEN_403,
62 message: acceptedResult.errorMessage || 'Refused local video file'
63 })
64 return false
65 }
66
67 return true
68}
69
70export function checkVideoFileCanBeEdited (video: MVideo, res: express.Response) {
71 if (video.isLive) {
72 res.fail({
73 status: HttpStatusCode.BAD_REQUEST_400,
74 message: 'Cannot edit a live video'
75 })
76
77 return false
78 }
79
80 if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) {
81 res.fail({
82 status: HttpStatusCode.CONFLICT_409,
83 message: 'Cannot edit video that is already waiting for transcoding/edition'
84 })
85
86 return false
87 }
88
89 const validStates = new Set([ VideoState.PUBLISHED, VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED, VideoState.TRANSCODING_FAILED ])
90 if (!validStates.has(video.state)) {
91 res.fail({
92 status: HttpStatusCode.BAD_REQUEST_400,
93 message: 'Video state is not compatible with edition'
94 })
95
96 return false
97 }
98
99 return true
100}
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
deleted file mode 100644
index 6b9aea07c..000000000
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ /dev/null
@@ -1,87 +0,0 @@
1import express from 'express'
2import { body, query } from 'express-validator'
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist'
6import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared'
7
8const videosBlacklistRemoveValidator = [
9 isValidVideoIdParam('videoId'),
10
11 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 if (areValidationErrors(req, res)) return
13 if (!await doesVideoExist(req.params.videoId, res)) return
14 if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
15
16 return next()
17 }
18]
19
20const videosBlacklistAddValidator = [
21 isValidVideoIdParam('videoId'),
22
23 body('unfederate')
24 .optional()
25 .customSanitizer(toBooleanOrNull)
26 .custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'),
27 body('reason')
28 .optional()
29 .custom(isVideoBlacklistReasonValid),
30
31 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
32 if (areValidationErrors(req, res)) return
33 if (!await doesVideoExist(req.params.videoId, res)) return
34
35 const video = res.locals.videoAll
36 if (req.body.unfederate === true && video.remote === true) {
37 return res.fail({
38 status: HttpStatusCode.CONFLICT_409,
39 message: 'You cannot unfederate a remote video.'
40 })
41 }
42
43 return next()
44 }
45]
46
47const videosBlacklistUpdateValidator = [
48 isValidVideoIdParam('videoId'),
49
50 body('reason')
51 .optional()
52 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
53
54 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
55 if (areValidationErrors(req, res)) return
56 if (!await doesVideoExist(req.params.videoId, res)) return
57 if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
58
59 return next()
60 }
61]
62
63const videosBlacklistFiltersValidator = [
64 query('type')
65 .optional()
66 .customSanitizer(toIntOrNull)
67 .custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
68 query('search')
69 .optional()
70 .not()
71 .isEmpty().withMessage('Should have a valid search'),
72
73 (req: express.Request, res: express.Response, next: express.NextFunction) => {
74 if (areValidationErrors(req, res)) return
75
76 return next()
77 }
78]
79
80// ---------------------------------------------------------------------------
81
82export {
83 videosBlacklistAddValidator,
84 videosBlacklistRemoveValidator,
85 videosBlacklistUpdateValidator,
86 videosBlacklistFiltersValidator
87}
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
deleted file mode 100644
index 077a58d2e..000000000
--- a/server/middlewares/validators/videos/video-captions.ts
+++ /dev/null
@@ -1,83 +0,0 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { UserRight } from '@shared/models'
4import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
5import { cleanUpReqFiles } from '../../../helpers/express-utils'
6import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
7import {
8 areValidationErrors,
9 checkCanSeeVideo,
10 checkUserCanManageVideo,
11 doesVideoCaptionExist,
12 doesVideoExist,
13 isValidVideoIdParam,
14 isValidVideoPasswordHeader
15} from '../shared'
16
17const addVideoCaptionValidator = [
18 isValidVideoIdParam('videoId'),
19
20 param('captionLanguage')
21 .custom(isVideoCaptionLanguageValid).not().isEmpty(),
22
23 body('captionfile')
24 .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile'))
25 .withMessage(
26 'This caption file is not supported or too large. ' +
27 `Please, make sure it is under ${CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max} bytes ` +
28 'and one of the following mimetypes: ' +
29 Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT).map(key => `${key} (${MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT[key]})`).join(', ')
30 ),
31
32 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
33 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
34 if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
35
36 // Check if the user who did the request is able to update the video
37 const user = res.locals.oauth.token.User
38 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
39
40 return next()
41 }
42]
43
44const deleteVideoCaptionValidator = [
45 isValidVideoIdParam('videoId'),
46
47 param('captionLanguage')
48 .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 if (areValidationErrors(req, res)) return
52 if (!await doesVideoExist(req.params.videoId, res)) return
53 if (!await doesVideoCaptionExist(res.locals.videoAll, req.params.captionLanguage, res)) return
54
55 // Check if the user who did the request is able to update the video
56 const user = res.locals.oauth.token.User
57 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
58
59 return next()
60 }
61]
62
63const listVideoCaptionsValidator = [
64 isValidVideoIdParam('videoId'),
65
66 isValidVideoPasswordHeader(),
67
68 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
69 if (areValidationErrors(req, res)) return
70 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
71
72 const video = res.locals.onlyVideo
73 if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
74
75 return next()
76 }
77]
78
79export {
80 addVideoCaptionValidator,
81 listVideoCaptionsValidator,
82 deleteVideoCaptionValidator
83}
diff --git a/server/middlewares/validators/videos/video-channel-sync.ts b/server/middlewares/validators/videos/video-channel-sync.ts
deleted file mode 100644
index 7e5b12471..000000000
--- a/server/middlewares/validators/videos/video-channel-sync.ts
+++ /dev/null
@@ -1,56 +0,0 @@
1import * as express from 'express'
2import { body, param } from 'express-validator'
3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { CONFIG } from '@server/initializers/config'
5import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
6import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models'
7import { areValidationErrors, doesVideoChannelIdExist } from '../shared'
8import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
9
10export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
12 return res.fail({
13 status: HttpStatusCode.FORBIDDEN_403,
14 message: 'Synchronization is impossible as video channel synchronization is not enabled on the server'
15 })
16 }
17
18 return next()
19}
20
21export const videoChannelSyncValidator = [
22 body('externalChannelUrl')
23 .custom(isUrlValid),
24
25 body('videoChannelId')
26 .isInt(),
27
28 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
29 if (areValidationErrors(req, res)) return
30
31 const body: VideoChannelSyncCreate = req.body
32 if (!await doesVideoChannelIdExist(body.videoChannelId, res)) return
33
34 const count = await VideoChannelSyncModel.countByAccount(res.locals.videoChannel.accountId)
35 if (count >= CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER) {
36 return res.fail({
37 message: `You cannot create more than ${CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER} channel synchronizations`
38 })
39 }
40
41 return next()
42 }
43]
44
45export const ensureSyncExists = [
46 param('id').exists().isInt().withMessage('Should have an sync id'),
47
48 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
50
51 if (!await doesVideoChannelSyncIdExist(+req.params.id, res)) return
52 if (!await doesVideoChannelIdExist(res.locals.videoChannelSync.videoChannelId, res)) return
53
54 return next()
55 }
56]
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
deleted file mode 100644
index ca6b57003..000000000
--- a/server/middlewares/validators/videos/video-channels.ts
+++ /dev/null
@@ -1,194 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { CONFIG } from '@server/initializers/config'
5import { MChannelAccountDefault } from '@server/types/models'
6import { VideosImportInChannelCreate } from '@shared/models'
7import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
8import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
9import {
10 isVideoChannelDescriptionValid,
11 isVideoChannelDisplayNameValid,
12 isVideoChannelSupportValid,
13 isVideoChannelUsernameValid
14} from '../../../helpers/custom-validators/video-channels'
15import { ActorModel } from '../../../models/actor/actor'
16import { VideoChannelModel } from '../../../models/video/video-channel'
17import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared'
18import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
19
20export const videoChannelsAddValidator = [
21 body('name')
22 .custom(isVideoChannelUsernameValid),
23 body('displayName')
24 .custom(isVideoChannelDisplayNameValid),
25 body('description')
26 .optional()
27 .custom(isVideoChannelDescriptionValid),
28 body('support')
29 .optional()
30 .custom(isVideoChannelSupportValid),
31
32 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
33 if (areValidationErrors(req, res)) return
34
35 const actor = await ActorModel.loadLocalByName(req.body.name)
36 if (actor) {
37 res.fail({
38 status: HttpStatusCode.CONFLICT_409,
39 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
40 })
41 return false
42 }
43
44 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
45 if (count >= CONFIG.VIDEO_CHANNELS.MAX_PER_USER) {
46 res.fail({ message: `You cannot create more than ${CONFIG.VIDEO_CHANNELS.MAX_PER_USER} channels` })
47 return false
48 }
49
50 return next()
51 }
52]
53
54export const videoChannelsUpdateValidator = [
55 param('nameWithHost')
56 .exists(),
57
58 body('displayName')
59 .optional()
60 .custom(isVideoChannelDisplayNameValid),
61 body('description')
62 .optional()
63 .custom(isVideoChannelDescriptionValid),
64 body('support')
65 .optional()
66 .custom(isVideoChannelSupportValid),
67 body('bulkVideosSupportUpdate')
68 .optional()
69 .custom(isBooleanValid).withMessage('Should have a valid bulkVideosSupportUpdate boolean field'),
70
71 (req: express.Request, res: express.Response, next: express.NextFunction) => {
72 if (areValidationErrors(req, res)) return
73
74 return next()
75 }
76]
77
78export const videoChannelsRemoveValidator = [
79 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
80 if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
81
82 return next()
83 }
84]
85
86export const videoChannelsNameWithHostValidator = [
87 param('nameWithHost')
88 .exists(),
89
90 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
91 if (areValidationErrors(req, res)) return
92
93 if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
94
95 return next()
96 }
97]
98
99export const ensureIsLocalChannel = [
100 (req: express.Request, res: express.Response, next: express.NextFunction) => {
101 if (res.locals.videoChannel.Actor.isOwned() === false) {
102 return res.fail({
103 status: HttpStatusCode.FORBIDDEN_403,
104 message: 'This channel is not owned.'
105 })
106 }
107
108 return next()
109 }
110]
111
112export const ensureChannelOwnerCanUpload = [
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 const channel = res.locals.videoChannel
115 const user = { id: channel.Account.userId }
116
117 if (!await checkUserQuota(user, 1, res)) return
118
119 next()
120 }
121]
122
123export const videoChannelStatsValidator = [
124 query('withStats')
125 .optional()
126 .customSanitizer(toBooleanOrNull)
127 .custom(isBooleanValid).withMessage('Should have a valid stats flag boolean'),
128
129 (req: express.Request, res: express.Response, next: express.NextFunction) => {
130 if (areValidationErrors(req, res)) return
131 return next()
132 }
133]
134
135export const videoChannelsListValidator = [
136 query('search')
137 .optional()
138 .not().isEmpty(),
139
140 (req: express.Request, res: express.Response, next: express.NextFunction) => {
141 if (areValidationErrors(req, res)) return
142
143 return next()
144 }
145]
146
147export const videoChannelImportVideosValidator = [
148 body('externalChannelUrl')
149 .custom(isUrlValid),
150
151 body('videoChannelSyncId')
152 .optional()
153 .custom(isIdValid),
154
155 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
156 if (areValidationErrors(req, res)) return
157
158 const body: VideosImportInChannelCreate = req.body
159
160 if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
161 return res.fail({
162 status: HttpStatusCode.FORBIDDEN_403,
163 message: 'Channel import is impossible as video upload via HTTP is not enabled on the server'
164 })
165 }
166
167 if (body.videoChannelSyncId && !await doesVideoChannelSyncIdExist(body.videoChannelSyncId, res)) return
168
169 if (res.locals.videoChannelSync && res.locals.videoChannelSync.videoChannelId !== res.locals.videoChannel.id) {
170 return res.fail({
171 status: HttpStatusCode.FORBIDDEN_403,
172 message: 'This channel sync is not owned by this channel'
173 })
174 }
175
176 return next()
177 }
178]
179
180// ---------------------------------------------------------------------------
181
182async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
183 const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
184
185 if (count <= 1) {
186 res.fail({
187 status: HttpStatusCode.CONFLICT_409,
188 message: 'Cannot remove the last channel of this user'
189 })
190 return false
191 }
192
193 return true
194}
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
deleted file mode 100644
index 70689b02e..000000000
--- a/server/middlewares/validators/videos/video-comments.ts
+++ /dev/null
@@ -1,249 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { MUserAccountUrl } from '@server/types/models'
4import { HttpStatusCode, UserRight } from '@shared/models'
5import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
6import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
7import { logger } from '../../../helpers/logger'
8import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
9import { Hooks } from '../../../lib/plugins/hooks'
10import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
11import {
12 areValidationErrors,
13 checkCanSeeVideo,
14 doesVideoCommentExist,
15 doesVideoCommentThreadExist,
16 doesVideoExist,
17 isValidVideoIdParam,
18 isValidVideoPasswordHeader
19} from '../shared'
20
21const listVideoCommentsValidator = [
22 query('isLocal')
23 .optional()
24 .customSanitizer(toBooleanOrNull)
25 .custom(isBooleanValid)
26 .withMessage('Should have a valid isLocal boolean'),
27
28 query('onLocalVideo')
29 .optional()
30 .customSanitizer(toBooleanOrNull)
31 .custom(isBooleanValid)
32 .withMessage('Should have a valid onLocalVideo boolean'),
33
34 query('search')
35 .optional()
36 .custom(exists),
37
38 query('searchAccount')
39 .optional()
40 .custom(exists),
41
42 query('searchVideo')
43 .optional()
44 .custom(exists),
45
46 (req: express.Request, res: express.Response, next: express.NextFunction) => {
47 if (areValidationErrors(req, res)) return
48
49 return next()
50 }
51]
52
53const listVideoCommentThreadsValidator = [
54 isValidVideoIdParam('videoId'),
55 isValidVideoPasswordHeader(),
56
57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
58 if (areValidationErrors(req, res)) return
59 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
60
61 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
62
63 return next()
64 }
65]
66
67const listVideoThreadCommentsValidator = [
68 isValidVideoIdParam('videoId'),
69
70 param('threadId')
71 .custom(isIdValid),
72 isValidVideoPasswordHeader(),
73
74 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
75 if (areValidationErrors(req, res)) return
76 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
77 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
78
79 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
80
81 return next()
82 }
83]
84
85const addVideoCommentThreadValidator = [
86 isValidVideoIdParam('videoId'),
87
88 body('text')
89 .custom(isValidVideoCommentText),
90 isValidVideoPasswordHeader(),
91
92 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
93 if (areValidationErrors(req, res)) return
94 if (!await doesVideoExist(req.params.videoId, res)) return
95
96 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
97
98 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
99 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
100
101 return next()
102 }
103]
104
105const addVideoCommentReplyValidator = [
106 isValidVideoIdParam('videoId'),
107
108 param('commentId').custom(isIdValid),
109 isValidVideoPasswordHeader(),
110
111 body('text').custom(isValidVideoCommentText),
112
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 if (areValidationErrors(req, res)) return
115 if (!await doesVideoExist(req.params.videoId, res)) return
116
117 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
118
119 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
120 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
121 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
122
123 return next()
124 }
125]
126
127const videoCommentGetValidator = [
128 isValidVideoIdParam('videoId'),
129
130 param('commentId')
131 .custom(isIdValid),
132
133 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
134 if (areValidationErrors(req, res)) return
135 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
136 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoId, res)) return
137
138 return next()
139 }
140]
141
142const removeVideoCommentValidator = [
143 isValidVideoIdParam('videoId'),
144
145 param('commentId')
146 .custom(isIdValid),
147
148 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
149 if (areValidationErrors(req, res)) return
150 if (!await doesVideoExist(req.params.videoId, res)) return
151 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
152
153 // Check if the user who did the request is able to delete the video
154 if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
155
156 return next()
157 }
158]
159
160// ---------------------------------------------------------------------------
161
162export {
163 listVideoCommentThreadsValidator,
164 listVideoThreadCommentsValidator,
165 addVideoCommentThreadValidator,
166 listVideoCommentsValidator,
167 addVideoCommentReplyValidator,
168 videoCommentGetValidator,
169 removeVideoCommentValidator
170}
171
172// ---------------------------------------------------------------------------
173
174function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
175 if (video.commentsEnabled !== true) {
176 res.fail({
177 status: HttpStatusCode.CONFLICT_409,
178 message: 'Video comments are disabled for this video.'
179 })
180 return false
181 }
182
183 return true
184}
185
186function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
187 if (videoComment.isDeleted()) {
188 res.fail({
189 status: HttpStatusCode.CONFLICT_409,
190 message: 'This comment is already deleted'
191 })
192 return false
193 }
194
195 const userAccount = user.Account
196
197 if (
198 user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && // Not a moderator
199 videoComment.accountId !== userAccount.id && // Not the comment owner
200 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
201 ) {
202 res.fail({
203 status: HttpStatusCode.FORBIDDEN_403,
204 message: 'Cannot remove video comment of another user'
205 })
206 return false
207 }
208
209 return true
210}
211
212async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
213 const acceptParameters = {
214 video,
215 commentBody: req.body,
216 user: res.locals.oauth.token.User,
217 req
218 }
219
220 let acceptedResult: AcceptResult
221
222 if (isReply) {
223 const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
224
225 acceptedResult = await Hooks.wrapFun(
226 isLocalVideoCommentReplyAccepted,
227 acceptReplyParameters,
228 'filter:api.video-comment-reply.create.accept.result'
229 )
230 } else {
231 acceptedResult = await Hooks.wrapFun(
232 isLocalVideoThreadAccepted,
233 acceptParameters,
234 'filter:api.video-thread.create.accept.result'
235 )
236 }
237
238 if (!acceptedResult || acceptedResult.accepted !== true) {
239 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
240
241 res.fail({
242 status: HttpStatusCode.FORBIDDEN_403,
243 message: acceptedResult?.errorMessage || 'Comment has been rejected.'
244 })
245 return false
246 }
247
248 return true
249}
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts
deleted file mode 100644
index 6c0ecda42..000000000
--- a/server/middlewares/validators/videos/video-files.ts
+++ /dev/null
@@ -1,163 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { isIdValid } from '@server/helpers/custom-validators/misc'
4import { MVideo } from '@server/types/models'
5import { HttpStatusCode } from '@shared/models'
6import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
7
8const videoFilesDeleteWebVideoValidator = [
9 isValidVideoIdParam('id'),
10
11 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 if (areValidationErrors(req, res)) return
13 if (!await doesVideoExist(req.params.id, res)) return
14
15 const video = res.locals.videoAll
16
17 if (!checkLocalVideo(video, res)) return
18
19 if (!video.hasWebVideoFiles()) {
20 return res.fail({
21 status: HttpStatusCode.BAD_REQUEST_400,
22 message: 'This video does not have Web Video files'
23 })
24 }
25
26 if (!video.getHLSPlaylist()) {
27 return res.fail({
28 status: HttpStatusCode.BAD_REQUEST_400,
29 message: 'Cannot delete Web Video files since this video does not have HLS playlist'
30 })
31 }
32
33 return next()
34 }
35]
36
37const videoFilesDeleteWebVideoFileValidator = [
38 isValidVideoIdParam('id'),
39
40 param('videoFileId')
41 .custom(isIdValid),
42
43 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 if (areValidationErrors(req, res)) return
45 if (!await doesVideoExist(req.params.id, res)) return
46
47 const video = res.locals.videoAll
48
49 if (!checkLocalVideo(video, res)) return
50
51 const files = video.VideoFiles
52 if (!files.find(f => f.id === +req.params.videoFileId)) {
53 return res.fail({
54 status: HttpStatusCode.NOT_FOUND_404,
55 message: 'This video does not have this Web Video file id'
56 })
57 }
58
59 if (files.length === 1 && !video.getHLSPlaylist()) {
60 return res.fail({
61 status: HttpStatusCode.BAD_REQUEST_400,
62 message: 'Cannot delete Web Video files since this video does not have HLS playlist'
63 })
64 }
65
66 return next()
67 }
68]
69
70// ---------------------------------------------------------------------------
71
72const videoFilesDeleteHLSValidator = [
73 isValidVideoIdParam('id'),
74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 if (areValidationErrors(req, res)) return
77 if (!await doesVideoExist(req.params.id, res)) return
78
79 const video = res.locals.videoAll
80
81 if (!checkLocalVideo(video, res)) return
82
83 if (!video.getHLSPlaylist()) {
84 return res.fail({
85 status: HttpStatusCode.BAD_REQUEST_400,
86 message: 'This video does not have HLS files'
87 })
88 }
89
90 if (!video.hasWebVideoFiles()) {
91 return res.fail({
92 status: HttpStatusCode.BAD_REQUEST_400,
93 message: 'Cannot delete HLS playlist since this video does not have Web Video files'
94 })
95 }
96
97 return next()
98 }
99]
100
101const videoFilesDeleteHLSFileValidator = [
102 isValidVideoIdParam('id'),
103
104 param('videoFileId')
105 .custom(isIdValid),
106
107 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
108 if (areValidationErrors(req, res)) return
109 if (!await doesVideoExist(req.params.id, res)) return
110
111 const video = res.locals.videoAll
112
113 if (!checkLocalVideo(video, res)) return
114
115 if (!video.getHLSPlaylist()) {
116 return res.fail({
117 status: HttpStatusCode.BAD_REQUEST_400,
118 message: 'This video does not have HLS files'
119 })
120 }
121
122 const hlsFiles = video.getHLSPlaylist().VideoFiles
123 if (!hlsFiles.find(f => f.id === +req.params.videoFileId)) {
124 return res.fail({
125 status: HttpStatusCode.NOT_FOUND_404,
126 message: 'This HLS playlist does not have this file id'
127 })
128 }
129
130 // Last file to delete
131 if (hlsFiles.length === 1 && !video.hasWebVideoFiles()) {
132 return res.fail({
133 status: HttpStatusCode.BAD_REQUEST_400,
134 message: 'Cannot delete last HLS playlist file since this video does not have Web Video files'
135 })
136 }
137
138 return next()
139 }
140]
141
142export {
143 videoFilesDeleteWebVideoValidator,
144 videoFilesDeleteWebVideoFileValidator,
145
146 videoFilesDeleteHLSValidator,
147 videoFilesDeleteHLSFileValidator
148}
149
150// ---------------------------------------------------------------------------
151
152function checkLocalVideo (video: MVideo, res: express.Response) {
153 if (video.remote) {
154 res.fail({
155 status: HttpStatusCode.BAD_REQUEST_400,
156 message: 'Cannot delete files of remote video'
157 })
158
159 return false
160 }
161
162 return true
163}
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
deleted file mode 100644
index a1cb65b70..000000000
--- a/server/middlewares/validators/videos/video-imports.ts
+++ /dev/null
@@ -1,209 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { isResolvingToUnicastOnly } from '@server/helpers/dns'
4import { isPreImportVideoAccepted } from '@server/lib/moderation'
5import { Hooks } from '@server/lib/plugins/hooks'
6import { MUserAccountId, MVideoImport } from '@server/types/models'
7import { forceNumber } from '@shared/core-utils'
8import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models'
9import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
10import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
11import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
12import {
13 isValidPasswordProtectedPrivacy,
14 isVideoMagnetUriValid,
15 isVideoNameValid
16} from '../../../helpers/custom-validators/videos'
17import { cleanUpReqFiles } from '../../../helpers/express-utils'
18import { logger } from '../../../helpers/logger'
19import { CONFIG } from '../../../initializers/config'
20import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
21import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared'
22import { getCommonVideoEditAttributes } from './videos'
23
24const videoImportAddValidator = getCommonVideoEditAttributes().concat([
25 body('channelId')
26 .customSanitizer(toIntOrNull)
27 .custom(isIdValid),
28 body('targetUrl')
29 .optional()
30 .custom(isVideoImportTargetUrlValid),
31 body('magnetUri')
32 .optional()
33 .custom(isVideoMagnetUriValid),
34 body('torrentfile')
35 .custom((value, { req }) => isVideoImportTorrentFile(req.files))
36 .withMessage(
37 'This torrent file is not supported or too large. Please, make sure it is of the following type: ' +
38 CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
39 ),
40 body('name')
41 .optional()
42 .custom(isVideoNameValid).withMessage(
43 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
44 ),
45 body('videoPasswords')
46 .optional()
47 .isArray()
48 .withMessage('Video passwords should be an array.'),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 const user = res.locals.oauth.token.User
52 const torrentFile = req.files?.['torrentfile'] ? req.files['torrentfile'][0] : undefined
53
54 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
55
56 if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
57
58 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
59 cleanUpReqFiles(req)
60
61 return res.fail({
62 status: HttpStatusCode.CONFLICT_409,
63 message: 'HTTP import is not enabled on this instance.'
64 })
65 }
66
67 if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
68 cleanUpReqFiles(req)
69
70 return res.fail({
71 status: HttpStatusCode.CONFLICT_409,
72 message: 'Torrent/magnet URI import is not enabled on this instance.'
73 })
74 }
75
76 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
77
78 // Check we have at least 1 required param
79 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
80 cleanUpReqFiles(req)
81
82 return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' })
83 }
84
85 if (req.body.targetUrl) {
86 const hostname = new URL(req.body.targetUrl).hostname
87
88 if (await isResolvingToUnicastOnly(hostname) !== true) {
89 cleanUpReqFiles(req)
90
91 return res.fail({
92 status: HttpStatusCode.FORBIDDEN_403,
93 message: 'Cannot use non unicast IP as targetUrl.'
94 })
95 }
96 }
97
98 if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
99
100 return next()
101 }
102])
103
104const getMyVideoImportsValidator = [
105 query('videoChannelSyncId')
106 .optional()
107 .custom(isIdValid),
108
109 (req: express.Request, res: express.Response, next: express.NextFunction) => {
110 if (areValidationErrors(req, res)) return
111
112 return next()
113 }
114]
115
116const videoImportDeleteValidator = [
117 param('id')
118 .custom(isIdValid),
119
120 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
121 if (areValidationErrors(req, res)) return
122
123 if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
124 if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
125
126 if (res.locals.videoImport.state === VideoImportState.PENDING) {
127 return res.fail({
128 status: HttpStatusCode.CONFLICT_409,
129 message: 'Cannot delete a pending video import. Cancel it or wait for the end of the import first.'
130 })
131 }
132
133 return next()
134 }
135]
136
137const videoImportCancelValidator = [
138 param('id')
139 .custom(isIdValid),
140
141 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
142 if (areValidationErrors(req, res)) return
143
144 if (!await doesVideoImportExist(forceNumber(req.params.id), res)) return
145 if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
146
147 if (res.locals.videoImport.state !== VideoImportState.PENDING) {
148 return res.fail({
149 status: HttpStatusCode.CONFLICT_409,
150 message: 'Cannot cancel a non pending video import.'
151 })
152 }
153
154 return next()
155 }
156]
157
158// ---------------------------------------------------------------------------
159
160export {
161 videoImportAddValidator,
162 videoImportCancelValidator,
163 videoImportDeleteValidator,
164 getMyVideoImportsValidator
165}
166
167// ---------------------------------------------------------------------------
168
169async function isImportAccepted (req: express.Request, res: express.Response) {
170 const body: VideoImportCreate = req.body
171 const hookName = body.targetUrl
172 ? 'filter:api.video.pre-import-url.accept.result'
173 : 'filter:api.video.pre-import-torrent.accept.result'
174
175 // Check we accept this video
176 const acceptParameters = {
177 videoImportBody: body,
178 user: res.locals.oauth.token.User
179 }
180 const acceptedResult = await Hooks.wrapFun(
181 isPreImportVideoAccepted,
182 acceptParameters,
183 hookName
184 )
185
186 if (!acceptedResult || acceptedResult.accepted !== true) {
187 logger.info('Refused to import video.', { acceptedResult, acceptParameters })
188
189 res.fail({
190 status: HttpStatusCode.FORBIDDEN_403,
191 message: acceptedResult.errorMessage || 'Refused to import video'
192 })
193 return false
194 }
195
196 return true
197}
198
199function checkUserCanManageImport (user: MUserAccountId, videoImport: MVideoImport, res: express.Response) {
200 if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === false && videoImport.userId !== user.id) {
201 res.fail({
202 status: HttpStatusCode.FORBIDDEN_403,
203 message: 'Cannot manage video import of another user'
204 })
205 return false
206 }
207
208 return true
209}
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts
deleted file mode 100644
index ec69a3011..000000000
--- a/server/middlewares/validators/videos/video-live.ts
+++ /dev/null
@@ -1,342 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives'
4import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
5import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { VideoModel } from '@server/models/video/video'
8import { VideoLiveModel } from '@server/models/video/video-live'
9import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
10import {
11 HttpStatusCode,
12 LiveVideoCreate,
13 LiveVideoLatencyMode,
14 LiveVideoUpdate,
15 ServerErrorCode,
16 UserRight,
17 VideoState
18} from '@shared/models'
19import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
20import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos'
21import { cleanUpReqFiles } from '../../../helpers/express-utils'
22import { logger } from '../../../helpers/logger'
23import { CONFIG } from '../../../initializers/config'
24import {
25 areValidationErrors,
26 checkUserCanManageVideo,
27 doesVideoChannelOfAccountExist,
28 doesVideoExist,
29 isValidVideoIdParam
30} from '../shared'
31import { getCommonVideoEditAttributes } from './videos'
32
33const videoLiveGetValidator = [
34 isValidVideoIdParam('videoId'),
35
36 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
37 if (areValidationErrors(req, res)) return
38 if (!await doesVideoExist(req.params.videoId, res, 'all')) return
39
40 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
41 if (!videoLive) {
42 return res.fail({
43 status: HttpStatusCode.NOT_FOUND_404,
44 message: 'Live video not found'
45 })
46 }
47
48 res.locals.videoLive = videoLive
49
50 return next()
51 }
52]
53
54const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
55 body('channelId')
56 .customSanitizer(toIntOrNull)
57 .custom(isIdValid),
58
59 body('name')
60 .custom(isVideoNameValid).withMessage(
61 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
62 ),
63
64 body('saveReplay')
65 .optional()
66 .customSanitizer(toBooleanOrNull)
67 .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
68
69 body('replaySettings.privacy')
70 .optional()
71 .customSanitizer(toIntOrNull)
72 .custom(isVideoReplayPrivacyValid),
73
74 body('permanentLive')
75 .optional()
76 .customSanitizer(toBooleanOrNull)
77 .custom(isBooleanValid).withMessage('Should have a valid permanentLive boolean'),
78
79 body('latencyMode')
80 .optional()
81 .customSanitizer(toIntOrNull)
82 .custom(isLiveLatencyModeValid),
83
84 body('videoPasswords')
85 .optional()
86 .isArray()
87 .withMessage('Video passwords should be an array.'),
88
89 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
90 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
91
92 if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
93
94 if (CONFIG.LIVE.ENABLED !== true) {
95 cleanUpReqFiles(req)
96
97 return res.fail({
98 status: HttpStatusCode.FORBIDDEN_403,
99 message: 'Live is not enabled on this instance',
100 type: ServerErrorCode.LIVE_NOT_ENABLED
101 })
102 }
103
104 const body: LiveVideoCreate = req.body
105
106 if (hasValidSaveReplay(body) !== true) {
107 cleanUpReqFiles(req)
108
109 return res.fail({
110 status: HttpStatusCode.FORBIDDEN_403,
111 message: 'Saving live replay is not enabled on this instance',
112 type: ServerErrorCode.LIVE_NOT_ALLOWING_REPLAY
113 })
114 }
115
116 if (hasValidLatencyMode(body) !== true) {
117 cleanUpReqFiles(req)
118
119 return res.fail({
120 status: HttpStatusCode.FORBIDDEN_403,
121 message: 'Custom latency mode is not allowed by this instance'
122 })
123 }
124
125 if (body.saveReplay && !body.replaySettings?.privacy) {
126 cleanUpReqFiles(req)
127
128 return res.fail({
129 status: HttpStatusCode.BAD_REQUEST_400,
130 message: 'Live replay is enabled but privacy replay setting is missing'
131 })
132 }
133
134 const user = res.locals.oauth.token.User
135 if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req)
136
137 if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
138 const totalInstanceLives = await VideoModel.countLives({ remote: false, mode: 'not-ended' })
139
140 if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
141 cleanUpReqFiles(req)
142
143 return res.fail({
144 status: HttpStatusCode.FORBIDDEN_403,
145 message: 'Cannot create this live because the max instance lives limit is reached.',
146 type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED
147 })
148 }
149 }
150
151 if (CONFIG.LIVE.MAX_USER_LIVES !== -1) {
152 const totalUserLives = await VideoModel.countLivesOfAccount(user.Account.id)
153
154 if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
155 cleanUpReqFiles(req)
156
157 return res.fail({
158 status: HttpStatusCode.FORBIDDEN_403,
159 message: 'Cannot create this live because the max user lives limit is reached.',
160 type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED
161 })
162 }
163 }
164
165 if (!await isLiveVideoAccepted(req, res)) return cleanUpReqFiles(req)
166
167 return next()
168 }
169])
170
171const videoLiveUpdateValidator = [
172 body('saveReplay')
173 .optional()
174 .customSanitizer(toBooleanOrNull)
175 .custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
176
177 body('replaySettings.privacy')
178 .optional()
179 .customSanitizer(toIntOrNull)
180 .custom(isVideoReplayPrivacyValid),
181
182 body('latencyMode')
183 .optional()
184 .customSanitizer(toIntOrNull)
185 .custom(isLiveLatencyModeValid),
186
187 (req: express.Request, res: express.Response, next: express.NextFunction) => {
188 if (areValidationErrors(req, res)) return
189
190 const body: LiveVideoUpdate = req.body
191
192 if (hasValidSaveReplay(body) !== true) {
193 return res.fail({
194 status: HttpStatusCode.FORBIDDEN_403,
195 message: 'Saving live replay is not allowed by this instance'
196 })
197 }
198
199 if (hasValidLatencyMode(body) !== true) {
200 return res.fail({
201 status: HttpStatusCode.FORBIDDEN_403,
202 message: 'Custom latency mode is not allowed by this instance'
203 })
204 }
205
206 if (!checkLiveSettingsReplayConsistency({ res, body })) return
207
208 if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
209 return res.fail({ message: 'Cannot update a live that has already started' })
210 }
211
212 // Check the user can manage the live
213 const user = res.locals.oauth.token.User
214 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
215
216 return next()
217 }
218]
219
220const videoLiveListSessionsValidator = [
221 (req: express.Request, res: express.Response, next: express.NextFunction) => {
222 // Check the user can manage the live
223 const user = res.locals.oauth.token.User
224 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
225
226 return next()
227 }
228]
229
230const videoLiveFindReplaySessionValidator = [
231 isValidVideoIdParam('videoId'),
232
233 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
234 if (areValidationErrors(req, res)) return
235 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
236
237 const session = await VideoLiveSessionModel.findSessionOfReplay(res.locals.videoId.id)
238 if (!session) {
239 return res.fail({
240 status: HttpStatusCode.NOT_FOUND_404,
241 message: 'No live replay found'
242 })
243 }
244
245 res.locals.videoLiveSession = session
246
247 return next()
248 }
249]
250
251// ---------------------------------------------------------------------------
252
253export {
254 videoLiveAddValidator,
255 videoLiveUpdateValidator,
256 videoLiveListSessionsValidator,
257 videoLiveFindReplaySessionValidator,
258 videoLiveGetValidator
259}
260
261// ---------------------------------------------------------------------------
262
263async function isLiveVideoAccepted (req: express.Request, res: express.Response) {
264 // Check we accept this video
265 const acceptParameters = {
266 liveVideoBody: req.body,
267 user: res.locals.oauth.token.User
268 }
269 const acceptedResult = await Hooks.wrapFun(
270 isLocalLiveVideoAccepted,
271 acceptParameters,
272 'filter:api.live-video.create.accept.result'
273 )
274
275 if (!acceptedResult || acceptedResult.accepted !== true) {
276 logger.info('Refused local live video.', { acceptedResult, acceptParameters })
277
278 res.fail({
279 status: HttpStatusCode.FORBIDDEN_403,
280 message: acceptedResult.errorMessage || 'Refused local live video'
281 })
282 return false
283 }
284
285 return true
286}
287
288function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) {
289 if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false
290
291 return true
292}
293
294function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
295 if (
296 CONFIG.LIVE.LATENCY_SETTING.ENABLED !== true &&
297 exists(body.latencyMode) &&
298 body.latencyMode !== LiveVideoLatencyMode.DEFAULT
299 ) return false
300
301 return true
302}
303
304function checkLiveSettingsReplayConsistency (options: {
305 res: express.Response
306 body: LiveVideoUpdate
307}) {
308 const { res, body } = options
309
310 // We now save replays of this live, so replay settings are mandatory
311 if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) {
312
313 if (!exists(body.replaySettings)) {
314 res.fail({
315 status: HttpStatusCode.BAD_REQUEST_400,
316 message: 'Replay settings are missing now the live replay is saved'
317 })
318 return false
319 }
320
321 if (!exists(body.replaySettings.privacy)) {
322 res.fail({
323 status: HttpStatusCode.BAD_REQUEST_400,
324 message: 'Privacy replay setting is missing now the live replay is saved'
325 })
326 return false
327 }
328 }
329
330 // Save replay was and is not enabled, so send an error the user if it specified replay settings
331 if ((!exists(body.saveReplay) && res.locals.videoLive.saveReplay === false) || body.saveReplay === false) {
332 if (exists(body.replaySettings)) {
333 res.fail({
334 status: HttpStatusCode.BAD_REQUEST_400,
335 message: 'Cannot save replay settings since live replay is not enabled'
336 })
337 return false
338 }
339 }
340
341 return true
342}
diff --git a/server/middlewares/validators/videos/video-ownership-changes.ts b/server/middlewares/validators/videos/video-ownership-changes.ts
deleted file mode 100644
index 3eca78c25..000000000
--- a/server/middlewares/validators/videos/video-ownership-changes.ts
+++ /dev/null
@@ -1,107 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { isIdValid } from '@server/helpers/custom-validators/misc'
4import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership'
5import { AccountModel } from '@server/models/account/account'
6import { MVideoWithAllFiles } from '@server/types/models'
7import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models'
8import {
9 areValidationErrors,
10 checkUserCanManageVideo,
11 checkUserQuota,
12 doesChangeVideoOwnershipExist,
13 doesVideoChannelOfAccountExist,
14 doesVideoExist,
15 isValidVideoIdParam
16} from '../shared'
17
18const videosChangeOwnershipValidator = [
19 isValidVideoIdParam('videoId'),
20
21 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
22 if (areValidationErrors(req, res)) return
23 if (!await doesVideoExist(req.params.videoId, res)) return
24
25 // Check if the user who did the request is able to change the ownership of the video
26 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
27
28 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
29 if (!nextOwner) {
30 res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
31 return
32 }
33
34 res.locals.nextOwner = nextOwner
35 return next()
36 }
37]
38
39const videosTerminateChangeOwnershipValidator = [
40 param('id')
41 .custom(isIdValid),
42
43 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 if (areValidationErrors(req, res)) return
45 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
46
47 // Check if the user who did the request is able to change the ownership of the video
48 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
49
50 const videoChangeOwnership = res.locals.videoChangeOwnership
51
52 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
53 res.fail({
54 status: HttpStatusCode.FORBIDDEN_403,
55 message: 'Ownership already accepted or refused'
56 })
57 return
58 }
59
60 return next()
61 }
62]
63
64const videosAcceptChangeOwnershipValidator = [
65 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
66 const body = req.body as VideoChangeOwnershipAccept
67 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
68
69 const videoChangeOwnership = res.locals.videoChangeOwnership
70
71 const video = videoChangeOwnership.Video
72
73 if (!await checkCanAccept(video, res)) return
74
75 return next()
76 }
77]
78
79export {
80 videosChangeOwnershipValidator,
81 videosTerminateChangeOwnershipValidator,
82 videosAcceptChangeOwnershipValidator
83}
84
85// ---------------------------------------------------------------------------
86
87async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response): Promise<boolean> {
88 if (video.isLive) {
89
90 if (video.state !== VideoState.WAITING_FOR_LIVE) {
91 res.fail({
92 status: HttpStatusCode.BAD_REQUEST_400,
93 message: 'You can accept an ownership change of a published live.'
94 })
95
96 return false
97 }
98
99 return true
100 }
101
102 const user = res.locals.oauth.token.User
103
104 if (!await checkUserQuota(user, video.getMaxQualityFile().size, res)) return false
105
106 return true
107}
diff --git a/server/middlewares/validators/videos/video-passwords.ts b/server/middlewares/validators/videos/video-passwords.ts
deleted file mode 100644
index 200e496f6..000000000
--- a/server/middlewares/validators/videos/video-passwords.ts
+++ /dev/null
@@ -1,77 +0,0 @@
1import express from 'express'
2import {
3 areValidationErrors,
4 doesVideoExist,
5 isVideoPasswordProtected,
6 isValidVideoIdParam,
7 doesVideoPasswordExist,
8 isVideoPasswordDeletable,
9 checkUserCanManageVideo
10} from '../shared'
11import { body, param } from 'express-validator'
12import { isIdValid } from '@server/helpers/custom-validators/misc'
13import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos'
14import { UserRight } from '@shared/models'
15
16const listVideoPasswordValidator = [
17 isValidVideoIdParam('videoId'),
18
19 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
20 if (areValidationErrors(req, res)) return
21
22 if (!await doesVideoExist(req.params.videoId, res)) return
23 if (!isVideoPasswordProtected(res)) return
24
25 // Check if the user who did the request is able to access video password list
26 const user = res.locals.oauth.token.User
27 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return
28
29 return next()
30 }
31]
32
33const updateVideoPasswordListValidator = [
34 body('passwords')
35 .optional()
36 .isArray()
37 .withMessage('Video passwords should be an array.'),
38
39 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
40 if (areValidationErrors(req, res)) return
41
42 if (!await doesVideoExist(req.params.videoId, res)) return
43 if (!isValidPasswordProtectedPrivacy(req, res)) return
44
45 // Check if the user who did the request is able to update video passwords
46 const user = res.locals.oauth.token.User
47 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
48
49 return next()
50 }
51]
52
53const removeVideoPasswordValidator = [
54 isValidVideoIdParam('videoId'),
55
56 param('passwordId')
57 .custom(isIdValid),
58
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 if (areValidationErrors(req, res)) return
61
62 if (!await doesVideoExist(req.params.videoId, res)) return
63 if (!isVideoPasswordProtected(res)) return
64 if (!await doesVideoPasswordExist(req.params.passwordId, res)) return
65 if (!await isVideoPasswordDeletable(res)) return
66
67 return next()
68 }
69]
70
71// ---------------------------------------------------------------------------
72
73export {
74 listVideoPasswordValidator,
75 updateVideoPasswordListValidator,
76 removeVideoPasswordValidator
77}
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
deleted file mode 100644
index 95a5ba63a..000000000
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ /dev/null
@@ -1,419 +0,0 @@
1import express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator'
3import { ExpressPromiseHandler } from '@server/types/express-handler'
4import { MUserAccountId } from '@server/types/models'
5import { forceNumber } from '@shared/core-utils'
6import {
7 HttpStatusCode,
8 UserRight,
9 VideoPlaylistCreate,
10 VideoPlaylistPrivacy,
11 VideoPlaylistType,
12 VideoPlaylistUpdate
13} from '@shared/models'
14import {
15 isArrayOf,
16 isIdOrUUIDValid,
17 isIdValid,
18 isUUIDValid,
19 toCompleteUUID,
20 toIntArray,
21 toIntOrNull,
22 toValueOrNull
23} from '../../../helpers/custom-validators/misc'
24import {
25 isVideoPlaylistDescriptionValid,
26 isVideoPlaylistNameValid,
27 isVideoPlaylistPrivacyValid,
28 isVideoPlaylistTimestampValid,
29 isVideoPlaylistTypeValid
30} from '../../../helpers/custom-validators/video-playlists'
31import { isVideoImageValid } from '../../../helpers/custom-validators/videos'
32import { cleanUpReqFiles } from '../../../helpers/express-utils'
33import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
34import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
35import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
36import { authenticatePromise } from '../../auth'
37import {
38 areValidationErrors,
39 doesVideoChannelIdExist,
40 doesVideoExist,
41 doesVideoPlaylistExist,
42 isValidPlaylistIdParam,
43 VideoPlaylistFetchType
44} from '../shared'
45
46const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
47 body('displayName')
48 .custom(isVideoPlaylistNameValid),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
52
53 const body: VideoPlaylistCreate = req.body
54 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
55
56 if (
57 !body.videoChannelId &&
58 (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
59 ) {
60 cleanUpReqFiles(req)
61
62 return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
63 }
64
65 return next()
66 }
67])
68
69const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
70 isValidPlaylistIdParam('playlistId'),
71
72 body('displayName')
73 .optional()
74 .custom(isVideoPlaylistNameValid),
75
76 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
77 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
78
79 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
80
81 const videoPlaylist = getPlaylist(res)
82
83 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
84 return cleanUpReqFiles(req)
85 }
86
87 const body: VideoPlaylistUpdate = req.body
88
89 const newPrivacy = body.privacy || videoPlaylist.privacy
90 if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
91 (
92 (!videoPlaylist.videoChannelId && !body.videoChannelId) ||
93 body.videoChannelId === null
94 )
95 ) {
96 cleanUpReqFiles(req)
97
98 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
99 }
100
101 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
102 cleanUpReqFiles(req)
103
104 return res.fail({ message: 'Cannot update a watch later playlist.' })
105 }
106
107 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
108
109 return next()
110 }
111])
112
113const videoPlaylistsDeleteValidator = [
114 isValidPlaylistIdParam('playlistId'),
115
116 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
117 if (areValidationErrors(req, res)) return
118
119 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
120
121 const videoPlaylist = getPlaylist(res)
122 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
123 return res.fail({ message: 'Cannot delete a watch later playlist.' })
124 }
125
126 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
127 return
128 }
129
130 return next()
131 }
132]
133
134const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
135 return [
136 isValidPlaylistIdParam('playlistId'),
137
138 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
139 if (areValidationErrors(req, res)) return
140
141 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
142
143 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
144
145 // Playlist is unlisted, check we used the uuid to fetch it
146 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
147 if (isUUIDValid(req.params.playlistId)) return next()
148
149 return res.fail({
150 status: HttpStatusCode.NOT_FOUND_404,
151 message: 'Playlist not found'
152 })
153 }
154
155 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
156 await authenticatePromise({ req, res })
157
158 const user = res.locals.oauth ? res.locals.oauth.token.User : null
159
160 if (
161 !user ||
162 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
163 ) {
164 return res.fail({
165 status: HttpStatusCode.FORBIDDEN_403,
166 message: 'Cannot get this private video playlist.'
167 })
168 }
169
170 return next()
171 }
172
173 return next()
174 }
175 ]
176}
177
178const videoPlaylistsSearchValidator = [
179 query('search')
180 .optional()
181 .not().isEmpty(),
182
183 (req: express.Request, res: express.Response, next: express.NextFunction) => {
184 if (areValidationErrors(req, res)) return
185
186 return next()
187 }
188]
189
190const videoPlaylistsAddVideoValidator = [
191 isValidPlaylistIdParam('playlistId'),
192
193 body('videoId')
194 .customSanitizer(toCompleteUUID)
195 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid/short uuid'),
196 body('startTimestamp')
197 .optional()
198 .custom(isVideoPlaylistTimestampValid),
199 body('stopTimestamp')
200 .optional()
201 .custom(isVideoPlaylistTimestampValid),
202
203 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
204 if (areValidationErrors(req, res)) return
205
206 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
207 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
208
209 const videoPlaylist = getPlaylist(res)
210
211 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
212 return
213 }
214
215 return next()
216 }
217]
218
219const videoPlaylistsUpdateOrRemoveVideoValidator = [
220 isValidPlaylistIdParam('playlistId'),
221 param('playlistElementId')
222 .customSanitizer(toCompleteUUID)
223 .custom(isIdValid).withMessage('Should have an element id/uuid/short uuid'),
224 body('startTimestamp')
225 .optional()
226 .custom(isVideoPlaylistTimestampValid),
227 body('stopTimestamp')
228 .optional()
229 .custom(isVideoPlaylistTimestampValid),
230
231 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
232 if (areValidationErrors(req, res)) return
233
234 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
235
236 const videoPlaylist = getPlaylist(res)
237
238 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
239 if (!videoPlaylistElement) {
240 res.fail({
241 status: HttpStatusCode.NOT_FOUND_404,
242 message: 'Video playlist element not found'
243 })
244 return
245 }
246 res.locals.videoPlaylistElement = videoPlaylistElement
247
248 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
249
250 return next()
251 }
252]
253
254const videoPlaylistElementAPGetValidator = [
255 isValidPlaylistIdParam('playlistId'),
256 param('playlistElementId')
257 .custom(isIdValid),
258
259 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
260 if (areValidationErrors(req, res)) return
261
262 const playlistElementId = forceNumber(req.params.playlistElementId)
263 const playlistId = req.params.playlistId
264
265 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
266 if (!videoPlaylistElement) {
267 res.fail({
268 status: HttpStatusCode.NOT_FOUND_404,
269 message: 'Video playlist element not found'
270 })
271 return
272 }
273
274 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
275 return res.fail({
276 status: HttpStatusCode.FORBIDDEN_403,
277 message: 'Cannot get this private video playlist.'
278 })
279 }
280
281 res.locals.videoPlaylistElementAP = videoPlaylistElement
282
283 return next()
284 }
285]
286
287const videoPlaylistsReorderVideosValidator = [
288 isValidPlaylistIdParam('playlistId'),
289
290 body('startPosition')
291 .isInt({ min: 1 }),
292 body('insertAfterPosition')
293 .isInt({ min: 0 }),
294 body('reorderLength')
295 .optional()
296 .isInt({ min: 1 }),
297
298 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
299 if (areValidationErrors(req, res)) return
300
301 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
302
303 const videoPlaylist = getPlaylist(res)
304 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
305
306 const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
307 const startPosition: number = req.body.startPosition
308 const insertAfterPosition: number = req.body.insertAfterPosition
309 const reorderLength: number = req.body.reorderLength
310
311 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
312 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
313 return
314 }
315
316 if (reorderLength && reorderLength + startPosition > nextPosition) {
317 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
318 return
319 }
320
321 return next()
322 }
323]
324
325const commonVideoPlaylistFiltersValidator = [
326 query('playlistType')
327 .optional()
328 .custom(isVideoPlaylistTypeValid),
329
330 (req: express.Request, res: express.Response, next: express.NextFunction) => {
331 if (areValidationErrors(req, res)) return
332
333 return next()
334 }
335]
336
337const doVideosInPlaylistExistValidator = [
338 query('videoIds')
339 .customSanitizer(toIntArray)
340 .custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
341
342 (req: express.Request, res: express.Response, next: express.NextFunction) => {
343 if (areValidationErrors(req, res)) return
344
345 return next()
346 }
347]
348
349// ---------------------------------------------------------------------------
350
351export {
352 videoPlaylistsAddValidator,
353 videoPlaylistsUpdateValidator,
354 videoPlaylistsDeleteValidator,
355 videoPlaylistsGetValidator,
356 videoPlaylistsSearchValidator,
357
358 videoPlaylistsAddVideoValidator,
359 videoPlaylistsUpdateOrRemoveVideoValidator,
360 videoPlaylistsReorderVideosValidator,
361
362 videoPlaylistElementAPGetValidator,
363
364 commonVideoPlaylistFiltersValidator,
365
366 doVideosInPlaylistExistValidator
367}
368
369// ---------------------------------------------------------------------------
370
371function getCommonPlaylistEditAttributes () {
372 return [
373 body('thumbnailfile')
374 .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
375 .withMessage(
376 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
377 CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
378 ),
379
380 body('description')
381 .optional()
382 .customSanitizer(toValueOrNull)
383 .custom(isVideoPlaylistDescriptionValid),
384 body('privacy')
385 .optional()
386 .customSanitizer(toIntOrNull)
387 .custom(isVideoPlaylistPrivacyValid),
388 body('videoChannelId')
389 .optional()
390 .customSanitizer(toIntOrNull)
391 ] as (ValidationChain | ExpressPromiseHandler)[]
392}
393
394function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
395 if (videoPlaylist.isOwned() === false) {
396 res.fail({
397 status: HttpStatusCode.FORBIDDEN_403,
398 message: 'Cannot manage video playlist of another server.'
399 })
400 return false
401 }
402
403 // Check if the user can manage the video playlist
404 // The user can delete it if s/he is an admin
405 // Or if s/he is the video playlist's owner
406 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
407 res.fail({
408 status: HttpStatusCode.FORBIDDEN_403,
409 message: 'Cannot manage video playlist of another user'
410 })
411 return false
412 }
413
414 return true
415}
416
417function getPlaylist (res: express.Response) {
418 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
419}
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
deleted file mode 100644
index c837b047b..000000000
--- a/server/middlewares/validators/videos/video-rates.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { VideoRateType } from '../../../../shared/models/videos'
5import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
6import { isIdValid } from '../../../helpers/custom-validators/misc'
7import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
8import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
9import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
10import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared'
11
12const videoUpdateRateValidator = [
13 isValidVideoIdParam('id'),
14
15 body('rating')
16 .custom(isVideoRatingTypeValid),
17 isValidVideoPasswordHeader(),
18
19 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
20 if (areValidationErrors(req, res)) return
21 if (!await doesVideoExist(req.params.id, res)) return
22
23 if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return
24
25 return next()
26 }
27]
28
29const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) {
30 return [
31 param('name')
32 .custom(isAccountNameValid),
33 param('videoId')
34 .custom(isIdValid),
35
36 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
37 if (areValidationErrors(req, res)) return
38
39 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId)
40 if (!rate) {
41 return res.fail({
42 status: HttpStatusCode.NOT_FOUND_404,
43 message: 'Video rate not found'
44 })
45 }
46
47 res.locals.accountVideoRate = rate
48
49 return next()
50 }
51 ]
52}
53
54const videoRatingValidator = [
55 query('rating')
56 .optional()
57 .custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
58
59 (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 if (areValidationErrors(req, res)) return
61
62 return next()
63 }
64]
65
66// ---------------------------------------------------------------------------
67
68export {
69 videoUpdateRateValidator,
70 getAccountVideoRateValidatorFactory,
71 videoRatingValidator
72}
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts
deleted file mode 100644
index c234de6ed..000000000
--- a/server/middlewares/validators/videos/video-shares.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { isIdValid } from '../../../helpers/custom-validators/misc'
5import { VideoShareModel } from '../../../models/video/video-share'
6import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
7
8const videosShareValidator = [
9 isValidVideoIdParam('id'),
10
11 param('actorId')
12 .custom(isIdValid),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 if (areValidationErrors(req, res)) return
16 if (!await doesVideoExist(req.params.id, res)) return
17
18 const video = res.locals.videoAll
19
20 const share = await VideoShareModel.load(req.params.actorId, video.id)
21 if (!share) {
22 return res.status(HttpStatusCode.NOT_FOUND_404)
23 .end()
24 }
25
26 res.locals.videoShare = share
27 return next()
28 }
29]
30
31// ---------------------------------------------------------------------------
32
33export {
34 videosShareValidator
35}
diff --git a/server/middlewares/validators/videos/video-source.ts b/server/middlewares/validators/videos/video-source.ts
deleted file mode 100644
index bbccb58b0..000000000
--- a/server/middlewares/validators/videos/video-source.ts
+++ /dev/null
@@ -1,130 +0,0 @@
1import express from 'express'
2import { body, header } from 'express-validator'
3import { getResumableUploadPath } from '@server/helpers/upload'
4import { getVideoWithAttributes } from '@server/helpers/video'
5import { CONFIG } from '@server/initializers/config'
6import { uploadx } from '@server/lib/uploadx'
7import { VideoSourceModel } from '@server/models/video/video-source'
8import { MVideoFullLight } from '@server/types/models'
9import { HttpStatusCode, UserRight } from '@shared/models'
10import { Metadata as UploadXMetadata } from '@uploadx/core'
11import { logger } from '../../../helpers/logger'
12import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
13import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared'
14
15export const videoSourceGetLatestValidator = [
16 isValidVideoIdParam('id'),
17
18 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 if (areValidationErrors(req, res)) return
20 if (!await doesVideoExist(req.params.id, res, 'all')) return
21
22 const video = getVideoWithAttributes(res) as MVideoFullLight
23
24 const user = res.locals.oauth.token.User
25 if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
26
27 res.locals.videoSource = await VideoSourceModel.loadLatest(video.id)
28
29 if (!res.locals.videoSource) {
30 return res.fail({
31 status: HttpStatusCode.NOT_FOUND_404,
32 message: 'Video source not found'
33 })
34 }
35
36 return next()
37 }
38]
39
40export const replaceVideoSourceResumableValidator = [
41 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 const body: express.CustomUploadXFile<UploadXMetadata> = req.body
43 const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename }
44 const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err }))
45
46 if (!await checkCanUpdateVideoFile({ req, res })) {
47 return cleanup()
48 }
49
50 if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'updateVideoFileResumableValidator' })) {
51 return cleanup()
52 }
53
54 if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.update-file.accept.result' })) {
55 return cleanup()
56 }
57
58 res.locals.updateVideoFileResumable = { ...file, originalname: file.filename }
59
60 return next()
61 }
62]
63
64export const replaceVideoSourceResumableInitValidator = [
65 body('filename')
66 .exists(),
67
68 header('x-upload-content-length')
69 .isNumeric()
70 .exists()
71 .withMessage('Should specify the file length'),
72 header('x-upload-content-type')
73 .isString()
74 .exists()
75 .withMessage('Should specify the file mimetype'),
76
77 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
78 const user = res.locals.oauth.token.User
79
80 logger.debug('Checking updateVideoFileResumableInitValidator parameters and headers', {
81 parameters: req.body,
82 headers: req.headers
83 })
84
85 if (areValidationErrors(req, res, { omitLog: true })) return
86
87 if (!await checkCanUpdateVideoFile({ req, res })) return
88
89 const videoFileMetadata = {
90 mimetype: req.headers['x-upload-content-type'] as string,
91 size: +req.headers['x-upload-content-length'],
92 originalname: req.body.filename
93 }
94
95 const files = { videofile: [ videoFileMetadata ] }
96 if (await commonVideoFileChecks({ res, user, videoFileSize: videoFileMetadata.size, files }) === false) return
97
98 return next()
99 }
100]
101
102// ---------------------------------------------------------------------------
103// Private
104// ---------------------------------------------------------------------------
105
106async function checkCanUpdateVideoFile (options: {
107 req: express.Request
108 res: express.Response
109}) {
110 const { req, res } = options
111
112 if (!CONFIG.VIDEO_FILE.UPDATE.ENABLED) {
113 res.fail({
114 status: HttpStatusCode.FORBIDDEN_403,
115 message: 'Updating the file of an existing video is not allowed on this instance'
116 })
117 return false
118 }
119
120 if (!await doesVideoExist(req.params.id, res)) return false
121
122 const user = res.locals.oauth.token.User
123 const video = res.locals.videoAll
124
125 if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return false
126
127 if (!checkVideoFileCanBeEdited(video, res)) return false
128
129 return true
130}
diff --git a/server/middlewares/validators/videos/video-stats.ts b/server/middlewares/validators/videos/video-stats.ts
deleted file mode 100644
index a79526d39..000000000
--- a/server/middlewares/validators/videos/video-stats.ts
+++ /dev/null
@@ -1,108 +0,0 @@
1import express from 'express'
2import { param, query } from 'express-validator'
3import { isDateValid } from '@server/helpers/custom-validators/misc'
4import { isValidStatTimeserieMetric } from '@server/helpers/custom-validators/video-stats'
5import { STATS_TIMESERIE } from '@server/initializers/constants'
6import { HttpStatusCode, UserRight, VideoStatsTimeserieQuery } from '@shared/models'
7import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
8
9const videoOverallStatsValidator = [
10 isValidVideoIdParam('videoId'),
11
12 query('startDate')
13 .optional()
14 .custom(isDateValid),
15
16 query('endDate')
17 .optional()
18 .custom(isDateValid),
19
20 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
21 if (areValidationErrors(req, res)) return
22 if (!await commonStatsCheck(req, res)) return
23
24 return next()
25 }
26]
27
28const videoRetentionStatsValidator = [
29 isValidVideoIdParam('videoId'),
30
31 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
32 if (areValidationErrors(req, res)) return
33 if (!await commonStatsCheck(req, res)) return
34
35 if (res.locals.videoAll.isLive) {
36 return res.fail({
37 status: HttpStatusCode.BAD_REQUEST_400,
38 message: 'Cannot get retention stats of live video'
39 })
40 }
41
42 return next()
43 }
44]
45
46const videoTimeserieStatsValidator = [
47 isValidVideoIdParam('videoId'),
48
49 param('metric')
50 .custom(isValidStatTimeserieMetric),
51
52 query('startDate')
53 .optional()
54 .custom(isDateValid),
55
56 query('endDate')
57 .optional()
58 .custom(isDateValid),
59
60 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
61 if (areValidationErrors(req, res)) return
62 if (!await commonStatsCheck(req, res)) return
63
64 const query: VideoStatsTimeserieQuery = req.query
65 if (
66 (query.startDate && !query.endDate) ||
67 (!query.startDate && query.endDate)
68 ) {
69 return res.fail({
70 status: HttpStatusCode.BAD_REQUEST_400,
71 message: 'Both start date and end date should be defined if one of them is specified'
72 })
73 }
74
75 if (query.startDate && getIntervalByDays(query.startDate, query.endDate) > STATS_TIMESERIE.MAX_DAYS) {
76 return res.fail({
77 status: HttpStatusCode.BAD_REQUEST_400,
78 message: 'Star date and end date interval is too big'
79 })
80 }
81
82 return next()
83 }
84]
85
86// ---------------------------------------------------------------------------
87
88export {
89 videoOverallStatsValidator,
90 videoTimeserieStatsValidator,
91 videoRetentionStatsValidator
92}
93
94// ---------------------------------------------------------------------------
95
96async function commonStatsCheck (req: express.Request, res: express.Response) {
97 if (!await doesVideoExist(req.params.videoId, res, 'all')) return false
98 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return false
99
100 return true
101}
102
103function getIntervalByDays (startDateString: string, endDateString: string) {
104 const startDate = new Date(startDateString)
105 const endDate = new Date(endDateString)
106
107 return (endDate.getTime() - startDate.getTime()) / 1000 / 86400
108}
diff --git a/server/middlewares/validators/videos/video-studio.ts b/server/middlewares/validators/videos/video-studio.ts
deleted file mode 100644
index a375af60a..000000000
--- a/server/middlewares/validators/videos/video-studio.ts
+++ /dev/null
@@ -1,105 +0,0 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc'
4import {
5 isStudioCutTaskValid,
6 isStudioTaskAddIntroOutroValid,
7 isStudioTaskAddWatermarkValid,
8 isValidStudioTasksArray
9} from '@server/helpers/custom-validators/video-studio'
10import { cleanUpReqFiles } from '@server/helpers/express-utils'
11import { CONFIG } from '@server/initializers/config'
12import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio'
13import { isAudioFile } from '@shared/ffmpeg'
14import { HttpStatusCode, UserRight, VideoStudioCreateEdition, VideoStudioTask } from '@shared/models'
15import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared'
16import { checkVideoFileCanBeEdited } from './shared'
17
18const videoStudioAddEditionValidator = [
19 param('videoId')
20 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid/short uuid'),
21
22 body('tasks')
23 .custom(isValidStudioTasksArray).withMessage('Should have a valid array of tasks'),
24
25 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 if (CONFIG.VIDEO_STUDIO.ENABLED !== true) {
27 res.fail({
28 status: HttpStatusCode.BAD_REQUEST_400,
29 message: 'Video studio is disabled on this instance'
30 })
31
32 return cleanUpReqFiles(req)
33 }
34
35 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
36
37 const body: VideoStudioCreateEdition = req.body
38 const files = req.files as Express.Multer.File[]
39
40 for (let i = 0; i < body.tasks.length; i++) {
41 const task = body.tasks[i]
42
43 if (!checkTask(req, task, i)) {
44 res.fail({
45 status: HttpStatusCode.BAD_REQUEST_400,
46 message: `Task ${task.name} is invalid`
47 })
48
49 return cleanUpReqFiles(req)
50 }
51
52 if (task.name === 'add-intro' || task.name === 'add-outro') {
53 const filePath = getTaskFileFromReq(files, i).path
54
55 // Our concat filter needs a video stream
56 if (await isAudioFile(filePath)) {
57 res.fail({
58 status: HttpStatusCode.BAD_REQUEST_400,
59 message: `Task ${task.name} is invalid: file does not contain a video stream`
60 })
61
62 return cleanUpReqFiles(req)
63 }
64 }
65 }
66
67 if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
68
69 const video = res.locals.videoAll
70 if (!checkVideoFileCanBeEdited(video, res)) return cleanUpReqFiles(req)
71
72 const user = res.locals.oauth.token.User
73 if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
74
75 // Try to make an approximation of bytes added by the intro/outro
76 const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFileFromReq(files, i).path)
77 if (await checkUserQuota(user, additionalBytes, res) === false) return cleanUpReqFiles(req)
78
79 return next()
80 }
81]
82
83// ---------------------------------------------------------------------------
84
85export {
86 videoStudioAddEditionValidator
87}
88
89// ---------------------------------------------------------------------------
90
91const taskCheckers: {
92 [id in VideoStudioTask['name']]: (task: VideoStudioTask, indice?: number, files?: Express.Multer.File[]) => boolean
93} = {
94 'cut': isStudioCutTaskValid,
95 'add-intro': isStudioTaskAddIntroOutroValid,
96 'add-outro': isStudioTaskAddIntroOutroValid,
97 'add-watermark': isStudioTaskAddWatermarkValid
98}
99
100function checkTask (req: express.Request, task: VideoStudioTask, indice?: number) {
101 const checker = taskCheckers[task.name]
102 if (!checker) return false
103
104 return checker(task, indice, req.files as Express.Multer.File[])
105}
diff --git a/server/middlewares/validators/videos/video-token.ts b/server/middlewares/validators/videos/video-token.ts
deleted file mode 100644
index d4253e21d..000000000
--- a/server/middlewares/validators/videos/video-token.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import express from 'express'
2import { VideoPrivacy } from '../../../../shared/models/videos'
3import { HttpStatusCode } from '@shared/models'
4import { exists } from '@server/helpers/custom-validators/misc'
5
6const videoFileTokenValidator = [
7 (req: express.Request, res: express.Response, next: express.NextFunction) => {
8 const video = res.locals.onlyVideo
9 if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED && !exists(res.locals.oauth.token.User)) {
10 return res.fail({
11 status: HttpStatusCode.UNAUTHORIZED_401,
12 message: 'Not authenticated'
13 })
14 }
15
16 return next()
17 }
18]
19
20// ---------------------------------------------------------------------------
21
22export {
23 videoFileTokenValidator
24}
diff --git a/server/middlewares/validators/videos/video-transcoding.ts b/server/middlewares/validators/videos/video-transcoding.ts
deleted file mode 100644
index 2f99ff42c..000000000
--- a/server/middlewares/validators/videos/video-transcoding.ts
+++ /dev/null
@@ -1,61 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
4import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding'
5import { CONFIG } from '@server/initializers/config'
6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
7import { HttpStatusCode, ServerErrorCode, VideoTranscodingCreate } from '@shared/models'
8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
9
10const createTranscodingValidator = [
11 isValidVideoIdParam('videoId'),
12
13 body('transcodingType')
14 .custom(isValidCreateTranscodingType),
15
16 body('forceTranscoding')
17 .optional()
18 .customSanitizer(toBooleanOrNull)
19 .custom(isBooleanValid),
20
21 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
22 if (areValidationErrors(req, res)) return
23 if (!await doesVideoExist(req.params.videoId, res, 'all')) return
24
25 const video = res.locals.videoAll
26
27 if (video.remote) {
28 return res.fail({
29 status: HttpStatusCode.BAD_REQUEST_400,
30 message: 'Cannot run transcoding job on a remote video'
31 })
32 }
33
34 if (CONFIG.TRANSCODING.ENABLED !== true) {
35 return res.fail({
36 status: HttpStatusCode.BAD_REQUEST_400,
37 message: 'Cannot run transcoding job because transcoding is disabled on this instance'
38 })
39 }
40
41 const body = req.body as VideoTranscodingCreate
42 if (body.forceTranscoding === true) return next()
43
44 const info = await VideoJobInfoModel.load(video.id)
45 if (info && info.pendingTranscode > 0) {
46 return res.fail({
47 status: HttpStatusCode.CONFLICT_409,
48 type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED,
49 message: 'This video is already being transcoded'
50 })
51 }
52
53 return next()
54 }
55]
56
57// ---------------------------------------------------------------------------
58
59export {
60 createTranscodingValidator
61}
diff --git a/server/middlewares/validators/videos/video-view.ts b/server/middlewares/validators/videos/video-view.ts
deleted file mode 100644
index a2f61f4ba..000000000
--- a/server/middlewares/validators/videos/video-view.ts
+++ /dev/null
@@ -1,61 +0,0 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { isVideoTimeValid } from '@server/helpers/custom-validators/video-view'
4import { getCachedVideoDuration } from '@server/lib/video'
5import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
6import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
7import { isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
9
10const getVideoLocalViewerValidator = [
11 param('localViewerId')
12 .custom(isIdValid),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 if (areValidationErrors(req, res)) return
16
17 const localViewer = await LocalVideoViewerModel.loadFullById(+req.params.localViewerId)
18 if (!localViewer) {
19 return res.fail({
20 status: HttpStatusCode.NOT_FOUND_404,
21 message: 'Local viewer not found'
22 })
23 }
24
25 res.locals.localViewerFull = localViewer
26
27 return next()
28 }
29]
30
31const videoViewValidator = [
32 isValidVideoIdParam('videoId'),
33
34 body('currentTime')
35 .customSanitizer(toIntOrNull)
36 .custom(isIntOrNull),
37
38 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
39 if (areValidationErrors(req, res)) return
40 if (!await doesVideoExist(req.params.videoId, res, 'only-immutable-attributes')) return
41
42 const video = res.locals.onlyImmutableVideo
43 const { duration } = await getCachedVideoDuration(video.id)
44
45 if (!isVideoTimeValid(req.body.currentTime, duration)) {
46 return res.fail({
47 status: HttpStatusCode.BAD_REQUEST_400,
48 message: 'Current time is invalid'
49 })
50 }
51
52 return next()
53 }
54]
55
56// ---------------------------------------------------------------------------
57
58export {
59 videoViewValidator,
60 getVideoLocalViewerValidator
61}
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}