aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares
diff options
context:
space:
mode:
Diffstat (limited to 'server/middlewares')
-rw-r--r--server/middlewares/activitypub.ts156
-rw-r--r--server/middlewares/async.ts44
-rw-r--r--server/middlewares/auth.ts113
-rw-r--r--server/middlewares/cache/cache.ts38
-rw-r--r--server/middlewares/cache/index.ts1
-rw-r--r--server/middlewares/cache/shared/api-cache.ts314
-rw-r--r--server/middlewares/cache/shared/index.ts1
-rw-r--r--server/middlewares/csp.ts40
-rw-r--r--server/middlewares/dnt.ts15
-rw-r--r--server/middlewares/doc.ts16
-rw-r--r--server/middlewares/error.ts63
-rw-r--r--server/middlewares/index.ts15
-rw-r--r--server/middlewares/pagination.ts19
-rw-r--r--server/middlewares/rate-limiter.ts59
-rw-r--r--server/middlewares/robots.ts13
-rw-r--r--server/middlewares/servers.ts29
-rw-r--r--server/middlewares/sort.ts29
-rw-r--r--server/middlewares/user-right.ts26
-rw-r--r--server/middlewares/validators/abuse.ts255
-rw-r--r--server/middlewares/validators/account.ts35
-rw-r--r--server/middlewares/validators/activitypub/activity.ts29
-rw-r--r--server/middlewares/validators/activitypub/index.ts3
-rw-r--r--server/middlewares/validators/activitypub/pagination.ts25
-rw-r--r--server/middlewares/validators/activitypub/signature.ts39
-rw-r--r--server/middlewares/validators/actor-image.ts27
-rw-r--r--server/middlewares/validators/blocklist.ts179
-rw-r--r--server/middlewares/validators/bulk.ts38
-rw-r--r--server/middlewares/validators/config.ts194
-rw-r--r--server/middlewares/validators/express.ts15
-rw-r--r--server/middlewares/validators/feeds.ts178
-rw-r--r--server/middlewares/validators/follows.ts158
-rw-r--r--server/middlewares/validators/index.ts31
-rw-r--r--server/middlewares/validators/jobs.ts29
-rw-r--r--server/middlewares/validators/logs.ts93
-rw-r--r--server/middlewares/validators/metrics.ts60
-rw-r--r--server/middlewares/validators/object-storage-proxy.ts20
-rw-r--r--server/middlewares/validators/oembed.ts158
-rw-r--r--server/middlewares/validators/pagination.ts30
-rw-r--r--server/middlewares/validators/plugins.ts218
-rw-r--r--server/middlewares/validators/redundancy.ts198
-rw-r--r--server/middlewares/validators/runners/index.ts3
-rw-r--r--server/middlewares/validators/runners/job-files.ts60
-rw-r--r--server/middlewares/validators/runners/jobs.ts216
-rw-r--r--server/middlewares/validators/runners/registration-token.ts37
-rw-r--r--server/middlewares/validators/runners/runners.ts104
-rw-r--r--server/middlewares/validators/search.ts112
-rw-r--r--server/middlewares/validators/server.ts75
-rw-r--r--server/middlewares/validators/shared/abuses.ts26
-rw-r--r--server/middlewares/validators/shared/accounts.ts66
-rw-r--r--server/middlewares/validators/shared/index.ts14
-rw-r--r--server/middlewares/validators/shared/user-registrations.ts60
-rw-r--r--server/middlewares/validators/shared/users.ts63
-rw-r--r--server/middlewares/validators/shared/utils.ts69
-rw-r--r--server/middlewares/validators/shared/video-blacklists.ts24
-rw-r--r--server/middlewares/validators/shared/video-captions.ts25
-rw-r--r--server/middlewares/validators/shared/video-channel-syncs.ts24
-rw-r--r--server/middlewares/validators/shared/video-channels.ts36
-rw-r--r--server/middlewares/validators/shared/video-comments.ts80
-rw-r--r--server/middlewares/validators/shared/video-imports.ts22
-rw-r--r--server/middlewares/validators/shared/video-ownerships.ts25
-rw-r--r--server/middlewares/validators/shared/video-passwords.ts80
-rw-r--r--server/middlewares/validators/shared/video-playlists.ts39
-rw-r--r--server/middlewares/validators/shared/videos.ts311
-rw-r--r--server/middlewares/validators/sort.ts66
-rw-r--r--server/middlewares/validators/static.ts184
-rw-r--r--server/middlewares/validators/themes.ts46
-rw-r--r--server/middlewares/validators/two-factor.ts81
-rw-r--r--server/middlewares/validators/user-email-verification.ts94
-rw-r--r--server/middlewares/validators/user-history.ts47
-rw-r--r--server/middlewares/validators/user-notifications.ts71
-rw-r--r--server/middlewares/validators/user-registrations.ts208
-rw-r--r--server/middlewares/validators/user-subscriptions.ts111
-rw-r--r--server/middlewares/validators/users.ts489
-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
-rw-r--r--server/middlewares/validators/webfinger.ts37
98 files changed, 0 insertions, 9325 deletions
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
deleted file mode 100644
index 261b9f690..000000000
--- a/server/middlewares/activitypub.ts
+++ /dev/null
@@ -1,156 +0,0 @@
1import { NextFunction, Request, Response } from 'express'
2import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
3import { getAPId } from '@server/lib/activitypub/activity'
4import { wrapWithSpanAndContext } from '@server/lib/opentelemetry/tracing'
5import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@shared/models'
6import { logger } from '../helpers/logger'
7import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
8import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
9import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors'
10
11async function checkSignature (req: Request, res: Response, next: NextFunction) {
12 try {
13 const httpSignatureChecked = await checkHttpSignature(req, res)
14 if (httpSignatureChecked !== true) return
15
16 const actor = res.locals.signature.actor
17
18 // Forwarded activity
19 const bodyActor = req.body.actor
20 const bodyActorId = getAPId(bodyActor)
21 if (bodyActorId && bodyActorId !== actor.url) {
22 const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
23 if (jsonLDSignatureChecked !== true) return
24 }
25
26 return next()
27 } catch (err) {
28 const activity: ActivityDelete = req.body
29 if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
30 logger.debug('Handling signature error on actor delete activity', { err })
31 return res.status(HttpStatusCode.NO_CONTENT_204).end()
32 }
33
34 logger.warn('Error in ActivityPub signature checker.', { err })
35 return res.fail({
36 status: HttpStatusCode.FORBIDDEN_403,
37 message: 'ActivityPub signature could not be checked'
38 })
39 }
40}
41
42function executeIfActivityPub (req: Request, res: Response, next: NextFunction) {
43 const accepted = req.accepts(ACCEPT_HEADERS)
44 if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.includes(accepted) === false) {
45 // Bypass this route
46 return next('route')
47 }
48
49 logger.debug('ActivityPub request for %s.', req.url)
50
51 return next()
52}
53
54// ---------------------------------------------------------------------------
55
56export {
57 checkSignature,
58 executeIfActivityPub,
59 checkHttpSignature
60}
61
62// ---------------------------------------------------------------------------
63
64async function checkHttpSignature (req: Request, res: Response) {
65 return wrapWithSpanAndContext('peertube.activitypub.checkHTTPSignature', async () => {
66 // FIXME: compatibility with http-signature < v1.3
67 const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string
68 if (sig && sig.startsWith('Signature ') === true) req.headers[HTTP_SIGNATURE.HEADER_NAME] = sig.replace(/^Signature /, '')
69
70 let parsed: any
71
72 try {
73 parsed = parseHTTPSignature(req, HTTP_SIGNATURE.CLOCK_SKEW_SECONDS)
74 } catch (err) {
75 logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err })
76
77 res.fail({
78 status: HttpStatusCode.FORBIDDEN_403,
79 message: err.message
80 })
81 return false
82 }
83
84 const keyId = parsed.keyId
85 if (!keyId) {
86 res.fail({
87 status: HttpStatusCode.FORBIDDEN_403,
88 message: 'Invalid key ID',
89 data: {
90 keyId
91 }
92 })
93 return false
94 }
95
96 logger.debug('Checking HTTP signature of actor %s...', keyId)
97
98 let [ actorUrl ] = keyId.split('#')
99 if (actorUrl.startsWith('acct:')) {
100 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
101 }
102
103 const actor = await getOrCreateAPActor(actorUrl)
104
105 const verified = isHTTPSignatureVerified(parsed, actor)
106 if (verified !== true) {
107 logger.warn('Signature from %s is invalid', actorUrl, { parsed })
108
109 res.fail({
110 status: HttpStatusCode.FORBIDDEN_403,
111 message: 'Invalid signature',
112 data: {
113 actorUrl
114 }
115 })
116 return false
117 }
118
119 res.locals.signature = { actor }
120 return true
121 })
122}
123
124async function checkJsonLDSignature (req: Request, res: Response) {
125 return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => {
126 const signatureObject: ActivityPubSignature = req.body.signature
127
128 if (!signatureObject?.creator) {
129 res.fail({
130 status: HttpStatusCode.FORBIDDEN_403,
131 message: 'Object and creator signature do not match'
132 })
133 return false
134 }
135
136 const [ creator ] = signatureObject.creator.split('#')
137
138 logger.debug('Checking JsonLD signature of actor %s...', creator)
139
140 const actor = await getOrCreateAPActor(creator)
141 const verified = await isJsonLDSignatureVerified(actor, req.body)
142
143 if (verified !== true) {
144 logger.warn('Signature not verified.', req.body)
145
146 res.fail({
147 status: HttpStatusCode.FORBIDDEN_403,
148 message: 'Signature could not be verified'
149 })
150 return false
151 }
152
153 res.locals.signature = { actor }
154 return true
155 })
156}
diff --git a/server/middlewares/async.ts b/server/middlewares/async.ts
deleted file mode 100644
index 7e131257d..000000000
--- a/server/middlewares/async.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import Bluebird from 'bluebird'
2import { NextFunction, Request, RequestHandler, Response } from 'express'
3import { ValidationChain } from 'express-validator'
4import { ExpressPromiseHandler } from '@server/types/express-handler'
5import { retryTransactionWrapper } from '../helpers/database-utils'
6
7// Syntactic sugar to avoid try/catch in express controllers/middlewares
8
9export type RequestPromiseHandler = ValidationChain | ExpressPromiseHandler
10
11function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]) {
12 return (req: Request, res: Response, next: NextFunction) => {
13 if (Array.isArray(fun) === true) {
14 return Bluebird.each(fun as RequestPromiseHandler[], f => {
15 return new Promise<void>((resolve, reject) => {
16 return asyncMiddleware(f)(req, res, err => {
17 if (err) return reject(err)
18
19 return resolve()
20 })
21 })
22 }).then(() => next())
23 .catch(err => next(err))
24 }
25
26 return Promise.resolve((fun as RequestHandler)(req, res, next))
27 .catch(err => next(err))
28 }
29}
30
31function asyncRetryTransactionMiddleware (fun: (req: Request, res: Response, next: NextFunction) => Promise<any>) {
32 return (req: Request, res: Response, next: NextFunction) => {
33 return Promise.resolve(
34 retryTransactionWrapper(fun, req, res, next)
35 ).catch(err => next(err))
36 }
37}
38
39// ---------------------------------------------------------------------------
40
41export {
42 asyncMiddleware,
43 asyncRetryTransactionMiddleware
44}
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts
deleted file mode 100644
index 39a7b2998..000000000
--- a/server/middlewares/auth.ts
+++ /dev/null
@@ -1,113 +0,0 @@
1import express from 'express'
2import { Socket } from 'socket.io'
3import { getAccessToken } from '@server/lib/auth/oauth-model'
4import { RunnerModel } from '@server/models/runner/runner'
5import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
6import { logger } from '../helpers/logger'
7import { handleOAuthAuthenticate } from '../lib/auth/oauth'
8import { ServerErrorCode } from '@shared/models'
9
10function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
11 handleOAuthAuthenticate(req, res)
12 .then((token: any) => {
13 res.locals.oauth = { token }
14 res.locals.authenticated = true
15
16 return next()
17 })
18 .catch(err => {
19 logger.info('Cannot authenticate.', { err })
20
21 return res.fail({
22 status: err.status,
23 message: 'Token is invalid',
24 type: err.name
25 })
26 })
27}
28
29function authenticateSocket (socket: Socket, next: (err?: any) => void) {
30 const accessToken = socket.handshake.query['accessToken']
31
32 logger.debug('Checking access token in runner.')
33
34 if (!accessToken) return next(new Error('No access token provided'))
35 if (typeof accessToken !== 'string') return next(new Error('Access token is invalid'))
36
37 getAccessToken(accessToken)
38 .then(tokenDB => {
39 const now = new Date()
40
41 if (!tokenDB || tokenDB.accessTokenExpiresAt < now || tokenDB.refreshTokenExpiresAt < now) {
42 return next(new Error('Invalid access token.'))
43 }
44
45 socket.handshake.auth.user = tokenDB.User
46
47 return next()
48 })
49 .catch(err => logger.error('Cannot get access token.', { err }))
50}
51
52function authenticatePromise (options: {
53 req: express.Request
54 res: express.Response
55 errorMessage?: string
56 errorStatus?: HttpStatusCode
57 errorType?: ServerErrorCode
58}) {
59 const { req, res, errorMessage = 'Not authenticated', errorStatus = HttpStatusCode.UNAUTHORIZED_401, errorType } = options
60 return new Promise<void>(resolve => {
61 // Already authenticated? (or tried to)
62 if (res.locals.oauth?.token.User) return resolve()
63
64 if (res.locals.authenticated === false) {
65 return res.fail({
66 status: errorStatus,
67 type: errorType,
68 message: errorMessage
69 })
70 }
71
72 authenticate(req, res, () => resolve())
73 })
74}
75
76function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
77 if (req.header('authorization')) return authenticate(req, res, next)
78
79 res.locals.authenticated = false
80
81 return next()
82}
83
84// ---------------------------------------------------------------------------
85
86function authenticateRunnerSocket (socket: Socket, next: (err?: any) => void) {
87 const runnerToken = socket.handshake.auth['runnerToken']
88
89 logger.debug('Checking runner token in socket.')
90
91 if (!runnerToken) return next(new Error('No runner token provided'))
92 if (typeof runnerToken !== 'string') return next(new Error('Runner token is invalid'))
93
94 RunnerModel.loadByToken(runnerToken)
95 .then(runner => {
96 if (!runner) return next(new Error('Invalid runner token.'))
97
98 socket.handshake.auth.runner = runner
99
100 return next()
101 })
102 .catch(err => logger.error('Cannot get runner token.', { err }))
103}
104
105// ---------------------------------------------------------------------------
106
107export {
108 authenticate,
109 authenticateSocket,
110 authenticatePromise,
111 optionalAuthenticate,
112 authenticateRunnerSocket
113}
diff --git a/server/middlewares/cache/cache.ts b/server/middlewares/cache/cache.ts
deleted file mode 100644
index 6041c76c3..000000000
--- a/server/middlewares/cache/cache.ts
+++ /dev/null
@@ -1,38 +0,0 @@
1import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
2import { ApiCache, APICacheOptions } from './shared'
3
4const defaultOptions: APICacheOptions = {
5 excludeStatus: [
6 HttpStatusCode.FORBIDDEN_403,
7 HttpStatusCode.NOT_FOUND_404
8 ]
9}
10
11function cacheRoute (duration: string) {
12 const instance = new ApiCache(defaultOptions)
13
14 return instance.buildMiddleware(duration)
15}
16
17function cacheRouteFactory (options: APICacheOptions) {
18 const instance = new ApiCache({ ...defaultOptions, ...options })
19
20 return { instance, middleware: instance.buildMiddleware.bind(instance) }
21}
22
23// ---------------------------------------------------------------------------
24
25function buildPodcastGroupsCache (options: {
26 channelId: number
27}) {
28 return 'podcast-feed-' + options.channelId
29}
30
31// ---------------------------------------------------------------------------
32
33export {
34 cacheRoute,
35 cacheRouteFactory,
36
37 buildPodcastGroupsCache
38}
diff --git a/server/middlewares/cache/index.ts b/server/middlewares/cache/index.ts
deleted file mode 100644
index 79b512828..000000000
--- a/server/middlewares/cache/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './cache'
diff --git a/server/middlewares/cache/shared/api-cache.ts b/server/middlewares/cache/shared/api-cache.ts
deleted file mode 100644
index b50b7dce4..000000000
--- a/server/middlewares/cache/shared/api-cache.ts
+++ /dev/null
@@ -1,314 +0,0 @@
1// Thanks: https://github.com/kwhitley/apicache
2// We duplicated the library because it is unmaintened and prevent us to upgrade to recent NodeJS versions
3
4import express from 'express'
5import { OutgoingHttpHeaders } from 'http'
6import { isTestInstance, parseDurationToMs } from '@server/helpers/core-utils'
7import { logger } from '@server/helpers/logger'
8import { Redis } from '@server/lib/redis'
9import { asyncMiddleware } from '@server/middlewares'
10import { HttpStatusCode } from '@shared/models'
11
12export interface APICacheOptions {
13 headerBlacklist?: string[]
14 excludeStatus?: HttpStatusCode[]
15}
16
17interface CacheObject {
18 status: number
19 headers: OutgoingHttpHeaders
20 data: any
21 encoding: BufferEncoding
22 timestamp: number
23}
24
25export class ApiCache {
26
27 private readonly options: APICacheOptions
28 private readonly timers: { [ id: string ]: NodeJS.Timeout } = {}
29
30 private readonly index = {
31 groups: [] as string[],
32 all: [] as string[]
33 }
34
35 // Cache keys per group
36 private groups: { [groupIndex: string]: string[] } = {}
37
38 private readonly seed: number
39
40 constructor (options: APICacheOptions) {
41 this.seed = new Date().getTime()
42
43 this.options = {
44 headerBlacklist: [],
45 excludeStatus: [],
46
47 ...options
48 }
49 }
50
51 buildMiddleware (strDuration: string) {
52 const duration = parseDurationToMs(strDuration)
53
54 return asyncMiddleware(
55 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
56 const key = this.getCacheKey(req)
57 const redis = Redis.Instance.getClient()
58
59 if (!Redis.Instance.isConnected()) return this.makeResponseCacheable(res, next, key, duration)
60
61 try {
62 const obj = await redis.hgetall(key)
63 if (obj?.response) {
64 return this.sendCachedResponse(req, res, JSON.parse(obj.response), duration)
65 }
66
67 return this.makeResponseCacheable(res, next, key, duration)
68 } catch (err) {
69 return this.makeResponseCacheable(res, next, key, duration)
70 }
71 }
72 )
73 }
74
75 clearGroupSafe (group: string) {
76 const run = async () => {
77 const cacheKeys = this.groups[group]
78 if (!cacheKeys) return
79
80 for (const key of cacheKeys) {
81 try {
82 await this.clear(key)
83 } catch (err) {
84 logger.error('Cannot clear ' + key, { err })
85 }
86 }
87
88 delete this.groups[group]
89 }
90
91 void run()
92 }
93
94 private getCacheKey (req: express.Request) {
95 return Redis.Instance.getPrefix() + 'api-cache-' + this.seed + '-' + req.originalUrl
96 }
97
98 private shouldCacheResponse (response: express.Response) {
99 if (!response) return false
100 if (this.options.excludeStatus.includes(response.statusCode)) return false
101
102 return true
103 }
104
105 private addIndexEntries (key: string, res: express.Response) {
106 this.index.all.unshift(key)
107
108 const groups = res.locals.apicacheGroups || []
109
110 for (const group of groups) {
111 if (!this.groups[group]) this.groups[group] = []
112
113 this.groups[group].push(key)
114 }
115 }
116
117 private filterBlacklistedHeaders (headers: OutgoingHttpHeaders) {
118 return Object.keys(headers)
119 .filter(key => !this.options.headerBlacklist.includes(key))
120 .reduce((acc, header) => {
121 acc[header] = headers[header]
122
123 return acc
124 }, {})
125 }
126
127 private createCacheObject (status: number, headers: OutgoingHttpHeaders, data: any, encoding: BufferEncoding) {
128 return {
129 status,
130 headers: this.filterBlacklistedHeaders(headers),
131 data,
132 encoding,
133
134 // Seconds since epoch, used to properly decrement max-age headers in cached responses.
135 timestamp: new Date().getTime() / 1000
136 } as CacheObject
137 }
138
139 private async cacheResponse (key: string, value: object, duration: number) {
140 const redis = Redis.Instance.getClient()
141
142 if (Redis.Instance.isConnected()) {
143 await Promise.all([
144 redis.hset(key, 'response', JSON.stringify(value)),
145 redis.hset(key, 'duration', duration + ''),
146 redis.expire(key, duration / 1000)
147 ])
148 }
149
150 // add automatic cache clearing from duration, includes max limit on setTimeout
151 this.timers[key] = setTimeout(() => {
152 this.clear(key)
153 .catch(err => logger.error('Cannot clear Redis key %s.', key, { err }))
154 }, Math.min(duration, 2147483647))
155 }
156
157 private accumulateContent (res: express.Response, content: any) {
158 if (!content) return
159
160 if (typeof content === 'string') {
161 res.locals.apicache.content = (res.locals.apicache.content || '') + content
162 return
163 }
164
165 if (Buffer.isBuffer(content)) {
166 let oldContent = res.locals.apicache.content
167
168 if (typeof oldContent === 'string') {
169 oldContent = Buffer.from(oldContent)
170 }
171
172 if (!oldContent) {
173 oldContent = Buffer.alloc(0)
174 }
175
176 res.locals.apicache.content = Buffer.concat(
177 [ oldContent, content ],
178 oldContent.length + content.length
179 )
180
181 return
182 }
183
184 res.locals.apicache.content = content
185 }
186
187 private makeResponseCacheable (res: express.Response, next: express.NextFunction, key: string, duration: number) {
188 const self = this
189
190 res.locals.apicache = {
191 write: res.write,
192 writeHead: res.writeHead,
193 end: res.end,
194 cacheable: true,
195 content: undefined,
196 headers: undefined
197 }
198
199 // Patch express
200 res.writeHead = function () {
201 if (self.shouldCacheResponse(res)) {
202 res.setHeader('cache-control', 'max-age=' + (duration / 1000).toFixed(0))
203 } else {
204 res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
205 }
206
207 res.locals.apicache.headers = Object.assign({}, res.getHeaders())
208 return res.locals.apicache.writeHead.apply(this, arguments as any)
209 }
210
211 res.write = function (chunk: any) {
212 self.accumulateContent(res, chunk)
213 return res.locals.apicache.write.apply(this, arguments as any)
214 }
215
216 res.end = function (content: any, encoding: BufferEncoding) {
217 if (self.shouldCacheResponse(res)) {
218 self.accumulateContent(res, content)
219
220 if (res.locals.apicache.cacheable && res.locals.apicache.content) {
221 self.addIndexEntries(key, res)
222
223 const headers = res.locals.apicache.headers || res.getHeaders()
224 const cacheObject = self.createCacheObject(
225 res.statusCode,
226 headers,
227 res.locals.apicache.content,
228 encoding
229 )
230 self.cacheResponse(key, cacheObject, duration)
231 .catch(err => logger.error('Cannot cache response', { err }))
232 }
233 }
234
235 res.locals.apicache.end.apply(this, arguments as any)
236 } as any
237
238 next()
239 }
240
241 private sendCachedResponse (request: express.Request, response: express.Response, cacheObject: CacheObject, duration: number) {
242 const headers = response.getHeaders()
243
244 if (isTestInstance()) {
245 Object.assign(headers, {
246 'x-api-cache-cached': 'true'
247 })
248 }
249
250 Object.assign(headers, this.filterBlacklistedHeaders(cacheObject.headers || {}), {
251 // Set properly decremented max-age header
252 // This ensures that max-age is in sync with the cache expiration
253 'cache-control':
254 'max-age=' +
255 Math.max(
256 0,
257 (duration / 1000 - (new Date().getTime() / 1000 - cacheObject.timestamp))
258 ).toFixed(0)
259 })
260
261 // unstringify buffers
262 let data = cacheObject.data
263 if (data && data.type === 'Buffer') {
264 data = typeof data.data === 'number'
265 ? Buffer.alloc(data.data)
266 : Buffer.from(data.data)
267 }
268
269 // Test Etag against If-None-Match for 304
270 const cachedEtag = cacheObject.headers.etag
271 const requestEtag = request.headers['if-none-match']
272
273 if (requestEtag && cachedEtag === requestEtag) {
274 response.writeHead(304, headers)
275 return response.end()
276 }
277
278 response.writeHead(cacheObject.status || 200, headers)
279
280 return response.end(data, cacheObject.encoding)
281 }
282
283 private async clear (target: string) {
284 const redis = Redis.Instance.getClient()
285
286 if (target) {
287 clearTimeout(this.timers[target])
288 delete this.timers[target]
289
290 try {
291 await redis.del(target)
292 } catch (err) {
293 logger.error('Cannot delete %s in redis cache.', target, { err })
294 }
295
296 this.index.all = this.index.all.filter(key => key !== target)
297 } else {
298 for (const key of this.index.all) {
299 clearTimeout(this.timers[key])
300 delete this.timers[key]
301
302 try {
303 await redis.del(key)
304 } catch (err) {
305 logger.error('Cannot delete %s in redis cache.', key, { err })
306 }
307 }
308
309 this.index.all = []
310 }
311
312 return this.index
313 }
314}
diff --git a/server/middlewares/cache/shared/index.ts b/server/middlewares/cache/shared/index.ts
deleted file mode 100644
index c707eaf7a..000000000
--- a/server/middlewares/cache/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './api-cache'
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
deleted file mode 100644
index e2a75a17e..000000000
--- a/server/middlewares/csp.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { contentSecurityPolicy } from 'helmet'
2import { CONFIG } from '../initializers/config'
3
4const baseDirectives = Object.assign({},
5 {
6 defaultSrc: [ '\'none\'' ], // by default, not specifying default-src = '*'
7 connectSrc: [ '*', 'data:' ],
8 mediaSrc: [ '\'self\'', 'https:', 'blob:' ],
9 fontSrc: [ '\'self\'', 'data:' ],
10 imgSrc: [ '\'self\'', 'data:', 'blob:' ],
11 scriptSrc: [ '\'self\' \'unsafe-inline\' \'unsafe-eval\'', 'blob:' ],
12 styleSrc: [ '\'self\' \'unsafe-inline\'' ],
13 objectSrc: [ '\'none\'' ], // only define to allow plugins, else let defaultSrc 'none' block it
14 formAction: [ '\'self\'' ],
15 frameAncestors: [ '\'none\'' ],
16 baseUri: [ '\'self\'' ],
17 manifestSrc: [ '\'self\'' ],
18 frameSrc: [ '\'self\'' ], // instead of deprecated child-src / self because of test-embed
19 workerSrc: [ '\'self\'', 'blob:' ] // instead of deprecated child-src
20 },
21 CONFIG.CSP.REPORT_URI ? { reportUri: CONFIG.CSP.REPORT_URI } : {},
22 CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: [] } : {}
23)
24
25const baseCSP = contentSecurityPolicy({
26 directives: baseDirectives,
27 reportOnly: CONFIG.CSP.REPORT_ONLY
28})
29
30const embedCSP = contentSecurityPolicy({
31 directives: Object.assign({}, baseDirectives, { frameAncestors: [ '*' ] }),
32 reportOnly: CONFIG.CSP.REPORT_ONLY
33})
34
35// ---------------------------------------------------------------------------
36
37export {
38 baseCSP,
39 embedCSP
40}
diff --git a/server/middlewares/dnt.ts b/server/middlewares/dnt.ts
deleted file mode 100644
index a128aadf7..000000000
--- a/server/middlewares/dnt.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import * as express from 'express'
2
3const advertiseDoNotTrack = (_, res: express.Response, next: express.NextFunction) => {
4 if (!res.headersSent) {
5 res.setHeader('Tk', 'N')
6 }
7
8 return next()
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 advertiseDoNotTrack
15}
diff --git a/server/middlewares/doc.ts b/server/middlewares/doc.ts
deleted file mode 100644
index eef76acaa..000000000
--- a/server/middlewares/doc.ts
+++ /dev/null
@@ -1,16 +0,0 @@
1import express from 'express'
2
3function openapiOperationDoc (options: {
4 url?: string
5 operationId?: string
6}) {
7 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
8 res.locals.docUrl = options.url || 'https://docs.joinpeertube.org/api-rest-reference.html#operation/' + options.operationId
9
10 if (next) return next()
11 }
12}
13
14export {
15 openapiOperationDoc
16}
diff --git a/server/middlewares/error.ts b/server/middlewares/error.ts
deleted file mode 100644
index 94762e355..000000000
--- a/server/middlewares/error.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1import express from 'express'
2import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
3import { logger } from '@server/helpers/logger'
4import { HttpStatusCode } from '@shared/models'
5
6function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
7 res.fail = options => {
8 const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance, tags } = options
9
10 const extension = new ProblemDocumentExtension({
11 ...data,
12
13 docs: res.locals.docUrl,
14 code: type,
15
16 // For <= 3.2 compatibility
17 error: message
18 })
19
20 res.status(status)
21
22 if (!res.headersSent) {
23 res.setHeader('Content-Type', 'application/problem+json')
24 }
25
26 const json = new ProblemDocument({
27 status,
28 title,
29 instance,
30
31 detail: message,
32
33 type: type
34 ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
35 : undefined
36 }, extension)
37
38 logger.debug('Bad HTTP request.', { json, tags })
39
40 res.json(json)
41 }
42
43 if (next) next()
44}
45
46function handleStaticError (err: any, req: express.Request, res: express.Response, next: express.NextFunction) {
47 const message = err.message || ''
48
49 if (message.includes('ENOENT')) {
50 return res.fail({
51 status: err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500,
52 message: err.message,
53 type: err.name
54 })
55 }
56
57 return next(err)
58}
59
60export {
61 apiFailMiddleware,
62 handleStaticError
63}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
deleted file mode 100644
index b40f864ce..000000000
--- a/server/middlewares/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1export * from './validators'
2export * from './cache'
3export * from './activitypub'
4export * from './async'
5export * from './auth'
6export * from './pagination'
7export * from './rate-limiter'
8export * from './robots'
9export * from './servers'
10export * from './sort'
11export * from './user-right'
12export * from './dnt'
13export * from './error'
14export * from './doc'
15export * from './csp'
diff --git a/server/middlewares/pagination.ts b/server/middlewares/pagination.ts
deleted file mode 100644
index 17e43f743..000000000
--- a/server/middlewares/pagination.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1import express from 'express'
2import { forceNumber } from '@shared/core-utils'
3import { PAGINATION } from '../initializers/constants'
4
5function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) {
6 if (!req.query.start) req.query.start = 0
7 else req.query.start = forceNumber(req.query.start)
8
9 if (!req.query.count) req.query.count = PAGINATION.GLOBAL.COUNT.DEFAULT
10 else req.query.count = forceNumber(req.query.count)
11
12 return next()
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 setDefaultPagination
19}
diff --git a/server/middlewares/rate-limiter.ts b/server/middlewares/rate-limiter.ts
deleted file mode 100644
index 143d43632..000000000
--- a/server/middlewares/rate-limiter.ts
+++ /dev/null
@@ -1,59 +0,0 @@
1import express from 'express'
2import RateLimit, { Options as RateLimitHandlerOptions } from 'express-rate-limit'
3import { CONFIG } from '@server/initializers/config'
4import { RunnerModel } from '@server/models/runner/runner'
5import { UserRole } from '@shared/models'
6import { optionalAuthenticate } from './auth'
7
8const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ])
9
10export function buildRateLimiter (options: {
11 windowMs: number
12 max: number
13 skipFailedRequests?: boolean
14}) {
15 return RateLimit({
16 windowMs: options.windowMs,
17 max: options.max,
18 skipFailedRequests: options.skipFailedRequests,
19
20 handler: (req, res, next, options) => {
21 // Bypass rate limit for registered runners
22 if (req.body?.runnerToken) {
23 return RunnerModel.loadByToken(req.body.runnerToken)
24 .then(runner => {
25 if (runner) return next()
26
27 return sendRateLimited(res, options)
28 })
29 }
30
31 // Bypass rate limit for admins/moderators
32 return optionalAuthenticate(req, res, () => {
33 if (res.locals.authenticated === true && whitelistRoles.has(res.locals.oauth.token.User.role)) {
34 return next()
35 }
36
37 return sendRateLimited(res, options)
38 })
39 }
40 })
41}
42
43export const apiRateLimiter = buildRateLimiter({
44 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
45 max: CONFIG.RATES_LIMIT.API.MAX
46})
47
48export const activityPubRateLimiter = buildRateLimiter({
49 windowMs: CONFIG.RATES_LIMIT.ACTIVITY_PUB.WINDOW_MS,
50 max: CONFIG.RATES_LIMIT.ACTIVITY_PUB.MAX
51})
52
53// ---------------------------------------------------------------------------
54// Private
55// ---------------------------------------------------------------------------
56
57function sendRateLimited (res: express.Response, options: RateLimitHandlerOptions) {
58 return res.status(options.statusCode).send(options.message)
59}
diff --git a/server/middlewares/robots.ts b/server/middlewares/robots.ts
deleted file mode 100644
index b22b24a9f..000000000
--- a/server/middlewares/robots.ts
+++ /dev/null
@@ -1,13 +0,0 @@
1import express from 'express'
2
3function disableRobots (req: express.Request, res: express.Response, next: express.NextFunction) {
4 res.setHeader('X-Robots-Tag', 'noindex')
5
6 return next()
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 disableRobots
13}
diff --git a/server/middlewares/servers.ts b/server/middlewares/servers.ts
deleted file mode 100644
index ebfa03e6c..000000000
--- a/server/middlewares/servers.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import express from 'express'
2import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
3import { getHostWithPort } from '../helpers/express-utils'
4
5function setBodyHostsPort (req: express.Request, res: express.Response, next: express.NextFunction) {
6 if (!req.body.hosts) return next()
7
8 for (let i = 0; i < req.body.hosts.length; i++) {
9 const hostWithPort = getHostWithPort(req.body.hosts[i])
10
11 // Problem with the url parsing?
12 if (hostWithPort === null) {
13 return res.fail({
14 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
15 message: 'Could not parse hosts'
16 })
17 }
18
19 req.body.hosts[i] = hostWithPort
20 }
21
22 return next()
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 setBodyHostsPort
29}
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts
deleted file mode 100644
index 77a532276..000000000
--- a/server/middlewares/sort.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import express from 'express'
2
3const setDefaultSort = setDefaultSortFactory('-createdAt')
4const setDefaultVideosSort = setDefaultSortFactory('-publishedAt')
5
6const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name')
7
8const setDefaultSearchSort = setDefaultSortFactory('-match')
9const setBlacklistSort = setDefaultSortFactory('-createdAt')
10
11// ---------------------------------------------------------------------------
12
13export {
14 setDefaultSort,
15 setDefaultSearchSort,
16 setDefaultVideosSort,
17 setDefaultVideoRedundanciesSort,
18 setBlacklistSort
19}
20
21// ---------------------------------------------------------------------------
22
23function setDefaultSortFactory (sort: string) {
24 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
25 if (!req.query.sort) req.query.sort = sort
26
27 return next()
28 }
29}
diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts
deleted file mode 100644
index 7d53e8341..000000000
--- a/server/middlewares/user-right.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import express from 'express'
2import { HttpStatusCode, UserRight } from '@shared/models'
3import { logger } from '../helpers/logger'
4
5function ensureUserHasRight (userRight: UserRight) {
6 return function (req: express.Request, res: express.Response, next: express.NextFunction) {
7 const user = res.locals.oauth.token.user
8 if (user.hasRight(userRight) === false) {
9 const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.`
10 logger.info(message)
11
12 return res.fail({
13 status: HttpStatusCode.FORBIDDEN_403,
14 message
15 })
16 }
17
18 return next()
19 }
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 ensureUserHasRight
26}
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
deleted file mode 100644
index 70bae1775..000000000
--- a/server/middlewares/validators/abuse.ts
+++ /dev/null
@@ -1,255 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import {
4 areAbusePredefinedReasonsValid,
5 isAbuseFilterValid,
6 isAbuseMessageValid,
7 isAbuseModerationCommentValid,
8 isAbusePredefinedReasonValid,
9 isAbuseReasonValid,
10 isAbuseStateValid,
11 isAbuseTimestampCoherent,
12 isAbuseTimestampValid,
13 isAbuseVideoIsValid
14} from '@server/helpers/custom-validators/abuses'
15import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc'
16import { logger } from '@server/helpers/logger'
17import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
18import { AbuseCreate, UserRight } from '@shared/models'
19import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
20import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared'
21import { forceNumber } from '@shared/core-utils'
22
23const abuseReportValidator = [
24 body('account.id')
25 .optional()
26 .custom(isIdValid),
27
28 body('video.id')
29 .optional()
30 .customSanitizer(toCompleteUUID)
31 .custom(isIdOrUUIDValid),
32 body('video.startAt')
33 .optional()
34 .customSanitizer(toIntOrNull)
35 .custom(isAbuseTimestampValid),
36 body('video.endAt')
37 .optional()
38 .customSanitizer(toIntOrNull)
39 .custom(isAbuseTimestampValid)
40 .bail()
41 .custom(isAbuseTimestampCoherent)
42 .withMessage('Should have a startAt timestamp beginning before endAt'),
43
44 body('comment.id')
45 .optional()
46 .custom(isIdValid),
47
48 body('reason')
49 .custom(isAbuseReasonValid),
50
51 body('predefinedReasons')
52 .optional()
53 .custom(areAbusePredefinedReasonsValid),
54
55 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
56 if (areValidationErrors(req, res)) return
57
58 const body: AbuseCreate = req.body
59
60 if (body.video?.id && !await doesVideoExist(body.video.id, res)) return
61 if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return
62 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
63
64 if (!body.video?.id && !body.account?.id && !body.comment?.id) {
65 res.fail({ message: 'video id or account id or comment id is required.' })
66 return
67 }
68
69 return next()
70 }
71]
72
73const abuseGetValidator = [
74 param('id')
75 .custom(isIdValid),
76
77 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
78 if (areValidationErrors(req, res)) return
79 if (!await doesAbuseExist(req.params.id, res)) return
80
81 return next()
82 }
83]
84
85const abuseUpdateValidator = [
86 param('id')
87 .custom(isIdValid),
88
89 body('state')
90 .optional()
91 .custom(isAbuseStateValid),
92 body('moderationComment')
93 .optional()
94 .custom(isAbuseModerationCommentValid),
95
96 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
97 if (areValidationErrors(req, res)) return
98 if (!await doesAbuseExist(req.params.id, res)) return
99
100 return next()
101 }
102]
103
104const abuseListForAdminsValidator = [
105 query('id')
106 .optional()
107 .custom(isIdValid),
108 query('filter')
109 .optional()
110 .custom(isAbuseFilterValid),
111 query('predefinedReason')
112 .optional()
113 .custom(isAbusePredefinedReasonValid),
114 query('search')
115 .optional()
116 .custom(exists),
117 query('state')
118 .optional()
119 .custom(isAbuseStateValid),
120 query('videoIs')
121 .optional()
122 .custom(isAbuseVideoIsValid),
123 query('searchReporter')
124 .optional()
125 .custom(exists),
126 query('searchReportee')
127 .optional()
128 .custom(exists),
129 query('searchVideo')
130 .optional()
131 .custom(exists),
132 query('searchVideoChannel')
133 .optional()
134 .custom(exists),
135
136 (req: express.Request, res: express.Response, next: express.NextFunction) => {
137 if (areValidationErrors(req, res)) return
138
139 return next()
140 }
141]
142
143const abuseListForUserValidator = [
144 query('id')
145 .optional()
146 .custom(isIdValid),
147
148 query('search')
149 .optional()
150 .custom(exists),
151
152 query('state')
153 .optional()
154 .custom(isAbuseStateValid),
155
156 (req: express.Request, res: express.Response, next: express.NextFunction) => {
157 if (areValidationErrors(req, res)) return
158
159 return next()
160 }
161]
162
163const getAbuseValidator = [
164 param('id')
165 .custom(isIdValid),
166
167 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
168 if (areValidationErrors(req, res)) return
169 if (!await doesAbuseExist(req.params.id, res)) return
170
171 const user = res.locals.oauth.token.user
172 const abuse = res.locals.abuse
173
174 if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuse.reporterAccountId !== user.Account.id) {
175 const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
176 logger.warn(message)
177
178 return res.fail({
179 status: HttpStatusCode.FORBIDDEN_403,
180 message
181 })
182 }
183
184 return next()
185 }
186]
187
188const checkAbuseValidForMessagesValidator = [
189 (req: express.Request, res: express.Response, next: express.NextFunction) => {
190 const abuse = res.locals.abuse
191 if (abuse.ReporterAccount.isOwned() === false) {
192 return res.fail({ message: 'This abuse was created by a user of your instance.' })
193 }
194
195 return next()
196 }
197]
198
199const addAbuseMessageValidator = [
200 body('message')
201 .custom(isAbuseMessageValid),
202
203 (req: express.Request, res: express.Response, next: express.NextFunction) => {
204 if (areValidationErrors(req, res)) return
205
206 return next()
207 }
208]
209
210const deleteAbuseMessageValidator = [
211 param('messageId')
212 .custom(isIdValid),
213
214 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
215 if (areValidationErrors(req, res)) return
216
217 const user = res.locals.oauth.token.user
218 const abuse = res.locals.abuse
219
220 const messageId = forceNumber(req.params.messageId)
221 const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
222
223 if (!abuseMessage) {
224 return res.fail({
225 status: HttpStatusCode.NOT_FOUND_404,
226 message: 'Abuse message not found'
227 })
228 }
229
230 if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
231 return res.fail({
232 status: HttpStatusCode.FORBIDDEN_403,
233 message: 'Cannot delete this abuse message'
234 })
235 }
236
237 res.locals.abuseMessage = abuseMessage
238
239 return next()
240 }
241]
242
243// ---------------------------------------------------------------------------
244
245export {
246 abuseListForAdminsValidator,
247 abuseReportValidator,
248 abuseGetValidator,
249 addAbuseMessageValidator,
250 checkAbuseValidForMessagesValidator,
251 abuseUpdateValidator,
252 deleteAbuseMessageValidator,
253 abuseListForUserValidator,
254 getAbuseValidator
255}
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
deleted file mode 100644
index 551f67d61..000000000
--- a/server/middlewares/validators/account.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { isAccountNameValid } from '../../helpers/custom-validators/accounts'
4import { areValidationErrors, doesAccountNameWithHostExist, doesLocalAccountNameExist } from './shared'
5
6const localAccountValidator = [
7 param('name')
8 .custom(isAccountNameValid),
9
10 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 if (areValidationErrors(req, res)) return
12 if (!await doesLocalAccountNameExist(req.params.name, res)) return
13
14 return next()
15 }
16]
17
18const accountNameWithHostGetValidator = [
19 param('accountName')
20 .exists(),
21
22 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
23 if (areValidationErrors(req, res)) return
24 if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
25
26 return next()
27 }
28]
29
30// ---------------------------------------------------------------------------
31
32export {
33 localAccountValidator,
34 accountNameWithHostGetValidator
35}
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
deleted file mode 100644
index e296b8be7..000000000
--- a/server/middlewares/validators/activitypub/activity.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import express from 'express'
2import { getServerActor } from '@server/models/application/application'
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity'
5import { logger } from '../../../helpers/logger'
6
7async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
8 logger.debug('Checking activity pub parameters')
9
10 if (!isRootActivityValid(req.body)) {
11 logger.warn('Incorrect activity parameters.', { activity: req.body })
12 return res.fail({ message: 'Incorrect activity' })
13 }
14
15 const serverActor = await getServerActor()
16 const remoteActor = res.locals.signature.actor
17 if (serverActor.id === remoteActor.id || remoteActor.serverId === null) {
18 logger.error('Receiving request in INBOX by ourselves!', req.body)
19 return res.status(HttpStatusCode.CONFLICT_409).end()
20 }
21
22 return next()
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 activityPubValidator
29}
diff --git a/server/middlewares/validators/activitypub/index.ts b/server/middlewares/validators/activitypub/index.ts
deleted file mode 100644
index 159338d26..000000000
--- a/server/middlewares/validators/activitypub/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './activity'
2export * from './signature'
3export * from './pagination'
diff --git a/server/middlewares/validators/activitypub/pagination.ts b/server/middlewares/validators/activitypub/pagination.ts
deleted file mode 100644
index 1259e4fef..000000000
--- a/server/middlewares/validators/activitypub/pagination.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { PAGINATION } from '@server/initializers/constants'
4import { areValidationErrors } from '../shared'
5
6const apPaginationValidator = [
7 query('page')
8 .optional()
9 .isInt({ min: 1 }),
10 query('size')
11 .optional()
12 .isInt({ min: 0, max: PAGINATION.OUTBOX.COUNT.MAX }).withMessage(`Should have a valid page size (max: ${PAGINATION.OUTBOX.COUNT.MAX})`),
13
14 (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 if (areValidationErrors(req, res)) return
16
17 return next()
18 }
19]
20
21// ---------------------------------------------------------------------------
22
23export {
24 apPaginationValidator
25}
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
deleted file mode 100644
index 998d0c0c4..000000000
--- a/server/middlewares/validators/activitypub/signature.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import {
4 isSignatureCreatorValid,
5 isSignatureTypeValid,
6 isSignatureValueValid
7} from '../../../helpers/custom-validators/activitypub/signature'
8import { isDateValid } from '../../../helpers/custom-validators/misc'
9import { logger } from '../../../helpers/logger'
10import { areValidationErrors } from '../shared'
11
12const signatureValidator = [
13 body('signature.type')
14 .optional()
15 .custom(isSignatureTypeValid),
16 body('signature.created')
17 .optional()
18 .custom(isDateValid).withMessage('Should have a signature created date that conforms to ISO 8601'),
19 body('signature.creator')
20 .optional()
21 .custom(isSignatureCreatorValid),
22 body('signature.signatureValue')
23 .optional()
24 .custom(isSignatureValueValid),
25
26 (req: express.Request, res: express.Response, next: express.NextFunction) => {
27 logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
28
29 if (areValidationErrors(req, res, { omitLog: true })) return
30
31 return next()
32 }
33]
34
35// ---------------------------------------------------------------------------
36
37export {
38 signatureValidator
39}
diff --git a/server/middlewares/validators/actor-image.ts b/server/middlewares/validators/actor-image.ts
deleted file mode 100644
index 9dcf5e871..000000000
--- a/server/middlewares/validators/actor-image.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isActorImageFile } from '@server/helpers/custom-validators/actor-images'
4import { cleanUpReqFiles } from '../../helpers/express-utils'
5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { areValidationErrors } from './shared'
7
8const updateActorImageValidatorFactory = (fieldname: string) => ([
9 body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage(
10 'This file is not supported or too large. Please, make sure it is of the following type : ' +
11 CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ')
12 ),
13
14 (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
16
17 return next()
18 }
19])
20
21const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
22const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
23
24export {
25 updateAvatarValidator,
26 updateBannerValidator
27}
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
deleted file mode 100644
index 8ec6cb01d..000000000
--- a/server/middlewares/validators/blocklist.ts
+++ /dev/null
@@ -1,179 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor'
4import { getServerActor } from '@server/models/application/application'
5import { arrayify } from '@shared/core-utils'
6import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
7import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
8import { WEBSERVER } from '../../initializers/constants'
9import { AccountBlocklistModel } from '../../models/account/account-blocklist'
10import { ServerModel } from '../../models/server/server'
11import { ServerBlocklistModel } from '../../models/server/server-blocklist'
12import { areValidationErrors, doesAccountNameWithHostExist } from './shared'
13
14const blockAccountValidator = [
15 body('accountName')
16 .exists(),
17
18 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 if (areValidationErrors(req, res)) return
20 if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
21
22 const user = res.locals.oauth.token.User
23 const accountToBlock = res.locals.account
24
25 if (user.Account.id === accountToBlock.id) {
26 res.fail({
27 status: HttpStatusCode.CONFLICT_409,
28 message: 'You cannot block yourself.'
29 })
30 return
31 }
32
33 return next()
34 }
35]
36
37const unblockAccountByAccountValidator = [
38 param('accountName')
39 .exists(),
40
41 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 if (areValidationErrors(req, res)) return
43 if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
44
45 const user = res.locals.oauth.token.User
46 const targetAccount = res.locals.account
47 if (!await doesUnblockAccountExist(user.Account.id, targetAccount.id, res)) return
48
49 return next()
50 }
51]
52
53const unblockAccountByServerValidator = [
54 param('accountName')
55 .exists(),
56
57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
58 if (areValidationErrors(req, res)) return
59 if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
60
61 const serverActor = await getServerActor()
62 const targetAccount = res.locals.account
63 if (!await doesUnblockAccountExist(serverActor.Account.id, targetAccount.id, res)) return
64
65 return next()
66 }
67]
68
69const blockServerValidator = [
70 body('host')
71 .custom(isHostValid),
72
73 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
74 if (areValidationErrors(req, res)) return
75
76 const host: string = req.body.host
77
78 if (host === WEBSERVER.HOST) {
79 return res.fail({
80 status: HttpStatusCode.CONFLICT_409,
81 message: 'You cannot block your own server.'
82 })
83 }
84
85 const server = await ServerModel.loadOrCreateByHost(host)
86
87 res.locals.server = server
88
89 return next()
90 }
91]
92
93const unblockServerByAccountValidator = [
94 param('host')
95 .custom(isHostValid),
96
97 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
98 if (areValidationErrors(req, res)) return
99
100 const user = res.locals.oauth.token.User
101 if (!await doesUnblockServerExist(user.Account.id, req.params.host, res)) return
102
103 return next()
104 }
105]
106
107const unblockServerByServerValidator = [
108 param('host')
109 .custom(isHostValid),
110
111 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
112 if (areValidationErrors(req, res)) return
113
114 const serverActor = await getServerActor()
115 if (!await doesUnblockServerExist(serverActor.Account.id, req.params.host, res)) return
116
117 return next()
118 }
119]
120
121const blocklistStatusValidator = [
122 query('hosts')
123 .optional()
124 .customSanitizer(arrayify)
125 .custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
126
127 query('accounts')
128 .optional()
129 .customSanitizer(arrayify)
130 .custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
131
132 (req: express.Request, res: express.Response, next: express.NextFunction) => {
133 if (areValidationErrors(req, res)) return
134
135 return next()
136 }
137]
138
139// ---------------------------------------------------------------------------
140
141export {
142 blockServerValidator,
143 blockAccountValidator,
144 unblockAccountByAccountValidator,
145 unblockServerByAccountValidator,
146 unblockAccountByServerValidator,
147 unblockServerByServerValidator,
148 blocklistStatusValidator
149}
150
151// ---------------------------------------------------------------------------
152
153async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
154 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
155 if (!accountBlock) {
156 res.fail({
157 status: HttpStatusCode.NOT_FOUND_404,
158 message: 'Account block entry not found.'
159 })
160 return false
161 }
162
163 res.locals.accountBlock = accountBlock
164 return true
165}
166
167async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
168 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
169 if (!serverBlock) {
170 res.fail({
171 status: HttpStatusCode.NOT_FOUND_404,
172 message: 'Server block entry not found.'
173 })
174 return false
175 }
176
177 res.locals.serverBlock = serverBlock
178 return true
179}
diff --git a/server/middlewares/validators/bulk.ts b/server/middlewares/validators/bulk.ts
deleted file mode 100644
index a1cea8032..000000000
--- a/server/middlewares/validators/bulk.ts
+++ /dev/null
@@ -1,38 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk'
4import { HttpStatusCode, UserRight } from '@shared/models'
5import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
6import { areValidationErrors, doesAccountNameWithHostExist } from './shared'
7
8const bulkRemoveCommentsOfValidator = [
9 body('accountName')
10 .exists(),
11 body('scope')
12 .custom(isBulkRemoveCommentsOfScopeValid),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 if (areValidationErrors(req, res)) return
16 if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
17
18 const user = res.locals.oauth.token.User
19 const body = req.body as BulkRemoveCommentsOfBody
20
21 if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
22 return res.fail({
23 status: HttpStatusCode.FORBIDDEN_403,
24 message: 'User cannot remove any comments of this instance.'
25 })
26 }
27
28 return next()
29 }
30]
31
32// ---------------------------------------------------------------------------
33
34export {
35 bulkRemoveCommentsOfValidator
36}
37
38// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
deleted file mode 100644
index 7790025e4..000000000
--- a/server/middlewares/validators/config.ts
+++ /dev/null
@@ -1,194 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isIntOrNull } from '@server/helpers/custom-validators/misc'
4import { CONFIG, isEmailEnabled } from '@server/initializers/config'
5import { HttpStatusCode } from '@shared/models/http/http-error-codes'
6import { CustomConfig } from '../../../shared/models/server/custom-config.model'
7import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
8import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
9import { isThemeRegistered } from '../../lib/plugins/theme-utils'
10import { areValidationErrors } from './shared'
11
12const customConfigUpdateValidator = [
13 body('instance.name').exists(),
14 body('instance.shortDescription').exists(),
15 body('instance.description').exists(),
16 body('instance.terms').exists(),
17 body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid),
18 body('instance.defaultClientRoute').exists(),
19 body('instance.customizations.css').exists(),
20 body('instance.customizations.javascript').exists(),
21
22 body('services.twitter.username').exists(),
23 body('services.twitter.whitelisted').isBoolean(),
24
25 body('cache.previews.size').isInt(),
26 body('cache.captions.size').isInt(),
27 body('cache.torrents.size').isInt(),
28 body('cache.storyboards.size').isInt(),
29
30 body('signup.enabled').isBoolean(),
31 body('signup.limit').isInt(),
32 body('signup.requiresEmailVerification').isBoolean(),
33 body('signup.requiresApproval').isBoolean(),
34 body('signup.minimumAge').isInt(),
35
36 body('admin.email').isEmail(),
37 body('contactForm.enabled').isBoolean(),
38
39 body('user.history.videos.enabled').isBoolean(),
40 body('user.videoQuota').custom(isUserVideoQuotaValid),
41 body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid),
42
43 body('videoChannels.maxPerUser').isInt(),
44
45 body('transcoding.enabled').isBoolean(),
46 body('transcoding.allowAdditionalExtensions').isBoolean(),
47 body('transcoding.threads').isInt(),
48 body('transcoding.concurrency').isInt({ min: 1 }),
49 body('transcoding.resolutions.0p').isBoolean(),
50 body('transcoding.resolutions.144p').isBoolean(),
51 body('transcoding.resolutions.240p').isBoolean(),
52 body('transcoding.resolutions.360p').isBoolean(),
53 body('transcoding.resolutions.480p').isBoolean(),
54 body('transcoding.resolutions.720p').isBoolean(),
55 body('transcoding.resolutions.1080p').isBoolean(),
56 body('transcoding.resolutions.1440p').isBoolean(),
57 body('transcoding.resolutions.2160p').isBoolean(),
58 body('transcoding.remoteRunners.enabled').isBoolean(),
59
60 body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
61
62 body('transcoding.webVideos.enabled').isBoolean(),
63 body('transcoding.hls.enabled').isBoolean(),
64
65 body('videoStudio.enabled').isBoolean(),
66 body('videoStudio.remoteRunners.enabled').isBoolean(),
67
68 body('videoFile.update.enabled').isBoolean(),
69
70 body('import.videos.concurrency').isInt({ min: 0 }),
71 body('import.videos.http.enabled').isBoolean(),
72 body('import.videos.torrent.enabled').isBoolean(),
73
74 body('import.videoChannelSynchronization.enabled').isBoolean(),
75
76 body('trending.videos.algorithms.default').exists(),
77 body('trending.videos.algorithms.enabled').exists(),
78
79 body('followers.instance.enabled').isBoolean(),
80 body('followers.instance.manualApproval').isBoolean(),
81
82 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
83
84 body('broadcastMessage.enabled').isBoolean(),
85 body('broadcastMessage.message').exists(),
86 body('broadcastMessage.level').exists(),
87 body('broadcastMessage.dismissable').isBoolean(),
88
89 body('live.enabled').isBoolean(),
90 body('live.allowReplay').isBoolean(),
91 body('live.maxDuration').isInt(),
92 body('live.maxInstanceLives').custom(isIntOrNull),
93 body('live.maxUserLives').custom(isIntOrNull),
94 body('live.transcoding.enabled').isBoolean(),
95 body('live.transcoding.threads').isInt(),
96 body('live.transcoding.resolutions.144p').isBoolean(),
97 body('live.transcoding.resolutions.240p').isBoolean(),
98 body('live.transcoding.resolutions.360p').isBoolean(),
99 body('live.transcoding.resolutions.480p').isBoolean(),
100 body('live.transcoding.resolutions.720p').isBoolean(),
101 body('live.transcoding.resolutions.1080p').isBoolean(),
102 body('live.transcoding.resolutions.1440p').isBoolean(),
103 body('live.transcoding.resolutions.2160p').isBoolean(),
104 body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
105 body('live.transcoding.remoteRunners.enabled').isBoolean(),
106
107 body('search.remoteUri.users').isBoolean(),
108 body('search.remoteUri.anonymous').isBoolean(),
109 body('search.searchIndex.enabled').isBoolean(),
110 body('search.searchIndex.url').exists(),
111 body('search.searchIndex.disableLocalSearch').isBoolean(),
112 body('search.searchIndex.isDefaultSearch').isBoolean(),
113
114 (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 if (areValidationErrors(req, res)) return
116 if (!checkInvalidConfigIfEmailDisabled(req.body, res)) return
117 if (!checkInvalidTranscodingConfig(req.body, res)) return
118 if (!checkInvalidSynchronizationConfig(req.body, res)) return
119 if (!checkInvalidLiveConfig(req.body, res)) return
120 if (!checkInvalidVideoStudioConfig(req.body, res)) return
121
122 return next()
123 }
124]
125
126function ensureConfigIsEditable (req: express.Request, res: express.Response, next: express.NextFunction) {
127 if (!CONFIG.WEBADMIN.CONFIGURATION.EDITION.ALLOWED) {
128 return res.fail({
129 status: HttpStatusCode.METHOD_NOT_ALLOWED_405,
130 message: 'Server configuration is static and cannot be edited'
131 })
132 }
133
134 return next()
135}
136
137// ---------------------------------------------------------------------------
138
139export {
140 customConfigUpdateValidator,
141 ensureConfigIsEditable
142}
143
144function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
145 if (isEmailEnabled()) return true
146
147 if (customConfig.signup.requiresEmailVerification === true) {
148 res.fail({ message: 'SMTP is not configured but you require signup email verification.' })
149 return false
150 }
151
152 return true
153}
154
155function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) {
156 if (customConfig.transcoding.enabled === false) return true
157
158 if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) {
159 res.fail({ message: 'You need to enable at least web_videos transcoding or hls transcoding' })
160 return false
161 }
162
163 return true
164}
165
166function checkInvalidSynchronizationConfig (customConfig: CustomConfig, res: express.Response) {
167 if (customConfig.import.videoChannelSynchronization.enabled && !customConfig.import.videos.http.enabled) {
168 res.fail({ message: 'You need to enable HTTP video import in order to enable channel synchronization' })
169 return false
170 }
171 return true
172}
173
174function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Response) {
175 if (customConfig.live.enabled === false) return true
176
177 if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
178 res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
179 return false
180 }
181
182 return true
183}
184
185function checkInvalidVideoStudioConfig (customConfig: CustomConfig, res: express.Response) {
186 if (customConfig.videoStudio.enabled === false) return true
187
188 if (customConfig.videoStudio.enabled === true && customConfig.transcoding.enabled === false) {
189 res.fail({ message: 'You cannot enable video studio if transcoding is not enabled' })
190 return false
191 }
192
193 return true
194}
diff --git a/server/middlewares/validators/express.ts b/server/middlewares/validators/express.ts
deleted file mode 100644
index 718aec55b..000000000
--- a/server/middlewares/validators/express.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import * as express from 'express'
2
3const methodsValidator = (methods: string[]) => {
4 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
5 if (methods.includes(req.method) !== true) {
6 return res.sendStatus(405)
7 }
8
9 return next()
10 }
11}
12
13export {
14 methodsValidator
15}
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
deleted file mode 100644
index 72804a259..000000000
--- a/server/middlewares/validators/feeds.ts
+++ /dev/null
@@ -1,178 +0,0 @@
1import express from 'express'
2import { param, query } from 'express-validator'
3import { HttpStatusCode } from '@shared/models'
4import { isValidRSSFeed } from '../../helpers/custom-validators/feeds'
5import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc'
6import { buildPodcastGroupsCache } from '../cache'
7import {
8 areValidationErrors,
9 checkCanSeeVideo,
10 doesAccountIdExist,
11 doesAccountNameWithHostExist,
12 doesUserFeedTokenCorrespond,
13 doesVideoChannelIdExist,
14 doesVideoChannelNameWithHostExist,
15 doesVideoExist
16} from './shared'
17
18const feedsFormatValidator = [
19 param('format')
20 .optional()
21 .custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
22 query('format')
23 .optional()
24 .custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
25
26 (req: express.Request, res: express.Response, next: express.NextFunction) => {
27 if (areValidationErrors(req, res)) return
28
29 return next()
30 }
31]
32
33function setFeedFormatContentType (req: express.Request, res: express.Response, next: express.NextFunction) {
34 const format = req.query.format || req.params.format || 'rss'
35
36 let acceptableContentTypes: string[]
37 if (format === 'atom' || format === 'atom1') {
38 acceptableContentTypes = [ 'application/atom+xml', 'application/xml', 'text/xml' ]
39 } else if (format === 'json' || format === 'json1') {
40 acceptableContentTypes = [ 'application/json' ]
41 } else if (format === 'rss' || format === 'rss2') {
42 acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
43 } else {
44 acceptableContentTypes = [ 'application/xml', 'text/xml' ]
45 }
46
47 return feedContentTypeResponse(req, res, next, acceptableContentTypes)
48}
49
50function setFeedPodcastContentType (req: express.Request, res: express.Response, next: express.NextFunction) {
51 const acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
52
53 return feedContentTypeResponse(req, res, next, acceptableContentTypes)
54}
55
56function feedContentTypeResponse (
57 req: express.Request,
58 res: express.Response,
59 next: express.NextFunction,
60 acceptableContentTypes: string[]
61) {
62 if (req.accepts(acceptableContentTypes)) {
63 res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
64 } else {
65 return res.fail({
66 status: HttpStatusCode.NOT_ACCEPTABLE_406,
67 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
68 })
69 }
70
71 return next()
72}
73
74// ---------------------------------------------------------------------------
75
76const feedsAccountOrChannelFiltersValidator = [
77 query('accountId')
78 .optional()
79 .custom(isIdValid),
80
81 query('accountName')
82 .optional(),
83
84 query('videoChannelId')
85 .optional()
86 .custom(isIdValid),
87
88 query('videoChannelName')
89 .optional(),
90
91 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
92 if (areValidationErrors(req, res)) return
93
94 if (req.query.accountId && !await doesAccountIdExist(req.query.accountId, res)) return
95 if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
96 if (req.query.accountName && !await doesAccountNameWithHostExist(req.query.accountName, res)) return
97 if (req.query.videoChannelName && !await doesVideoChannelNameWithHostExist(req.query.videoChannelName, res)) return
98
99 return next()
100 }
101]
102
103// ---------------------------------------------------------------------------
104
105const videoFeedsPodcastValidator = [
106 query('videoChannelId')
107 .custom(isIdValid),
108
109 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
110 if (areValidationErrors(req, res)) return
111 if (!await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
112
113 return next()
114 }
115]
116
117const videoFeedsPodcastSetCacheKey = [
118 (req: express.Request, res: express.Response, next: express.NextFunction) => {
119 if (req.query.videoChannelId) {
120 res.locals.apicacheGroups = [ buildPodcastGroupsCache({ channelId: req.query.videoChannelId }) ]
121 }
122
123 return next()
124 }
125]
126// ---------------------------------------------------------------------------
127
128const videoSubscriptionFeedsValidator = [
129 query('accountId')
130 .custom(isIdValid),
131
132 query('token')
133 .custom(exists),
134
135 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
136 if (areValidationErrors(req, res)) return
137
138 if (!await doesAccountIdExist(req.query.accountId, res)) return
139 if (!await doesUserFeedTokenCorrespond(res.locals.account.userId, req.query.token, res)) return
140
141 return next()
142 }
143]
144
145const videoCommentsFeedsValidator = [
146 query('videoId')
147 .optional()
148 .customSanitizer(toCompleteUUID)
149 .custom(isIdOrUUIDValid),
150
151 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
152 if (areValidationErrors(req, res)) return
153
154 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
155 return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
156 }
157
158 if (req.query.videoId) {
159 if (!await doesVideoExist(req.query.videoId, res)) return
160 if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return
161 }
162
163 return next()
164 }
165]
166
167// ---------------------------------------------------------------------------
168
169export {
170 feedsFormatValidator,
171 setFeedFormatContentType,
172 setFeedPodcastContentType,
173 feedsAccountOrChannelFiltersValidator,
174 videoFeedsPodcastValidator,
175 videoSubscriptionFeedsValidator,
176 videoFeedsPodcastSetCacheKey,
177 videoCommentsFeedsValidator
178}
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
deleted file mode 100644
index be98a4c04..000000000
--- a/server/middlewares/validators/follows.ts
+++ /dev/null
@@ -1,158 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { isProdInstance } from '@server/helpers/core-utils'
4import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows'
5import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors'
6import { getRemoteNameAndHost } from '@server/lib/activitypub/follow'
7import { getServerActor } from '@server/models/application/application'
8import { MActorFollowActorsDefault } from '@server/types/models'
9import { ServerFollowCreate } from '@shared/models'
10import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
11import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
12import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
13import { logger } from '../../helpers/logger'
14import { WEBSERVER } from '../../initializers/constants'
15import { ActorModel } from '../../models/actor/actor'
16import { ActorFollowModel } from '../../models/actor/actor-follow'
17import { areValidationErrors } from './shared'
18
19const listFollowsValidator = [
20 query('state')
21 .optional()
22 .custom(isFollowStateValid),
23 query('actorType')
24 .optional()
25 .custom(isActorTypeValid),
26
27 (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 if (areValidationErrors(req, res)) return
29
30 return next()
31 }
32]
33
34const followValidator = [
35 body('hosts')
36 .toArray()
37 .custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
38
39 body('handles')
40 .toArray()
41 .custom(isEachUniqueHandleValid).withMessage('Should have an array of handles'),
42
43 (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 // Force https if the administrator wants to follow remote actors
45 if (isProdInstance() && WEBSERVER.SCHEME === 'http') {
46 return res
47 .status(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
48 .json({
49 error: 'Cannot follow on a non HTTPS web server.'
50 })
51 }
52
53 if (areValidationErrors(req, res)) return
54
55 const body: ServerFollowCreate = req.body
56 if (body.hosts.length === 0 && body.handles.length === 0) {
57
58 return res
59 .status(HttpStatusCode.BAD_REQUEST_400)
60 .json({
61 error: 'You must provide at least one handle or one host.'
62 })
63 }
64
65 return next()
66 }
67]
68
69const removeFollowingValidator = [
70 param('hostOrHandle')
71 .custom(value => isHostValid(value) || isRemoteHandleValid(value)),
72
73 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
74 if (areValidationErrors(req, res)) return
75
76 const serverActor = await getServerActor()
77
78 const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle)
79 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
80 actorId: serverActor.id,
81 targetName: name,
82 targetHost: host
83 })
84
85 if (!follow) {
86 return res.fail({
87 status: HttpStatusCode.NOT_FOUND_404,
88 message: `Follow ${req.params.hostOrHandle} not found.`
89 })
90 }
91
92 res.locals.follow = follow
93 return next()
94 }
95]
96
97const getFollowerValidator = [
98 param('nameWithHost')
99 .custom(isValidActorHandle),
100
101 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
102 if (areValidationErrors(req, res)) return
103
104 let follow: MActorFollowActorsDefault
105 try {
106 const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
107 const actor = await ActorModel.loadByUrl(actorUrl)
108
109 const serverActor = await getServerActor()
110 follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
111 } catch (err) {
112 logger.warn('Cannot get actor from handle.', { handle: req.params.nameWithHost, err })
113 }
114
115 if (!follow) {
116 return res.fail({
117 status: HttpStatusCode.NOT_FOUND_404,
118 message: `Follower ${req.params.nameWithHost} not found.`
119 })
120 }
121
122 res.locals.follow = follow
123 return next()
124 }
125]
126
127const acceptFollowerValidator = [
128 (req: express.Request, res: express.Response, next: express.NextFunction) => {
129 const follow = res.locals.follow
130 if (follow.state !== 'pending' && follow.state !== 'rejected') {
131 return res.fail({ message: 'Follow is not in pending/rejected state.' })
132 }
133
134 return next()
135 }
136]
137
138const rejectFollowerValidator = [
139 (req: express.Request, res: express.Response, next: express.NextFunction) => {
140 const follow = res.locals.follow
141 if (follow.state !== 'pending' && follow.state !== 'accepted') {
142 return res.fail({ message: 'Follow is not in pending/accepted state.' })
143 }
144
145 return next()
146 }
147]
148
149// ---------------------------------------------------------------------------
150
151export {
152 followValidator,
153 removeFollowingValidator,
154 getFollowerValidator,
155 acceptFollowerValidator,
156 rejectFollowerValidator,
157 listFollowsValidator
158}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
deleted file mode 100644
index 1d0964667..000000000
--- a/server/middlewares/validators/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1export * from './abuse'
2export * from './account'
3export * from './activitypub'
4export * from './actor-image'
5export * from './blocklist'
6export * from './bulk'
7export * from './config'
8export * from './express'
9export * from './feeds'
10export * from './follows'
11export * from './jobs'
12export * from './logs'
13export * from './metrics'
14export * from './object-storage-proxy'
15export * from './oembed'
16export * from './pagination'
17export * from './plugins'
18export * from './redundancy'
19export * from './search'
20export * from './server'
21export * from './sort'
22export * from './static'
23export * from './themes'
24export * from './user-email-verification'
25export * from './user-history'
26export * from './user-notifications'
27export * from './user-registrations'
28export * from './user-subscriptions'
29export * from './users'
30export * from './videos'
31export * from './webfinger'
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts
deleted file mode 100644
index e5008adc3..000000000
--- a/server/middlewares/validators/jobs.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import express from 'express'
2import { param, query } from 'express-validator'
3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs'
4import { loggerTagsFactory } from '../../helpers/logger'
5import { areValidationErrors } from './shared'
6
7const lTags = loggerTagsFactory('validators', 'jobs')
8
9const listJobsValidator = [
10 param('state')
11 .optional()
12 .custom(isValidJobState),
13
14 query('jobType')
15 .optional()
16 .custom(isValidJobType),
17
18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 if (areValidationErrors(req, res, lTags())) return
20
21 return next()
22 }
23]
24
25// ---------------------------------------------------------------------------
26
27export {
28 listJobsValidator
29}
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts
deleted file mode 100644
index 2d828bb42..000000000
--- a/server/middlewares/validators/logs.ts
+++ /dev/null
@@ -1,93 +0,0 @@
1import express from 'express'
2import { body, query } from 'express-validator'
3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { isStringArray } from '@server/helpers/custom-validators/search'
5import { CONFIG } from '@server/initializers/config'
6import { arrayify } from '@shared/core-utils'
7import { HttpStatusCode } from '@shared/models'
8import {
9 isValidClientLogLevel,
10 isValidClientLogMessage,
11 isValidClientLogMeta,
12 isValidClientLogStackTrace,
13 isValidClientLogUserAgent,
14 isValidLogLevel
15} from '../../helpers/custom-validators/logs'
16import { isDateValid } from '../../helpers/custom-validators/misc'
17import { areValidationErrors } from './shared'
18
19const createClientLogValidator = [
20 body('message')
21 .custom(isValidClientLogMessage),
22
23 body('url')
24 .custom(isUrlValid),
25
26 body('level')
27 .custom(isValidClientLogLevel),
28
29 body('stackTrace')
30 .optional()
31 .custom(isValidClientLogStackTrace),
32
33 body('meta')
34 .optional()
35 .custom(isValidClientLogMeta),
36
37 body('userAgent')
38 .optional()
39 .custom(isValidClientLogUserAgent),
40
41 (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 if (CONFIG.LOG.ACCEPT_CLIENT_LOG !== true) {
43 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
44 }
45
46 if (areValidationErrors(req, res)) return
47
48 return next()
49 }
50]
51
52const getLogsValidator = [
53 query('startDate')
54 .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
55 query('level')
56 .optional()
57 .custom(isValidLogLevel),
58 query('tagsOneOf')
59 .optional()
60 .customSanitizer(arrayify)
61 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
62 query('endDate')
63 .optional()
64 .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),
65
66 (req: express.Request, res: express.Response, next: express.NextFunction) => {
67 if (areValidationErrors(req, res)) return
68
69 return next()
70 }
71]
72
73const getAuditLogsValidator = [
74 query('startDate')
75 .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
76 query('endDate')
77 .optional()
78 .custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
79
80 (req: express.Request, res: express.Response, next: express.NextFunction) => {
81 if (areValidationErrors(req, res)) return
82
83 return next()
84 }
85]
86
87// ---------------------------------------------------------------------------
88
89export {
90 getLogsValidator,
91 getAuditLogsValidator,
92 createClientLogValidator
93}
diff --git a/server/middlewares/validators/metrics.ts b/server/middlewares/validators/metrics.ts
deleted file mode 100644
index 986b30a19..000000000
--- a/server/middlewares/validators/metrics.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isValidPlayerMode } from '@server/helpers/custom-validators/metrics'
4import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
5import { CONFIG } from '@server/initializers/config'
6import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
7import { areValidationErrors, doesVideoExist } from './shared'
8
9const addPlaybackMetricValidator = [
10 body('resolution')
11 .isInt({ min: 0 }),
12 body('fps')
13 .optional()
14 .isInt({ min: 0 }),
15
16 body('p2pPeers')
17 .optional()
18 .isInt({ min: 0 }),
19
20 body('p2pEnabled')
21 .isBoolean(),
22
23 body('playerMode')
24 .custom(isValidPlayerMode),
25
26 body('resolutionChanges')
27 .isInt({ min: 0 }),
28
29 body('errors')
30 .isInt({ min: 0 }),
31
32 body('downloadedBytesP2P')
33 .isInt({ min: 0 }),
34 body('downloadedBytesHTTP')
35 .isInt({ min: 0 }),
36
37 body('uploadedBytesP2P')
38 .isInt({ min: 0 }),
39
40 body('videoId')
41 .customSanitizer(toCompleteUUID)
42 .custom(isIdOrUUIDValid),
43
44 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
45 if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
46
47 const body: PlaybackMetricCreate = req.body
48
49 if (areValidationErrors(req, res)) return
50 if (!await doesVideoExist(body.videoId, res, 'only-immutable-attributes')) return
51
52 return next()
53 }
54]
55
56// ---------------------------------------------------------------------------
57
58export {
59 addPlaybackMetricValidator
60}
diff --git a/server/middlewares/validators/object-storage-proxy.ts b/server/middlewares/validators/object-storage-proxy.ts
deleted file mode 100644
index bbd77f262..000000000
--- a/server/middlewares/validators/object-storage-proxy.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import express from 'express'
2import { CONFIG } from '@server/initializers/config'
3import { HttpStatusCode } from '@shared/models'
4
5const ensurePrivateObjectStorageProxyIsEnabled = [
6 (req: express.Request, res: express.Response, next: express.NextFunction) => {
7 if (CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES !== true) {
8 return res.fail({
9 message: 'Private object storage proxy is not enabled',
10 status: HttpStatusCode.BAD_REQUEST_400
11 })
12 }
13
14 return next()
15 }
16]
17
18export {
19 ensurePrivateObjectStorageProxyIsEnabled
20}
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts
deleted file mode 100644
index ef9a227a0..000000000
--- a/server/middlewares/validators/oembed.ts
+++ /dev/null
@@ -1,158 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { join } from 'path'
4import { loadVideo } from '@server/lib/model-loaders'
5import { VideoPlaylistModel } from '@server/models/video/video-playlist'
6import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
8import { isTestOrDevInstance } from '../../helpers/core-utils'
9import { isIdOrUUIDValid, isUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc'
10import { WEBSERVER } from '../../initializers/constants'
11import { areValidationErrors } from './shared'
12
13const playlistPaths = [
14 join('videos', 'watch', 'playlist'),
15 join('w', 'p')
16]
17
18const videoPaths = [
19 join('videos', 'watch'),
20 'w'
21]
22
23function buildUrls (paths: string[]) {
24 return paths.map(p => WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, p) + '/')
25}
26
27const startPlaylistURLs = buildUrls(playlistPaths)
28const startVideoURLs = buildUrls(videoPaths)
29
30const isURLOptions = {
31 require_host: true,
32 require_tld: true
33}
34
35// We validate 'localhost', so we don't have the top level domain
36if (isTestOrDevInstance()) {
37 isURLOptions.require_tld = false
38}
39
40const oembedValidator = [
41 query('url')
42 .isURL(isURLOptions),
43 query('maxwidth')
44 .optional()
45 .isInt(),
46 query('maxheight')
47 .optional()
48 .isInt(),
49 query('format')
50 .optional()
51 .isIn([ 'xml', 'json' ]),
52
53 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
54 if (areValidationErrors(req, res)) return
55
56 if (req.query.format !== undefined && req.query.format !== 'json') {
57 return res.fail({
58 status: HttpStatusCode.NOT_IMPLEMENTED_501,
59 message: 'Requested format is not implemented on server.',
60 data: {
61 format: req.query.format
62 }
63 })
64 }
65
66 const url = req.query.url as string
67
68 let urlPath: string
69
70 try {
71 urlPath = new URL(url).pathname
72 } catch (err) {
73 return res.fail({
74 status: HttpStatusCode.BAD_REQUEST_400,
75 message: err.message,
76 data: {
77 url
78 }
79 })
80 }
81
82 const isPlaylist = startPlaylistURLs.some(u => url.startsWith(u))
83 const isVideo = isPlaylist ? false : startVideoURLs.some(u => url.startsWith(u))
84
85 const startIsOk = isVideo || isPlaylist
86
87 const parts = urlPath.split('/')
88
89 if (startIsOk === false || parts.length === 0) {
90 return res.fail({
91 status: HttpStatusCode.BAD_REQUEST_400,
92 message: 'Invalid url.',
93 data: {
94 url
95 }
96 })
97 }
98
99 const elementId = toCompleteUUID(parts.pop())
100 if (isIdOrUUIDValid(elementId) === false) {
101 return res.fail({ message: 'Invalid video or playlist id.' })
102 }
103
104 if (isVideo) {
105 const video = await loadVideo(elementId, 'all')
106
107 if (!video) {
108 return res.fail({
109 status: HttpStatusCode.NOT_FOUND_404,
110 message: 'Video not found'
111 })
112 }
113
114 if (
115 video.privacy === VideoPrivacy.PUBLIC ||
116 (video.privacy === VideoPrivacy.UNLISTED && isUUIDValid(elementId) === true)
117 ) {
118 res.locals.videoAll = video
119 return next()
120 }
121
122 return res.fail({
123 status: HttpStatusCode.FORBIDDEN_403,
124 message: 'Video is not publicly available'
125 })
126 }
127
128 // Is playlist
129
130 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
131 if (!videoPlaylist) {
132 return res.fail({
133 status: HttpStatusCode.NOT_FOUND_404,
134 message: 'Video playlist not found'
135 })
136 }
137
138 if (
139 videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC ||
140 (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED && isUUIDValid(elementId))
141 ) {
142 res.locals.videoPlaylistSummary = videoPlaylist
143 return next()
144 }
145
146 return res.fail({
147 status: HttpStatusCode.FORBIDDEN_403,
148 message: 'Playlist is not public'
149 })
150 }
151
152]
153
154// ---------------------------------------------------------------------------
155
156export {
157 oembedValidator
158}
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts
deleted file mode 100644
index 79ddbbf18..000000000
--- a/server/middlewares/validators/pagination.ts
+++ /dev/null
@@ -1,30 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { PAGINATION } from '@server/initializers/constants'
4import { areValidationErrors } from './shared'
5
6const paginationValidator = paginationValidatorBuilder()
7
8function paginationValidatorBuilder (tags: string[] = []) {
9 return [
10 query('start')
11 .optional()
12 .isInt({ min: 0 }),
13 query('count')
14 .optional()
15 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
16
17 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 if (areValidationErrors(req, res, { tags })) return
19
20 return next()
21 }
22 ]
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 paginationValidator,
29 paginationValidatorBuilder
30}
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
deleted file mode 100644
index 64bef2648..000000000
--- a/server/middlewares/validators/plugins.ts
+++ /dev/null
@@ -1,218 +0,0 @@
1import express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator'
3import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
4import { PluginType } from '../../../shared/models/plugins/plugin.type'
5import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model'
6import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
7import {
8 isNpmPluginNameValid,
9 isPluginNameValid,
10 isPluginStableOrUnstableVersionValid,
11 isPluginTypeValid
12} from '../../helpers/custom-validators/plugins'
13import { CONFIG } from '../../initializers/config'
14import { PluginManager } from '../../lib/plugins/plugin-manager'
15import { PluginModel } from '../../models/server/plugin'
16import { areValidationErrors } from './shared'
17
18const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
19 const validators: (ValidationChain | express.Handler)[] = [
20 param('pluginName')
21 .custom(isPluginNameValid)
22 ]
23
24 if (withVersion) {
25 validators.push(
26 param('pluginVersion')
27 .custom(isPluginStableOrUnstableVersionValid)
28 )
29 }
30
31 return validators.concat([
32 (req: express.Request, res: express.Response, next: express.NextFunction) => {
33 if (areValidationErrors(req, res)) return
34
35 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
36 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
37
38 if (!plugin) {
39 return res.fail({
40 status: HttpStatusCode.NOT_FOUND_404,
41 message: 'No plugin found named ' + npmName
42 })
43 }
44 if (withVersion && plugin.version !== req.params.pluginVersion) {
45 return res.fail({
46 status: HttpStatusCode.NOT_FOUND_404,
47 message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion
48 })
49 }
50
51 res.locals.registeredPlugin = plugin
52
53 return next()
54 }
55 ])
56}
57
58const getExternalAuthValidator = [
59 param('authName')
60 .custom(exists),
61
62 (req: express.Request, res: express.Response, next: express.NextFunction) => {
63 if (areValidationErrors(req, res)) return
64
65 const plugin = res.locals.registeredPlugin
66 if (!plugin.registerHelpers) {
67 return res.fail({
68 status: HttpStatusCode.NOT_FOUND_404,
69 message: 'No registered helpers were found for this plugin'
70 })
71 }
72
73 const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
74 if (!externalAuth) {
75 return res.fail({
76 status: HttpStatusCode.NOT_FOUND_404,
77 message: 'No external auths were found for this plugin'
78 })
79 }
80
81 res.locals.externalAuth = externalAuth
82
83 return next()
84 }
85]
86
87const pluginStaticDirectoryValidator = [
88 param('staticEndpoint')
89 .custom(isSafePath),
90
91 (req: express.Request, res: express.Response, next: express.NextFunction) => {
92 if (areValidationErrors(req, res)) return
93
94 return next()
95 }
96]
97
98const listPluginsValidator = [
99 query('pluginType')
100 .optional()
101 .customSanitizer(toIntOrNull)
102 .custom(isPluginTypeValid),
103 query('uninstalled')
104 .optional()
105 .customSanitizer(toBooleanOrNull)
106 .custom(isBooleanValid),
107
108 (req: express.Request, res: express.Response, next: express.NextFunction) => {
109 if (areValidationErrors(req, res)) return
110
111 return next()
112 }
113]
114
115const installOrUpdatePluginValidator = [
116 body('npmName')
117 .optional()
118 .custom(isNpmPluginNameValid),
119 body('pluginVersion')
120 .optional()
121 .custom(isPluginStableOrUnstableVersionValid),
122 body('path')
123 .optional()
124 .custom(isSafePath),
125
126 (req: express.Request, res: express.Response, next: express.NextFunction) => {
127 if (areValidationErrors(req, res)) return
128
129 const body: InstallOrUpdatePlugin = req.body
130 if (!body.path && !body.npmName) {
131 return res.fail({ message: 'Should have either a npmName or a path' })
132 }
133 if (body.pluginVersion && !body.npmName) {
134 return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
135 }
136
137 return next()
138 }
139]
140
141const uninstallPluginValidator = [
142 body('npmName')
143 .custom(isNpmPluginNameValid),
144
145 (req: express.Request, res: express.Response, next: express.NextFunction) => {
146 if (areValidationErrors(req, res)) return
147
148 return next()
149 }
150]
151
152const existingPluginValidator = [
153 param('npmName')
154 .custom(isNpmPluginNameValid),
155
156 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
157 if (areValidationErrors(req, res)) return
158
159 const plugin = await PluginModel.loadByNpmName(req.params.npmName)
160 if (!plugin) {
161 return res.fail({
162 status: HttpStatusCode.NOT_FOUND_404,
163 message: 'Plugin not found'
164 })
165 }
166
167 res.locals.plugin = plugin
168 return next()
169 }
170]
171
172const updatePluginSettingsValidator = [
173 body('settings')
174 .exists(),
175
176 (req: express.Request, res: express.Response, next: express.NextFunction) => {
177 if (areValidationErrors(req, res)) return
178
179 return next()
180 }
181]
182
183const listAvailablePluginsValidator = [
184 query('search')
185 .optional()
186 .exists(),
187 query('pluginType')
188 .optional()
189 .customSanitizer(toIntOrNull)
190 .custom(isPluginTypeValid),
191 query('currentPeerTubeEngine')
192 .optional()
193 .custom(isPluginStableOrUnstableVersionValid),
194
195 (req: express.Request, res: express.Response, next: express.NextFunction) => {
196 if (areValidationErrors(req, res)) return
197
198 if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
199 return res.fail({ message: 'Plugin index is not enabled' })
200 }
201
202 return next()
203 }
204]
205
206// ---------------------------------------------------------------------------
207
208export {
209 pluginStaticDirectoryValidator,
210 getPluginValidator,
211 updatePluginSettingsValidator,
212 uninstallPluginValidator,
213 listAvailablePluginsValidator,
214 existingPluginValidator,
215 installOrUpdatePluginValidator,
216 listPluginsValidator,
217 getExternalAuthValidator
218}
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts
deleted file mode 100644
index c80f9b728..000000000
--- a/server/middlewares/validators/redundancy.ts
+++ /dev/null
@@ -1,198 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies'
4import { forceNumber } from '@shared/core-utils'
5import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
6import {
7 exists,
8 isBooleanValid,
9 isIdOrUUIDValid,
10 isIdValid,
11 toBooleanOrNull,
12 toCompleteUUID,
13 toIntOrNull
14} from '../../helpers/custom-validators/misc'
15import { isHostValid } from '../../helpers/custom-validators/servers'
16import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
17import { ServerModel } from '../../models/server/server'
18import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
19
20const videoFileRedundancyGetValidator = [
21 isValidVideoIdParam('videoId'),
22
23 param('resolution')
24 .customSanitizer(toIntOrNull)
25 .custom(exists),
26 param('fps')
27 .optional()
28 .customSanitizer(toIntOrNull)
29 .custom(exists),
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
37 const paramResolution = req.params.resolution as unknown as number // We casted to int above
38 const paramFPS = req.params.fps as unknown as number // We casted to int above
39
40 const videoFile = video.VideoFiles.find(f => {
41 return f.resolution === paramResolution && (!req.params.fps || paramFPS)
42 })
43
44 if (!videoFile) {
45 return res.fail({
46 status: HttpStatusCode.NOT_FOUND_404,
47 message: 'Video file not found.'
48 })
49 }
50 res.locals.videoFile = videoFile
51
52 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
53 if (!videoRedundancy) {
54 return res.fail({
55 status: HttpStatusCode.NOT_FOUND_404,
56 message: 'Video redundancy not found.'
57 })
58 }
59 res.locals.videoRedundancy = videoRedundancy
60
61 return next()
62 }
63]
64
65const videoPlaylistRedundancyGetValidator = [
66 isValidVideoIdParam('videoId'),
67
68 param('streamingPlaylistType')
69 .customSanitizer(toIntOrNull)
70 .custom(exists),
71
72 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
73 if (areValidationErrors(req, res)) return
74 if (!await doesVideoExist(req.params.videoId, res)) return
75
76 const video = res.locals.videoAll
77
78 const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above
79 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType)
80
81 if (!videoStreamingPlaylist) {
82 return res.fail({
83 status: HttpStatusCode.NOT_FOUND_404,
84 message: 'Video playlist not found.'
85 })
86 }
87 res.locals.videoStreamingPlaylist = videoStreamingPlaylist
88
89 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
90 if (!videoRedundancy) {
91 return res.fail({
92 status: HttpStatusCode.NOT_FOUND_404,
93 message: 'Video redundancy not found.'
94 })
95 }
96 res.locals.videoRedundancy = videoRedundancy
97
98 return next()
99 }
100]
101
102const updateServerRedundancyValidator = [
103 param('host')
104 .custom(isHostValid),
105
106 body('redundancyAllowed')
107 .customSanitizer(toBooleanOrNull)
108 .custom(isBooleanValid).withMessage('Should have a valid redundancyAllowed boolean'),
109
110 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111 if (areValidationErrors(req, res)) return
112
113 const server = await ServerModel.loadByHost(req.params.host)
114
115 if (!server) {
116 return res.fail({
117 status: HttpStatusCode.NOT_FOUND_404,
118 message: `Server ${req.params.host} not found.`
119 })
120 }
121
122 res.locals.server = server
123 return next()
124 }
125]
126
127const listVideoRedundanciesValidator = [
128 query('target')
129 .custom(isVideoRedundancyTarget),
130
131 (req: express.Request, res: express.Response, next: express.NextFunction) => {
132 if (areValidationErrors(req, res)) return
133
134 return next()
135 }
136]
137
138const addVideoRedundancyValidator = [
139 body('videoId')
140 .customSanitizer(toCompleteUUID)
141 .custom(isIdOrUUIDValid),
142
143 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
144 if (areValidationErrors(req, res)) return
145
146 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
147
148 if (res.locals.onlyVideo.remote === false) {
149 return res.fail({ message: 'Cannot create a redundancy on a local video' })
150 }
151
152 if (res.locals.onlyVideo.isLive) {
153 return res.fail({ message: 'Cannot create a redundancy of a live video' })
154 }
155
156 const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
157 if (alreadyExists) {
158 return res.fail({
159 status: HttpStatusCode.CONFLICT_409,
160 message: 'This video is already duplicated by your instance.'
161 })
162 }
163
164 return next()
165 }
166]
167
168const removeVideoRedundancyValidator = [
169 param('redundancyId')
170 .custom(isIdValid),
171
172 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
173 if (areValidationErrors(req, res)) return
174
175 const redundancy = await VideoRedundancyModel.loadByIdWithVideo(forceNumber(req.params.redundancyId))
176 if (!redundancy) {
177 return res.fail({
178 status: HttpStatusCode.NOT_FOUND_404,
179 message: 'Video redundancy not found'
180 })
181 }
182
183 res.locals.videoRedundancy = redundancy
184
185 return next()
186 }
187]
188
189// ---------------------------------------------------------------------------
190
191export {
192 videoFileRedundancyGetValidator,
193 videoPlaylistRedundancyGetValidator,
194 updateServerRedundancyValidator,
195 listVideoRedundanciesValidator,
196 addVideoRedundancyValidator,
197 removeVideoRedundancyValidator
198}
diff --git a/server/middlewares/validators/runners/index.ts b/server/middlewares/validators/runners/index.ts
deleted file mode 100644
index 9a9629a80..000000000
--- a/server/middlewares/validators/runners/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './jobs'
2export * from './registration-token'
3export * from './runners'
diff --git a/server/middlewares/validators/runners/job-files.ts b/server/middlewares/validators/runners/job-files.ts
deleted file mode 100644
index 57c27fcfe..000000000
--- a/server/middlewares/validators/runners/job-files.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { basename } from 'path'
4import { isSafeFilename } from '@server/helpers/custom-validators/misc'
5import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobStudioTranscodingPayload } from '@shared/models'
6import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
7
8const tags = [ 'runner' ]
9
10export const runnerJobGetVideoTranscodingFileValidator = [
11 isValidVideoIdParam('videoId'),
12
13 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 if (areValidationErrors(req, res)) return
15
16 if (!await doesVideoExist(req.params.videoId, res, 'all')) return
17
18 const runnerJob = res.locals.runnerJob
19
20 if (runnerJob.privatePayload.videoUUID !== res.locals.videoAll.uuid) {
21 return res.fail({
22 status: HttpStatusCode.FORBIDDEN_403,
23 message: 'Job is not associated to this video',
24 tags: [ ...tags, res.locals.videoAll.uuid ]
25 })
26 }
27
28 return next()
29 }
30]
31
32export const runnerJobGetVideoStudioTaskFileValidator = [
33 param('filename').custom(v => isSafeFilename(v)),
34
35 (req: express.Request, res: express.Response, next: express.NextFunction) => {
36 if (areValidationErrors(req, res)) return
37
38 const filename = req.params.filename
39
40 const payload = res.locals.runnerJob.payload as RunnerJobStudioTranscodingPayload
41
42 const found = Array.isArray(payload?.tasks) && payload.tasks.some(t => {
43 if (hasVideoStudioTaskFile(t)) {
44 return basename(t.options.file) === filename
45 }
46
47 return false
48 })
49
50 if (!found) {
51 return res.fail({
52 status: HttpStatusCode.BAD_REQUEST_400,
53 message: 'File is not associated to this edition task',
54 tags: [ ...tags, res.locals.videoAll.uuid ]
55 })
56 }
57
58 return next()
59 }
60]
diff --git a/server/middlewares/validators/runners/jobs.ts b/server/middlewares/validators/runners/jobs.ts
deleted file mode 100644
index 62f9340a5..000000000
--- a/server/middlewares/validators/runners/jobs.ts
+++ /dev/null
@@ -1,216 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc'
4import {
5 isRunnerJobAbortReasonValid,
6 isRunnerJobArrayOfStateValid,
7 isRunnerJobErrorMessageValid,
8 isRunnerJobProgressValid,
9 isRunnerJobSuccessPayloadValid,
10 isRunnerJobTokenValid,
11 isRunnerJobUpdatePayloadValid
12} from '@server/helpers/custom-validators/runners/jobs'
13import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners'
14import { cleanUpReqFiles } from '@server/helpers/express-utils'
15import { LiveManager } from '@server/lib/live'
16import { runnerJobCanBeCancelled } from '@server/lib/runners'
17import { RunnerJobModel } from '@server/models/runner/runner-job'
18import { arrayify } from '@shared/core-utils'
19import {
20 HttpStatusCode,
21 RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
22 RunnerJobState,
23 RunnerJobSuccessBody,
24 RunnerJobUpdateBody,
25 ServerErrorCode
26} from '@shared/models'
27import { areValidationErrors } from '../shared'
28
29const tags = [ 'runner' ]
30
31export const acceptRunnerJobValidator = [
32 (req: express.Request, res: express.Response, next: express.NextFunction) => {
33 if (res.locals.runnerJob.state !== RunnerJobState.PENDING) {
34 return res.fail({
35 status: HttpStatusCode.BAD_REQUEST_400,
36 message: 'This runner job is not in pending state',
37 tags
38 })
39 }
40
41 return next()
42 }
43]
44
45export const abortRunnerJobValidator = [
46 body('reason').custom(isRunnerJobAbortReasonValid),
47
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res, { tags })) return
50
51 return next()
52 }
53]
54
55export const updateRunnerJobValidator = [
56 body('progress').optional().custom(isRunnerJobProgressValid),
57
58 (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 if (areValidationErrors(req, res, { tags })) return cleanUpReqFiles(req)
60
61 const body = req.body as RunnerJobUpdateBody
62 const job = res.locals.runnerJob
63
64 if (isRunnerJobUpdatePayloadValid(body.payload, job.type, req.files) !== true) {
65 cleanUpReqFiles(req)
66
67 return res.fail({
68 status: HttpStatusCode.BAD_REQUEST_400,
69 message: 'Payload is invalid',
70 tags
71 })
72 }
73
74 if (res.locals.runnerJob.type === 'live-rtmp-hls-transcoding') {
75 const privatePayload = job.privatePayload as RunnerJobLiveRTMPHLSTranscodingPrivatePayload
76
77 if (!LiveManager.Instance.hasSession(privatePayload.sessionId)) {
78 cleanUpReqFiles(req)
79
80 return res.fail({
81 status: HttpStatusCode.BAD_REQUEST_400,
82 type: ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE,
83 message: 'Session of this live ended',
84 tags
85 })
86 }
87 }
88
89 return next()
90 }
91]
92
93export const errorRunnerJobValidator = [
94 body('message').custom(isRunnerJobErrorMessageValid),
95
96 (req: express.Request, res: express.Response, next: express.NextFunction) => {
97 if (areValidationErrors(req, res, { tags })) return
98
99 return next()
100 }
101]
102
103export const successRunnerJobValidator = [
104 (req: express.Request, res: express.Response, next: express.NextFunction) => {
105 const body = req.body as RunnerJobSuccessBody
106
107 if (isRunnerJobSuccessPayloadValid(body.payload, res.locals.runnerJob.type, req.files) !== true) {
108 cleanUpReqFiles(req)
109
110 return res.fail({
111 status: HttpStatusCode.BAD_REQUEST_400,
112 message: 'Payload is invalid',
113 tags
114 })
115 }
116
117 return next()
118 }
119]
120
121export const cancelRunnerJobValidator = [
122 (req: express.Request, res: express.Response, next: express.NextFunction) => {
123 const runnerJob = res.locals.runnerJob
124
125 if (runnerJobCanBeCancelled(runnerJob) !== true) {
126 return res.fail({
127 status: HttpStatusCode.BAD_REQUEST_400,
128 message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
129 tags
130 })
131 }
132
133 return next()
134 }
135]
136
137export const listRunnerJobsValidator = [
138 query('search')
139 .optional()
140 .custom(exists),
141
142 query('stateOneOf')
143 .optional()
144 .customSanitizer(arrayify)
145 .custom(isRunnerJobArrayOfStateValid),
146
147 (req: express.Request, res: express.Response, next: express.NextFunction) => {
148 return next()
149 }
150]
151
152export const runnerJobGetValidator = [
153 param('jobUUID').custom(isUUIDValid),
154
155 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
156 if (areValidationErrors(req, res, { tags })) return
157
158 const runnerJob = await RunnerJobModel.loadWithRunner(req.params.jobUUID)
159
160 if (!runnerJob) {
161 return res.fail({
162 status: HttpStatusCode.NOT_FOUND_404,
163 message: 'Unknown runner job',
164 tags
165 })
166 }
167
168 res.locals.runnerJob = runnerJob
169
170 return next()
171 }
172]
173
174export function jobOfRunnerGetValidatorFactory (allowedStates: RunnerJobState[]) {
175 return [
176 param('jobUUID').custom(isUUIDValid),
177
178 body('runnerToken').custom(isRunnerTokenValid),
179 body('jobToken').custom(isRunnerJobTokenValid),
180
181 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
182 if (areValidationErrors(req, res, { tags })) return cleanUpReqFiles(req)
183
184 const runnerJob = await RunnerJobModel.loadByRunnerAndJobTokensWithRunner({
185 uuid: req.params.jobUUID,
186 runnerToken: req.body.runnerToken,
187 jobToken: req.body.jobToken
188 })
189
190 if (!runnerJob) {
191 cleanUpReqFiles(req)
192
193 return res.fail({
194 status: HttpStatusCode.NOT_FOUND_404,
195 message: 'Unknown runner job',
196 tags
197 })
198 }
199
200 if (!allowedStates.includes(runnerJob.state)) {
201 cleanUpReqFiles(req)
202
203 return res.fail({
204 status: HttpStatusCode.BAD_REQUEST_400,
205 type: ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE,
206 message: 'Job is not in "processing" state',
207 tags
208 })
209 }
210
211 res.locals.runnerJob = runnerJob
212
213 return next()
214 }
215 ]
216}
diff --git a/server/middlewares/validators/runners/registration-token.ts b/server/middlewares/validators/runners/registration-token.ts
deleted file mode 100644
index cc31d4a7e..000000000
--- a/server/middlewares/validators/runners/registration-token.ts
+++ /dev/null
@@ -1,37 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { isIdValid } from '@server/helpers/custom-validators/misc'
4import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
5import { forceNumber } from '@shared/core-utils'
6import { HttpStatusCode } from '@shared/models'
7import { areValidationErrors } from '../shared/utils'
8
9const tags = [ 'runner' ]
10
11const deleteRegistrationTokenValidator = [
12 param('id').custom(isIdValid),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 if (areValidationErrors(req, res, { tags })) return
16
17 const registrationToken = await RunnerRegistrationTokenModel.load(forceNumber(req.params.id))
18
19 if (!registrationToken) {
20 return res.fail({
21 status: HttpStatusCode.NOT_FOUND_404,
22 message: 'Registration token not found',
23 tags
24 })
25 }
26
27 res.locals.runnerRegistrationToken = registrationToken
28
29 return next()
30 }
31]
32
33// ---------------------------------------------------------------------------
34
35export {
36 deleteRegistrationTokenValidator
37}
diff --git a/server/middlewares/validators/runners/runners.ts b/server/middlewares/validators/runners/runners.ts
deleted file mode 100644
index 4d4d79b4c..000000000
--- a/server/middlewares/validators/runners/runners.ts
+++ /dev/null
@@ -1,104 +0,0 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { isIdValid } from '@server/helpers/custom-validators/misc'
4import {
5 isRunnerDescriptionValid,
6 isRunnerNameValid,
7 isRunnerRegistrationTokenValid,
8 isRunnerTokenValid
9} from '@server/helpers/custom-validators/runners/runners'
10import { RunnerModel } from '@server/models/runner/runner'
11import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
12import { forceNumber } from '@shared/core-utils'
13import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@shared/models'
14import { areValidationErrors } from '../shared/utils'
15
16const tags = [ 'runner' ]
17
18const registerRunnerValidator = [
19 body('registrationToken').custom(isRunnerRegistrationTokenValid),
20 body('name').custom(isRunnerNameValid),
21 body('description').optional().custom(isRunnerDescriptionValid),
22
23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
24 if (areValidationErrors(req, res, { tags })) return
25
26 const body: RegisterRunnerBody = req.body
27
28 const runnerRegistrationToken = await RunnerRegistrationTokenModel.loadByRegistrationToken(body.registrationToken)
29
30 if (!runnerRegistrationToken) {
31 return res.fail({
32 status: HttpStatusCode.NOT_FOUND_404,
33 message: 'Registration token is invalid',
34 tags
35 })
36 }
37
38 const existing = await RunnerModel.loadByName(body.name)
39 if (existing) {
40 return res.fail({
41 status: HttpStatusCode.BAD_REQUEST_400,
42 message: 'This runner name already exists on this instance',
43 tags
44 })
45 }
46
47 res.locals.runnerRegistrationToken = runnerRegistrationToken
48
49 return next()
50 }
51]
52
53const deleteRunnerValidator = [
54 param('runnerId').custom(isIdValid),
55
56 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
57 if (areValidationErrors(req, res, { tags })) return
58
59 const runner = await RunnerModel.load(forceNumber(req.params.runnerId))
60
61 if (!runner) {
62 return res.fail({
63 status: HttpStatusCode.NOT_FOUND_404,
64 message: 'Runner not found',
65 tags
66 })
67 }
68
69 res.locals.runner = runner
70
71 return next()
72 }
73]
74
75const getRunnerFromTokenValidator = [
76 body('runnerToken').custom(isRunnerTokenValid),
77
78 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
79 if (areValidationErrors(req, res, { tags })) return
80
81 const runner = await RunnerModel.loadByToken(req.body.runnerToken)
82
83 if (!runner) {
84 return res.fail({
85 status: HttpStatusCode.NOT_FOUND_404,
86 message: 'Unknown runner token',
87 type: ServerErrorCode.UNKNOWN_RUNNER_TOKEN,
88 tags
89 })
90 }
91
92 res.locals.runner = runner
93
94 return next()
95 }
96]
97
98// ---------------------------------------------------------------------------
99
100export {
101 registerRunnerValidator,
102 deleteRunnerValidator,
103 getRunnerFromTokenValidator
104}
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
deleted file mode 100644
index a63fd0893..000000000
--- a/server/middlewares/validators/search.ts
+++ /dev/null
@@ -1,112 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { isSearchTargetValid } from '@server/helpers/custom-validators/search'
4import { isHostValid } from '@server/helpers/custom-validators/servers'
5import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc'
6import { areValidationErrors } from './shared'
7
8const videosSearchValidator = [
9 query('search')
10 .optional()
11 .not().isEmpty(),
12
13 query('host')
14 .optional()
15 .custom(isHostValid),
16
17 query('startDate')
18 .optional()
19 .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
20 query('endDate')
21 .optional()
22 .custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
23
24 query('originallyPublishedStartDate')
25 .optional()
26 .custom(isDateValid).withMessage('Should have a published start date that conforms to ISO 8601'),
27 query('originallyPublishedEndDate')
28 .optional()
29 .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'),
30
31 query('durationMin')
32 .optional()
33 .isInt(),
34 query('durationMax')
35 .optional()
36 .isInt(),
37
38 query('uuids')
39 .optional()
40 .toArray()
41 .customSanitizer(toCompleteUUIDs)
42 .custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
43
44 query('searchTarget')
45 .optional()
46 .custom(isSearchTargetValid),
47
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
50
51 return next()
52 }
53]
54
55const videoChannelsListSearchValidator = [
56 query('search')
57 .optional()
58 .not().isEmpty(),
59
60 query('host')
61 .optional()
62 .custom(isHostValid),
63
64 query('searchTarget')
65 .optional()
66 .custom(isSearchTargetValid),
67
68 query('handles')
69 .optional()
70 .toArray()
71 .custom(isNotEmptyStringArray).withMessage('Should have valid array of handles'),
72
73 (req: express.Request, res: express.Response, next: express.NextFunction) => {
74 if (areValidationErrors(req, res)) return
75
76 return next()
77 }
78]
79
80const videoPlaylistsListSearchValidator = [
81 query('search')
82 .optional()
83 .not().isEmpty(),
84
85 query('host')
86 .optional()
87 .custom(isHostValid),
88
89 query('searchTarget')
90 .optional()
91 .custom(isSearchTargetValid),
92
93 query('uuids')
94 .optional()
95 .toArray()
96 .customSanitizer(toCompleteUUIDs)
97 .custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
98
99 (req: express.Request, res: express.Response, next: express.NextFunction) => {
100 if (areValidationErrors(req, res)) return
101
102 return next()
103 }
104]
105
106// ---------------------------------------------------------------------------
107
108export {
109 videosSearchValidator,
110 videoChannelsListSearchValidator,
111 videoPlaylistsListSearchValidator
112}
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
deleted file mode 100644
index 7d37ae229..000000000
--- a/server/middlewares/validators/server.ts
+++ /dev/null
@@ -1,75 +0,0 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
4import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
5import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
6import { logger } from '../../helpers/logger'
7import { CONFIG, isEmailEnabled } from '../../initializers/config'
8import { Redis } from '../../lib/redis'
9import { ServerModel } from '../../models/server/server'
10import { areValidationErrors } from './shared'
11
12const serverGetValidator = [
13 body('host').custom(isHostValid).withMessage('Should have a valid host'),
14
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 if (areValidationErrors(req, res)) return
17
18 const server = await ServerModel.loadByHost(req.body.host)
19 if (!server) {
20 return res.fail({
21 status: HttpStatusCode.NOT_FOUND_404,
22 message: 'Server host not found.'
23 })
24 }
25
26 res.locals.server = server
27
28 return next()
29 }
30]
31
32const contactAdministratorValidator = [
33 body('fromName')
34 .custom(isUserDisplayNameValid),
35 body('fromEmail')
36 .isEmail(),
37 body('body')
38 .custom(isValidContactBody),
39
40 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
41 if (areValidationErrors(req, res)) return
42
43 if (CONFIG.CONTACT_FORM.ENABLED === false) {
44 return res.fail({
45 status: HttpStatusCode.CONFLICT_409,
46 message: 'Contact form is not enabled on this instance.'
47 })
48 }
49
50 if (isEmailEnabled() === false) {
51 return res.fail({
52 status: HttpStatusCode.CONFLICT_409,
53 message: 'SMTP is not configured on this instance.'
54 })
55 }
56
57 if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
58 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
59
60 return res.fail({
61 status: HttpStatusCode.FORBIDDEN_403,
62 message: 'You already sent a contact form recently.'
63 })
64 }
65
66 return next()
67 }
68]
69
70// ---------------------------------------------------------------------------
71
72export {
73 serverGetValidator,
74 contactAdministratorValidator
75}
diff --git a/server/middlewares/validators/shared/abuses.ts b/server/middlewares/validators/shared/abuses.ts
deleted file mode 100644
index 2c988f9ec..000000000
--- a/server/middlewares/validators/shared/abuses.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import { Response } from 'express'
2import { AbuseModel } from '@server/models/abuse/abuse'
3import { HttpStatusCode } from '@shared/models'
4import { forceNumber } from '@shared/core-utils'
5
6async function doesAbuseExist (abuseId: number | string, res: Response) {
7 const abuse = await AbuseModel.loadByIdWithReporter(forceNumber(abuseId))
8
9 if (!abuse) {
10 res.fail({
11 status: HttpStatusCode.NOT_FOUND_404,
12 message: 'Abuse not found'
13 })
14
15 return false
16 }
17
18 res.locals.abuse = abuse
19 return true
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 doesAbuseExist
26}
diff --git a/server/middlewares/validators/shared/accounts.ts b/server/middlewares/validators/shared/accounts.ts
deleted file mode 100644
index 72b0e235e..000000000
--- a/server/middlewares/validators/shared/accounts.ts
+++ /dev/null
@@ -1,66 +0,0 @@
1import { Response } from 'express'
2import { AccountModel } from '@server/models/account/account'
3import { UserModel } from '@server/models/user/user'
4import { MAccountDefault } from '@server/types/models'
5import { forceNumber } from '@shared/core-utils'
6import { HttpStatusCode } from '@shared/models'
7
8function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
9 const promise = AccountModel.load(forceNumber(id))
10
11 return doesAccountExist(promise, res, sendNotFound)
12}
13
14function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) {
15 const promise = AccountModel.loadLocalByName(name)
16
17 return doesAccountExist(promise, res, sendNotFound)
18}
19
20function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
21 const promise = AccountModel.loadByNameWithHost(nameWithDomain)
22
23 return doesAccountExist(promise, res, sendNotFound)
24}
25
26async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sendNotFound: boolean) {
27 const account = await p
28
29 if (!account) {
30 if (sendNotFound === true) {
31 res.fail({
32 status: HttpStatusCode.NOT_FOUND_404,
33 message: 'Account not found'
34 })
35 }
36 return false
37 }
38
39 res.locals.account = account
40 return true
41}
42
43async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) {
44 const user = await UserModel.loadByIdWithChannels(forceNumber(id))
45
46 if (token !== user.feedToken) {
47 res.fail({
48 status: HttpStatusCode.FORBIDDEN_403,
49 message: 'User and token mismatch'
50 })
51 return false
52 }
53
54 res.locals.user = user
55 return true
56}
57
58// ---------------------------------------------------------------------------
59
60export {
61 doesAccountIdExist,
62 doesLocalAccountNameExist,
63 doesAccountNameWithHostExist,
64 doesAccountExist,
65 doesUserFeedTokenCorrespond
66}
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts
deleted file mode 100644
index e5cff2dda..000000000
--- a/server/middlewares/validators/shared/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1export * from './abuses'
2export * from './accounts'
3export * from './users'
4export * from './utils'
5export * from './video-blacklists'
6export * from './video-captions'
7export * from './video-channels'
8export * from './video-channel-syncs'
9export * from './video-comments'
10export * from './video-imports'
11export * from './video-ownerships'
12export * from './video-playlists'
13export * from './video-passwords'
14export * from './videos'
diff --git a/server/middlewares/validators/shared/user-registrations.ts b/server/middlewares/validators/shared/user-registrations.ts
deleted file mode 100644
index dbc7dda06..000000000
--- a/server/middlewares/validators/shared/user-registrations.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import express from 'express'
2import { UserRegistrationModel } from '@server/models/user/user-registration'
3import { MRegistration } from '@server/types/models'
4import { forceNumber, pick } from '@shared/core-utils'
5import { HttpStatusCode } from '@shared/models'
6
7function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
8 const id = forceNumber(idArg)
9 return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
10}
11
12function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) {
13 return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse)
14}
15
16async function checkRegistrationHandlesDoNotAlreadyExist (options: {
17 username: string
18 channelHandle: string
19 email: string
20 res: express.Response
21}) {
22 const { res } = options
23
24 const registration = await UserRegistrationModel.loadByEmailOrHandle(pick(options, [ 'username', 'email', 'channelHandle' ]))
25
26 if (registration) {
27 res.fail({
28 status: HttpStatusCode.CONFLICT_409,
29 message: 'Registration with this username, channel name or email already exists.'
30 })
31 return false
32 }
33
34 return true
35}
36
37async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) {
38 const registration = await finder()
39
40 if (!registration) {
41 if (abortResponse === true) {
42 res.fail({
43 status: HttpStatusCode.NOT_FOUND_404,
44 message: 'User not found'
45 })
46 }
47
48 return false
49 }
50
51 res.locals.userRegistration = registration
52 return true
53}
54
55export {
56 checkRegistrationIdExist,
57 checkRegistrationEmailExist,
58 checkRegistrationHandlesDoNotAlreadyExist,
59 checkRegistrationExist
60}
diff --git a/server/middlewares/validators/shared/users.ts b/server/middlewares/validators/shared/users.ts
deleted file mode 100644
index 030adc9f7..000000000
--- a/server/middlewares/validators/shared/users.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1import express from 'express'
2import { ActorModel } from '@server/models/actor/actor'
3import { UserModel } from '@server/models/user/user'
4import { MUserDefault } from '@server/types/models'
5import { forceNumber } from '@shared/core-utils'
6import { HttpStatusCode } from '@shared/models'
7
8function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
9 const id = forceNumber(idArg)
10 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
11}
12
13function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
14 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
15}
16
17async function checkUserNameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
18 const user = await UserModel.loadByUsernameOrEmail(username, email)
19
20 if (user) {
21 res.fail({
22 status: HttpStatusCode.CONFLICT_409,
23 message: 'User with this username or email already exists.'
24 })
25 return false
26 }
27
28 const actor = await ActorModel.loadLocalByName(username)
29 if (actor) {
30 res.fail({
31 status: HttpStatusCode.CONFLICT_409,
32 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
33 })
34 return false
35 }
36
37 return true
38}
39
40async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
41 const user = await finder()
42
43 if (!user) {
44 if (abortResponse === true) {
45 res.fail({
46 status: HttpStatusCode.NOT_FOUND_404,
47 message: 'User not found'
48 })
49 }
50
51 return false
52 }
53
54 res.locals.user = user
55 return true
56}
57
58export {
59 checkUserIdExist,
60 checkUserEmailExist,
61 checkUserNameOrEmailDoNotAlreadyExist,
62 checkUserExist
63}
diff --git a/server/middlewares/validators/shared/utils.ts b/server/middlewares/validators/shared/utils.ts
deleted file mode 100644
index f39128fdd..000000000
--- a/server/middlewares/validators/shared/utils.ts
+++ /dev/null
@@ -1,69 +0,0 @@
1import express from 'express'
2import { param, validationResult } from 'express-validator'
3import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
4import { logger } from '../../../helpers/logger'
5
6function areValidationErrors (
7 req: express.Request,
8 res: express.Response,
9 options: {
10 omitLog?: boolean
11 omitBodyLog?: boolean
12 tags?: string[]
13 } = {}) {
14 const { omitLog = false, omitBodyLog = false, tags = [] } = options
15
16 if (!omitLog) {
17 logger.debug(
18 'Checking %s - %s parameters',
19 req.method, req.originalUrl,
20 {
21 body: omitBodyLog
22 ? 'omitted'
23 : req.body,
24 params: req.params,
25 query: req.query,
26 files: req.files,
27 tags
28 }
29 )
30 }
31
32 const errors = validationResult(req)
33
34 if (!errors.isEmpty()) {
35 logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
36
37 res.fail({
38 message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '),
39 instance: req.originalUrl,
40 data: {
41 'invalid-params': errors.mapped()
42 }
43 })
44
45 return true
46 }
47
48 return false
49}
50
51function isValidVideoIdParam (paramName: string) {
52 return param(paramName)
53 .customSanitizer(toCompleteUUID)
54 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id (id, short UUID or UUID)')
55}
56
57function isValidPlaylistIdParam (paramName: string) {
58 return param(paramName)
59 .customSanitizer(toCompleteUUID)
60 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id (id, short UUID or UUID)')
61}
62
63// ---------------------------------------------------------------------------
64
65export {
66 areValidationErrors,
67 isValidVideoIdParam,
68 isValidPlaylistIdParam
69}
diff --git a/server/middlewares/validators/shared/video-blacklists.ts b/server/middlewares/validators/shared/video-blacklists.ts
deleted file mode 100644
index f85b39b23..000000000
--- a/server/middlewares/validators/shared/video-blacklists.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import { Response } from 'express'
2import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
3import { HttpStatusCode } from '@shared/models'
4
5async function doesVideoBlacklistExist (videoId: number, res: Response) {
6 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
7
8 if (videoBlacklist === null) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Blacklisted video not found'
12 })
13 return false
14 }
15
16 res.locals.videoBlacklist = videoBlacklist
17 return true
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 doesVideoBlacklistExist
24}
diff --git a/server/middlewares/validators/shared/video-captions.ts b/server/middlewares/validators/shared/video-captions.ts
deleted file mode 100644
index 831b366ea..000000000
--- a/server/middlewares/validators/shared/video-captions.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { Response } from 'express'
2import { VideoCaptionModel } from '@server/models/video/video-caption'
3import { MVideoId } from '@server/types/models'
4import { HttpStatusCode } from '@shared/models'
5
6async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
7 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
8
9 if (!videoCaption) {
10 res.fail({
11 status: HttpStatusCode.NOT_FOUND_404,
12 message: 'Video caption not found'
13 })
14 return false
15 }
16
17 res.locals.videoCaption = videoCaption
18 return true
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 doesVideoCaptionExist
25}
diff --git a/server/middlewares/validators/shared/video-channel-syncs.ts b/server/middlewares/validators/shared/video-channel-syncs.ts
deleted file mode 100644
index a6e51eb97..000000000
--- a/server/middlewares/validators/shared/video-channel-syncs.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import express from 'express'
2import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
3import { HttpStatusCode } from '@shared/models'
4
5async function doesVideoChannelSyncIdExist (id: number, res: express.Response) {
6 const sync = await VideoChannelSyncModel.loadWithChannel(+id)
7
8 if (!sync) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Video channel sync not found'
12 })
13 return false
14 }
15
16 res.locals.videoChannelSync = sync
17 return true
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 doesVideoChannelSyncIdExist
24}
diff --git a/server/middlewares/validators/shared/video-channels.ts b/server/middlewares/validators/shared/video-channels.ts
deleted file mode 100644
index bed9f5dbe..000000000
--- a/server/middlewares/validators/shared/video-channels.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import express from 'express'
2import { VideoChannelModel } from '@server/models/video/video-channel'
3import { MChannelBannerAccountDefault } from '@server/types/models'
4import { HttpStatusCode } from '@shared/models'
5
6async function doesVideoChannelIdExist (id: number, res: express.Response) {
7 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
8
9 return processVideoChannelExist(videoChannel, res)
10}
11
12async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
13 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
14
15 return processVideoChannelExist(videoChannel, res)
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 doesVideoChannelIdExist,
22 doesVideoChannelNameWithHostExist
23}
24
25function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
26 if (!videoChannel) {
27 res.fail({
28 status: HttpStatusCode.NOT_FOUND_404,
29 message: 'Video channel not found'
30 })
31 return false
32 }
33
34 res.locals.videoChannel = videoChannel
35 return true
36}
diff --git a/server/middlewares/validators/shared/video-comments.ts b/server/middlewares/validators/shared/video-comments.ts
deleted file mode 100644
index 0961b3ec9..000000000
--- a/server/middlewares/validators/shared/video-comments.ts
+++ /dev/null
@@ -1,80 +0,0 @@
1import express from 'express'
2import { VideoCommentModel } from '@server/models/video/video-comment'
3import { MVideoId } from '@server/types/models'
4import { forceNumber } from '@shared/core-utils'
5import { HttpStatusCode, ServerErrorCode } from '@shared/models'
6
7async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
8 const id = forceNumber(idArg)
9 const videoComment = await VideoCommentModel.loadById(id)
10
11 if (!videoComment) {
12 res.fail({
13 status: HttpStatusCode.NOT_FOUND_404,
14 message: 'Video comment thread not found'
15 })
16 return false
17 }
18
19 if (videoComment.videoId !== video.id) {
20 res.fail({
21 type: ServerErrorCode.COMMENT_NOT_ASSOCIATED_TO_VIDEO,
22 message: 'Video comment is not associated to this video.'
23 })
24 return false
25 }
26
27 if (videoComment.inReplyToCommentId !== null) {
28 res.fail({ message: 'Video comment is not a thread.' })
29 return false
30 }
31
32 res.locals.videoCommentThread = videoComment
33 return true
34}
35
36async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
37 const id = forceNumber(idArg)
38 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
39
40 if (!videoComment) {
41 res.fail({
42 status: HttpStatusCode.NOT_FOUND_404,
43 message: 'Video comment thread not found'
44 })
45 return false
46 }
47
48 if (videoComment.videoId !== video.id) {
49 res.fail({
50 type: ServerErrorCode.COMMENT_NOT_ASSOCIATED_TO_VIDEO,
51 message: 'Video comment is not associated to this video.'
52 })
53 return false
54 }
55
56 res.locals.videoCommentFull = videoComment
57 return true
58}
59
60async function doesCommentIdExist (idArg: number | string, res: express.Response) {
61 const id = forceNumber(idArg)
62 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
63
64 if (!videoComment) {
65 res.fail({
66 status: HttpStatusCode.NOT_FOUND_404,
67 message: 'Video comment thread not found'
68 })
69 return false
70 }
71
72 res.locals.videoCommentFull = videoComment
73 return true
74}
75
76export {
77 doesVideoCommentThreadExist,
78 doesVideoCommentExist,
79 doesCommentIdExist
80}
diff --git a/server/middlewares/validators/shared/video-imports.ts b/server/middlewares/validators/shared/video-imports.ts
deleted file mode 100644
index 69fda4b32..000000000
--- a/server/middlewares/validators/shared/video-imports.ts
+++ /dev/null
@@ -1,22 +0,0 @@
1import express from 'express'
2import { VideoImportModel } from '@server/models/video/video-import'
3import { HttpStatusCode } from '@shared/models'
4
5async function doesVideoImportExist (id: number, res: express.Response) {
6 const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
7
8 if (!videoImport) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Video import not found'
12 })
13 return false
14 }
15
16 res.locals.videoImport = videoImport
17 return true
18}
19
20export {
21 doesVideoImportExist
22}
diff --git a/server/middlewares/validators/shared/video-ownerships.ts b/server/middlewares/validators/shared/video-ownerships.ts
deleted file mode 100644
index 33ac9c8b6..000000000
--- a/server/middlewares/validators/shared/video-ownerships.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import express from 'express'
2import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
3import { forceNumber } from '@shared/core-utils'
4import { HttpStatusCode } from '@shared/models'
5
6async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) {
7 const id = forceNumber(idArg)
8 const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
9
10 if (!videoChangeOwnership) {
11 res.fail({
12 status: HttpStatusCode.NOT_FOUND_404,
13 message: 'Video change ownership not found'
14 })
15 return false
16 }
17
18 res.locals.videoChangeOwnership = videoChangeOwnership
19
20 return true
21}
22
23export {
24 doesChangeVideoOwnershipExist
25}
diff --git a/server/middlewares/validators/shared/video-passwords.ts b/server/middlewares/validators/shared/video-passwords.ts
deleted file mode 100644
index efcc95dc4..000000000
--- a/server/middlewares/validators/shared/video-passwords.ts
+++ /dev/null
@@ -1,80 +0,0 @@
1import express from 'express'
2import { HttpStatusCode, UserRight, VideoPrivacy } from '@shared/models'
3import { forceNumber } from '@shared/core-utils'
4import { VideoPasswordModel } from '@server/models/video/video-password'
5import { header } from 'express-validator'
6import { getVideoWithAttributes } from '@server/helpers/video'
7
8function isValidVideoPasswordHeader () {
9 return header('x-peertube-video-password')
10 .optional()
11 .isString()
12}
13
14function checkVideoIsPasswordProtected (res: express.Response) {
15 const video = getVideoWithAttributes(res)
16 if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) {
17 res.fail({
18 status: HttpStatusCode.BAD_REQUEST_400,
19 message: 'Video is not password protected'
20 })
21 return false
22 }
23
24 return true
25}
26
27async function doesVideoPasswordExist (idArg: number | string, res: express.Response) {
28 const video = getVideoWithAttributes(res)
29 const id = forceNumber(idArg)
30 const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id })
31
32 if (!videoPassword) {
33 res.fail({
34 status: HttpStatusCode.NOT_FOUND_404,
35 message: 'Video password not found'
36 })
37 return false
38 }
39
40 res.locals.videoPassword = videoPassword
41
42 return true
43}
44
45async function isVideoPasswordDeletable (res: express.Response) {
46 const user = res.locals.oauth.token.User
47 const userAccount = user.Account
48 const video = res.locals.videoAll
49
50 // Check if the user who did the request is able to delete the video passwords
51 if (
52 user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator
53 video.VideoChannel.accountId !== userAccount.id // Not the video owner
54 ) {
55 res.fail({
56 status: HttpStatusCode.FORBIDDEN_403,
57 message: 'Cannot remove passwords of another user\'s video'
58 })
59 return false
60 }
61
62 const passwordCount = await VideoPasswordModel.countByVideoId(video.id)
63
64 if (passwordCount <= 1) {
65 res.fail({
66 status: HttpStatusCode.BAD_REQUEST_400,
67 message: 'Cannot delete the last password of the protected video'
68 })
69 return false
70 }
71
72 return true
73}
74
75export {
76 isValidVideoPasswordHeader,
77 checkVideoIsPasswordProtected as isVideoPasswordProtected,
78 doesVideoPasswordExist,
79 isVideoPasswordDeletable
80}
diff --git a/server/middlewares/validators/shared/video-playlists.ts b/server/middlewares/validators/shared/video-playlists.ts
deleted file mode 100644
index 4342fe552..000000000
--- a/server/middlewares/validators/shared/video-playlists.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import express from 'express'
2import { VideoPlaylistModel } from '@server/models/video/video-playlist'
3import { MVideoPlaylist } from '@server/types/models'
4import { HttpStatusCode } from '@shared/models'
5
6export type VideoPlaylistFetchType = 'summary' | 'all'
7async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
8 if (fetchType === 'summary') {
9 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
10 res.locals.videoPlaylistSummary = videoPlaylist
11
12 return handleVideoPlaylist(videoPlaylist, res)
13 }
14
15 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
16 res.locals.videoPlaylistFull = videoPlaylist
17
18 return handleVideoPlaylist(videoPlaylist, res)
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 doesVideoPlaylistExist
25}
26
27// ---------------------------------------------------------------------------
28
29function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
30 if (!videoPlaylist) {
31 res.fail({
32 status: HttpStatusCode.NOT_FOUND_404,
33 message: 'Video playlist not found'
34 })
35 return false
36 }
37
38 return true
39}
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts
deleted file mode 100644
index 9a7497007..000000000
--- a/server/middlewares/validators/shared/videos.ts
+++ /dev/null
@@ -1,311 +0,0 @@
1import { Request, Response } from 'express'
2import { loadVideo, VideoLoadType } from '@server/lib/model-loaders'
3import { isAbleToUploadVideo } from '@server/lib/user'
4import { VideoTokensManager } from '@server/lib/video-tokens-manager'
5import { authenticatePromise } from '@server/middlewares/auth'
6import { VideoModel } from '@server/models/video/video'
7import { VideoChannelModel } from '@server/models/video/video-channel'
8import { VideoFileModel } from '@server/models/video/video-file'
9import {
10 MUser,
11 MUserAccountId,
12 MUserId,
13 MVideo,
14 MVideoAccountLight,
15 MVideoFormattableDetails,
16 MVideoFullLight,
17 MVideoId,
18 MVideoImmutable,
19 MVideoThumbnail,
20 MVideoWithRights
21} from '@server/types/models'
22import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models'
23import { VideoPasswordModel } from '@server/models/video/video-password'
24import { exists } from '@server/helpers/custom-validators/misc'
25
26async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
27 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
28
29 const video = await loadVideo(id, fetchType, userId)
30
31 if (!video) {
32 res.fail({
33 status: HttpStatusCode.NOT_FOUND_404,
34 message: 'Video not found'
35 })
36
37 return false
38 }
39
40 switch (fetchType) {
41 case 'for-api':
42 res.locals.videoAPI = video as MVideoFormattableDetails
43 break
44
45 case 'all':
46 res.locals.videoAll = video as MVideoFullLight
47 break
48
49 case 'only-immutable-attributes':
50 res.locals.onlyImmutableVideo = video as MVideoImmutable
51 break
52
53 case 'id':
54 res.locals.videoId = video as MVideoId
55 break
56
57 case 'only-video':
58 res.locals.onlyVideo = video as MVideoThumbnail
59 break
60 }
61
62 return true
63}
64
65// ---------------------------------------------------------------------------
66
67async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
68 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
69 res.fail({
70 status: HttpStatusCode.NOT_FOUND_404,
71 message: 'VideoFile matching Video not found'
72 })
73 return false
74 }
75
76 return true
77}
78
79// ---------------------------------------------------------------------------
80
81async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
82 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
83
84 if (videoChannel === null) {
85 res.fail({ message: 'Unknown video "video channel" for this instance.' })
86 return false
87 }
88
89 // Don't check account id if the user can update any video
90 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
91 res.locals.videoChannel = videoChannel
92 return true
93 }
94
95 if (videoChannel.Account.id !== user.Account.id) {
96 res.fail({
97 message: 'Unknown video "video channel" for this account.'
98 })
99 return false
100 }
101
102 res.locals.videoChannel = videoChannel
103 return true
104}
105
106// ---------------------------------------------------------------------------
107
108async function checkCanSeeVideo (options: {
109 req: Request
110 res: Response
111 paramId: string
112 video: MVideo
113}) {
114 const { req, res, video, paramId } = options
115
116 if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) {
117 return checkCanSeeUserAuthVideo({ req, res, video })
118 }
119
120 if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
121 return checkCanSeePasswordProtectedVideo({ req, res, video })
122 }
123
124 if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) {
125 return true
126 }
127
128 throw new Error('Unknown video privacy when checking video right ' + video.url)
129}
130
131async function checkCanSeeUserAuthVideo (options: {
132 req: Request
133 res: Response
134 video: MVideoId | MVideoWithRights
135}) {
136 const { req, res, video } = options
137
138 const fail = () => {
139 res.fail({
140 status: HttpStatusCode.FORBIDDEN_403,
141 message: 'Cannot fetch information of private/internal/blocked video'
142 })
143
144 return false
145 }
146
147 await authenticatePromise({ req, res })
148
149 const user = res.locals.oauth?.token.User
150 if (!user) return fail()
151
152 const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
153
154 const privacy = videoWithRights.privacy
155
156 if (privacy === VideoPrivacy.INTERNAL) {
157 // We know we have a user
158 return true
159 }
160
161 if (videoWithRights.isBlacklisted()) {
162 if (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true
163
164 return fail()
165 }
166
167 if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
168 if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
169
170 return fail()
171 }
172
173 // Should not happen
174 return fail()
175}
176
177async function checkCanSeePasswordProtectedVideo (options: {
178 req: Request
179 res: Response
180 video: MVideo
181}) {
182 const { req, res, video } = options
183
184 const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
185
186 const videoPassword = req.header('x-peertube-video-password')
187
188 if (!exists(videoPassword)) {
189 const errorMessage = 'Please provide a password to access this password protected video'
190 const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD
191
192 if (req.header('authorization')) {
193 await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType })
194 const user = res.locals.oauth?.token.User
195
196 if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
197 }
198
199 res.fail({
200 status: HttpStatusCode.FORBIDDEN_403,
201 type: errorType,
202 message: errorMessage
203 })
204 return false
205 }
206
207 if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true
208
209 res.fail({
210 status: HttpStatusCode.FORBIDDEN_403,
211 type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD,
212 message: 'Incorrect video password. Access to the video is denied.'
213 })
214
215 return false
216}
217
218function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRight) {
219 const isOwnedByUser = video.VideoChannel.Account.userId === user.id
220
221 return isOwnedByUser || user.hasRight(right)
222}
223
224async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
225 return video.VideoChannel?.Account?.userId
226 ? video
227 : VideoModel.loadFull(video.id)
228}
229
230// ---------------------------------------------------------------------------
231
232async function checkCanAccessVideoStaticFiles (options: {
233 video: MVideo
234 req: Request
235 res: Response
236 paramId: string
237}) {
238 const { video, req, res } = options
239
240 if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) {
241 return checkCanSeeVideo(options)
242 }
243
244 const videoFileToken = req.query.videoFileToken
245 if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
246 const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
247
248 res.locals.videoFileToken = { user }
249 return true
250 }
251
252 if (!video.hasPrivateStaticPath()) return true
253
254 res.sendStatus(HttpStatusCode.FORBIDDEN_403)
255 return false
256}
257
258// ---------------------------------------------------------------------------
259
260function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
261 // Retrieve the user who did the request
262 if (onlyOwned && video.isOwned() === false) {
263 res.fail({
264 status: HttpStatusCode.FORBIDDEN_403,
265 message: 'Cannot manage a video of another server.'
266 })
267 return false
268 }
269
270 // Check if the user can delete the video
271 // The user can delete it if he has the right
272 // Or if s/he is the video's account
273 const account = video.VideoChannel.Account
274 if (user.hasRight(right) === false && account.userId !== user.id) {
275 res.fail({
276 status: HttpStatusCode.FORBIDDEN_403,
277 message: 'Cannot manage a video of another user.'
278 })
279 return false
280 }
281
282 return true
283}
284
285// ---------------------------------------------------------------------------
286
287async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
288 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
289 res.fail({
290 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
291 message: 'The user video quota is exceeded with this video.',
292 type: ServerErrorCode.QUOTA_REACHED
293 })
294 return false
295 }
296
297 return true
298}
299
300// ---------------------------------------------------------------------------
301
302export {
303 doesVideoChannelOfAccountExist,
304 doesVideoExist,
305 doesVideoFileOfVideoExist,
306
307 checkCanAccessVideoStaticFiles,
308 checkUserCanManageVideo,
309 checkCanSeeVideo,
310 checkUserQuota
311}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
deleted file mode 100644
index 07d6cba82..000000000
--- a/server/middlewares/validators/sort.ts
+++ /dev/null
@@ -1,66 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { SORTABLE_COLUMNS } from '../../initializers/constants'
4import { areValidationErrors } from './shared'
5
6export const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
7export const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
8export const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
9export const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
10export const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS)
11export const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS)
12export const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH)
13export const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
14export const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH)
15export const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS)
16export const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
17export const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES)
18export const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS)
19export const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS)
20export const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS)
21export const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING)
22export const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
23export const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
24export const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
25export const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
26export const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
27export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
28export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
29export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
30export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS)
31export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS)
32
33export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
34export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
35
36export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS)
37
38export const runnersSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNERS)
39export const runnerRegistrationTokensSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_REGISTRATION_TOKENS)
40export const runnerJobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_JOBS)
41
42// ---------------------------------------------------------------------------
43
44function checkSortFactory (columns: string[], tags: string[] = []) {
45 return checkSort(createSortableColumns(columns), tags)
46}
47
48function checkSort (sortableColumns: string[], tags: string[] = []) {
49 return [
50 query('sort')
51 .optional()
52 .isIn(sortableColumns),
53
54 (req: express.Request, res: express.Response, next: express.NextFunction) => {
55 if (areValidationErrors(req, res, { tags })) return
56
57 return next()
58 }
59 ]
60}
61
62function createSortableColumns (sortableColumns: string[]) {
63 const sortableColumnDesc = sortableColumns.map(sortableColumn => '-' + sortableColumn)
64
65 return sortableColumns.concat(sortableColumnDesc)
66}
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts
deleted file mode 100644
index 86cc0a8d7..000000000
--- a/server/middlewares/validators/static.ts
+++ /dev/null
@@ -1,184 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { LRUCache } from 'lru-cache'
4import { basename, dirname } from 'path'
5import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
6import { logger } from '@server/helpers/logger'
7import { LRU_CACHE } from '@server/initializers/constants'
8import { VideoModel } from '@server/models/video/video'
9import { VideoFileModel } from '@server/models/video/video-file'
10import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models'
11import { HttpStatusCode } from '@shared/models'
12import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared'
13
14type LRUValue = {
15 allowed: boolean
16 video?: MVideoThumbnail
17 file?: MVideoFile
18 playlist?: MStreamingPlaylist }
19
20const staticFileTokenBypass = new LRUCache<string, LRUValue>({
21 max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
22 ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
23})
24
25const ensureCanAccessVideoPrivateWebVideoFiles = [
26 query('videoFileToken').optional().custom(exists),
27
28 isValidVideoPasswordHeader(),
29
30 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 if (areValidationErrors(req, res)) return
32
33 const token = extractTokenOrDie(req, res)
34 if (!token) return
35
36 const cacheKey = token + '-' + req.originalUrl
37
38 if (staticFileTokenBypass.has(cacheKey)) {
39 const { allowed, file, video } = staticFileTokenBypass.get(cacheKey)
40
41 if (allowed === true) {
42 res.locals.onlyVideo = video
43 res.locals.videoFile = file
44
45 return next()
46 }
47
48 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
49 }
50
51 const result = await isWebVideoAllowed(req, res)
52
53 staticFileTokenBypass.set(cacheKey, result)
54
55 if (result.allowed !== true) return
56
57 res.locals.onlyVideo = result.video
58 res.locals.videoFile = result.file
59
60 return next()
61 }
62]
63
64const ensureCanAccessPrivateVideoHLSFiles = [
65 query('videoFileToken')
66 .optional()
67 .custom(exists),
68
69 query('reinjectVideoFileToken')
70 .optional()
71 .customSanitizer(toBooleanOrNull)
72 .isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
73
74 query('playlistName')
75 .optional()
76 .customSanitizer(isSafePeerTubeFilenameWithoutExtension),
77
78 isValidVideoPasswordHeader(),
79
80 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
81 if (areValidationErrors(req, res)) return
82
83 const videoUUID = basename(dirname(req.originalUrl))
84
85 if (!isUUIDValid(videoUUID)) {
86 logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
87
88 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
89 }
90
91 const token = extractTokenOrDie(req, res)
92 if (!token) return
93
94 const cacheKey = token + '-' + videoUUID
95
96 if (staticFileTokenBypass.has(cacheKey)) {
97 const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey)
98
99 if (allowed === true) {
100 res.locals.onlyVideo = video
101 res.locals.videoFile = file
102 res.locals.videoStreamingPlaylist = playlist
103
104 return next()
105 }
106
107 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
108 }
109
110 const result = await isHLSAllowed(req, res, videoUUID)
111
112 staticFileTokenBypass.set(cacheKey, result)
113
114 if (result.allowed !== true) return
115
116 res.locals.onlyVideo = result.video
117 res.locals.videoFile = result.file
118 res.locals.videoStreamingPlaylist = result.playlist
119
120 return next()
121 }
122]
123
124export {
125 ensureCanAccessVideoPrivateWebVideoFiles,
126 ensureCanAccessPrivateVideoHLSFiles
127}
128
129// ---------------------------------------------------------------------------
130
131async function isWebVideoAllowed (req: express.Request, res: express.Response) {
132 const filename = basename(req.path)
133
134 const file = await VideoFileModel.loadWithVideoByFilename(filename)
135 if (!file) {
136 logger.debug('Unknown static file %s to serve', req.originalUrl, { filename })
137
138 res.sendStatus(HttpStatusCode.FORBIDDEN_403)
139 return { allowed: false }
140 }
141
142 const video = await VideoModel.load(file.getVideo().id)
143
144 return {
145 file,
146 video,
147 allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
148 }
149}
150
151async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) {
152 const filename = basename(req.path)
153
154 const video = await VideoModel.loadWithFiles(videoUUID)
155
156 if (!video) {
157 logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID })
158
159 res.sendStatus(HttpStatusCode.FORBIDDEN_403)
160 return { allowed: false }
161 }
162
163 const file = await VideoFileModel.loadByFilename(filename)
164
165 return {
166 file,
167 video,
168 playlist: video.getHLSPlaylist(),
169 allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
170 }
171}
172
173function extractTokenOrDie (req: express.Request, res: express.Response) {
174 const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken
175
176 if (!token) {
177 return res.fail({
178 message: 'Video password header, video file token query parameter and bearer token are all missing', //
179 status: HttpStatusCode.FORBIDDEN_403
180 })
181 }
182
183 return token
184}
diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts
deleted file mode 100644
index 080b3e096..000000000
--- a/server/middlewares/validators/themes.ts
+++ /dev/null
@@ -1,46 +0,0 @@
1import express from 'express'
2import { param } from 'express-validator'
3import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
4import { isSafePath } from '../../helpers/custom-validators/misc'
5import { isPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins'
6import { PluginManager } from '../../lib/plugins/plugin-manager'
7import { areValidationErrors } from './shared'
8
9const serveThemeCSSValidator = [
10 param('themeName')
11 .custom(isPluginNameValid),
12 param('themeVersion')
13 .custom(isPluginStableOrUnstableVersionValid),
14 param('staticEndpoint')
15 .custom(isSafePath),
16
17 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 if (areValidationErrors(req, res)) return
19
20 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
21
22 if (!theme || theme.version !== req.params.themeVersion) {
23 return res.fail({
24 status: HttpStatusCode.NOT_FOUND_404,
25 message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion
26 })
27 }
28
29 if (theme.css.includes(req.params.staticEndpoint) === false) {
30 return res.fail({
31 status: HttpStatusCode.NOT_FOUND_404,
32 message: 'No static endpoint was found for this theme'
33 })
34 }
35
36 res.locals.registeredPlugin = theme
37
38 return next()
39 }
40]
41
42// ---------------------------------------------------------------------------
43
44export {
45 serveThemeCSSValidator
46}
diff --git a/server/middlewares/validators/two-factor.ts b/server/middlewares/validators/two-factor.ts
deleted file mode 100644
index 106b579b5..000000000
--- a/server/middlewares/validators/two-factor.ts
+++ /dev/null
@@ -1,81 +0,0 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { HttpStatusCode, UserRight } from '@shared/models'
4import { exists, isIdValid } from '../../helpers/custom-validators/misc'
5import { areValidationErrors, checkUserIdExist } from './shared'
6
7const requestOrConfirmTwoFactorValidator = [
8 param('id').custom(isIdValid),
9
10 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 if (areValidationErrors(req, res)) return
12
13 if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return
14
15 if (res.locals.user.otpSecret) {
16 return res.fail({
17 status: HttpStatusCode.BAD_REQUEST_400,
18 message: `Two factor is already enabled.`
19 })
20 }
21
22 return next()
23 }
24]
25
26const confirmTwoFactorValidator = [
27 body('requestToken').custom(exists),
28 body('otpToken').custom(exists),
29
30 (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 if (areValidationErrors(req, res)) return
32
33 return next()
34 }
35]
36
37const disableTwoFactorValidator = [
38 param('id').custom(isIdValid),
39
40 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
41 if (areValidationErrors(req, res)) return
42
43 if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return
44
45 if (!res.locals.user.otpSecret) {
46 return res.fail({
47 status: HttpStatusCode.BAD_REQUEST_400,
48 message: `Two factor is already disabled.`
49 })
50 }
51
52 return next()
53 }
54]
55
56// ---------------------------------------------------------------------------
57
58export {
59 requestOrConfirmTwoFactorValidator,
60 confirmTwoFactorValidator,
61 disableTwoFactorValidator
62}
63
64// ---------------------------------------------------------------------------
65
66async function checkCanEnableOrDisableTwoFactor (userId: number | string, res: express.Response) {
67 const authUser = res.locals.oauth.token.user
68
69 if (!await checkUserIdExist(userId, res)) return
70
71 if (res.locals.user.id !== authUser.id && authUser.hasRight(UserRight.MANAGE_USERS) !== true) {
72 res.fail({
73 status: HttpStatusCode.FORBIDDEN_403,
74 message: `User ${authUser.username} does not have right to change two factor setting of this user.`
75 })
76
77 return false
78 }
79
80 return true
81}
diff --git a/server/middlewares/validators/user-email-verification.ts b/server/middlewares/validators/user-email-verification.ts
deleted file mode 100644
index 74702a8f5..000000000
--- a/server/middlewares/validators/user-email-verification.ts
+++ /dev/null
@@ -1,94 +0,0 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { toBooleanOrNull } from '@server/helpers/custom-validators/misc'
4import { HttpStatusCode } from '@shared/models'
5import { logger } from '../../helpers/logger'
6import { Redis } from '../../lib/redis'
7import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from './shared'
8import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations'
9
10const usersAskSendVerifyEmailValidator = [
11 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
12
13 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 if (areValidationErrors(req, res)) return
15
16 const [ userExists, registrationExists ] = await Promise.all([
17 checkUserEmailExist(req.body.email, res, false),
18 checkRegistrationEmailExist(req.body.email, res, false)
19 ])
20
21 if (!userExists && !registrationExists) {
22 logger.debug('User or registration with email %s does not exist (asking verify email).', req.body.email)
23 // Do not leak our emails
24 return res.status(HttpStatusCode.NO_CONTENT_204).end()
25 }
26
27 if (res.locals.user?.pluginAuth) {
28 return res.fail({
29 status: HttpStatusCode.CONFLICT_409,
30 message: 'Cannot ask verification email of a user that uses a plugin authentication.'
31 })
32 }
33
34 return next()
35 }
36]
37
38const usersVerifyEmailValidator = [
39 param('id')
40 .isInt().not().isEmpty().withMessage('Should have a valid id'),
41
42 body('verificationString')
43 .not().isEmpty().withMessage('Should have a valid verification string'),
44 body('isPendingEmail')
45 .optional()
46 .customSanitizer(toBooleanOrNull),
47
48 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
50 if (!await checkUserIdExist(req.params.id, res)) return
51
52 const user = res.locals.user
53 const redisVerificationString = await Redis.Instance.getUserVerifyEmailLink(user.id)
54
55 if (redisVerificationString !== req.body.verificationString) {
56 return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
57 }
58
59 return next()
60 }
61]
62
63// ---------------------------------------------------------------------------
64
65const registrationVerifyEmailValidator = [
66 param('registrationId')
67 .isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
68
69 body('verificationString')
70 .not().isEmpty().withMessage('Should have a valid verification string'),
71
72 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
73 if (areValidationErrors(req, res)) return
74 if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
75
76 const registration = res.locals.userRegistration
77 const redisVerificationString = await Redis.Instance.getRegistrationVerifyEmailLink(registration.id)
78
79 if (redisVerificationString !== req.body.verificationString) {
80 return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
81 }
82
83 return next()
84 }
85]
86
87// ---------------------------------------------------------------------------
88
89export {
90 usersAskSendVerifyEmailValidator,
91 usersVerifyEmailValidator,
92
93 registrationVerifyEmailValidator
94}
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts
deleted file mode 100644
index f2dae3134..000000000
--- a/server/middlewares/validators/user-history.ts
+++ /dev/null
@@ -1,47 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { exists, isDateValid, isIdValid } from '../../helpers/custom-validators/misc'
4import { areValidationErrors } from './shared'
5
6const userHistoryListValidator = [
7 query('search')
8 .optional()
9 .custom(exists),
10
11 (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 if (areValidationErrors(req, res)) return
13
14 return next()
15 }
16]
17
18const userHistoryRemoveAllValidator = [
19 body('beforeDate')
20 .optional()
21 .custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
22
23 (req: express.Request, res: express.Response, next: express.NextFunction) => {
24 if (areValidationErrors(req, res)) return
25
26 return next()
27 }
28]
29
30const userHistoryRemoveElementValidator = [
31 param('videoId')
32 .custom(isIdValid),
33
34 (req: express.Request, res: express.Response, next: express.NextFunction) => {
35 if (areValidationErrors(req, res)) return
36
37 return next()
38 }
39]
40
41// ---------------------------------------------------------------------------
42
43export {
44 userHistoryListValidator,
45 userHistoryRemoveElementValidator,
46 userHistoryRemoveAllValidator
47}
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
deleted file mode 100644
index 8d70dcdd2..000000000
--- a/server/middlewares/validators/user-notifications.ts
+++ /dev/null
@@ -1,71 +0,0 @@
1import express from 'express'
2import { body, query } from 'express-validator'
3import { isNotEmptyIntArray, toBooleanOrNull } from '../../helpers/custom-validators/misc'
4import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
5import { areValidationErrors } from './shared'
6
7const listUserNotificationsValidator = [
8 query('unread')
9 .optional()
10 .customSanitizer(toBooleanOrNull)
11 .isBoolean().withMessage('Should have a valid unread boolean'),
12
13 (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 if (areValidationErrors(req, res)) return
15
16 return next()
17 }
18]
19
20const updateNotificationSettingsValidator = [
21 body('newVideoFromSubscription')
22 .custom(isUserNotificationSettingValid),
23 body('newCommentOnMyVideo')
24 .custom(isUserNotificationSettingValid),
25 body('abuseAsModerator')
26 .custom(isUserNotificationSettingValid),
27 body('videoAutoBlacklistAsModerator')
28 .custom(isUserNotificationSettingValid),
29 body('blacklistOnMyVideo')
30 .custom(isUserNotificationSettingValid),
31 body('myVideoImportFinished')
32 .custom(isUserNotificationSettingValid),
33 body('myVideoPublished')
34 .custom(isUserNotificationSettingValid),
35 body('commentMention')
36 .custom(isUserNotificationSettingValid),
37 body('newFollow')
38 .custom(isUserNotificationSettingValid),
39 body('newUserRegistration')
40 .custom(isUserNotificationSettingValid),
41 body('newInstanceFollower')
42 .custom(isUserNotificationSettingValid),
43 body('autoInstanceFollowing')
44 .custom(isUserNotificationSettingValid),
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 markAsReadUserNotificationsValidator = [
54 body('ids')
55 .optional()
56 .custom(isNotEmptyIntArray).withMessage('Should have a valid array of notification ids'),
57
58 (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 if (areValidationErrors(req, res)) return
60
61 return next()
62 }
63]
64
65// ---------------------------------------------------------------------------
66
67export {
68 listUserNotificationsValidator,
69 updateNotificationSettingsValidator,
70 markAsReadUserNotificationsValidator
71}
diff --git a/server/middlewares/validators/user-registrations.ts b/server/middlewares/validators/user-registrations.ts
deleted file mode 100644
index 47397391b..000000000
--- a/server/middlewares/validators/user-registrations.ts
+++ /dev/null
@@ -1,208 +0,0 @@
1import express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator'
3import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc'
4import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration'
5import { CONFIG } from '@server/initializers/config'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@shared/models'
8import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from '../../helpers/custom-validators/users'
9import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
10import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../lib/signup'
11import { ActorModel } from '../../models/actor/actor'
12import { areValidationErrors, checkUserNameOrEmailDoNotAlreadyExist } from './shared'
13import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations'
14
15const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory()
16
17const usersRequestRegistrationValidator = [
18 ...usersCommonRegistrationValidatorFactory([
19 body('registrationReason')
20 .custom(isRegistrationReasonValid)
21 ]),
22
23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
24 const body: UserRegistrationRequest = req.body
25
26 if (CONFIG.SIGNUP.REQUIRES_APPROVAL !== true) {
27 return res.fail({
28 status: HttpStatusCode.BAD_REQUEST_400,
29 message: 'Signup approval is not enabled on this instance'
30 })
31 }
32
33 const options = { username: body.username, email: body.email, channelHandle: body.channel?.name, res }
34 if (!await checkRegistrationHandlesDoNotAlreadyExist(options)) return
35
36 return next()
37 }
38]
39
40// ---------------------------------------------------------------------------
41
42function ensureUserRegistrationAllowedFactory (signupMode: SignupMode) {
43 return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 const allowedParams = {
45 body: req.body,
46 ip: req.ip,
47 signupMode
48 }
49
50 const allowedResult = await Hooks.wrapPromiseFun(
51 isSignupAllowed,
52 allowedParams,
53
54 signupMode === 'direct-registration'
55 ? 'filter:api.user.signup.allowed.result'
56 : 'filter:api.user.request-signup.allowed.result'
57 )
58
59 if (allowedResult.allowed === false) {
60 return res.fail({
61 status: HttpStatusCode.FORBIDDEN_403,
62 message: allowedResult.errorMessage || 'User registration is not allowed'
63 })
64 }
65
66 return next()
67 }
68}
69
70const ensureUserRegistrationAllowedForIP = [
71 (req: express.Request, res: express.Response, next: express.NextFunction) => {
72 const allowed = isSignupAllowedForCurrentIP(req.ip)
73
74 if (allowed === false) {
75 return res.fail({
76 status: HttpStatusCode.FORBIDDEN_403,
77 message: 'You are not on a network authorized for registration.'
78 })
79 }
80
81 return next()
82 }
83]
84
85// ---------------------------------------------------------------------------
86
87const acceptOrRejectRegistrationValidator = [
88 param('registrationId')
89 .custom(isIdValid),
90
91 body('moderationResponse')
92 .custom(isRegistrationModerationResponseValid),
93
94 body('preventEmailDelivery')
95 .optional()
96 .customSanitizer(toBooleanOrNull)
97 .custom(isBooleanValid).withMessage('Should have preventEmailDelivery boolean'),
98
99 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
100 if (areValidationErrors(req, res)) return
101 if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
102
103 if (res.locals.userRegistration.state !== UserRegistrationState.PENDING) {
104 return res.fail({
105 status: HttpStatusCode.CONFLICT_409,
106 message: 'This registration is already accepted or rejected.'
107 })
108 }
109
110 return next()
111 }
112]
113
114// ---------------------------------------------------------------------------
115
116const getRegistrationValidator = [
117 param('registrationId')
118 .custom(isIdValid),
119
120 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
121 if (areValidationErrors(req, res)) return
122 if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
123
124 return next()
125 }
126]
127
128// ---------------------------------------------------------------------------
129
130const listRegistrationsValidator = [
131 query('search')
132 .optional()
133 .custom(exists),
134
135 (req: express.Request, res: express.Response, next: express.NextFunction) => {
136 if (areValidationErrors(req, res)) return
137
138 return next()
139 }
140]
141
142// ---------------------------------------------------------------------------
143
144export {
145 usersDirectRegistrationValidator,
146 usersRequestRegistrationValidator,
147
148 ensureUserRegistrationAllowedFactory,
149 ensureUserRegistrationAllowedForIP,
150
151 getRegistrationValidator,
152 listRegistrationsValidator,
153
154 acceptOrRejectRegistrationValidator
155}
156
157// ---------------------------------------------------------------------------
158
159function usersCommonRegistrationValidatorFactory (additionalValidationChain: ValidationChain[] = []) {
160 return [
161 body('username')
162 .custom(isUserUsernameValid),
163 body('password')
164 .custom(isUserPasswordValid),
165 body('email')
166 .isEmail(),
167 body('displayName')
168 .optional()
169 .custom(isUserDisplayNameValid),
170
171 body('channel.name')
172 .optional()
173 .custom(isVideoChannelUsernameValid),
174 body('channel.displayName')
175 .optional()
176 .custom(isVideoChannelDisplayNameValid),
177
178 ...additionalValidationChain,
179
180 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
181 if (areValidationErrors(req, res, { omitBodyLog: true })) return
182
183 const body: UserRegister | UserRegistrationRequest = req.body
184
185 if (!await checkUserNameOrEmailDoNotAlreadyExist(body.username, body.email, res)) return
186
187 if (body.channel) {
188 if (!body.channel.name || !body.channel.displayName) {
189 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
190 }
191
192 if (body.channel.name === body.username) {
193 return res.fail({ message: 'Channel name cannot be the same as user username.' })
194 }
195
196 const existing = await ActorModel.loadLocalByName(body.channel.name)
197 if (existing) {
198 return res.fail({
199 status: HttpStatusCode.CONFLICT_409,
200 message: `Channel with name ${body.channel.name} already exists.`
201 })
202 }
203 }
204
205 return next()
206 }
207 ]
208}
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts
deleted file mode 100644
index 68d83add5..000000000
--- a/server/middlewares/validators/user-subscriptions.ts
+++ /dev/null
@@ -1,111 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { arrayify } from '@shared/core-utils'
4import { FollowState } from '@shared/models'
5import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
6import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
7import { WEBSERVER } from '../../initializers/constants'
8import { ActorFollowModel } from '../../models/actor/actor-follow'
9import { areValidationErrors } from './shared'
10
11const userSubscriptionListValidator = [
12 query('search')
13 .optional()
14 .not().isEmpty(),
15
16 (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 if (areValidationErrors(req, res)) return
18
19 return next()
20 }
21]
22
23const userSubscriptionAddValidator = [
24 body('uri')
25 .custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
26
27 (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 if (areValidationErrors(req, res)) return
29
30 return next()
31 }
32]
33
34const areSubscriptionsExistValidator = [
35 query('uris')
36 .customSanitizer(arrayify)
37 .custom(areValidActorHandles).withMessage('Should have a valid array of URIs'),
38
39 (req: express.Request, res: express.Response, next: express.NextFunction) => {
40 if (areValidationErrors(req, res)) return
41
42 return next()
43 }
44]
45
46const userSubscriptionGetValidator = [
47 param('uri')
48 .custom(isValidActorHandle),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 if (areValidationErrors(req, res)) return
52 if (!await doesSubscriptionExist({ uri: req.params.uri, res, state: 'accepted' })) return
53
54 return next()
55 }
56]
57
58const userSubscriptionDeleteValidator = [
59 param('uri')
60 .custom(isValidActorHandle),
61
62 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
63 if (areValidationErrors(req, res)) return
64 if (!await doesSubscriptionExist({ uri: req.params.uri, res })) return
65
66 return next()
67 }
68]
69
70// ---------------------------------------------------------------------------
71
72export {
73 areSubscriptionsExistValidator,
74 userSubscriptionListValidator,
75 userSubscriptionAddValidator,
76 userSubscriptionGetValidator,
77 userSubscriptionDeleteValidator
78}
79
80// ---------------------------------------------------------------------------
81
82async function doesSubscriptionExist (options: {
83 uri: string
84 res: express.Response
85 state?: FollowState
86}) {
87 const { uri, res, state } = options
88
89 let [ name, host ] = uri.split('@')
90 if (host === WEBSERVER.HOST) host = null
91
92 const user = res.locals.oauth.token.User
93 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
94 actorId: user.Account.Actor.id,
95 targetName: name,
96 targetHost: host,
97 state
98 })
99
100 if (!subscription?.ActorFollowing.VideoChannel) {
101 res.fail({
102 status: HttpStatusCode.NOT_FOUND_404,
103 message: `Subscription ${uri} not found.`
104 })
105 return false
106 }
107
108 res.locals.subscription = subscription
109
110 return true
111}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
deleted file mode 100644
index 3d311b15b..000000000
--- a/server/middlewares/validators/users.ts
+++ /dev/null
@@ -1,489 +0,0 @@
1import express from 'express'
2import { body, param, query } from 'express-validator'
3import { forceNumber } from '@shared/core-utils'
4import { HttpStatusCode, UserRight, UserRole } from '@shared/models'
5import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
7import {
8 isUserAdminFlagsValid,
9 isUserAutoPlayNextVideoValid,
10 isUserAutoPlayVideoValid,
11 isUserBlockedReasonValid,
12 isUserDescriptionValid,
13 isUserDisplayNameValid,
14 isUserEmailPublicValid,
15 isUserNoModal,
16 isUserNSFWPolicyValid,
17 isUserP2PEnabledValid,
18 isUserPasswordValid,
19 isUserPasswordValidOrEmpty,
20 isUserRoleValid,
21 isUserUsernameValid,
22 isUserVideoLanguages,
23 isUserVideoQuotaDailyValid,
24 isUserVideoQuotaValid,
25 isUserVideosHistoryEnabledValid
26} from '../../helpers/custom-validators/users'
27import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
28import { logger } from '../../helpers/logger'
29import { isThemeRegistered } from '../../lib/plugins/theme-utils'
30import { Redis } from '../../lib/redis'
31import { ActorModel } from '../../models/actor/actor'
32import {
33 areValidationErrors,
34 checkUserEmailExist,
35 checkUserIdExist,
36 checkUserNameOrEmailDoNotAlreadyExist,
37 doesVideoChannelIdExist,
38 doesVideoExist,
39 isValidVideoIdParam
40} from './shared'
41
42const usersListValidator = [
43 query('blocked')
44 .optional()
45 .customSanitizer(toBooleanOrNull)
46 .isBoolean().withMessage('Should be a valid blocked boolean'),
47
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
50
51 return next()
52 }
53]
54
55const usersAddValidator = [
56 body('username')
57 .custom(isUserUsernameValid)
58 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
59 body('password')
60 .custom(isUserPasswordValidOrEmpty),
61 body('email')
62 .isEmail(),
63
64 body('channelName')
65 .optional()
66 .custom(isVideoChannelUsernameValid),
67
68 body('videoQuota')
69 .optional()
70 .custom(isUserVideoQuotaValid),
71
72 body('videoQuotaDaily')
73 .optional()
74 .custom(isUserVideoQuotaDailyValid),
75
76 body('role')
77 .customSanitizer(toIntOrNull)
78 .custom(isUserRoleValid),
79
80 body('adminFlags')
81 .optional()
82 .custom(isUserAdminFlagsValid),
83
84 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
85 if (areValidationErrors(req, res, { omitBodyLog: true })) return
86 if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
87
88 const authUser = res.locals.oauth.token.User
89 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
90 return res.fail({
91 status: HttpStatusCode.FORBIDDEN_403,
92 message: 'You can only create users (and not administrators or moderators)'
93 })
94 }
95
96 if (req.body.channelName) {
97 if (req.body.channelName === req.body.username) {
98 return res.fail({ message: 'Channel name cannot be the same as user username.' })
99 }
100
101 const existing = await ActorModel.loadLocalByName(req.body.channelName)
102 if (existing) {
103 return res.fail({
104 status: HttpStatusCode.CONFLICT_409,
105 message: `Channel with name ${req.body.channelName} already exists.`
106 })
107 }
108 }
109
110 return next()
111 }
112]
113
114const usersRemoveValidator = [
115 param('id')
116 .custom(isIdValid),
117
118 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
119 if (areValidationErrors(req, res)) return
120 if (!await checkUserIdExist(req.params.id, res)) return
121
122 const user = res.locals.user
123 if (user.username === 'root') {
124 return res.fail({ message: 'Cannot remove the root user' })
125 }
126
127 return next()
128 }
129]
130
131const usersBlockingValidator = [
132 param('id')
133 .custom(isIdValid),
134 body('reason')
135 .optional()
136 .custom(isUserBlockedReasonValid),
137
138 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
139 if (areValidationErrors(req, res)) return
140 if (!await checkUserIdExist(req.params.id, res)) return
141
142 const user = res.locals.user
143 if (user.username === 'root') {
144 return res.fail({ message: 'Cannot block the root user' })
145 }
146
147 return next()
148 }
149]
150
151const deleteMeValidator = [
152 (req: express.Request, res: express.Response, next: express.NextFunction) => {
153 const user = res.locals.oauth.token.User
154 if (user.username === 'root') {
155 return res.fail({ message: 'You cannot delete your root account.' })
156 }
157
158 return next()
159 }
160]
161
162const usersUpdateValidator = [
163 param('id').custom(isIdValid),
164
165 body('password')
166 .optional()
167 .custom(isUserPasswordValid),
168 body('email')
169 .optional()
170 .isEmail(),
171 body('emailVerified')
172 .optional()
173 .isBoolean(),
174 body('videoQuota')
175 .optional()
176 .custom(isUserVideoQuotaValid),
177 body('videoQuotaDaily')
178 .optional()
179 .custom(isUserVideoQuotaDailyValid),
180 body('pluginAuth')
181 .optional()
182 .exists(),
183 body('role')
184 .optional()
185 .customSanitizer(toIntOrNull)
186 .custom(isUserRoleValid),
187 body('adminFlags')
188 .optional()
189 .custom(isUserAdminFlagsValid),
190
191 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
192 if (areValidationErrors(req, res, { omitBodyLog: true })) return
193 if (!await checkUserIdExist(req.params.id, res)) return
194
195 const user = res.locals.user
196 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
197 return res.fail({ message: 'Cannot change root role.' })
198 }
199
200 return next()
201 }
202]
203
204const usersUpdateMeValidator = [
205 body('displayName')
206 .optional()
207 .custom(isUserDisplayNameValid),
208 body('description')
209 .optional()
210 .custom(isUserDescriptionValid),
211 body('currentPassword')
212 .optional()
213 .custom(isUserPasswordValid),
214 body('password')
215 .optional()
216 .custom(isUserPasswordValid),
217 body('emailPublic')
218 .optional()
219 .custom(isUserEmailPublicValid),
220 body('email')
221 .optional()
222 .isEmail(),
223 body('nsfwPolicy')
224 .optional()
225 .custom(isUserNSFWPolicyValid),
226 body('autoPlayVideo')
227 .optional()
228 .custom(isUserAutoPlayVideoValid),
229 body('p2pEnabled')
230 .optional()
231 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
232 body('videoLanguages')
233 .optional()
234 .custom(isUserVideoLanguages),
235 body('videosHistoryEnabled')
236 .optional()
237 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
238 body('theme')
239 .optional()
240 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
241
242 body('noInstanceConfigWarningModal')
243 .optional()
244 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
245 body('noWelcomeModal')
246 .optional()
247 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
248 body('noAccountSetupWarningModal')
249 .optional()
250 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
251
252 body('autoPlayNextVideo')
253 .optional()
254 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
255
256 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
257 const user = res.locals.oauth.token.User
258
259 if (req.body.password || req.body.email) {
260 if (user.pluginAuth !== null) {
261 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
262 }
263
264 if (!req.body.currentPassword) {
265 return res.fail({ message: 'currentPassword parameter is missing.' })
266 }
267
268 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
269 return res.fail({
270 status: HttpStatusCode.UNAUTHORIZED_401,
271 message: 'currentPassword is invalid.'
272 })
273 }
274 }
275
276 if (areValidationErrors(req, res, { omitBodyLog: true })) return
277
278 return next()
279 }
280]
281
282const usersGetValidator = [
283 param('id')
284 .custom(isIdValid),
285 query('withStats')
286 .optional()
287 .isBoolean().withMessage('Should have a valid withStats boolean'),
288
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 if (areValidationErrors(req, res)) return
291 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
292
293 return next()
294 }
295]
296
297const usersVideoRatingValidator = [
298 isValidVideoIdParam('videoId'),
299
300 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
301 if (areValidationErrors(req, res)) return
302 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
303
304 return next()
305 }
306]
307
308const usersVideosValidator = [
309 query('isLive')
310 .optional()
311 .customSanitizer(toBooleanOrNull)
312 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
313
314 query('channelId')
315 .optional()
316 .customSanitizer(toIntOrNull)
317 .custom(isIdValid),
318
319 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
320 if (areValidationErrors(req, res)) return
321
322 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
323
324 return next()
325 }
326]
327
328const usersAskResetPasswordValidator = [
329 body('email')
330 .isEmail(),
331
332 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
333 if (areValidationErrors(req, res)) return
334
335 const exists = await checkUserEmailExist(req.body.email, res, false)
336 if (!exists) {
337 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
338 // Do not leak our emails
339 return res.status(HttpStatusCode.NO_CONTENT_204).end()
340 }
341
342 if (res.locals.user.pluginAuth) {
343 return res.fail({
344 status: HttpStatusCode.CONFLICT_409,
345 message: 'Cannot recover password of a user that uses a plugin authentication.'
346 })
347 }
348
349 return next()
350 }
351]
352
353const usersResetPasswordValidator = [
354 param('id')
355 .custom(isIdValid),
356 body('verificationString')
357 .not().isEmpty(),
358 body('password')
359 .custom(isUserPasswordValid),
360
361 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
362 if (areValidationErrors(req, res)) return
363 if (!await checkUserIdExist(req.params.id, res)) return
364
365 const user = res.locals.user
366 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
367
368 if (redisVerificationString !== req.body.verificationString) {
369 return res.fail({
370 status: HttpStatusCode.FORBIDDEN_403,
371 message: 'Invalid verification string.'
372 })
373 }
374
375 return next()
376 }
377]
378
379const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
380 return [
381 body('currentPassword').optional().custom(exists),
382
383 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
384 if (areValidationErrors(req, res)) return
385
386 const user = res.locals.oauth.token.User
387 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
388 const targetUserId = forceNumber(targetUserIdGetter(req))
389
390 // Admin/moderator action on another user, skip the password check
391 if (isAdminOrModerator && targetUserId !== user.id) {
392 return next()
393 }
394
395 if (!req.body.currentPassword) {
396 return res.fail({
397 status: HttpStatusCode.BAD_REQUEST_400,
398 message: 'currentPassword is missing'
399 })
400 }
401
402 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
403 return res.fail({
404 status: HttpStatusCode.FORBIDDEN_403,
405 message: 'currentPassword is invalid.'
406 })
407 }
408
409 return next()
410 }
411 ]
412}
413
414const userAutocompleteValidator = [
415 param('search')
416 .isString()
417 .not().isEmpty()
418]
419
420const ensureAuthUserOwnsAccountValidator = [
421 (req: express.Request, res: express.Response, next: express.NextFunction) => {
422 const user = res.locals.oauth.token.User
423
424 if (res.locals.account.id !== user.Account.id) {
425 return res.fail({
426 status: HttpStatusCode.FORBIDDEN_403,
427 message: 'Only owner of this account can access this resource.'
428 })
429 }
430
431 return next()
432 }
433]
434
435const ensureCanManageChannelOrAccount = [
436 (req: express.Request, res: express.Response, next: express.NextFunction) => {
437 const user = res.locals.oauth.token.user
438 const account = res.locals.videoChannel?.Account ?? res.locals.account
439 const isUserOwner = account.userId === user.id
440
441 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
442 const message = `User ${user.username} does not have right this channel or account.`
443
444 return res.fail({
445 status: HttpStatusCode.FORBIDDEN_403,
446 message
447 })
448 }
449
450 return next()
451 }
452]
453
454const ensureCanModerateUser = [
455 (req: express.Request, res: express.Response, next: express.NextFunction) => {
456 const authUser = res.locals.oauth.token.User
457 const onUser = res.locals.user
458
459 if (authUser.role === UserRole.ADMINISTRATOR) return next()
460 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
461
462 return res.fail({
463 status: HttpStatusCode.FORBIDDEN_403,
464 message: 'A moderator can only manage users.'
465 })
466 }
467]
468
469// ---------------------------------------------------------------------------
470
471export {
472 usersListValidator,
473 usersAddValidator,
474 deleteMeValidator,
475 usersBlockingValidator,
476 usersRemoveValidator,
477 usersUpdateValidator,
478 usersUpdateMeValidator,
479 usersVideoRatingValidator,
480 usersCheckCurrentPasswordFactory,
481 usersGetValidator,
482 usersVideosValidator,
483 usersAskResetPasswordValidator,
484 usersResetPasswordValidator,
485 userAutocompleteValidator,
486 ensureAuthUserOwnsAccountValidator,
487 ensureCanModerateUser,
488 ensureCanManageChannelOrAccount
489}
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}
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts
deleted file mode 100644
index dcfba99fa..000000000
--- a/server/middlewares/validators/webfinger.ts
+++ /dev/null
@@ -1,37 +0,0 @@
1import express from 'express'
2import { query } from 'express-validator'
3import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
4import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger'
5import { getHostWithPort } from '../../helpers/express-utils'
6import { ActorModel } from '../../models/actor/actor'
7import { areValidationErrors } from './shared'
8
9const webfingerValidator = [
10 query('resource')
11 .custom(isWebfingerLocalResourceValid),
12
13 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 if (areValidationErrors(req, res)) return
15
16 // Remove 'acct:' from the beginning of the string
17 const nameWithHost = getHostWithPort(req.query.resource.substr(5))
18 const [ name ] = nameWithHost.split('@')
19
20 const actor = await ActorModel.loadLocalUrlByName(name)
21 if (!actor) {
22 return res.fail({
23 status: HttpStatusCode.NOT_FOUND_404,
24 message: 'Actor not found'
25 })
26 }
27
28 res.locals.actorUrl = actor
29 return next()
30 }
31]
32
33// ---------------------------------------------------------------------------
34
35export {
36 webfingerValidator
37}