diff options
Diffstat (limited to 'server/middlewares')
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 @@ | |||
1 | import { NextFunction, Request, Response } from 'express' | ||
2 | import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor' | ||
3 | import { getAPId } from '@server/lib/activitypub/activity' | ||
4 | import { wrapWithSpanAndContext } from '@server/lib/opentelemetry/tracing' | ||
5 | import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@shared/models' | ||
6 | import { logger } from '../helpers/logger' | ||
7 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' | ||
8 | import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants' | ||
9 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors' | ||
10 | |||
11 | async 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 | |||
42 | function 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 | |||
56 | export { | ||
57 | checkSignature, | ||
58 | executeIfActivityPub, | ||
59 | checkHttpSignature | ||
60 | } | ||
61 | |||
62 | // --------------------------------------------------------------------------- | ||
63 | |||
64 | async 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 | |||
124 | async 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 @@ | |||
1 | import Bluebird from 'bluebird' | ||
2 | import { NextFunction, Request, RequestHandler, Response } from 'express' | ||
3 | import { ValidationChain } from 'express-validator' | ||
4 | import { ExpressPromiseHandler } from '@server/types/express-handler' | ||
5 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
6 | |||
7 | // Syntactic sugar to avoid try/catch in express controllers/middlewares | ||
8 | |||
9 | export type RequestPromiseHandler = ValidationChain | ExpressPromiseHandler | ||
10 | |||
11 | function 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 | |||
31 | function 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 | |||
41 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { Socket } from 'socket.io' | ||
3 | import { getAccessToken } from '@server/lib/auth/oauth-model' | ||
4 | import { RunnerModel } from '@server/models/runner/runner' | ||
5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | ||
6 | import { logger } from '../helpers/logger' | ||
7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | ||
8 | import { ServerErrorCode } from '@shared/models' | ||
9 | |||
10 | function 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 | |||
29 | function 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 | |||
52 | function 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 | |||
76 | function 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 | |||
86 | function 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 | |||
107 | export { | ||
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 @@ | |||
1 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
2 | import { ApiCache, APICacheOptions } from './shared' | ||
3 | |||
4 | const defaultOptions: APICacheOptions = { | ||
5 | excludeStatus: [ | ||
6 | HttpStatusCode.FORBIDDEN_403, | ||
7 | HttpStatusCode.NOT_FOUND_404 | ||
8 | ] | ||
9 | } | ||
10 | |||
11 | function cacheRoute (duration: string) { | ||
12 | const instance = new ApiCache(defaultOptions) | ||
13 | |||
14 | return instance.buildMiddleware(duration) | ||
15 | } | ||
16 | |||
17 | function cacheRouteFactory (options: APICacheOptions) { | ||
18 | const instance = new ApiCache({ ...defaultOptions, ...options }) | ||
19 | |||
20 | return { instance, middleware: instance.buildMiddleware.bind(instance) } | ||
21 | } | ||
22 | |||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
25 | function buildPodcastGroupsCache (options: { | ||
26 | channelId: number | ||
27 | }) { | ||
28 | return 'podcast-feed-' + options.channelId | ||
29 | } | ||
30 | |||
31 | // --------------------------------------------------------------------------- | ||
32 | |||
33 | export { | ||
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 @@ | |||
1 | export * 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 | |||
4 | import express from 'express' | ||
5 | import { OutgoingHttpHeaders } from 'http' | ||
6 | import { isTestInstance, parseDurationToMs } from '@server/helpers/core-utils' | ||
7 | import { logger } from '@server/helpers/logger' | ||
8 | import { Redis } from '@server/lib/redis' | ||
9 | import { asyncMiddleware } from '@server/middlewares' | ||
10 | import { HttpStatusCode } from '@shared/models' | ||
11 | |||
12 | export interface APICacheOptions { | ||
13 | headerBlacklist?: string[] | ||
14 | excludeStatus?: HttpStatusCode[] | ||
15 | } | ||
16 | |||
17 | interface CacheObject { | ||
18 | status: number | ||
19 | headers: OutgoingHttpHeaders | ||
20 | data: any | ||
21 | encoding: BufferEncoding | ||
22 | timestamp: number | ||
23 | } | ||
24 | |||
25 | export 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 @@ | |||
1 | export * 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 @@ | |||
1 | import { contentSecurityPolicy } from 'helmet' | ||
2 | import { CONFIG } from '../initializers/config' | ||
3 | |||
4 | const 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 | |||
25 | const baseCSP = contentSecurityPolicy({ | ||
26 | directives: baseDirectives, | ||
27 | reportOnly: CONFIG.CSP.REPORT_ONLY | ||
28 | }) | ||
29 | |||
30 | const embedCSP = contentSecurityPolicy({ | ||
31 | directives: Object.assign({}, baseDirectives, { frameAncestors: [ '*' ] }), | ||
32 | reportOnly: CONFIG.CSP.REPORT_ONLY | ||
33 | }) | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
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 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | const 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 | |||
13 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | function 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 | |||
14 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | function 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 | |||
46 | function 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 | |||
60 | export { | ||
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 @@ | |||
1 | export * from './validators' | ||
2 | export * from './cache' | ||
3 | export * from './activitypub' | ||
4 | export * from './async' | ||
5 | export * from './auth' | ||
6 | export * from './pagination' | ||
7 | export * from './rate-limiter' | ||
8 | export * from './robots' | ||
9 | export * from './servers' | ||
10 | export * from './sort' | ||
11 | export * from './user-right' | ||
12 | export * from './dnt' | ||
13 | export * from './error' | ||
14 | export * from './doc' | ||
15 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { PAGINATION } from '../initializers/constants' | ||
4 | |||
5 | function 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 | |||
17 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import RateLimit, { Options as RateLimitHandlerOptions } from 'express-rate-limit' | ||
3 | import { CONFIG } from '@server/initializers/config' | ||
4 | import { RunnerModel } from '@server/models/runner/runner' | ||
5 | import { UserRole } from '@shared/models' | ||
6 | import { optionalAuthenticate } from './auth' | ||
7 | |||
8 | const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ]) | ||
9 | |||
10 | export 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 | |||
43 | export const apiRateLimiter = buildRateLimiter({ | ||
44 | windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, | ||
45 | max: CONFIG.RATES_LIMIT.API.MAX | ||
46 | }) | ||
47 | |||
48 | export 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 | |||
57 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | function 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 | |||
11 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | ||
3 | import { getHostWithPort } from '../helpers/express-utils' | ||
4 | |||
5 | function 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 | |||
27 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | const setDefaultSort = setDefaultSortFactory('-createdAt') | ||
4 | const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') | ||
5 | |||
6 | const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') | ||
7 | |||
8 | const setDefaultSearchSort = setDefaultSortFactory('-match') | ||
9 | const setBlacklistSort = setDefaultSortFactory('-createdAt') | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
14 | setDefaultSort, | ||
15 | setDefaultSearchSort, | ||
16 | setDefaultVideosSort, | ||
17 | setDefaultVideoRedundanciesSort, | ||
18 | setBlacklistSort | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
3 | import { logger } from '../helpers/logger' | ||
4 | |||
5 | function 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 | |||
24 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { | ||
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' | ||
15 | import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc' | ||
16 | import { logger } from '@server/helpers/logger' | ||
17 | import { AbuseMessageModel } from '@server/models/abuse/abuse-message' | ||
18 | import { AbuseCreate, UserRight } from '@shared/models' | ||
19 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
20 | import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared' | ||
21 | import { forceNumber } from '@shared/core-utils' | ||
22 | |||
23 | const 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 | |||
73 | const 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 | |||
85 | const 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 | |||
104 | const 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 | |||
143 | const 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 | |||
163 | const 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 | |||
188 | const 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 | |||
199 | const 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 | |||
210 | const 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 | |||
245 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { isAccountNameValid } from '../../helpers/custom-validators/accounts' | ||
4 | import { areValidationErrors, doesAccountNameWithHostExist, doesLocalAccountNameExist } from './shared' | ||
5 | |||
6 | const 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 | |||
18 | const 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 | |||
32 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | |||
7 | async 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 | |||
27 | export { | ||
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 @@ | |||
1 | export * from './activity' | ||
2 | export * from './signature' | ||
3 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { PAGINATION } from '@server/initializers/constants' | ||
4 | import { areValidationErrors } from '../shared' | ||
5 | |||
6 | const 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 | |||
23 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { | ||
4 | isSignatureCreatorValid, | ||
5 | isSignatureTypeValid, | ||
6 | isSignatureValueValid | ||
7 | } from '../../../helpers/custom-validators/activitypub/signature' | ||
8 | import { isDateValid } from '../../../helpers/custom-validators/misc' | ||
9 | import { logger } from '../../../helpers/logger' | ||
10 | import { areValidationErrors } from '../shared' | ||
11 | |||
12 | const 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 | |||
37 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isActorImageFile } from '@server/helpers/custom-validators/actor-images' | ||
4 | import { cleanUpReqFiles } from '../../helpers/express-utils' | ||
5 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
6 | import { areValidationErrors } from './shared' | ||
7 | |||
8 | const 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 | |||
21 | const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile') | ||
22 | const updateBannerValidator = updateActorImageValidatorFactory('bannerfile') | ||
23 | |||
24 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor' | ||
4 | import { getServerActor } from '@server/models/application/application' | ||
5 | import { arrayify } from '@shared/core-utils' | ||
6 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
7 | import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' | ||
8 | import { WEBSERVER } from '../../initializers/constants' | ||
9 | import { AccountBlocklistModel } from '../../models/account/account-blocklist' | ||
10 | import { ServerModel } from '../../models/server/server' | ||
11 | import { ServerBlocklistModel } from '../../models/server/server-blocklist' | ||
12 | import { areValidationErrors, doesAccountNameWithHostExist } from './shared' | ||
13 | |||
14 | const 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 | |||
37 | const 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 | |||
53 | const 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 | |||
69 | const 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 | |||
93 | const 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 | |||
107 | const 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 | |||
121 | const 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 | |||
141 | export { | ||
142 | blockServerValidator, | ||
143 | blockAccountValidator, | ||
144 | unblockAccountByAccountValidator, | ||
145 | unblockServerByAccountValidator, | ||
146 | unblockAccountByServerValidator, | ||
147 | unblockServerByServerValidator, | ||
148 | blocklistStatusValidator | ||
149 | } | ||
150 | |||
151 | // --------------------------------------------------------------------------- | ||
152 | |||
153 | async 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 | |||
167 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk' | ||
4 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
5 | import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' | ||
6 | import { areValidationErrors, doesAccountNameWithHostExist } from './shared' | ||
7 | |||
8 | const 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 | |||
34 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isIntOrNull } from '@server/helpers/custom-validators/misc' | ||
4 | import { CONFIG, isEmailEnabled } from '@server/initializers/config' | ||
5 | import { HttpStatusCode } from '@shared/models/http/http-error-codes' | ||
6 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | ||
7 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | ||
8 | import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' | ||
9 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | ||
10 | import { areValidationErrors } from './shared' | ||
11 | |||
12 | const 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 | |||
126 | function 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 | |||
139 | export { | ||
140 | customConfigUpdateValidator, | ||
141 | ensureConfigIsEditable | ||
142 | } | ||
143 | |||
144 | function 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 | |||
155 | function 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 | |||
166 | function 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 | |||
174 | function 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 | |||
185 | function 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 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | const 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 | |||
13 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { param, query } from 'express-validator' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' | ||
5 | import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc' | ||
6 | import { buildPodcastGroupsCache } from '../cache' | ||
7 | import { | ||
8 | areValidationErrors, | ||
9 | checkCanSeeVideo, | ||
10 | doesAccountIdExist, | ||
11 | doesAccountNameWithHostExist, | ||
12 | doesUserFeedTokenCorrespond, | ||
13 | doesVideoChannelIdExist, | ||
14 | doesVideoChannelNameWithHostExist, | ||
15 | doesVideoExist | ||
16 | } from './shared' | ||
17 | |||
18 | const 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 | |||
33 | function 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 | |||
50 | function 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 | |||
56 | function 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 | |||
76 | const 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 | |||
105 | const 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 | |||
117 | const 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 | |||
128 | const 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 | |||
145 | const 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 | |||
169 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { isProdInstance } from '@server/helpers/core-utils' | ||
4 | import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows' | ||
5 | import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors' | ||
6 | import { getRemoteNameAndHost } from '@server/lib/activitypub/follow' | ||
7 | import { getServerActor } from '@server/models/application/application' | ||
8 | import { MActorFollowActorsDefault } from '@server/types/models' | ||
9 | import { ServerFollowCreate } from '@shared/models' | ||
10 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
11 | import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
12 | import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' | ||
13 | import { logger } from '../../helpers/logger' | ||
14 | import { WEBSERVER } from '../../initializers/constants' | ||
15 | import { ActorModel } from '../../models/actor/actor' | ||
16 | import { ActorFollowModel } from '../../models/actor/actor-follow' | ||
17 | import { areValidationErrors } from './shared' | ||
18 | |||
19 | const 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 | |||
34 | const 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 | |||
69 | const 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 | |||
97 | const 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 | |||
127 | const 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 | |||
138 | const 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 | |||
151 | export { | ||
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 @@ | |||
1 | export * from './abuse' | ||
2 | export * from './account' | ||
3 | export * from './activitypub' | ||
4 | export * from './actor-image' | ||
5 | export * from './blocklist' | ||
6 | export * from './bulk' | ||
7 | export * from './config' | ||
8 | export * from './express' | ||
9 | export * from './feeds' | ||
10 | export * from './follows' | ||
11 | export * from './jobs' | ||
12 | export * from './logs' | ||
13 | export * from './metrics' | ||
14 | export * from './object-storage-proxy' | ||
15 | export * from './oembed' | ||
16 | export * from './pagination' | ||
17 | export * from './plugins' | ||
18 | export * from './redundancy' | ||
19 | export * from './search' | ||
20 | export * from './server' | ||
21 | export * from './sort' | ||
22 | export * from './static' | ||
23 | export * from './themes' | ||
24 | export * from './user-email-verification' | ||
25 | export * from './user-history' | ||
26 | export * from './user-notifications' | ||
27 | export * from './user-registrations' | ||
28 | export * from './user-subscriptions' | ||
29 | export * from './users' | ||
30 | export * from './videos' | ||
31 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param, query } from 'express-validator' | ||
3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' | ||
4 | import { loggerTagsFactory } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './shared' | ||
6 | |||
7 | const lTags = loggerTagsFactory('validators', 'jobs') | ||
8 | |||
9 | const 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 | |||
27 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, query } from 'express-validator' | ||
3 | import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | ||
4 | import { isStringArray } from '@server/helpers/custom-validators/search' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { arrayify } from '@shared/core-utils' | ||
7 | import { HttpStatusCode } from '@shared/models' | ||
8 | import { | ||
9 | isValidClientLogLevel, | ||
10 | isValidClientLogMessage, | ||
11 | isValidClientLogMeta, | ||
12 | isValidClientLogStackTrace, | ||
13 | isValidClientLogUserAgent, | ||
14 | isValidLogLevel | ||
15 | } from '../../helpers/custom-validators/logs' | ||
16 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
17 | import { areValidationErrors } from './shared' | ||
18 | |||
19 | const 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 | |||
52 | const 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 | |||
73 | const 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 | |||
89 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isValidPlayerMode } from '@server/helpers/custom-validators/metrics' | ||
4 | import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models' | ||
7 | import { areValidationErrors, doesVideoExist } from './shared' | ||
8 | |||
9 | const 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 | |||
58 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | |||
5 | const 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 | |||
18 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { join } from 'path' | ||
4 | import { loadVideo } from '@server/lib/model-loaders' | ||
5 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
6 | import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | ||
7 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
8 | import { isTestOrDevInstance } from '../../helpers/core-utils' | ||
9 | import { isIdOrUUIDValid, isUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc' | ||
10 | import { WEBSERVER } from '../../initializers/constants' | ||
11 | import { areValidationErrors } from './shared' | ||
12 | |||
13 | const playlistPaths = [ | ||
14 | join('videos', 'watch', 'playlist'), | ||
15 | join('w', 'p') | ||
16 | ] | ||
17 | |||
18 | const videoPaths = [ | ||
19 | join('videos', 'watch'), | ||
20 | 'w' | ||
21 | ] | ||
22 | |||
23 | function buildUrls (paths: string[]) { | ||
24 | return paths.map(p => WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, p) + '/') | ||
25 | } | ||
26 | |||
27 | const startPlaylistURLs = buildUrls(playlistPaths) | ||
28 | const startVideoURLs = buildUrls(videoPaths) | ||
29 | |||
30 | const isURLOptions = { | ||
31 | require_host: true, | ||
32 | require_tld: true | ||
33 | } | ||
34 | |||
35 | // We validate 'localhost', so we don't have the top level domain | ||
36 | if (isTestOrDevInstance()) { | ||
37 | isURLOptions.require_tld = false | ||
38 | } | ||
39 | |||
40 | const 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 | |||
156 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { PAGINATION } from '@server/initializers/constants' | ||
4 | import { areValidationErrors } from './shared' | ||
5 | |||
6 | const paginationValidator = paginationValidatorBuilder() | ||
7 | |||
8 | function 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 | |||
27 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query, ValidationChain } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
4 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | ||
5 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model' | ||
6 | import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | ||
7 | import { | ||
8 | isNpmPluginNameValid, | ||
9 | isPluginNameValid, | ||
10 | isPluginStableOrUnstableVersionValid, | ||
11 | isPluginTypeValid | ||
12 | } from '../../helpers/custom-validators/plugins' | ||
13 | import { CONFIG } from '../../initializers/config' | ||
14 | import { PluginManager } from '../../lib/plugins/plugin-manager' | ||
15 | import { PluginModel } from '../../models/server/plugin' | ||
16 | import { areValidationErrors } from './shared' | ||
17 | |||
18 | const 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 | |||
58 | const 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 | |||
87 | const 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 | |||
98 | const 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 | |||
115 | const 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 | |||
141 | const 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 | |||
152 | const 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 | |||
172 | const 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 | |||
183 | const 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 | |||
208 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' | ||
4 | import { forceNumber } from '@shared/core-utils' | ||
5 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
6 | import { | ||
7 | exists, | ||
8 | isBooleanValid, | ||
9 | isIdOrUUIDValid, | ||
10 | isIdValid, | ||
11 | toBooleanOrNull, | ||
12 | toCompleteUUID, | ||
13 | toIntOrNull | ||
14 | } from '../../helpers/custom-validators/misc' | ||
15 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
16 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
17 | import { ServerModel } from '../../models/server/server' | ||
18 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared' | ||
19 | |||
20 | const 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 | |||
65 | const 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 | |||
102 | const 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 | |||
127 | const 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 | |||
138 | const 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 | |||
168 | const 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 | |||
191 | export { | ||
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 @@ | |||
1 | export * from './jobs' | ||
2 | export * from './registration-token' | ||
3 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { basename } from 'path' | ||
4 | import { isSafeFilename } from '@server/helpers/custom-validators/misc' | ||
5 | import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobStudioTranscodingPayload } from '@shared/models' | ||
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
7 | |||
8 | const tags = [ 'runner' ] | ||
9 | |||
10 | export 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 | |||
32 | export 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { | ||
5 | isRunnerJobAbortReasonValid, | ||
6 | isRunnerJobArrayOfStateValid, | ||
7 | isRunnerJobErrorMessageValid, | ||
8 | isRunnerJobProgressValid, | ||
9 | isRunnerJobSuccessPayloadValid, | ||
10 | isRunnerJobTokenValid, | ||
11 | isRunnerJobUpdatePayloadValid | ||
12 | } from '@server/helpers/custom-validators/runners/jobs' | ||
13 | import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners' | ||
14 | import { cleanUpReqFiles } from '@server/helpers/express-utils' | ||
15 | import { LiveManager } from '@server/lib/live' | ||
16 | import { runnerJobCanBeCancelled } from '@server/lib/runners' | ||
17 | import { RunnerJobModel } from '@server/models/runner/runner-job' | ||
18 | import { arrayify } from '@shared/core-utils' | ||
19 | import { | ||
20 | HttpStatusCode, | ||
21 | RunnerJobLiveRTMPHLSTranscodingPrivatePayload, | ||
22 | RunnerJobState, | ||
23 | RunnerJobSuccessBody, | ||
24 | RunnerJobUpdateBody, | ||
25 | ServerErrorCode | ||
26 | } from '@shared/models' | ||
27 | import { areValidationErrors } from '../shared' | ||
28 | |||
29 | const tags = [ 'runner' ] | ||
30 | |||
31 | export 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 | |||
45 | export 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 | |||
55 | export 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 | |||
93 | export 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 | |||
103 | export 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 | |||
121 | export 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 | |||
137 | export 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 | |||
152 | export 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 | |||
174 | export 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token' | ||
5 | import { forceNumber } from '@shared/core-utils' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { areValidationErrors } from '../shared/utils' | ||
8 | |||
9 | const tags = [ 'runner' ] | ||
10 | |||
11 | const 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 | |||
35 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { | ||
5 | isRunnerDescriptionValid, | ||
6 | isRunnerNameValid, | ||
7 | isRunnerRegistrationTokenValid, | ||
8 | isRunnerTokenValid | ||
9 | } from '@server/helpers/custom-validators/runners/runners' | ||
10 | import { RunnerModel } from '@server/models/runner/runner' | ||
11 | import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token' | ||
12 | import { forceNumber } from '@shared/core-utils' | ||
13 | import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@shared/models' | ||
14 | import { areValidationErrors } from '../shared/utils' | ||
15 | |||
16 | const tags = [ 'runner' ] | ||
17 | |||
18 | const 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 | |||
53 | const 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 | |||
75 | const 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 | |||
100 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { isSearchTargetValid } from '@server/helpers/custom-validators/search' | ||
4 | import { isHostValid } from '@server/helpers/custom-validators/servers' | ||
5 | import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc' | ||
6 | import { areValidationErrors } from './shared' | ||
7 | |||
8 | const 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 | |||
55 | const 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 | |||
80 | const 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 | |||
108 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
4 | import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers' | ||
5 | import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { CONFIG, isEmailEnabled } from '../../initializers/config' | ||
8 | import { Redis } from '../../lib/redis' | ||
9 | import { ServerModel } from '../../models/server/server' | ||
10 | import { areValidationErrors } from './shared' | ||
11 | |||
12 | const 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 | |||
32 | const 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 | |||
72 | export { | ||
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 @@ | |||
1 | import { Response } from 'express' | ||
2 | import { AbuseModel } from '@server/models/abuse/abuse' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | import { forceNumber } from '@shared/core-utils' | ||
5 | |||
6 | async 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 | |||
24 | export { | ||
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 @@ | |||
1 | import { Response } from 'express' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { MAccountDefault } from '@server/types/models' | ||
5 | import { forceNumber } from '@shared/core-utils' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | |||
8 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { | ||
9 | const promise = AccountModel.load(forceNumber(id)) | ||
10 | |||
11 | return doesAccountExist(promise, res, sendNotFound) | ||
12 | } | ||
13 | |||
14 | function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) { | ||
15 | const promise = AccountModel.loadLocalByName(name) | ||
16 | |||
17 | return doesAccountExist(promise, res, sendNotFound) | ||
18 | } | ||
19 | |||
20 | function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { | ||
21 | const promise = AccountModel.loadByNameWithHost(nameWithDomain) | ||
22 | |||
23 | return doesAccountExist(promise, res, sendNotFound) | ||
24 | } | ||
25 | |||
26 | async 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 | |||
43 | async 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 | |||
60 | export { | ||
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 @@ | |||
1 | export * from './abuses' | ||
2 | export * from './accounts' | ||
3 | export * from './users' | ||
4 | export * from './utils' | ||
5 | export * from './video-blacklists' | ||
6 | export * from './video-captions' | ||
7 | export * from './video-channels' | ||
8 | export * from './video-channel-syncs' | ||
9 | export * from './video-comments' | ||
10 | export * from './video-imports' | ||
11 | export * from './video-ownerships' | ||
12 | export * from './video-playlists' | ||
13 | export * from './video-passwords' | ||
14 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
3 | import { MRegistration } from '@server/types/models' | ||
4 | import { forceNumber, pick } from '@shared/core-utils' | ||
5 | import { HttpStatusCode } from '@shared/models' | ||
6 | |||
7 | function checkRegistrationIdExist (idArg: number | string, res: express.Response) { | ||
8 | const id = forceNumber(idArg) | ||
9 | return checkRegistrationExist(() => UserRegistrationModel.load(id), res) | ||
10 | } | ||
11 | |||
12 | function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
13 | return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse) | ||
14 | } | ||
15 | |||
16 | async 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 | |||
37 | async 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 | |||
55 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { ActorModel } from '@server/models/actor/actor' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { MUserDefault } from '@server/types/models' | ||
5 | import { forceNumber } from '@shared/core-utils' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | |||
8 | function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { | ||
9 | const id = forceNumber(idArg) | ||
10 | return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) | ||
11 | } | ||
12 | |||
13 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
14 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) | ||
15 | } | ||
16 | |||
17 | async 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 | |||
40 | async 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 | |||
58 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { param, validationResult } from 'express-validator' | ||
3 | import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | |||
6 | function 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 | |||
51 | function 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 | |||
57 | function 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 | |||
65 | export { | ||
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 @@ | |||
1 | import { Response } from 'express' | ||
2 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | |||
5 | async 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 | |||
22 | export { | ||
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 @@ | |||
1 | import { Response } from 'express' | ||
2 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
3 | import { MVideoId } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | async 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 | |||
23 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | |||
5 | async 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 | |||
22 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
3 | import { MChannelBannerAccountDefault } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | async function doesVideoChannelIdExist (id: number, res: express.Response) { | ||
7 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) | ||
8 | |||
9 | return processVideoChannelExist(videoChannel, res) | ||
10 | } | ||
11 | |||
12 | async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { | ||
13 | const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain) | ||
14 | |||
15 | return processVideoChannelExist(videoChannel, res) | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | doesVideoChannelIdExist, | ||
22 | doesVideoChannelNameWithHostExist | ||
23 | } | ||
24 | |||
25 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
3 | import { MVideoId } from '@server/types/models' | ||
4 | import { forceNumber } from '@shared/core-utils' | ||
5 | import { HttpStatusCode, ServerErrorCode } from '@shared/models' | ||
6 | |||
7 | async 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 | |||
36 | async 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 | |||
60 | async 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 | |||
76 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoImportModel } from '@server/models/video/video-import' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | |||
5 | async 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 | |||
20 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | async 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 | |||
23 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode, UserRight, VideoPrivacy } from '@shared/models' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
5 | import { header } from 'express-validator' | ||
6 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
7 | |||
8 | function isValidVideoPasswordHeader () { | ||
9 | return header('x-peertube-video-password') | ||
10 | .optional() | ||
11 | .isString() | ||
12 | } | ||
13 | |||
14 | function 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 | |||
27 | async 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 | |||
45 | async 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 | |||
75 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
3 | import { MVideoPlaylist } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | export type VideoPlaylistFetchType = 'summary' | 'all' | ||
7 | async 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 | |||
23 | export { | ||
24 | doesVideoPlaylistExist | ||
25 | } | ||
26 | |||
27 | // --------------------------------------------------------------------------- | ||
28 | |||
29 | function 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 @@ | |||
1 | import { Request, Response } from 'express' | ||
2 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' | ||
3 | import { isAbleToUploadVideo } from '@server/lib/user' | ||
4 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | ||
5 | import { authenticatePromise } from '@server/middlewares/auth' | ||
6 | import { VideoModel } from '@server/models/video/video' | ||
7 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
8 | import { VideoFileModel } from '@server/models/video/video-file' | ||
9 | import { | ||
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' | ||
22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' | ||
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
25 | |||
26 | async 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 | |||
67 | async 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 | |||
81 | async 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 | |||
108 | async 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 | |||
131 | async 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 | |||
177 | async 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 | |||
218 | function 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 | |||
224 | async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> { | ||
225 | return video.VideoChannel?.Account?.userId | ||
226 | ? video | ||
227 | : VideoModel.loadFull(video.id) | ||
228 | } | ||
229 | |||
230 | // --------------------------------------------------------------------------- | ||
231 | |||
232 | async 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 | |||
260 | function 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 | |||
287 | async 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 | |||
302 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { SORTABLE_COLUMNS } from '../../initializers/constants' | ||
4 | import { areValidationErrors } from './shared' | ||
5 | |||
6 | export const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS) | ||
7 | export const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) | ||
8 | export const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) | ||
9 | export const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) | ||
10 | export const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS) | ||
11 | export const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS) | ||
12 | export const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH) | ||
13 | export const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) | ||
14 | export const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH) | ||
15 | export const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS) | ||
16 | export const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | ||
17 | export const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES) | ||
18 | export const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS) | ||
19 | export const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS) | ||
20 | export const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS) | ||
21 | export const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING) | ||
22 | export const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | ||
23 | export const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | ||
24 | export const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | ||
25 | export const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
26 | export const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) | ||
27 | export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | ||
28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | ||
29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | ||
30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) | ||
31 | export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS) | ||
32 | |||
33 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | ||
34 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | ||
35 | |||
36 | export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS) | ||
37 | |||
38 | export const runnersSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNERS) | ||
39 | export const runnerRegistrationTokensSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_REGISTRATION_TOKENS) | ||
40 | export const runnerJobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_JOBS) | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | function checkSortFactory (columns: string[], tags: string[] = []) { | ||
45 | return checkSort(createSortableColumns(columns), tags) | ||
46 | } | ||
47 | |||
48 | function 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 | |||
62 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { LRUCache } from 'lru-cache' | ||
4 | import { basename, dirname } from 'path' | ||
5 | import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc' | ||
6 | import { logger } from '@server/helpers/logger' | ||
7 | import { LRU_CACHE } from '@server/initializers/constants' | ||
8 | import { VideoModel } from '@server/models/video/video' | ||
9 | import { VideoFileModel } from '@server/models/video/video-file' | ||
10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' | ||
11 | import { HttpStatusCode } from '@shared/models' | ||
12 | import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared' | ||
13 | |||
14 | type LRUValue = { | ||
15 | allowed: boolean | ||
16 | video?: MVideoThumbnail | ||
17 | file?: MVideoFile | ||
18 | playlist?: MStreamingPlaylist } | ||
19 | |||
20 | const 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 | |||
25 | const 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 | |||
64 | const 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 | |||
124 | export { | ||
125 | ensureCanAccessVideoPrivateWebVideoFiles, | ||
126 | ensureCanAccessPrivateVideoHLSFiles | ||
127 | } | ||
128 | |||
129 | // --------------------------------------------------------------------------- | ||
130 | |||
131 | async 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 | |||
151 | async 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 | |||
173 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
4 | import { isSafePath } from '../../helpers/custom-validators/misc' | ||
5 | import { isPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins' | ||
6 | import { PluginManager } from '../../lib/plugins/plugin-manager' | ||
7 | import { areValidationErrors } from './shared' | ||
8 | |||
9 | const 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 | |||
44 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
4 | import { exists, isIdValid } from '../../helpers/custom-validators/misc' | ||
5 | import { areValidationErrors, checkUserIdExist } from './shared' | ||
6 | |||
7 | const 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 | |||
26 | const 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 | |||
37 | const 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 | |||
58 | export { | ||
59 | requestOrConfirmTwoFactorValidator, | ||
60 | confirmTwoFactorValidator, | ||
61 | disableTwoFactorValidator | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { toBooleanOrNull } from '@server/helpers/custom-validators/misc' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { Redis } from '../../lib/redis' | ||
7 | import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from './shared' | ||
8 | import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations' | ||
9 | |||
10 | const 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 | |||
38 | const 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 | |||
65 | const 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 | |||
89 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { exists, isDateValid, isIdValid } from '../../helpers/custom-validators/misc' | ||
4 | import { areValidationErrors } from './shared' | ||
5 | |||
6 | const 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 | |||
18 | const 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 | |||
30 | const 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 | |||
43 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, query } from 'express-validator' | ||
3 | import { isNotEmptyIntArray, toBooleanOrNull } from '../../helpers/custom-validators/misc' | ||
4 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
5 | import { areValidationErrors } from './shared' | ||
6 | |||
7 | const 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 | |||
20 | const 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 | |||
53 | const 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 | |||
67 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query, ValidationChain } from 'express-validator' | ||
3 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc' | ||
4 | import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@shared/models' | ||
8 | import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from '../../helpers/custom-validators/users' | ||
9 | import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' | ||
10 | import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../lib/signup' | ||
11 | import { ActorModel } from '../../models/actor/actor' | ||
12 | import { areValidationErrors, checkUserNameOrEmailDoNotAlreadyExist } from './shared' | ||
13 | import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations' | ||
14 | |||
15 | const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory() | ||
16 | |||
17 | const 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 | |||
42 | function 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 | |||
70 | const 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 | |||
87 | const 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 | |||
116 | const 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 | |||
130 | const 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 | |||
144 | export { | ||
145 | usersDirectRegistrationValidator, | ||
146 | usersRequestRegistrationValidator, | ||
147 | |||
148 | ensureUserRegistrationAllowedFactory, | ||
149 | ensureUserRegistrationAllowedForIP, | ||
150 | |||
151 | getRegistrationValidator, | ||
152 | listRegistrationsValidator, | ||
153 | |||
154 | acceptOrRejectRegistrationValidator | ||
155 | } | ||
156 | |||
157 | // --------------------------------------------------------------------------- | ||
158 | |||
159 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { arrayify } from '@shared/core-utils' | ||
4 | import { FollowState } from '@shared/models' | ||
5 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
6 | import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
7 | import { WEBSERVER } from '../../initializers/constants' | ||
8 | import { ActorFollowModel } from '../../models/actor/actor-follow' | ||
9 | import { areValidationErrors } from './shared' | ||
10 | |||
11 | const 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 | |||
23 | const 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 | |||
34 | const 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 | |||
46 | const 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 | |||
58 | const 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 | |||
72 | export { | ||
73 | areSubscriptionsExistValidator, | ||
74 | userSubscriptionListValidator, | ||
75 | userSubscriptionAddValidator, | ||
76 | userSubscriptionGetValidator, | ||
77 | userSubscriptionDeleteValidator | ||
78 | } | ||
79 | |||
80 | // --------------------------------------------------------------------------- | ||
81 | |||
82 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { HttpStatusCode, UserRight, UserRole } from '@shared/models' | ||
5 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | ||
6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | ||
7 | import { | ||
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' | ||
27 | import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' | ||
28 | import { logger } from '../../helpers/logger' | ||
29 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | ||
30 | import { Redis } from '../../lib/redis' | ||
31 | import { ActorModel } from '../../models/actor/actor' | ||
32 | import { | ||
33 | areValidationErrors, | ||
34 | checkUserEmailExist, | ||
35 | checkUserIdExist, | ||
36 | checkUserNameOrEmailDoNotAlreadyExist, | ||
37 | doesVideoChannelIdExist, | ||
38 | doesVideoExist, | ||
39 | isValidVideoIdParam | ||
40 | } from './shared' | ||
41 | |||
42 | const 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 | |||
55 | const 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 | |||
114 | const 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 | |||
131 | const 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 | |||
151 | const 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 | |||
162 | const 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 | |||
204 | const 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 | |||
282 | const 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 | |||
297 | const 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 | |||
308 | const 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 | |||
328 | const 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 | |||
353 | const 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 | |||
379 | const 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 | |||
414 | const userAutocompleteValidator = [ | ||
415 | param('search') | ||
416 | .isString() | ||
417 | .not().isEmpty() | ||
418 | ] | ||
419 | |||
420 | const 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 | |||
435 | const 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 | |||
454 | const 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 | |||
471 | export { | ||
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 @@ | |||
1 | export * from './video-blacklist' | ||
2 | export * from './video-captions' | ||
3 | export * from './video-channel-sync' | ||
4 | export * from './video-channels' | ||
5 | export * from './video-comments' | ||
6 | export * from './video-files' | ||
7 | export * from './video-imports' | ||
8 | export * from './video-live' | ||
9 | export * from './video-ownership-changes' | ||
10 | export * from './video-passwords' | ||
11 | export * from './video-rates' | ||
12 | export * from './video-shares' | ||
13 | export * from './video-source' | ||
14 | export * from './video-stats' | ||
15 | export * from './video-studio' | ||
16 | export * from './video-token' | ||
17 | export * from './video-transcoding' | ||
18 | export * from './video-view' | ||
19 | export * 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 @@ | |||
1 | export * from './upload' | ||
2 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { getVideoStreamDuration } from '@shared/ffmpeg' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | export 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 | |||
32 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { isVideoFileMimeTypeValid, isVideoFileSizeValid } from '@server/helpers/custom-validators/videos' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
5 | import { isLocalVideoFileAccepted } from '@server/lib/moderation' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { MUserAccountId, MVideo } from '@server/types/models' | ||
8 | import { HttpStatusCode, ServerErrorCode, ServerFilterHookName, VideoState } from '@shared/models' | ||
9 | import { checkUserQuota } from '../../shared' | ||
10 | |||
11 | export 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 | |||
42 | export 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 | |||
70 | export 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, query } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist' | ||
6 | import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
7 | |||
8 | const 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 | |||
20 | const 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 | |||
47 | const 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 | |||
63 | const 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 | |||
82 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { UserRight } from '@shared/models' | ||
4 | import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' | ||
5 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | ||
6 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' | ||
7 | import { | ||
8 | areValidationErrors, | ||
9 | checkCanSeeVideo, | ||
10 | checkUserCanManageVideo, | ||
11 | doesVideoCaptionExist, | ||
12 | doesVideoExist, | ||
13 | isValidVideoIdParam, | ||
14 | isValidVideoPasswordHeader | ||
15 | } from '../shared' | ||
16 | |||
17 | const 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 | |||
44 | const 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 | |||
63 | const 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 | |||
79 | export { | ||
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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' | ||
6 | import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models' | ||
7 | import { areValidationErrors, doesVideoChannelIdExist } from '../shared' | ||
8 | import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs' | ||
9 | |||
10 | export 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 | |||
21 | export 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 | |||
45 | export 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { MChannelAccountDefault } from '@server/types/models' | ||
6 | import { VideosImportInChannelCreate } from '@shared/models' | ||
7 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
8 | import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' | ||
9 | import { | ||
10 | isVideoChannelDescriptionValid, | ||
11 | isVideoChannelDisplayNameValid, | ||
12 | isVideoChannelSupportValid, | ||
13 | isVideoChannelUsernameValid | ||
14 | } from '../../../helpers/custom-validators/video-channels' | ||
15 | import { ActorModel } from '../../../models/actor/actor' | ||
16 | import { VideoChannelModel } from '../../../models/video/video-channel' | ||
17 | import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared' | ||
18 | import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs' | ||
19 | |||
20 | export 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 | |||
54 | export 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 | |||
78 | export 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 | |||
86 | export 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 | |||
99 | export 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 | |||
112 | export 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 | |||
123 | export 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 | |||
135 | export 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 | |||
147 | export 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 | |||
182 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { MUserAccountUrl } from '@server/types/models' | ||
4 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
5 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' | ||
6 | import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' | ||
7 | import { logger } from '../../../helpers/logger' | ||
8 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' | ||
9 | import { Hooks } from '../../../lib/plugins/hooks' | ||
10 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' | ||
11 | import { | ||
12 | areValidationErrors, | ||
13 | checkCanSeeVideo, | ||
14 | doesVideoCommentExist, | ||
15 | doesVideoCommentThreadExist, | ||
16 | doesVideoExist, | ||
17 | isValidVideoIdParam, | ||
18 | isValidVideoPasswordHeader | ||
19 | } from '../shared' | ||
20 | |||
21 | const 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 | |||
53 | const 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 | |||
67 | const 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 | |||
85 | const 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 | |||
105 | const 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 | |||
127 | const 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 | |||
142 | const 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 | |||
162 | export { | ||
163 | listVideoCommentThreadsValidator, | ||
164 | listVideoThreadCommentsValidator, | ||
165 | addVideoCommentThreadValidator, | ||
166 | listVideoCommentsValidator, | ||
167 | addVideoCommentReplyValidator, | ||
168 | videoCommentGetValidator, | ||
169 | removeVideoCommentValidator | ||
170 | } | ||
171 | |||
172 | // --------------------------------------------------------------------------- | ||
173 | |||
174 | function 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 | |||
186 | function 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 | |||
212 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { MVideo } from '@server/types/models' | ||
5 | import { HttpStatusCode } from '@shared/models' | ||
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
7 | |||
8 | const 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 | |||
37 | const 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 | |||
72 | const 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 | |||
101 | const 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 | |||
142 | export { | ||
143 | videoFilesDeleteWebVideoValidator, | ||
144 | videoFilesDeleteWebVideoFileValidator, | ||
145 | |||
146 | videoFilesDeleteHLSValidator, | ||
147 | videoFilesDeleteHLSFileValidator | ||
148 | } | ||
149 | |||
150 | // --------------------------------------------------------------------------- | ||
151 | |||
152 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { isResolvingToUnicastOnly } from '@server/helpers/dns' | ||
4 | import { isPreImportVideoAccepted } from '@server/lib/moderation' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { MUserAccountId, MVideoImport } from '@server/types/models' | ||
7 | import { forceNumber } from '@shared/core-utils' | ||
8 | import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' | ||
9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | ||
10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' | ||
12 | import { | ||
13 | isValidPasswordProtectedPrivacy, | ||
14 | isVideoMagnetUriValid, | ||
15 | isVideoNameValid | ||
16 | } from '../../../helpers/custom-validators/videos' | ||
17 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | ||
18 | import { logger } from '../../../helpers/logger' | ||
19 | import { CONFIG } from '../../../initializers/config' | ||
20 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | ||
21 | import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared' | ||
22 | import { getCommonVideoEditAttributes } from './videos' | ||
23 | |||
24 | const 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 | |||
104 | const 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 | |||
116 | const 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 | |||
137 | const 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 | |||
160 | export { | ||
161 | videoImportAddValidator, | ||
162 | videoImportCancelValidator, | ||
163 | videoImportDeleteValidator, | ||
164 | getMyVideoImportsValidator | ||
165 | } | ||
166 | |||
167 | // --------------------------------------------------------------------------- | ||
168 | |||
169 | async 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 | |||
199 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives' | ||
4 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
5 | import { isLocalLiveVideoAccepted } from '@server/lib/moderation' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { VideoModel } from '@server/models/video/video' | ||
8 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
9 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | ||
10 | import { | ||
11 | HttpStatusCode, | ||
12 | LiveVideoCreate, | ||
13 | LiveVideoLatencyMode, | ||
14 | LiveVideoUpdate, | ||
15 | ServerErrorCode, | ||
16 | UserRight, | ||
17 | VideoState | ||
18 | } from '@shared/models' | ||
19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
20 | import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos' | ||
21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | ||
22 | import { logger } from '../../../helpers/logger' | ||
23 | import { CONFIG } from '../../../initializers/config' | ||
24 | import { | ||
25 | areValidationErrors, | ||
26 | checkUserCanManageVideo, | ||
27 | doesVideoChannelOfAccountExist, | ||
28 | doesVideoExist, | ||
29 | isValidVideoIdParam | ||
30 | } from '../shared' | ||
31 | import { getCommonVideoEditAttributes } from './videos' | ||
32 | |||
33 | const 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 | |||
54 | const 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 | |||
171 | const 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 | |||
220 | const 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 | |||
230 | const 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 | |||
253 | export { | ||
254 | videoLiveAddValidator, | ||
255 | videoLiveUpdateValidator, | ||
256 | videoLiveListSessionsValidator, | ||
257 | videoLiveFindReplaySessionValidator, | ||
258 | videoLiveGetValidator | ||
259 | } | ||
260 | |||
261 | // --------------------------------------------------------------------------- | ||
262 | |||
263 | async 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 | |||
288 | function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) { | ||
289 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false | ||
290 | |||
291 | return true | ||
292 | } | ||
293 | |||
294 | function 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 | |||
304 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership' | ||
5 | import { AccountModel } from '@server/models/account/account' | ||
6 | import { MVideoWithAllFiles } from '@server/types/models' | ||
7 | import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models' | ||
8 | import { | ||
9 | areValidationErrors, | ||
10 | checkUserCanManageVideo, | ||
11 | checkUserQuota, | ||
12 | doesChangeVideoOwnershipExist, | ||
13 | doesVideoChannelOfAccountExist, | ||
14 | doesVideoExist, | ||
15 | isValidVideoIdParam | ||
16 | } from '../shared' | ||
17 | |||
18 | const 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 | |||
39 | const 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 | |||
64 | const 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 | |||
79 | export { | ||
80 | videosChangeOwnershipValidator, | ||
81 | videosTerminateChangeOwnershipValidator, | ||
82 | videosAcceptChangeOwnershipValidator | ||
83 | } | ||
84 | |||
85 | // --------------------------------------------------------------------------- | ||
86 | |||
87 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { | ||
3 | areValidationErrors, | ||
4 | doesVideoExist, | ||
5 | isVideoPasswordProtected, | ||
6 | isValidVideoIdParam, | ||
7 | doesVideoPasswordExist, | ||
8 | isVideoPasswordDeletable, | ||
9 | checkUserCanManageVideo | ||
10 | } from '../shared' | ||
11 | import { body, param } from 'express-validator' | ||
12 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
13 | import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos' | ||
14 | import { UserRight } from '@shared/models' | ||
15 | |||
16 | const 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 | |||
33 | const 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 | |||
53 | const 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 | |||
73 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query, ValidationChain } from 'express-validator' | ||
3 | import { ExpressPromiseHandler } from '@server/types/express-handler' | ||
4 | import { MUserAccountId } from '@server/types/models' | ||
5 | import { forceNumber } from '@shared/core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | UserRight, | ||
9 | VideoPlaylistCreate, | ||
10 | VideoPlaylistPrivacy, | ||
11 | VideoPlaylistType, | ||
12 | VideoPlaylistUpdate | ||
13 | } from '@shared/models' | ||
14 | import { | ||
15 | isArrayOf, | ||
16 | isIdOrUUIDValid, | ||
17 | isIdValid, | ||
18 | isUUIDValid, | ||
19 | toCompleteUUID, | ||
20 | toIntArray, | ||
21 | toIntOrNull, | ||
22 | toValueOrNull | ||
23 | } from '../../../helpers/custom-validators/misc' | ||
24 | import { | ||
25 | isVideoPlaylistDescriptionValid, | ||
26 | isVideoPlaylistNameValid, | ||
27 | isVideoPlaylistPrivacyValid, | ||
28 | isVideoPlaylistTimestampValid, | ||
29 | isVideoPlaylistTypeValid | ||
30 | } from '../../../helpers/custom-validators/video-playlists' | ||
31 | import { isVideoImageValid } from '../../../helpers/custom-validators/videos' | ||
32 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | ||
33 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | ||
34 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' | ||
35 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' | ||
36 | import { authenticatePromise } from '../../auth' | ||
37 | import { | ||
38 | areValidationErrors, | ||
39 | doesVideoChannelIdExist, | ||
40 | doesVideoExist, | ||
41 | doesVideoPlaylistExist, | ||
42 | isValidPlaylistIdParam, | ||
43 | VideoPlaylistFetchType | ||
44 | } from '../shared' | ||
45 | |||
46 | const 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 | |||
69 | const 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 | |||
113 | const 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 | |||
134 | const 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 | |||
178 | const 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 | |||
190 | const 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 | |||
219 | const 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 | |||
254 | const 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 | |||
287 | const 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 | |||
325 | const 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 | |||
337 | const 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 | |||
351 | export { | ||
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 | |||
371 | function 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 | |||
394 | function 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 | |||
417 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { VideoRateType } from '../../../../shared/models/videos' | ||
5 | import { isAccountNameValid } from '../../../helpers/custom-validators/accounts' | ||
6 | import { isIdValid } from '../../../helpers/custom-validators/misc' | ||
7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' | ||
8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | ||
9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared' | ||
11 | |||
12 | const 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 | |||
29 | const 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 | |||
54 | const 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 | |||
68 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { isIdValid } from '../../../helpers/custom-validators/misc' | ||
5 | import { VideoShareModel } from '../../../models/video/video-share' | ||
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
7 | |||
8 | const 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 | |||
33 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, header } from 'express-validator' | ||
3 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
4 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { uploadx } from '@server/lib/uploadx' | ||
7 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
8 | import { MVideoFullLight } from '@server/types/models' | ||
9 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
10 | import { Metadata as UploadXMetadata } from '@uploadx/core' | ||
11 | import { logger } from '../../../helpers/logger' | ||
12 | import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
13 | import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared' | ||
14 | |||
15 | export 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 | |||
40 | export 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 | |||
64 | export 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 | |||
106 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { param, query } from 'express-validator' | ||
3 | import { isDateValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { isValidStatTimeserieMetric } from '@server/helpers/custom-validators/video-stats' | ||
5 | import { STATS_TIMESERIE } from '@server/initializers/constants' | ||
6 | import { HttpStatusCode, UserRight, VideoStatsTimeserieQuery } from '@shared/models' | ||
7 | import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
8 | |||
9 | const 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 | |||
28 | const 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 | |||
46 | const 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 | |||
88 | export { | ||
89 | videoOverallStatsValidator, | ||
90 | videoTimeserieStatsValidator, | ||
91 | videoRetentionStatsValidator | ||
92 | } | ||
93 | |||
94 | // --------------------------------------------------------------------------- | ||
95 | |||
96 | async 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 | |||
103 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { | ||
5 | isStudioCutTaskValid, | ||
6 | isStudioTaskAddIntroOutroValid, | ||
7 | isStudioTaskAddWatermarkValid, | ||
8 | isValidStudioTasksArray | ||
9 | } from '@server/helpers/custom-validators/video-studio' | ||
10 | import { cleanUpReqFiles } from '@server/helpers/express-utils' | ||
11 | import { CONFIG } from '@server/initializers/config' | ||
12 | import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio' | ||
13 | import { isAudioFile } from '@shared/ffmpeg' | ||
14 | import { HttpStatusCode, UserRight, VideoStudioCreateEdition, VideoStudioTask } from '@shared/models' | ||
15 | import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared' | ||
16 | import { checkVideoFileCanBeEdited } from './shared' | ||
17 | |||
18 | const 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 | |||
85 | export { | ||
86 | videoStudioAddEditionValidator | ||
87 | } | ||
88 | |||
89 | // --------------------------------------------------------------------------- | ||
90 | |||
91 | const 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 | |||
100 | function 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 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | import { exists } from '@server/helpers/custom-validators/misc' | ||
5 | |||
6 | const 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 | |||
22 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc' | ||
4 | import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | ||
7 | import { HttpStatusCode, ServerErrorCode, VideoTranscodingCreate } from '@shared/models' | ||
8 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
9 | |||
10 | const 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 | |||
59 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { isVideoTimeValid } from '@server/helpers/custom-validators/video-view' | ||
4 | import { getCachedVideoDuration } from '@server/lib/video' | ||
5 | import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' | ||
6 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
7 | import { isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
8 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
9 | |||
10 | const 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 | |||
31 | const 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 | |||
58 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, header, param, query, ValidationChain } from 'express-validator' | ||
3 | import { isTestInstance } from '@server/helpers/core-utils' | ||
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { Redis } from '@server/lib/redis' | ||
6 | import { uploadx } from '@server/lib/uploadx' | ||
7 | import { getServerActor } from '@server/models/application/application' | ||
8 | import { ExpressPromiseHandler } from '@server/types/express-handler' | ||
9 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | ||
10 | import { arrayify } from '@shared/core-utils' | ||
11 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@shared/models' | ||
12 | import { | ||
13 | exists, | ||
14 | isBooleanValid, | ||
15 | isDateValid, | ||
16 | isFileValid, | ||
17 | isIdValid, | ||
18 | toBooleanOrNull, | ||
19 | toIntOrNull, | ||
20 | toValueOrNull | ||
21 | } from '../../../helpers/custom-validators/misc' | ||
22 | import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | ||
23 | import { | ||
24 | areVideoTagsValid, | ||
25 | isScheduleVideoUpdatePrivacyValid, | ||
26 | isValidPasswordProtectedPrivacy, | ||
27 | isVideoCategoryValid, | ||
28 | isVideoDescriptionValid, | ||
29 | isVideoImageValid, | ||
30 | isVideoIncludeValid, | ||
31 | isVideoLanguageValid, | ||
32 | isVideoLicenceValid, | ||
33 | isVideoNameValid, | ||
34 | isVideoOriginallyPublishedAtValid, | ||
35 | isVideoPrivacyValid, | ||
36 | isVideoSupportValid | ||
37 | } from '../../../helpers/custom-validators/videos' | ||
38 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | ||
39 | import { logger } from '../../../helpers/logger' | ||
40 | import { getVideoWithAttributes } from '../../../helpers/video' | ||
41 | import { CONFIG } from '../../../initializers/config' | ||
42 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | ||
43 | import { VideoModel } from '../../../models/video/video' | ||
44 | import { | ||
45 | areValidationErrors, | ||
46 | checkCanAccessVideoStaticFiles, | ||
47 | checkCanSeeVideo, | ||
48 | checkUserCanManageVideo, | ||
49 | doesVideoChannelOfAccountExist, | ||
50 | doesVideoExist, | ||
51 | doesVideoFileOfVideoExist, | ||
52 | isValidVideoIdParam, | ||
53 | isValidVideoPasswordHeader | ||
54 | } from '../shared' | ||
55 | import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared' | ||
56 | |||
57 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | ||
58 | body('videofile') | ||
59 | .custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null })) | ||
60 | .withMessage('Should have a file'), | ||
61 | body('name') | ||
62 | .trim() | ||
63 | .custom(isVideoNameValid).withMessage( | ||
64 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
65 | ), | ||
66 | body('channelId') | ||
67 | .customSanitizer(toIntOrNull) | ||
68 | .custom(isIdValid), | ||
69 | body('videoPasswords') | ||
70 | .optional() | ||
71 | .isArray() | ||
72 | .withMessage('Video passwords should be an array.'), | ||
73 | |||
74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
76 | |||
77 | const videoFile: express.VideoUploadFile = req.files['videofile'][0] | ||
78 | const user = res.locals.oauth.token.User | ||
79 | |||
80 | if ( | ||
81 | !await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) || | ||
82 | !isValidPasswordProtectedPrivacy(req, res) || | ||
83 | !await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) || | ||
84 | !await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' }) | ||
85 | ) { | ||
86 | return cleanUpReqFiles(req) | ||
87 | } | ||
88 | |||
89 | return next() | ||
90 | } | ||
91 | ]) | ||
92 | |||
93 | /** | ||
94 | * Gets called after the last PUT request | ||
95 | */ | ||
96 | const videosAddResumableValidator = [ | ||
97 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
98 | const user = res.locals.oauth.token.User | ||
99 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | ||
100 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } | ||
101 | const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err })) | ||
102 | |||
103 | const uploadId = req.query.upload_id | ||
104 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) | ||
105 | |||
106 | if (sessionExists) { | ||
107 | const sessionResponse = await Redis.Instance.getUploadSession(uploadId) | ||
108 | |||
109 | if (!sessionResponse) { | ||
110 | res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion | ||
111 | |||
112 | return res.fail({ | ||
113 | status: HttpStatusCode.SERVICE_UNAVAILABLE_503, | ||
114 | message: 'The upload is already being processed' | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | const videoStillExists = await VideoModel.load(sessionResponse.video.id) | ||
119 | |||
120 | if (videoStillExists) { | ||
121 | if (isTestInstance()) { | ||
122 | res.setHeader('x-resumable-upload-cached', 'true') | ||
123 | } | ||
124 | |||
125 | return res.json(sessionResponse) | ||
126 | } | ||
127 | } | ||
128 | |||
129 | await Redis.Instance.setUploadSession(uploadId) | ||
130 | |||
131 | if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup() | ||
132 | if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'videosAddResumableValidator' })) return cleanup() | ||
133 | if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.upload.accept.result' })) return cleanup() | ||
134 | |||
135 | res.locals.uploadVideoFileResumable = { ...file, originalname: file.filename } | ||
136 | |||
137 | return next() | ||
138 | } | ||
139 | ] | ||
140 | |||
141 | /** | ||
142 | * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use. | ||
143 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts | ||
144 | * | ||
145 | * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx | ||
146 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts | ||
147 | * | ||
148 | */ | ||
149 | const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | ||
150 | body('filename') | ||
151 | .exists(), | ||
152 | body('name') | ||
153 | .trim() | ||
154 | .custom(isVideoNameValid).withMessage( | ||
155 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
156 | ), | ||
157 | body('channelId') | ||
158 | .customSanitizer(toIntOrNull) | ||
159 | .custom(isIdValid), | ||
160 | body('videoPasswords') | ||
161 | .optional() | ||
162 | .isArray() | ||
163 | .withMessage('Video passwords should be an array.'), | ||
164 | |||
165 | header('x-upload-content-length') | ||
166 | .isNumeric() | ||
167 | .exists() | ||
168 | .withMessage('Should specify the file length'), | ||
169 | header('x-upload-content-type') | ||
170 | .isString() | ||
171 | .exists() | ||
172 | .withMessage('Should specify the file mimetype'), | ||
173 | |||
174 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
175 | const videoFileMetadata = { | ||
176 | mimetype: req.headers['x-upload-content-type'] as string, | ||
177 | size: +req.headers['x-upload-content-length'], | ||
178 | originalname: req.body.filename | ||
179 | } | ||
180 | |||
181 | const user = res.locals.oauth.token.User | ||
182 | const cleanup = () => cleanUpReqFiles(req) | ||
183 | |||
184 | logger.debug('Checking videosAddResumableInitValidator parameters and headers', { | ||
185 | parameters: req.body, | ||
186 | headers: req.headers, | ||
187 | files: req.files | ||
188 | }) | ||
189 | |||
190 | if (areValidationErrors(req, res, { omitLog: true })) return cleanup() | ||
191 | |||
192 | const files = { videofile: [ videoFileMetadata ] } | ||
193 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | ||
194 | |||
195 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup() | ||
196 | |||
197 | // Multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
198 | req.headers['content-type'] = 'application/json; charset=utf-8' | ||
199 | |||
200 | // Place thumbnail/previewfile in metadata so that uploadx saves it in .META | ||
201 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] | ||
202 | if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile'] | ||
203 | |||
204 | return next() | ||
205 | } | ||
206 | ]) | ||
207 | |||
208 | const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | ||
209 | isValidVideoIdParam('id'), | ||
210 | |||
211 | body('name') | ||
212 | .optional() | ||
213 | .trim() | ||
214 | .custom(isVideoNameValid).withMessage( | ||
215 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
216 | ), | ||
217 | body('channelId') | ||
218 | .optional() | ||
219 | .customSanitizer(toIntOrNull) | ||
220 | .custom(isIdValid), | ||
221 | body('videoPasswords') | ||
222 | .optional() | ||
223 | .isArray() | ||
224 | .withMessage('Video passwords should be an array.'), | ||
225 | |||
226 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
227 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
228 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | ||
229 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | ||
230 | |||
231 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
232 | |||
233 | const video = getVideoWithAttributes(res) | ||
234 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { | ||
235 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | ||
236 | } | ||
237 | |||
238 | // Check if the user who did the request is able to update the video | ||
239 | const user = res.locals.oauth.token.User | ||
240 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) | ||
241 | |||
242 | if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | ||
243 | |||
244 | return next() | ||
245 | } | ||
246 | ]) | ||
247 | |||
248 | async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
249 | const video = getVideoWithAttributes(res) | ||
250 | |||
251 | // Anybody can watch local videos | ||
252 | if (video.isOwned() === true) return next() | ||
253 | |||
254 | // Logged user | ||
255 | if (res.locals.oauth) { | ||
256 | // Users can search or watch remote videos | ||
257 | if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next() | ||
258 | } | ||
259 | |||
260 | // Anybody can search or watch remote videos | ||
261 | if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next() | ||
262 | |||
263 | // Check our instance follows an actor that shared this video | ||
264 | const serverActor = await getServerActor() | ||
265 | if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() | ||
266 | |||
267 | return res.fail({ | ||
268 | status: HttpStatusCode.FORBIDDEN_403, | ||
269 | message: 'Cannot get this video regarding follow constraints', | ||
270 | type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, | ||
271 | data: { | ||
272 | originUrl: video.url | ||
273 | } | ||
274 | }) | ||
275 | } | ||
276 | |||
277 | const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes') => { | ||
278 | return [ | ||
279 | isValidVideoIdParam('id'), | ||
280 | |||
281 | isValidVideoPasswordHeader(), | ||
282 | |||
283 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
284 | if (areValidationErrors(req, res)) return | ||
285 | if (!await doesVideoExist(req.params.id, res, fetchType)) return | ||
286 | |||
287 | // Controllers does not need to check video rights | ||
288 | if (fetchType === 'only-immutable-attributes') return next() | ||
289 | |||
290 | const video = getVideoWithAttributes(res) as MVideoFullLight | ||
291 | |||
292 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id })) return | ||
293 | |||
294 | return next() | ||
295 | } | ||
296 | ] | ||
297 | } | ||
298 | |||
299 | const videosGetValidator = videosCustomGetValidator('all') | ||
300 | |||
301 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | ||
302 | isValidVideoIdParam('id'), | ||
303 | |||
304 | param('videoFileId') | ||
305 | .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), | ||
306 | |||
307 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
308 | if (areValidationErrors(req, res)) return | ||
309 | if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return | ||
310 | |||
311 | return next() | ||
312 | } | ||
313 | ]) | ||
314 | |||
315 | const videosDownloadValidator = [ | ||
316 | isValidVideoIdParam('id'), | ||
317 | |||
318 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
319 | if (areValidationErrors(req, res)) return | ||
320 | if (!await doesVideoExist(req.params.id, res, 'all')) return | ||
321 | |||
322 | const video = getVideoWithAttributes(res) | ||
323 | |||
324 | if (!await checkCanAccessVideoStaticFiles({ req, res, video, paramId: req.params.id })) return | ||
325 | |||
326 | return next() | ||
327 | } | ||
328 | ] | ||
329 | |||
330 | const videosRemoveValidator = [ | ||
331 | isValidVideoIdParam('id'), | ||
332 | |||
333 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
334 | if (areValidationErrors(req, res)) return | ||
335 | if (!await doesVideoExist(req.params.id, res)) return | ||
336 | |||
337 | // Check if the user who did the request is able to delete the video | ||
338 | if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return | ||
339 | |||
340 | return next() | ||
341 | } | ||
342 | ] | ||
343 | |||
344 | const videosOverviewValidator = [ | ||
345 | query('page') | ||
346 | .optional() | ||
347 | .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT }), | ||
348 | |||
349 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
350 | if (areValidationErrors(req, res)) return | ||
351 | |||
352 | return next() | ||
353 | } | ||
354 | ] | ||
355 | |||
356 | function getCommonVideoEditAttributes () { | ||
357 | return [ | ||
358 | body('thumbnailfile') | ||
359 | .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage( | ||
360 | 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + | ||
361 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') | ||
362 | ), | ||
363 | body('previewfile') | ||
364 | .custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage( | ||
365 | 'This preview file is not supported or too large. Please, make sure it is of the following type: ' + | ||
366 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') | ||
367 | ), | ||
368 | |||
369 | body('category') | ||
370 | .optional() | ||
371 | .customSanitizer(toIntOrNull) | ||
372 | .custom(isVideoCategoryValid), | ||
373 | body('licence') | ||
374 | .optional() | ||
375 | .customSanitizer(toIntOrNull) | ||
376 | .custom(isVideoLicenceValid), | ||
377 | body('language') | ||
378 | .optional() | ||
379 | .customSanitizer(toValueOrNull) | ||
380 | .custom(isVideoLanguageValid), | ||
381 | body('nsfw') | ||
382 | .optional() | ||
383 | .customSanitizer(toBooleanOrNull) | ||
384 | .custom(isBooleanValid).withMessage('Should have a valid nsfw boolean'), | ||
385 | body('waitTranscoding') | ||
386 | .optional() | ||
387 | .customSanitizer(toBooleanOrNull) | ||
388 | .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'), | ||
389 | body('privacy') | ||
390 | .optional() | ||
391 | .customSanitizer(toIntOrNull) | ||
392 | .custom(isVideoPrivacyValid), | ||
393 | body('description') | ||
394 | .optional() | ||
395 | .customSanitizer(toValueOrNull) | ||
396 | .custom(isVideoDescriptionValid), | ||
397 | body('support') | ||
398 | .optional() | ||
399 | .customSanitizer(toValueOrNull) | ||
400 | .custom(isVideoSupportValid), | ||
401 | body('tags') | ||
402 | .optional() | ||
403 | .customSanitizer(toValueOrNull) | ||
404 | .custom(areVideoTagsValid) | ||
405 | .withMessage( | ||
406 | `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` + | ||
407 | `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each` | ||
408 | ), | ||
409 | body('commentsEnabled') | ||
410 | .optional() | ||
411 | .customSanitizer(toBooleanOrNull) | ||
412 | .custom(isBooleanValid).withMessage('Should have commentsEnabled boolean'), | ||
413 | body('downloadEnabled') | ||
414 | .optional() | ||
415 | .customSanitizer(toBooleanOrNull) | ||
416 | .custom(isBooleanValid).withMessage('Should have downloadEnabled boolean'), | ||
417 | body('originallyPublishedAt') | ||
418 | .optional() | ||
419 | .customSanitizer(toValueOrNull) | ||
420 | .custom(isVideoOriginallyPublishedAtValid), | ||
421 | body('scheduleUpdate') | ||
422 | .optional() | ||
423 | .customSanitizer(toValueOrNull), | ||
424 | body('scheduleUpdate.updateAt') | ||
425 | .optional() | ||
426 | .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'), | ||
427 | body('scheduleUpdate.privacy') | ||
428 | .optional() | ||
429 | .customSanitizer(toIntOrNull) | ||
430 | .custom(isScheduleVideoUpdatePrivacyValid) | ||
431 | ] as (ValidationChain | ExpressPromiseHandler)[] | ||
432 | } | ||
433 | |||
434 | const commonVideosFiltersValidator = [ | ||
435 | query('categoryOneOf') | ||
436 | .optional() | ||
437 | .customSanitizer(arrayify) | ||
438 | .custom(isNumberArray).withMessage('Should have a valid categoryOneOf array'), | ||
439 | query('licenceOneOf') | ||
440 | .optional() | ||
441 | .customSanitizer(arrayify) | ||
442 | .custom(isNumberArray).withMessage('Should have a valid licenceOneOf array'), | ||
443 | query('languageOneOf') | ||
444 | .optional() | ||
445 | .customSanitizer(arrayify) | ||
446 | .custom(isStringArray).withMessage('Should have a valid languageOneOf array'), | ||
447 | query('privacyOneOf') | ||
448 | .optional() | ||
449 | .customSanitizer(arrayify) | ||
450 | .custom(isNumberArray).withMessage('Should have a valid privacyOneOf array'), | ||
451 | query('tagsOneOf') | ||
452 | .optional() | ||
453 | .customSanitizer(arrayify) | ||
454 | .custom(isStringArray).withMessage('Should have a valid tagsOneOf array'), | ||
455 | query('tagsAllOf') | ||
456 | .optional() | ||
457 | .customSanitizer(arrayify) | ||
458 | .custom(isStringArray).withMessage('Should have a valid tagsAllOf array'), | ||
459 | query('nsfw') | ||
460 | .optional() | ||
461 | .custom(isBooleanBothQueryValid), | ||
462 | query('isLive') | ||
463 | .optional() | ||
464 | .customSanitizer(toBooleanOrNull) | ||
465 | .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'), | ||
466 | query('include') | ||
467 | .optional() | ||
468 | .custom(isVideoIncludeValid), | ||
469 | query('isLocal') | ||
470 | .optional() | ||
471 | .customSanitizer(toBooleanOrNull) | ||
472 | .custom(isBooleanValid).withMessage('Should have a valid isLocal boolean'), | ||
473 | query('hasHLSFiles') | ||
474 | .optional() | ||
475 | .customSanitizer(toBooleanOrNull) | ||
476 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), | ||
477 | query('hasWebtorrentFiles') // TODO: remove in v7 | ||
478 | .optional() | ||
479 | .customSanitizer(toBooleanOrNull) | ||
480 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), | ||
481 | query('hasWebVideoFiles') | ||
482 | .optional() | ||
483 | .customSanitizer(toBooleanOrNull) | ||
484 | .custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'), | ||
485 | query('skipCount') | ||
486 | .optional() | ||
487 | .customSanitizer(toBooleanOrNull) | ||
488 | .custom(isBooleanValid).withMessage('Should have a valid skipCount boolean'), | ||
489 | query('search') | ||
490 | .optional() | ||
491 | .custom(exists), | ||
492 | query('excludeAlreadyWatched') | ||
493 | .optional() | ||
494 | .customSanitizer(toBooleanOrNull) | ||
495 | .isBoolean().withMessage('Should be a valid excludeAlreadyWatched boolean'), | ||
496 | |||
497 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
498 | if (areValidationErrors(req, res)) return | ||
499 | |||
500 | const user = res.locals.oauth?.token.User | ||
501 | |||
502 | if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { | ||
503 | if (req.query.include || req.query.privacyOneOf) { | ||
504 | return res.fail({ | ||
505 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
506 | message: 'You are not allowed to see all videos.' | ||
507 | }) | ||
508 | } | ||
509 | } | ||
510 | |||
511 | if (!user && exists(req.query.excludeAlreadyWatched)) { | ||
512 | res.fail({ | ||
513 | status: HttpStatusCode.BAD_REQUEST_400, | ||
514 | message: 'Cannot use excludeAlreadyWatched parameter when auth token is not provided' | ||
515 | }) | ||
516 | return false | ||
517 | } | ||
518 | return next() | ||
519 | } | ||
520 | ] | ||
521 | |||
522 | // --------------------------------------------------------------------------- | ||
523 | |||
524 | export { | ||
525 | videosAddLegacyValidator, | ||
526 | videosAddResumableValidator, | ||
527 | videosAddResumableInitValidator, | ||
528 | |||
529 | videosUpdateValidator, | ||
530 | videosGetValidator, | ||
531 | videoFileMetadataGetValidator, | ||
532 | videosDownloadValidator, | ||
533 | checkVideoFollowConstraints, | ||
534 | videosCustomGetValidator, | ||
535 | videosRemoveValidator, | ||
536 | |||
537 | getCommonVideoEditAttributes, | ||
538 | |||
539 | commonVideosFiltersValidator, | ||
540 | |||
541 | videosOverviewValidator | ||
542 | } | ||
543 | |||
544 | // --------------------------------------------------------------------------- | ||
545 | |||
546 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { | ||
547 | if (req.body.scheduleUpdate) { | ||
548 | if (!req.body.scheduleUpdate.updateAt) { | ||
549 | logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') | ||
550 | |||
551 | res.fail({ message: 'Schedule update at is mandatory.' }) | ||
552 | return true | ||
553 | } | ||
554 | } | ||
555 | |||
556 | return false | ||
557 | } | ||
558 | |||
559 | async function commonVideoChecksPass (options: { | ||
560 | req: express.Request | ||
561 | res: express.Response | ||
562 | user: MUserAccountId | ||
563 | videoFileSize: number | ||
564 | files: express.UploadFilesForCheck | ||
565 | }): Promise<boolean> { | ||
566 | const { req, res, user } = options | ||
567 | |||
568 | if (areErrorsInScheduleUpdate(req, res)) return false | ||
569 | |||
570 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false | ||
571 | |||
572 | if (!await commonVideoFileChecks(options)) return false | ||
573 | |||
574 | return true | ||
575 | } | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | ||
4 | import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger' | ||
5 | import { getHostWithPort } from '../../helpers/express-utils' | ||
6 | import { ActorModel } from '../../models/actor/actor' | ||
7 | import { areValidationErrors } from './shared' | ||
8 | |||
9 | const 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 | |||
35 | export { | ||
36 | webfingerValidator | ||
37 | } | ||