aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares
diff options
context:
space:
mode:
Diffstat (limited to 'server/middlewares')
-rw-r--r--server/middlewares/activitypub.ts53
-rw-r--r--server/middlewares/auth.ts17
-rw-r--r--server/middlewares/doc.ts16
-rw-r--r--server/middlewares/error.ts39
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/servers.ts5
-rw-r--r--server/middlewares/user-right.ts6
-rw-r--r--server/middlewares/validators/abuse.ts34
-rw-r--r--server/middlewares/validators/account.ts3
-rw-r--r--server/middlewares/validators/activitypub/activity.ts6
-rw-r--r--server/middlewares/validators/activitypub/pagination.ts4
-rw-r--r--server/middlewares/validators/activitypub/signature.ts7
-rw-r--r--server/middlewares/validators/actor-image.ts2
-rw-r--r--server/middlewares/validators/blocklist.ts44
-rw-r--r--server/middlewares/validators/bulk.ts11
-rw-r--r--server/middlewares/validators/config.ts16
-rw-r--r--server/middlewares/validators/feeds.ts31
-rw-r--r--server/middlewares/validators/follows.ts44
-rw-r--r--server/middlewares/validators/index.ts2
-rw-r--r--server/middlewares/validators/jobs.ts2
-rw-r--r--server/middlewares/validators/logs.ts14
-rw-r--r--server/middlewares/validators/oembed.ts79
-rw-r--r--server/middlewares/validators/pagination.ts4
-rw-r--r--server/middlewares/validators/plugins.ts56
-rw-r--r--server/middlewares/validators/redundancy.ts87
-rw-r--r--server/middlewares/validators/search.ts31
-rw-r--r--server/middlewares/validators/server.ts43
-rw-r--r--server/middlewares/validators/shared/abuses.ts25
-rw-r--r--server/middlewares/validators/shared/accounts.ts65
-rw-r--r--server/middlewares/validators/shared/index.ts11
-rw-r--r--server/middlewares/validators/shared/utils.ts (renamed from server/middlewares/validators/utils.ts)31
-rw-r--r--server/middlewares/validators/shared/video-blacklists.ts24
-rw-r--r--server/middlewares/validators/shared/video-captions.ts25
-rw-r--r--server/middlewares/validators/shared/video-channels.ts43
-rw-r--r--server/middlewares/validators/shared/video-comments.ts73
-rw-r--r--server/middlewares/validators/shared/video-imports.ts22
-rw-r--r--server/middlewares/validators/shared/video-ownerships.ts24
-rw-r--r--server/middlewares/validators/shared/video-playlists.ts39
-rw-r--r--server/middlewares/validators/shared/videos.ts125
-rw-r--r--server/middlewares/validators/sort.ts5
-rw-r--r--server/middlewares/validators/themes.ts18
-rw-r--r--server/middlewares/validators/user-history.ts6
-rw-r--r--server/middlewares/validators/user-notifications.ts6
-rw-r--r--server/middlewares/validators/user-subscriptions.ts17
-rw-r--r--server/middlewares/validators/users.ts126
-rw-r--r--server/middlewares/validators/videos/index.ts2
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts25
-rw-r--r--server/middlewares/validators/videos/video-captions.ts23
-rw-r--r--server/middlewares/validators/videos/video-channels.ts67
-rw-r--r--server/middlewares/validators/videos/video-comments.ts67
-rw-r--r--server/middlewares/validators/videos/video-imports.ts35
-rw-r--r--server/middlewares/validators/videos/video-live.ts91
-rw-r--r--server/middlewares/validators/videos/video-ownership-changes.ts121
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts113
-rw-r--r--server/middlewares/validators/videos/video-rates.ts20
-rw-r--r--server/middlewares/validators/videos/video-shares.ts13
-rw-r--r--server/middlewares/validators/videos/video-watch.ts17
-rw-r--r--server/middlewares/validators/videos/videos.ts218
-rw-r--r--server/middlewares/validators/webfinger.ts15
59 files changed, 1490 insertions, 680 deletions
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index ce94a2129..6b43b7764 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -1,13 +1,12 @@
1import { NextFunction, Request, Response } from 'express' 1import { NextFunction, Request, Response } from 'express'
2import { getAPId } from '@server/helpers/activitypub'
3import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
2import { ActivityDelete, ActivityPubSignature } from '../../shared' 4import { ActivityDelete, ActivityPubSignature } from '../../shared'
5import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
3import { logger } from '../helpers/logger' 6import { logger } from '../helpers/logger'
4import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' 7import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
5import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants' 8import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
6import { getOrCreateActorAndServerAndModel } from '../lib/activitypub/actor' 9import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors'
7import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
8import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
9import { getAPId } from '@server/helpers/activitypub'
10import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
11 10
12async function checkSignature (req: Request, res: Response, next: NextFunction) { 11async function checkSignature (req: Request, res: Response, next: NextFunction) {
13 try { 12 try {
@@ -29,11 +28,14 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
29 const activity: ActivityDelete = req.body 28 const activity: ActivityDelete = req.body
30 if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) { 29 if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
31 logger.debug('Handling signature error on actor delete activity', { err }) 30 logger.debug('Handling signature error on actor delete activity', { err })
32 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 31 return res.status(HttpStatusCode.NO_CONTENT_204).end()
33 } 32 }
34 33
35 logger.warn('Error in ActivityPub signature checker.', { err }) 34 logger.warn('Error in ActivityPub signature checker.', { err })
36 return res.sendStatus(HttpStatusCode.FORBIDDEN_403) 35 return res.fail({
36 status: HttpStatusCode.FORBIDDEN_403,
37 message: 'ActivityPub signature could not be checked'
38 })
37 } 39 }
38} 40}
39 41
@@ -71,13 +73,22 @@ async function checkHttpSignature (req: Request, res: Response) {
71 } catch (err) { 73 } catch (err) {
72 logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err }) 74 logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err })
73 75
74 res.status(HttpStatusCode.FORBIDDEN_403).json({ error: err.message }) 76 res.fail({
77 status: HttpStatusCode.FORBIDDEN_403,
78 message: err.message
79 })
75 return false 80 return false
76 } 81 }
77 82
78 const keyId = parsed.keyId 83 const keyId = parsed.keyId
79 if (!keyId) { 84 if (!keyId) {
80 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 85 res.fail({
86 status: HttpStatusCode.FORBIDDEN_403,
87 message: 'Invalid key ID',
88 data: {
89 keyId
90 }
91 })
81 return false 92 return false
82 } 93 }
83 94
@@ -88,18 +99,23 @@ async function checkHttpSignature (req: Request, res: Response) {
88 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, '')) 99 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
89 } 100 }
90 101
91 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 102 const actor = await getOrCreateAPActor(actorUrl)
92 103
93 const verified = isHTTPSignatureVerified(parsed, actor) 104 const verified = isHTTPSignatureVerified(parsed, actor)
94 if (verified !== true) { 105 if (verified !== true) {
95 logger.warn('Signature from %s is invalid', actorUrl, { parsed }) 106 logger.warn('Signature from %s is invalid', actorUrl, { parsed })
96 107
97 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 108 res.fail({
109 status: HttpStatusCode.FORBIDDEN_403,
110 message: 'Invalid signature',
111 data: {
112 actorUrl
113 }
114 })
98 return false 115 return false
99 } 116 }
100 117
101 res.locals.signature = { actor } 118 res.locals.signature = { actor }
102
103 return true 119 return true
104} 120}
105 121
@@ -107,7 +123,10 @@ async function checkJsonLDSignature (req: Request, res: Response) {
107 const signatureObject: ActivityPubSignature = req.body.signature 123 const signatureObject: ActivityPubSignature = req.body.signature
108 124
109 if (!signatureObject || !signatureObject.creator) { 125 if (!signatureObject || !signatureObject.creator) {
110 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 126 res.fail({
127 status: HttpStatusCode.FORBIDDEN_403,
128 message: 'Object and creator signature do not match'
129 })
111 return false 130 return false
112 } 131 }
113 132
@@ -115,17 +134,19 @@ async function checkJsonLDSignature (req: Request, res: Response) {
115 134
116 logger.debug('Checking JsonLD signature of actor %s...', creator) 135 logger.debug('Checking JsonLD signature of actor %s...', creator)
117 136
118 const actor = await getOrCreateActorAndServerAndModel(creator) 137 const actor = await getOrCreateAPActor(creator)
119 const verified = await isJsonLDSignatureVerified(actor, req.body) 138 const verified = await isJsonLDSignatureVerified(actor, req.body)
120 139
121 if (verified !== true) { 140 if (verified !== true) {
122 logger.warn('Signature not verified.', req.body) 141 logger.warn('Signature not verified.', req.body)
123 142
124 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 143 res.fail({
144 status: HttpStatusCode.FORBIDDEN_403,
145 message: 'Signature could not be verified'
146 })
125 return false 147 return false
126 } 148 }
127 149
128 res.locals.signature = { actor } 150 res.locals.signature = { actor }
129
130 return true 151 return true
131} 152}
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts
index f38373624..176461cc2 100644
--- a/server/middlewares/auth.ts
+++ b/server/middlewares/auth.ts
@@ -16,11 +16,11 @@ function authenticate (req: express.Request, res: express.Response, next: expres
16 .catch(err => { 16 .catch(err => {
17 logger.warn('Cannot authenticate.', { err }) 17 logger.warn('Cannot authenticate.', { err })
18 18
19 return res.status(err.status) 19 return res.fail({
20 .json({ 20 status: err.status,
21 error: 'Token is invalid.', 21 message: 'Token is invalid',
22 code: err.name 22 type: err.name
23 }) 23 })
24 }) 24 })
25} 25}
26 26
@@ -52,7 +52,12 @@ function authenticatePromiseIfNeeded (req: express.Request, res: express.Respons
52 // Already authenticated? (or tried to) 52 // Already authenticated? (or tried to)
53 if (res.locals.oauth?.token.User) return resolve() 53 if (res.locals.oauth?.token.User) return resolve()
54 54
55 if (res.locals.authenticated === false) return res.sendStatus(HttpStatusCode.UNAUTHORIZED_401) 55 if (res.locals.authenticated === false) {
56 return res.fail({
57 status: HttpStatusCode.UNAUTHORIZED_401,
58 message: 'Not authenticated'
59 })
60 }
56 61
57 authenticate(req, res, () => resolve(), authenticateInQuery) 62 authenticate(req, res, () => resolve(), authenticateInQuery)
58 }) 63 })
diff --git a/server/middlewares/doc.ts b/server/middlewares/doc.ts
new file mode 100644
index 000000000..3db85c68d
--- /dev/null
+++ b/server/middlewares/doc.ts
@@ -0,0 +1,16 @@
1import * as express from 'express'
2
3function openapiOperationDoc (options: {
4 url?: string
5 operationId?: string
6}) {
7 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
8 res.locals.docUrl = options.url || 'https://docs.joinpeertube.org/api-rest-reference.html#operation/' + options.operationId
9
10 if (next) return next()
11 }
12}
13
14export {
15 openapiOperationDoc
16}
diff --git a/server/middlewares/error.ts b/server/middlewares/error.ts
new file mode 100644
index 000000000..e3eb1c8f5
--- /dev/null
+++ b/server/middlewares/error.ts
@@ -0,0 +1,39 @@
1import * as express from 'express'
2import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
3import { HttpStatusCode } from '@shared/core-utils'
4
5function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
6 res.fail = options => {
7 const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance } = options
8
9 const extension = new ProblemDocumentExtension({
10 ...data,
11
12 docs: res.locals.docUrl,
13 code: type,
14
15 // For <= 3.2 compatibility
16 error: message
17 })
18
19 res.status(status)
20 res.setHeader('Content-Type', 'application/problem+json')
21 res.json(new ProblemDocument({
22 status,
23 title,
24 instance,
25
26 detail: message,
27
28 type: type
29 ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
30 : undefined
31 }, extension))
32 }
33
34 if (next) next()
35}
36
37export {
38 apiFailMiddleware
39}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index 3e280e16f..413653dac 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -7,4 +7,6 @@ export * from './servers'
7export * from './sort' 7export * from './sort'
8export * from './user-right' 8export * from './user-right'
9export * from './dnt' 9export * from './dnt'
10export * from './error'
11export * from './doc'
10export * from './csp' 12export * from './csp'
diff --git a/server/middlewares/servers.ts b/server/middlewares/servers.ts
index 5e1c165f0..9aa56bc93 100644
--- a/server/middlewares/servers.ts
+++ b/server/middlewares/servers.ts
@@ -10,7 +10,10 @@ function setBodyHostsPort (req: express.Request, res: express.Response, next: ex
10 10
11 // Problem with the url parsing? 11 // Problem with the url parsing?
12 if (hostWithPort === null) { 12 if (hostWithPort === null) {
13 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) 13 return res.fail({
14 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
15 message: 'Could not parse hosts'
16 })
14 } 17 }
15 18
16 req.body.hosts[i] = hostWithPort 19 req.body.hosts[i] = hostWithPort
diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts
index 45dda4781..d1888c2d3 100644
--- a/server/middlewares/user-right.ts
+++ b/server/middlewares/user-right.ts
@@ -10,8 +10,10 @@ function ensureUserHasRight (userRight: UserRight) {
10 const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.` 10 const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.`
11 logger.info(message) 11 logger.info(message)
12 12
13 return res.status(HttpStatusCode.FORBIDDEN_403) 13 return res.fail({
14 .json({ error: message }) 14 status: HttpStatusCode.FORBIDDEN_403,
15 message
16 })
15 } 17 }
16 18
17 return next() 19 return next()
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
index 3b897fdef..c048bc6af 100644
--- a/server/middlewares/validators/abuse.ts
+++ b/server/middlewares/validators/abuse.ts
@@ -12,14 +12,12 @@ import {
12 isAbuseTimestampValid, 12 isAbuseTimestampValid,
13 isAbuseVideoIsValid 13 isAbuseVideoIsValid
14} from '@server/helpers/custom-validators/abuses' 14} from '@server/helpers/custom-validators/abuses'
15import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' 15import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc'
16import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments'
17import { logger } from '@server/helpers/logger' 16import { logger } from '@server/helpers/logger'
18import { doesAbuseExist, doesAccountIdExist, doesVideoExist } from '@server/helpers/middlewares'
19import { AbuseMessageModel } from '@server/models/abuse/abuse-message' 17import { AbuseMessageModel } from '@server/models/abuse/abuse-message'
20import { AbuseCreate, UserRight } from '@shared/models' 18import { AbuseCreate, UserRight } from '@shared/models'
21import { areValidationErrors } from './utils'
22import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 19import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
20import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared'
23 21
24const abuseReportValidator = [ 22const abuseReportValidator = [
25 body('account.id') 23 body('account.id')
@@ -29,6 +27,7 @@ const abuseReportValidator = [
29 27
30 body('video.id') 28 body('video.id')
31 .optional() 29 .optional()
30 .customSanitizer(toCompleteUUID)
32 .custom(isIdOrUUIDValid) 31 .custom(isIdOrUUIDValid)
33 .withMessage('Should have a valid videoId'), 32 .withMessage('Should have a valid videoId'),
34 body('video.startAt') 33 body('video.startAt')
@@ -71,9 +70,7 @@ const abuseReportValidator = [
71 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return 70 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
72 71
73 if (!body.video?.id && !body.account?.id && !body.comment?.id) { 72 if (!body.video?.id && !body.account?.id && !body.comment?.id) {
74 res.status(HttpStatusCode.BAD_REQUEST_400) 73 res.fail({ message: 'video id or account id or comment id is required.' })
75 .json({ error: 'video id or account id or comment id is required.' })
76
77 return 74 return
78 } 75 }
79 76
@@ -195,8 +192,10 @@ const getAbuseValidator = [
195 const message = `User ${user.username} does not have right to get abuse ${abuse.id}` 192 const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
196 logger.warn(message) 193 logger.warn(message)
197 194
198 return res.status(HttpStatusCode.FORBIDDEN_403) 195 return res.fail({
199 .json({ error: message }) 196 status: HttpStatusCode.FORBIDDEN_403,
197 message
198 })
200 } 199 }
201 200
202 return next() 201 return next()
@@ -209,10 +208,7 @@ const checkAbuseValidForMessagesValidator = [
209 208
210 const abuse = res.locals.abuse 209 const abuse = res.locals.abuse
211 if (abuse.ReporterAccount.isOwned() === false) { 210 if (abuse.ReporterAccount.isOwned() === false) {
212 return res.status(HttpStatusCode.BAD_REQUEST_400) 211 return res.fail({ message: 'This abuse was created by a user of your instance.' })
213 .json({
214 error: 'This abuse was created by a user of your instance.'
215 })
216 } 212 }
217 213
218 return next() 214 return next()
@@ -246,13 +242,17 @@ const deleteAbuseMessageValidator = [
246 const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) 242 const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
247 243
248 if (!abuseMessage) { 244 if (!abuseMessage) {
249 return res.status(HttpStatusCode.NOT_FOUND_404) 245 return res.fail({
250 .json({ error: 'Abuse message not found' }) 246 status: HttpStatusCode.NOT_FOUND_404,
247 message: 'Abuse message not found'
248 })
251 } 249 }
252 250
253 if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) { 251 if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
254 return res.status(HttpStatusCode.FORBIDDEN_403) 252 return res.fail({
255 .json({ error: 'Cannot delete this abuse message' }) 253 status: HttpStatusCode.FORBIDDEN_403,
254 message: 'Cannot delete this abuse message'
255 })
256 } 256 }
257 257
258 res.locals.abuseMessage = abuseMessage 258 res.locals.abuseMessage = abuseMessage
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
index cbdcef2fd..599eb10bb 100644
--- a/server/middlewares/validators/account.ts
+++ b/server/middlewares/validators/account.ts
@@ -2,8 +2,7 @@ import * as express from 'express'
2import { param } from 'express-validator' 2import { param } from 'express-validator'
3import { isAccountNameValid } from '../../helpers/custom-validators/accounts' 3import { isAccountNameValid } from '../../helpers/custom-validators/accounts'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors, doesAccountNameWithHostExist, doesLocalAccountNameExist } from './shared'
6import { doesAccountNameWithHostExist, doesLocalAccountNameExist } from '../../helpers/middlewares'
7 6
8const localAccountValidator = [ 7const localAccountValidator = [
9 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), 8 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
index e78ef07ef..cc6acd4b1 100644
--- a/server/middlewares/validators/activitypub/activity.ts
+++ b/server/middlewares/validators/activitypub/activity.ts
@@ -9,16 +9,14 @@ async function activityPubValidator (req: express.Request, res: express.Response
9 9
10 if (!isRootActivityValid(req.body)) { 10 if (!isRootActivityValid(req.body)) {
11 logger.warn('Incorrect activity parameters.', { activity: req.body }) 11 logger.warn('Incorrect activity parameters.', { activity: req.body })
12 return res.status(HttpStatusCode.BAD_REQUEST_400) 12 return res.fail({ message: 'Incorrect activity' })
13 .json({ error: 'Incorrect activity.' })
14 } 13 }
15 14
16 const serverActor = await getServerActor() 15 const serverActor = await getServerActor()
17 const remoteActor = res.locals.signature.actor 16 const remoteActor = res.locals.signature.actor
18 if (serverActor.id === remoteActor.id || remoteActor.serverId === null) { 17 if (serverActor.id === remoteActor.id || remoteActor.serverId === null) {
19 logger.error('Receiving request in INBOX by ourselves!', req.body) 18 logger.error('Receiving request in INBOX by ourselves!', req.body)
20 return res.status(HttpStatusCode.CONFLICT_409) 19 return res.status(HttpStatusCode.CONFLICT_409).end()
21 .end()
22 } 20 }
23 21
24 return next() 22 return next()
diff --git a/server/middlewares/validators/activitypub/pagination.ts b/server/middlewares/validators/activitypub/pagination.ts
index fa21f063d..c8ec34eb6 100644
--- a/server/middlewares/validators/activitypub/pagination.ts
+++ b/server/middlewares/validators/activitypub/pagination.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { query } from 'express-validator' 2import { query } from 'express-validator'
3import { logger } from '../../../helpers/logger'
4import { areValidationErrors } from '../utils'
5import { PAGINATION } from '@server/initializers/constants' 3import { PAGINATION } from '@server/initializers/constants'
4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from '../shared'
6 6
7const apPaginationValidator = [ 7const apPaginationValidator = [
8 query('page') 8 query('page')
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
index 7c4e49463..f2f7d5848 100644
--- a/server/middlewares/validators/activitypub/signature.ts
+++ b/server/middlewares/validators/activitypub/signature.ts
@@ -1,12 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { 3import {
4 isSignatureCreatorValid, isSignatureTypeValid, 4 isSignatureCreatorValid,
5 isSignatureTypeValid,
5 isSignatureValueValid 6 isSignatureValueValid
6} from '../../../helpers/custom-validators/activitypub/signature' 7} from '../../../helpers/custom-validators/activitypub/signature'
7import { isDateValid } from '../../../helpers/custom-validators/misc' 8import { isDateValid } from '../../../helpers/custom-validators/misc'
8import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
9import { areValidationErrors } from '../utils' 10import { areValidationErrors } from '../shared'
10 11
11const signatureValidator = [ 12const signatureValidator = [
12 body('signature.type') 13 body('signature.type')
@@ -14,7 +15,7 @@ const signatureValidator = [
14 .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), 15 .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
15 body('signature.created') 16 body('signature.created')
16 .optional() 17 .optional()
17 .custom(isDateValid).withMessage('Should have a valid signature created date'), 18 .custom(isDateValid).withMessage('Should have a signature created date that conforms to ISO 8601'),
18 body('signature.creator') 19 body('signature.creator')
19 .optional() 20 .optional()
20 .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), 21 .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'),
diff --git a/server/middlewares/validators/actor-image.ts b/server/middlewares/validators/actor-image.ts
index 961d7a7e5..49daadd61 100644
--- a/server/middlewares/validators/actor-image.ts
+++ b/server/middlewares/validators/actor-image.ts
@@ -4,7 +4,7 @@ import { isActorImageFile } from '@server/helpers/custom-validators/actor-images
4import { cleanUpReqFiles } from '../../helpers/express-utils' 4import { cleanUpReqFiles } from '../../helpers/express-utils'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 6import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
7import { areValidationErrors } from './utils' 7import { areValidationErrors } from './shared'
8 8
9const updateActorImageValidatorFactory = (fieldname: string) => ([ 9const updateActorImageValidatorFactory = (fieldname: string) => ([
10 body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage( 10 body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage(
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index f61811a1a..826b16fc8 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -1,15 +1,14 @@
1import { body, param } from 'express-validator'
2import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator'
3import { getServerActor } from '@server/models/application/application'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { isHostValid } from '../../helpers/custom-validators/servers'
3import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 7import { WEBSERVER } from '../../initializers/constants'
5import { AccountBlocklistModel } from '../../models/account/account-blocklist' 8import { AccountBlocklistModel } from '../../models/account/account-blocklist'
6import { isHostValid } from '../../helpers/custom-validators/servers'
7import { ServerBlocklistModel } from '../../models/server/server-blocklist'
8import { ServerModel } from '../../models/server/server' 9import { ServerModel } from '../../models/server/server'
9import { WEBSERVER } from '../../initializers/constants' 10import { ServerBlocklistModel } from '../../models/server/server-blocklist'
10import { doesAccountNameWithHostExist } from '../../helpers/middlewares' 11import { areValidationErrors, doesAccountNameWithHostExist } from './shared'
11import { getServerActor } from '@server/models/application/application'
12import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
13 12
14const blockAccountValidator = [ 13const blockAccountValidator = [
15 body('accountName').exists().withMessage('Should have an account name with host'), 14 body('accountName').exists().withMessage('Should have an account name with host'),
@@ -24,9 +23,10 @@ const blockAccountValidator = [
24 const accountToBlock = res.locals.account 23 const accountToBlock = res.locals.account
25 24
26 if (user.Account.id === accountToBlock.id) { 25 if (user.Account.id === accountToBlock.id) {
27 res.status(HttpStatusCode.CONFLICT_409) 26 res.fail({
28 .json({ error: 'You cannot block yourself.' }) 27 status: HttpStatusCode.CONFLICT_409,
29 28 message: 'You cannot block yourself.'
29 })
30 return 30 return
31 } 31 }
32 32
@@ -79,8 +79,10 @@ const blockServerValidator = [
79 const host: string = req.body.host 79 const host: string = req.body.host
80 80
81 if (host === WEBSERVER.HOST) { 81 if (host === WEBSERVER.HOST) {
82 return res.status(HttpStatusCode.CONFLICT_409) 82 return res.fail({
83 .json({ error: 'You cannot block your own server.' }) 83 status: HttpStatusCode.CONFLICT_409,
84 message: 'You cannot block your own server.'
85 })
84 } 86 }
85 87
86 const server = await ServerModel.loadOrCreateByHost(host) 88 const server = await ServerModel.loadOrCreateByHost(host)
@@ -137,27 +139,27 @@ export {
137async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) { 139async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
138 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) 140 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
139 if (!accountBlock) { 141 if (!accountBlock) {
140 res.status(HttpStatusCode.NOT_FOUND_404) 142 res.fail({
141 .json({ error: 'Account block entry not found.' }) 143 status: HttpStatusCode.NOT_FOUND_404,
142 144 message: 'Account block entry not found.'
145 })
143 return false 146 return false
144 } 147 }
145 148
146 res.locals.accountBlock = accountBlock 149 res.locals.accountBlock = accountBlock
147
148 return true 150 return true
149} 151}
150 152
151async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) { 153async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
152 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) 154 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
153 if (!serverBlock) { 155 if (!serverBlock) {
154 res.status(HttpStatusCode.NOT_FOUND_404) 156 res.fail({
155 .json({ error: 'Server block entry not found.' }) 157 status: HttpStatusCode.NOT_FOUND_404,
156 158 message: 'Server block entry not found.'
159 })
157 return false 160 return false
158 } 161 }
159 162
160 res.locals.serverBlock = serverBlock 163 res.locals.serverBlock = serverBlock
161
162 return true 164 return true
163} 165}
diff --git a/server/middlewares/validators/bulk.ts b/server/middlewares/validators/bulk.ts
index cfb16d352..9bb95f5b7 100644
--- a/server/middlewares/validators/bulk.ts
+++ b/server/middlewares/validators/bulk.ts
@@ -1,12 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk' 3import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk'
4import { doesAccountNameWithHostExist } from '@server/helpers/middlewares' 4import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
5import { UserRight } from '@shared/models' 5import { UserRight } from '@shared/models'
6import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' 6import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
8import { areValidationErrors } from './utils' 8import { areValidationErrors, doesAccountNameWithHostExist } from './shared'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10 9
11const bulkRemoveCommentsOfValidator = [ 10const bulkRemoveCommentsOfValidator = [
12 body('accountName').exists().withMessage('Should have an account name with host'), 11 body('accountName').exists().withMessage('Should have an account name with host'),
@@ -23,9 +22,9 @@ const bulkRemoveCommentsOfValidator = [
23 const body = req.body as BulkRemoveCommentsOfBody 22 const body = req.body as BulkRemoveCommentsOfBody
24 23
25 if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) { 24 if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
26 return res.status(HttpStatusCode.FORBIDDEN_403) 25 return res.fail({
27 .json({ 26 status: HttpStatusCode.FORBIDDEN_403,
28 error: 'User cannot remove any comments of this instance.' 27 message: 'User cannot remove any comments of this instance.'
29 }) 28 })
30 } 29 }
31 30
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index a85883b19..1aeadbe65 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -2,13 +2,12 @@ import * as express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isIntOrNull } from '@server/helpers/custom-validators/misc' 3import { isIntOrNull } from '@server/helpers/custom-validators/misc'
4import { isEmailEnabled } from '@server/initializers/config' 4import { isEmailEnabled } from '@server/initializers/config'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
7import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 6import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
8import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 7import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
9import { logger } from '../../helpers/logger' 8import { logger } from '../../helpers/logger'
10import { isThemeRegistered } from '../../lib/plugins/theme-utils' 9import { isThemeRegistered } from '../../lib/plugins/theme-utils'
11import { areValidationErrors } from './utils' 10import { areValidationErrors } from './shared'
12 11
13const customConfigUpdateValidator = [ 12const customConfigUpdateValidator = [
14 body('instance.name').exists().withMessage('Should have a valid instance name'), 13 body('instance.name').exists().withMessage('Should have a valid instance name'),
@@ -30,6 +29,7 @@ const customConfigUpdateValidator = [
30 body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), 29 body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'),
31 body('signup.limit').isInt().withMessage('Should have a valid signup limit'), 30 body('signup.limit').isInt().withMessage('Should have a valid signup limit'),
32 body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'), 31 body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'),
32 body('signup.minimumAge').isInt().withMessage("Should have a valid minimum age required"),
33 33
34 body('admin.email').isEmail().withMessage('Should have a valid administrator email'), 34 body('admin.email').isEmail().withMessage('Should have a valid administrator email'),
35 body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'), 35 body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'),
@@ -114,9 +114,7 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp
114 if (isEmailEnabled()) return true 114 if (isEmailEnabled()) return true
115 115
116 if (customConfig.signup.requiresEmailVerification === true) { 116 if (customConfig.signup.requiresEmailVerification === true) {
117 res.status(HttpStatusCode.BAD_REQUEST_400) 117 res.fail({ message: 'Emailer is disabled but you require signup email verification.' })
118 .send({ error: 'Emailer is disabled but you require signup email verification.' })
119 .end()
120 return false 118 return false
121 } 119 }
122 120
@@ -127,9 +125,7 @@ function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express
127 if (customConfig.transcoding.enabled === false) return true 125 if (customConfig.transcoding.enabled === false) return true
128 126
129 if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { 127 if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) {
130 res.status(HttpStatusCode.BAD_REQUEST_400) 128 res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' })
131 .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' })
132 .end()
133 return false 129 return false
134 } 130 }
135 131
@@ -140,9 +136,7 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon
140 if (customConfig.live.enabled === false) return true 136 if (customConfig.live.enabled === false) return true
141 137
142 if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) { 138 if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
143 res.status(HttpStatusCode.BAD_REQUEST_400) 139 res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
144 .send({ error: 'You cannot allow live replay if transcoding is not enabled' })
145 .end()
146 return false 140 return false
147 } 141 }
148 142
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index 617661813..51b8fdd19 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -1,18 +1,19 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator' 2import { param, query } from 'express-validator'
3
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' 5import { isValidRSSFeed } from '../../helpers/custom-validators/feeds'
4import { exists, isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 6import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
6import { 8import {
9 areValidationErrors,
7 doesAccountIdExist, 10 doesAccountIdExist,
8 doesAccountNameWithHostExist, 11 doesAccountNameWithHostExist,
9 doesUserFeedTokenCorrespond, 12 doesUserFeedTokenCorrespond,
10 doesVideoChannelIdExist, 13 doesVideoChannelIdExist,
11 doesVideoChannelNameWithHostExist 14 doesVideoChannelNameWithHostExist,
12} from '../../helpers/middlewares' 15 doesVideoExist
13import { doesVideoExist } from '../../helpers/middlewares/videos' 16} from './shared'
14import { areValidationErrors } from './utils'
15import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
16 17
17const feedsFormatValidator = [ 18const feedsFormatValidator = [
18 param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'), 19 param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
@@ -36,10 +37,10 @@ function setFeedFormatContentType (req: express.Request, res: express.Response,
36 if (req.accepts(acceptableContentTypes)) { 37 if (req.accepts(acceptableContentTypes)) {
37 res.set('Content-Type', req.accepts(acceptableContentTypes) as string) 38 res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
38 } else { 39 } else {
39 return res.status(HttpStatusCode.NOT_ACCEPTABLE_406) 40 return res.fail({
40 .json({ 41 status: HttpStatusCode.NOT_ACCEPTABLE_406,
41 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` 42 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
42 }) 43 })
43 } 44 }
44 45
45 return next() 46 return next()
@@ -98,7 +99,10 @@ const videoSubscriptionFeedsValidator = [
98] 99]
99 100
100const videoCommentsFeedsValidator = [ 101const videoCommentsFeedsValidator = [
101 query('videoId').optional().custom(isIdOrUUIDValid), 102 query('videoId')
103 .customSanitizer(toCompleteUUID)
104 .optional()
105 .custom(isIdOrUUIDValid),
102 106
103 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 107 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
104 logger.debug('Checking feeds parameters', { parameters: req.query }) 108 logger.debug('Checking feeds parameters', { parameters: req.query })
@@ -106,10 +110,7 @@ const videoCommentsFeedsValidator = [
106 if (areValidationErrors(req, res)) return 110 if (areValidationErrors(req, res)) return
107 111
108 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { 112 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
109 return res.status(HttpStatusCode.BAD_REQUEST_400) 113 return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
110 .json({
111 message: 'videoId cannot be mixed with a channel filter'
112 })
113 } 114 }
114 115
115 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return 116 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index bb849dc72..205baca48 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -1,18 +1,18 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
4import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors'
5import { getServerActor } from '@server/models/application/application'
6import { MActorFollowActorsDefault } from '@server/types/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { isTestInstance } from '../../helpers/core-utils' 8import { isTestInstance } from '../../helpers/core-utils'
9import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
4import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' 10import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
5import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
6import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' 12import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
7import { ActorFollowModel } from '../../models/activitypub/actor-follow' 13import { ActorModel } from '../../models/actor/actor'
8import { areValidationErrors } from './utils' 14import { ActorFollowModel } from '../../models/actor/actor-follow'
9import { ActorModel } from '../../models/activitypub/actor' 15import { areValidationErrors } from './shared'
10import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
11import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
12import { MActorFollowActorsDefault } from '@server/types/models'
13import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
14import { getServerActor } from '@server/models/application/application'
15import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
16 16
17const listFollowsValidator = [ 17const listFollowsValidator = [
18 query('state') 18 query('state')
@@ -63,11 +63,10 @@ const removeFollowingValidator = [
63 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host) 63 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
64 64
65 if (!follow) { 65 if (!follow) {
66 return res 66 return res.fail({
67 .status(HttpStatusCode.NOT_FOUND_404) 67 status: HttpStatusCode.NOT_FOUND_404,
68 .json({ 68 message: `Following ${req.params.host} not found.`
69 error: `Following ${req.params.host} not found.` 69 })
70 })
71 } 70 }
72 71
73 res.locals.follow = follow 72 res.locals.follow = follow
@@ -95,12 +94,10 @@ const getFollowerValidator = [
95 } 94 }
96 95
97 if (!follow) { 96 if (!follow) {
98 return res 97 return res.fail({
99 .status(HttpStatusCode.NOT_FOUND_404) 98 status: HttpStatusCode.NOT_FOUND_404,
100 .json({ 99 message: `Follower ${req.params.nameWithHost} not found.`
101 error: `Follower ${req.params.nameWithHost} not found.` 100 })
102 })
103 .end()
104 } 101 }
105 102
106 res.locals.follow = follow 103 res.locals.follow = follow
@@ -114,12 +111,7 @@ const acceptOrRejectFollowerValidator = [
114 111
115 const follow = res.locals.follow 112 const follow = res.locals.follow
116 if (follow.state !== 'pending') { 113 if (follow.state !== 'pending') {
117 return res 114 return res.fail({ message: 'Follow is not in pending state.' })
118 .status(HttpStatusCode.BAD_REQUEST_400)
119 .json({
120 error: 'Follow is not in pending state.'
121 })
122 .end()
123 } 115 }
124 116
125 return next() 117 return next()
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 24faeea3e..94a3c2dea 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -11,7 +11,7 @@ export * from './sort'
11export * from './users' 11export * from './users'
12export * from './user-subscriptions' 12export * from './user-subscriptions'
13export * from './videos' 13export * from './videos'
14export * from './webfinger'
15export * from './search' 14export * from './search'
16export * from './server' 15export * from './server'
17export * from './user-history' 16export * from './user-history'
17export * from './webfinger'
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts
index d87b28c06..5d89d167f 100644
--- a/server/middlewares/validators/jobs.ts
+++ b/server/middlewares/validators/jobs.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { param, query } from 'express-validator' 2import { param, query } from 'express-validator'
3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' 3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs'
4import { logger, loggerTagsFactory } from '../../helpers/logger' 4import { logger, loggerTagsFactory } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './shared'
6 6
7const lTags = loggerTagsFactory('validators', 'jobs') 7const lTags = loggerTagsFactory('validators', 'jobs')
8 8
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts
index 70e4d0d99..c55baaee3 100644
--- a/server/middlewares/validators/logs.ts
+++ b/server/middlewares/validators/logs.ts
@@ -1,19 +1,19 @@
1import * as express from 'express' 1import * as express from 'express'
2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils'
4import { isDateValid } from '../../helpers/custom-validators/misc'
5import { query } from 'express-validator' 2import { query } from 'express-validator'
6import { isValidLogLevel } from '../../helpers/custom-validators/logs' 3import { isValidLogLevel } from '../../helpers/custom-validators/logs'
4import { isDateValid } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger'
6import { areValidationErrors } from './shared'
7 7
8const getLogsValidator = [ 8const getLogsValidator = [
9 query('startDate') 9 query('startDate')
10 .custom(isDateValid).withMessage('Should have a valid start date'), 10 .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
11 query('level') 11 query('level')
12 .optional() 12 .optional()
13 .custom(isValidLogLevel).withMessage('Should have a valid level'), 13 .custom(isValidLogLevel).withMessage('Should have a valid level'),
14 query('endDate') 14 query('endDate')
15 .optional() 15 .optional()
16 .custom(isDateValid).withMessage('Should have a valid end date'), 16 .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),
17 17
18 (req: express.Request, res: express.Response, next: express.NextFunction) => { 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking getLogsValidator parameters.', { parameters: req.query }) 19 logger.debug('Checking getLogsValidator parameters.', { parameters: req.query })
@@ -26,10 +26,10 @@ const getLogsValidator = [
26 26
27const getAuditLogsValidator = [ 27const getAuditLogsValidator = [
28 query('startDate') 28 query('startDate')
29 .custom(isDateValid).withMessage('Should have a valid start date'), 29 .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
30 query('endDate') 30 query('endDate')
31 .optional() 31 .optional()
32 .custom(isDateValid).withMessage('Should have a valid end date'), 32 .custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
33 33
34 (req: express.Request, res: express.Response, next: express.NextFunction) => { 34 (req: express.Request, res: express.Response, next: express.NextFunction) => {
35 logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query }) 35 logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query })
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts
index 2a7dc257b..0a82e6932 100644
--- a/server/middlewares/validators/oembed.ts
+++ b/server/middlewares/validators/oembed.ts
@@ -1,18 +1,32 @@
1import * as express from 'express' 1import * as express from 'express'
2import { query } from 'express-validator' 2import { query } from 'express-validator'
3import { join } from 'path' 3import { join } from 'path'
4import { fetchVideo } from '@server/helpers/video' 4import { loadVideo } from '@server/lib/model-loaders'
5import { VideoPlaylistModel } from '@server/models/video/video-playlist' 5import { VideoPlaylistModel } from '@server/models/video/video-playlist'
6import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' 6import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { isTestInstance } from '../../helpers/core-utils' 8import { isTestInstance } from '../../helpers/core-utils'
8import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 9import { isIdOrUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc'
9import { logger } from '../../helpers/logger' 10import { logger } from '../../helpers/logger'
10import { WEBSERVER } from '../../initializers/constants' 11import { WEBSERVER } from '../../initializers/constants'
11import { areValidationErrors } from './utils' 12import { areValidationErrors } from './shared'
12import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 13
14const playlistPaths = [
15 join('videos', 'watch', 'playlist'),
16 join('w', 'p')
17]
18
19const videoPaths = [
20 join('videos', 'watch'),
21 'w'
22]
23
24function buildUrls (paths: string[]) {
25 return paths.map(p => WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, p) + '/')
26}
13 27
14const startVideoPlaylistsURL = WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, 'videos', 'watch', 'playlist') + '/' 28const startPlaylistURLs = buildUrls(playlistPaths)
15const startVideosURL = WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, 'videos', 'watch') + '/' 29const startVideoURLs = buildUrls(videoPaths)
16 30
17const watchRegex = /([^/]+)$/ 31const watchRegex = /([^/]+)$/
18const isURLOptions = { 32const isURLOptions = {
@@ -37,41 +51,54 @@ const oembedValidator = [
37 if (areValidationErrors(req, res)) return 51 if (areValidationErrors(req, res)) return
38 52
39 if (req.query.format !== undefined && req.query.format !== 'json') { 53 if (req.query.format !== undefined && req.query.format !== 'json') {
40 return res.status(HttpStatusCode.NOT_IMPLEMENTED_501) 54 return res.fail({
41 .json({ error: 'Requested format is not implemented on server.' }) 55 status: HttpStatusCode.NOT_IMPLEMENTED_501,
56 message: 'Requested format is not implemented on server.',
57 data: {
58 format: req.query.format
59 }
60 })
42 } 61 }
43 62
44 const url = req.query.url as string 63 const url = req.query.url as string
45 64
46 const isPlaylist = url.startsWith(startVideoPlaylistsURL) 65 const isPlaylist = startPlaylistURLs.some(u => url.startsWith(u))
47 const isVideo = isPlaylist ? false : url.startsWith(startVideosURL) 66 const isVideo = isPlaylist ? false : startVideoURLs.some(u => url.startsWith(u))
48 67
49 const startIsOk = isVideo || isPlaylist 68 const startIsOk = isVideo || isPlaylist
50 69
51 const matches = watchRegex.exec(url) 70 const matches = watchRegex.exec(url)
52 71
53 if (startIsOk === false || matches === null) { 72 if (startIsOk === false || matches === null) {
54 return res.status(HttpStatusCode.BAD_REQUEST_400) 73 return res.fail({
55 .json({ error: 'Invalid url.' }) 74 status: HttpStatusCode.BAD_REQUEST_400,
75 message: 'Invalid url.',
76 data: {
77 url
78 }
79 })
56 } 80 }
57 81
58 const elementId = matches[1] 82 const elementId = toCompleteUUID(matches[1])
59 if (isIdOrUUIDValid(elementId) === false) { 83 if (isIdOrUUIDValid(elementId) === false) {
60 return res.status(HttpStatusCode.BAD_REQUEST_400) 84 return res.fail({ message: 'Invalid video or playlist id.' })
61 .json({ error: 'Invalid video or playlist id.' })
62 } 85 }
63 86
64 if (isVideo) { 87 if (isVideo) {
65 const video = await fetchVideo(elementId, 'all') 88 const video = await loadVideo(elementId, 'all')
66 89
67 if (!video) { 90 if (!video) {
68 return res.status(HttpStatusCode.NOT_FOUND_404) 91 return res.fail({
69 .json({ error: 'Video not found' }) 92 status: HttpStatusCode.NOT_FOUND_404,
93 message: 'Video not found'
94 })
70 } 95 }
71 96
72 if (video.privacy !== VideoPrivacy.PUBLIC) { 97 if (video.privacy !== VideoPrivacy.PUBLIC) {
73 return res.status(HttpStatusCode.FORBIDDEN_403) 98 return res.fail({
74 .json({ error: 'Video is not public' }) 99 status: HttpStatusCode.FORBIDDEN_403,
100 message: 'Video is not public'
101 })
75 } 102 }
76 103
77 res.locals.videoAll = video 104 res.locals.videoAll = video
@@ -82,13 +109,17 @@ const oembedValidator = [
82 109
83 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined) 110 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
84 if (!videoPlaylist) { 111 if (!videoPlaylist) {
85 return res.status(HttpStatusCode.NOT_FOUND_404) 112 return res.fail({
86 .json({ error: 'Video playlist not found' }) 113 status: HttpStatusCode.NOT_FOUND_404,
114 message: 'Video playlist not found'
115 })
87 } 116 }
88 117
89 if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) { 118 if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) {
90 return res.status(HttpStatusCode.FORBIDDEN_403) 119 return res.fail({
91 .json({ error: 'Playlist is not public' }) 120 status: HttpStatusCode.FORBIDDEN_403,
121 message: 'Playlist is not public'
122 })
92 } 123 }
93 124
94 res.locals.videoPlaylistSummary = videoPlaylist 125 res.locals.videoPlaylistSummary = videoPlaylist
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts
index 6b0a83d80..74eae251e 100644
--- a/server/middlewares/validators/pagination.ts
+++ b/server/middlewares/validators/pagination.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { query } from 'express-validator' 2import { query } from 'express-validator'
3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils'
5import { PAGINATION } from '@server/initializers/constants' 3import { PAGINATION } from '@server/initializers/constants'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './shared'
6 6
7const paginationValidator = paginationValidatorBuilder() 7const paginationValidator = paginationValidatorBuilder()
8 8
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index ab87fe720..8c76d2e36 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -1,15 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator' 2import { body, param, query, ValidationChain } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { areValidationErrors } from './utils' 4import { PluginType } from '../../../shared/models/plugins/plugin.type'
5import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model'
6import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
5import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' 7import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
8import { logger } from '../../helpers/logger'
9import { CONFIG } from '../../initializers/config'
6import { PluginManager } from '../../lib/plugins/plugin-manager' 10import { PluginManager } from '../../lib/plugins/plugin-manager'
7import { isBooleanValid, isSafePath, toBooleanOrNull, exists, toIntOrNull } from '../../helpers/custom-validators/misc'
8import { PluginModel } from '../../models/server/plugin' 11import { PluginModel } from '../../models/server/plugin'
9import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' 12import { areValidationErrors } from './shared'
10import { PluginType } from '../../../shared/models/plugins/plugin.type'
11import { CONFIG } from '../../initializers/config'
12import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
13 13
14const getPluginValidator = (pluginType: PluginType, withVersion = true) => { 14const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
15 const validators: (ValidationChain | express.Handler)[] = [ 15 const validators: (ValidationChain | express.Handler)[] = [
@@ -31,8 +31,18 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
31 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) 31 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
32 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) 32 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
33 33
34 if (!plugin) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 34 if (!plugin) {
35 if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 35 return res.fail({
36 status: HttpStatusCode.NOT_FOUND_404,
37 message: 'No plugin found named ' + npmName
38 })
39 }
40 if (withVersion && plugin.version !== req.params.pluginVersion) {
41 return res.fail({
42 status: HttpStatusCode.NOT_FOUND_404,
43 message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion
44 })
45 }
36 46
37 res.locals.registeredPlugin = plugin 47 res.locals.registeredPlugin = plugin
38 48
@@ -50,10 +60,20 @@ const getExternalAuthValidator = [
50 if (areValidationErrors(req, res)) return 60 if (areValidationErrors(req, res)) return
51 61
52 const plugin = res.locals.registeredPlugin 62 const plugin = res.locals.registeredPlugin
53 if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 63 if (!plugin.registerHelpers) {
64 return res.fail({
65 status: HttpStatusCode.NOT_FOUND_404,
66 message: 'No registered helpers were found for this plugin'
67 })
68 }
54 69
55 const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) 70 const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
56 if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 71 if (!externalAuth) {
72 return res.fail({
73 status: HttpStatusCode.NOT_FOUND_404,
74 message: 'No external auths were found for this plugin'
75 })
76 }
57 77
58 res.locals.externalAuth = externalAuth 78 res.locals.externalAuth = externalAuth
59 79
@@ -107,8 +127,7 @@ const installOrUpdatePluginValidator = [
107 127
108 const body: InstallOrUpdatePlugin = req.body 128 const body: InstallOrUpdatePlugin = req.body
109 if (!body.path && !body.npmName) { 129 if (!body.path && !body.npmName) {
110 return res.status(HttpStatusCode.BAD_REQUEST_400) 130 return res.fail({ message: 'Should have either a npmName or a path' })
111 .json({ error: 'Should have either a npmName or a path' })
112 } 131 }
113 132
114 return next() 133 return next()
@@ -137,12 +156,13 @@ const existingPluginValidator = [
137 156
138 const plugin = await PluginModel.loadByNpmName(req.params.npmName) 157 const plugin = await PluginModel.loadByNpmName(req.params.npmName)
139 if (!plugin) { 158 if (!plugin) {
140 return res.status(HttpStatusCode.NOT_FOUND_404) 159 return res.fail({
141 .json({ error: 'Plugin not found' }) 160 status: HttpStatusCode.NOT_FOUND_404,
161 message: 'Plugin not found'
162 })
142 } 163 }
143 164
144 res.locals.plugin = plugin 165 res.locals.plugin = plugin
145
146 return next() 166 return next()
147 } 167 }
148] 168]
@@ -177,9 +197,7 @@ const listAvailablePluginsValidator = [
177 if (areValidationErrors(req, res)) return 197 if (areValidationErrors(req, res)) return
178 198
179 if (CONFIG.PLUGINS.INDEX.ENABLED === false) { 199 if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
180 return res.status(HttpStatusCode.BAD_REQUEST_400) 200 return res.fail({ message: 'Plugin index is not enabled' })
181 .json({ error: 'Plugin index is not enabled' })
182 .end()
183 } 201 }
184 202
185 return next() 203 return next()
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts
index c379aebe4..116c8c611 100644
--- a/server/middlewares/validators/redundancy.ts
+++ b/server/middlewares/validators/redundancy.ts
@@ -1,17 +1,25 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 3import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import {
6 exists,
7 isBooleanValid,
8 isIdOrUUIDValid,
9 isIdValid,
10 toBooleanOrNull,
11 toCompleteUUID,
12 toIntOrNull
13} from '../../helpers/custom-validators/misc'
14import { isHostValid } from '../../helpers/custom-validators/servers'
4import { logger } from '../../helpers/logger' 15import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 16import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
7import { isHostValid } from '../../helpers/custom-validators/servers'
8import { ServerModel } from '../../models/server/server' 17import { ServerModel } from '../../models/server/server'
9import { doesVideoExist } from '../../helpers/middlewares' 18import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
10import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies'
11import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
12 19
13const videoFileRedundancyGetValidator = [ 20const videoFileRedundancyGetValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 21 isValidVideoIdParam('videoId'),
22
15 param('resolution') 23 param('resolution')
16 .customSanitizer(toIntOrNull) 24 .customSanitizer(toIntOrNull)
17 .custom(exists).withMessage('Should have a valid resolution'), 25 .custom(exists).withMessage('Should have a valid resolution'),
@@ -35,11 +43,21 @@ const videoFileRedundancyGetValidator = [
35 return f.resolution === paramResolution && (!req.params.fps || paramFPS) 43 return f.resolution === paramResolution && (!req.params.fps || paramFPS)
36 }) 44 })
37 45
38 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video file not found.' }) 46 if (!videoFile) {
47 return res.fail({
48 status: HttpStatusCode.NOT_FOUND_404,
49 message: 'Video file not found.'
50 })
51 }
39 res.locals.videoFile = videoFile 52 res.locals.videoFile = videoFile
40 53
41 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) 54 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
42 if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) 55 if (!videoRedundancy) {
56 return res.fail({
57 status: HttpStatusCode.NOT_FOUND_404,
58 message: 'Video redundancy not found.'
59 })
60 }
43 res.locals.videoRedundancy = videoRedundancy 61 res.locals.videoRedundancy = videoRedundancy
44 62
45 return next() 63 return next()
@@ -47,9 +65,8 @@ const videoFileRedundancyGetValidator = [
47] 65]
48 66
49const videoPlaylistRedundancyGetValidator = [ 67const videoPlaylistRedundancyGetValidator = [
50 param('videoId') 68 isValidVideoIdParam('videoId'),
51 .custom(isIdOrUUIDValid) 69
52 .not().isEmpty().withMessage('Should have a valid video id'),
53 param('streamingPlaylistType') 70 param('streamingPlaylistType')
54 .customSanitizer(toIntOrNull) 71 .customSanitizer(toIntOrNull)
55 .custom(exists).withMessage('Should have a valid streaming playlist type'), 72 .custom(exists).withMessage('Should have a valid streaming playlist type'),
@@ -65,11 +82,21 @@ const videoPlaylistRedundancyGetValidator = [
65 const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above 82 const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above
66 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType) 83 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType)
67 84
68 if (!videoStreamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video playlist not found.' }) 85 if (!videoStreamingPlaylist) {
86 return res.fail({
87 status: HttpStatusCode.NOT_FOUND_404,
88 message: 'Video playlist not found.'
89 })
90 }
69 res.locals.videoStreamingPlaylist = videoStreamingPlaylist 91 res.locals.videoStreamingPlaylist = videoStreamingPlaylist
70 92
71 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) 93 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
72 if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) 94 if (!videoRedundancy) {
95 return res.fail({
96 status: HttpStatusCode.NOT_FOUND_404,
97 message: 'Video redundancy not found.'
98 })
99 }
73 res.locals.videoRedundancy = videoRedundancy 100 res.locals.videoRedundancy = videoRedundancy
74 101
75 return next() 102 return next()
@@ -90,12 +117,10 @@ const updateServerRedundancyValidator = [
90 const server = await ServerModel.loadByHost(req.params.host) 117 const server = await ServerModel.loadByHost(req.params.host)
91 118
92 if (!server) { 119 if (!server) {
93 return res 120 return res.fail({
94 .status(HttpStatusCode.NOT_FOUND_404) 121 status: HttpStatusCode.NOT_FOUND_404,
95 .json({ 122 message: `Server ${req.params.host} not found.`
96 error: `Server ${req.params.host} not found.` 123 })
97 })
98 .end()
99 } 124 }
100 125
101 res.locals.server = server 126 res.locals.server = server
@@ -118,7 +143,8 @@ const listVideoRedundanciesValidator = [
118 143
119const addVideoRedundancyValidator = [ 144const addVideoRedundancyValidator = [
120 body('videoId') 145 body('videoId')
121 .custom(isIdValid) 146 .customSanitizer(toCompleteUUID)
147 .custom(isIdOrUUIDValid)
122 .withMessage('Should have a valid video id'), 148 .withMessage('Should have a valid video id'),
123 149
124 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 150 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -129,19 +155,19 @@ const addVideoRedundancyValidator = [
129 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return 155 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
130 156
131 if (res.locals.onlyVideo.remote === false) { 157 if (res.locals.onlyVideo.remote === false) {
132 return res.status(HttpStatusCode.BAD_REQUEST_400) 158 return res.fail({ message: 'Cannot create a redundancy on a local video' })
133 .json({ error: 'Cannot create a redundancy on a local video' })
134 } 159 }
135 160
136 if (res.locals.onlyVideo.isLive) { 161 if (res.locals.onlyVideo.isLive) {
137 return res.status(HttpStatusCode.BAD_REQUEST_400) 162 return res.fail({ message: 'Cannot create a redundancy of a live video' })
138 .json({ error: 'Cannot create a redundancy of a live video' })
139 } 163 }
140 164
141 const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) 165 const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
142 if (alreadyExists) { 166 if (alreadyExists) {
143 return res.status(HttpStatusCode.CONFLICT_409) 167 return res.fail({
144 .json({ error: 'This video is already duplicated by your instance.' }) 168 status: HttpStatusCode.CONFLICT_409,
169 message: 'This video is already duplicated by your instance.'
170 })
145 } 171 }
146 172
147 return next() 173 return next()
@@ -160,9 +186,10 @@ const removeVideoRedundancyValidator = [
160 186
161 const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) 187 const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10))
162 if (!redundancy) { 188 if (!redundancy) {
163 return res.status(HttpStatusCode.NOT_FOUND_404) 189 return res.fail({
164 .json({ error: 'Video redundancy not found' }) 190 status: HttpStatusCode.NOT_FOUND_404,
165 .end() 191 message: 'Video redundancy not found'
192 })
166 } 193 }
167 194
168 res.locals.videoRedundancy = redundancy 195 res.locals.videoRedundancy = redundancy
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 78213c70d..7bbf81048 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -1,18 +1,26 @@
1import * as express from 'express' 1import * as express from 'express'
2import { areValidationErrors } from './utils'
3import { logger } from '../../helpers/logger'
4import { query } from 'express-validator' 2import { query } from 'express-validator'
5import { isDateValid } from '../../helpers/custom-validators/misc'
6import { isSearchTargetValid } from '@server/helpers/custom-validators/search' 3import { isSearchTargetValid } from '@server/helpers/custom-validators/search'
4import { isDateValid } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger'
6import { areValidationErrors } from './shared'
7 7
8const videosSearchValidator = [ 8const videosSearchValidator = [
9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
10 10
11 query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'), 11 query('startDate')
12 query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'), 12 .optional()
13 .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
14 query('endDate')
15 .optional()
16 .custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
13 17
14 query('originallyPublishedStartDate').optional().custom(isDateValid).withMessage('Should have a valid published start date'), 18 query('originallyPublishedStartDate')
15 query('originallyPublishedEndDate').optional().custom(isDateValid).withMessage('Should have a valid published end date'), 19 .optional()
20 .custom(isDateValid).withMessage('Should have a published start date that conforms to ISO 8601'),
21 query('originallyPublishedEndDate')
22 .optional()
23 .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'),
16 24
17 query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), 25 query('durationMin').optional().isInt().withMessage('Should have a valid min duration'),
18 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), 26 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
@@ -41,11 +49,12 @@ const videoChannelsListSearchValidator = [
41 } 49 }
42] 50]
43 51
44const videoChannelsOwnSearchValidator = [ 52const videoPlaylistsListSearchValidator = [
45 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 53 query('search').not().isEmpty().withMessage('Should have a valid search'),
54 query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
46 55
47 (req: express.Request, res: express.Response, next: express.NextFunction) => { 56 (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 logger.debug('Checking video channels search query', { parameters: req.query }) 57 logger.debug('Checking video playlists search query', { parameters: req.query })
49 58
50 if (areValidationErrors(req, res)) return 59 if (areValidationErrors(req, res)) return
51 60
@@ -58,5 +67,5 @@ const videoChannelsOwnSearchValidator = [
58export { 67export {
59 videosSearchValidator, 68 videosSearchValidator,
60 videoChannelsListSearchValidator, 69 videoChannelsListSearchValidator,
61 videoChannelsOwnSearchValidator 70 videoPlaylistsListSearchValidator
62} 71}
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
index fe6704716..fc7239b25 100644
--- a/server/middlewares/validators/server.ts
+++ b/server/middlewares/validators/server.ts
@@ -1,13 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils'
4import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
5import { ServerModel } from '../../models/server/server'
6import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' 5import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
8import { Redis } from '../../lib/redis' 6import { logger } from '../../helpers/logger'
9import { CONFIG, isEmailEnabled } from '../../initializers/config' 7import { CONFIG, isEmailEnabled } from '../../initializers/config'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { Redis } from '../../lib/redis'
9import { ServerModel } from '../../models/server/server'
10import { areValidationErrors } from './shared'
11 11
12const serverGetValidator = [ 12const serverGetValidator = [
13 body('host').custom(isHostValid).withMessage('Should have a valid host'), 13 body('host').custom(isHostValid).withMessage('Should have a valid host'),
@@ -19,9 +19,10 @@ const serverGetValidator = [
19 19
20 const server = await ServerModel.loadByHost(req.body.host) 20 const server = await ServerModel.loadByHost(req.body.host)
21 if (!server) { 21 if (!server) {
22 return res.status(HttpStatusCode.NOT_FOUND_404) 22 return res.fail({
23 .send({ error: 'Server host not found.' }) 23 status: HttpStatusCode.NOT_FOUND_404,
24 .end() 24 message: 'Server host not found.'
25 })
25 } 26 }
26 27
27 res.locals.server = server 28 res.locals.server = server
@@ -44,26 +45,26 @@ const contactAdministratorValidator = [
44 if (areValidationErrors(req, res)) return 45 if (areValidationErrors(req, res)) return
45 46
46 if (CONFIG.CONTACT_FORM.ENABLED === false) { 47 if (CONFIG.CONTACT_FORM.ENABLED === false) {
47 return res 48 return res.fail({
48 .status(HttpStatusCode.CONFLICT_409) 49 status: HttpStatusCode.CONFLICT_409,
49 .send({ error: 'Contact form is not enabled on this instance.' }) 50 message: 'Contact form is not enabled on this instance.'
50 .end() 51 })
51 } 52 }
52 53
53 if (isEmailEnabled() === false) { 54 if (isEmailEnabled() === false) {
54 return res 55 return res.fail({
55 .status(HttpStatusCode.CONFLICT_409) 56 status: HttpStatusCode.CONFLICT_409,
56 .send({ error: 'Emailer is not enabled on this instance.' }) 57 message: 'Emailer is not enabled on this instance.'
57 .end() 58 })
58 } 59 }
59 60
60 if (await Redis.Instance.doesContactFormIpExist(req.ip)) { 61 if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
61 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) 62 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
62 63
63 return res 64 return res.fail({
64 .status(HttpStatusCode.FORBIDDEN_403) 65 status: HttpStatusCode.FORBIDDEN_403,
65 .send({ error: 'You already sent a contact form recently.' }) 66 message: 'You already sent a contact form recently.'
66 .end() 67 })
67 } 68 }
68 69
69 return next() 70 return next()
diff --git a/server/middlewares/validators/shared/abuses.ts b/server/middlewares/validators/shared/abuses.ts
new file mode 100644
index 000000000..4a20a55fa
--- /dev/null
+++ b/server/middlewares/validators/shared/abuses.ts
@@ -0,0 +1,25 @@
1import { Response } from 'express'
2import { AbuseModel } from '@server/models/abuse/abuse'
3import { HttpStatusCode } from '@shared/core-utils'
4
5async function doesAbuseExist (abuseId: number | string, res: Response) {
6 const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10))
7
8 if (!abuse) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Abuse not found'
12 })
13
14 return false
15 }
16
17 res.locals.abuse = abuse
18 return true
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 doesAbuseExist
25}
diff --git a/server/middlewares/validators/shared/accounts.ts b/server/middlewares/validators/shared/accounts.ts
new file mode 100644
index 000000000..04da15441
--- /dev/null
+++ b/server/middlewares/validators/shared/accounts.ts
@@ -0,0 +1,65 @@
1import { Response } from 'express'
2import { AccountModel } from '@server/models/account/account'
3import { UserModel } from '@server/models/user/user'
4import { MAccountDefault } from '@server/types/models'
5import { HttpStatusCode } from '@shared/core-utils'
6
7function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
8 const promise = AccountModel.load(parseInt(id + '', 10))
9
10 return doesAccountExist(promise, res, sendNotFound)
11}
12
13function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) {
14 const promise = AccountModel.loadLocalByName(name)
15
16 return doesAccountExist(promise, res, sendNotFound)
17}
18
19function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
20 const promise = AccountModel.loadByNameWithHost(nameWithDomain)
21
22 return doesAccountExist(promise, res, sendNotFound)
23}
24
25async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sendNotFound: boolean) {
26 const account = await p
27
28 if (!account) {
29 if (sendNotFound === true) {
30 res.fail({
31 status: HttpStatusCode.NOT_FOUND_404,
32 message: 'Account not found'
33 })
34 }
35 return false
36 }
37
38 res.locals.account = account
39 return true
40}
41
42async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) {
43 const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10))
44
45 if (token !== user.feedToken) {
46 res.fail({
47 status: HttpStatusCode.FORBIDDEN_403,
48 message: 'User and token mismatch'
49 })
50 return false
51 }
52
53 res.locals.user = user
54 return true
55}
56
57// ---------------------------------------------------------------------------
58
59export {
60 doesAccountIdExist,
61 doesLocalAccountNameExist,
62 doesAccountNameWithHostExist,
63 doesAccountExist,
64 doesUserFeedTokenCorrespond
65}
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts
new file mode 100644
index 000000000..fa89d05f2
--- /dev/null
+++ b/server/middlewares/validators/shared/index.ts
@@ -0,0 +1,11 @@
1export * from './abuses'
2export * from './accounts'
3export * from './utils'
4export * from './video-blacklists'
5export * from './video-captions'
6export * from './video-channels'
7export * from './video-comments'
8export * from './video-imports'
9export * from './video-ownerships'
10export * from './video-playlists'
11export * from './videos'
diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/shared/utils.ts
index 4167f6d43..4f08560af 100644
--- a/server/middlewares/validators/utils.ts
+++ b/server/middlewares/validators/shared/utils.ts
@@ -1,15 +1,20 @@
1import * as express from 'express' 1import * as express from 'express'
2import { query, validationResult } from 'express-validator' 2import { param, query, validationResult } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 4import { logger } from '../../../helpers/logger'
5 5
6function areValidationErrors (req: express.Request, res: express.Response) { 6function areValidationErrors (req: express.Request, res: express.Response) {
7 const errors = validationResult(req) 7 const errors = validationResult(req)
8 8
9 if (!errors.isEmpty()) { 9 if (!errors.isEmpty()) {
10 logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) 10 logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
11 res.status(HttpStatusCode.BAD_REQUEST_400) 11 res.fail({
12 .json({ errors: errors.mapped() }) 12 message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '),
13 instance: req.originalUrl,
14 data: {
15 'invalid-params': errors.mapped()
16 }
17 })
13 18
14 return true 19 return true
15 } 20 }
@@ -37,10 +42,24 @@ function createSortableColumns (sortableColumns: string[]) {
37 return sortableColumns.concat(sortableColumnDesc) 42 return sortableColumns.concat(sortableColumnDesc)
38} 43}
39 44
45function isValidVideoIdParam (paramName: string) {
46 return param(paramName)
47 .customSanitizer(toCompleteUUID)
48 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id')
49}
50
51function isValidPlaylistIdParam (paramName: string) {
52 return param(paramName)
53 .customSanitizer(toCompleteUUID)
54 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id')
55}
56
40// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
41 58
42export { 59export {
43 areValidationErrors, 60 areValidationErrors,
44 checkSort, 61 checkSort,
45 createSortableColumns 62 createSortableColumns,
63 isValidVideoIdParam,
64 isValidPlaylistIdParam
46} 65}
diff --git a/server/middlewares/validators/shared/video-blacklists.ts b/server/middlewares/validators/shared/video-blacklists.ts
new file mode 100644
index 000000000..01491c10f
--- /dev/null
+++ b/server/middlewares/validators/shared/video-blacklists.ts
@@ -0,0 +1,24 @@
1import { Response } from 'express'
2import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
3import { HttpStatusCode } from '@shared/core-utils'
4
5async function doesVideoBlacklistExist (videoId: number, res: Response) {
6 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
7
8 if (videoBlacklist === null) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Blacklisted video not found'
12 })
13 return false
14 }
15
16 res.locals.videoBlacklist = videoBlacklist
17 return true
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 doesVideoBlacklistExist
24}
diff --git a/server/middlewares/validators/shared/video-captions.ts b/server/middlewares/validators/shared/video-captions.ts
new file mode 100644
index 000000000..80f6c5a52
--- /dev/null
+++ b/server/middlewares/validators/shared/video-captions.ts
@@ -0,0 +1,25 @@
1import { Response } from 'express'
2import { VideoCaptionModel } from '@server/models/video/video-caption'
3import { MVideoId } from '@server/types/models'
4import { HttpStatusCode } from '@shared/core-utils'
5
6async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
7 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
8
9 if (!videoCaption) {
10 res.fail({
11 status: HttpStatusCode.NOT_FOUND_404,
12 message: 'Video caption not found'
13 })
14 return false
15 }
16
17 res.locals.videoCaption = videoCaption
18 return true
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 doesVideoCaptionExist
25}
diff --git a/server/middlewares/validators/shared/video-channels.ts b/server/middlewares/validators/shared/video-channels.ts
new file mode 100644
index 000000000..fe2e663b7
--- /dev/null
+++ b/server/middlewares/validators/shared/video-channels.ts
@@ -0,0 +1,43 @@
1import * as express from 'express'
2import { VideoChannelModel } from '@server/models/video/video-channel'
3import { MChannelBannerAccountDefault } from '@server/types/models'
4import { HttpStatusCode } from '@shared/core-utils'
5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
8
9 return processVideoChannelExist(videoChannel, res)
10}
11
12async function doesVideoChannelIdExist (id: number, res: express.Response) {
13 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
14
15 return processVideoChannelExist(videoChannel, res)
16}
17
18async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
19 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
20
21 return processVideoChannelExist(videoChannel, res)
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 doesLocalVideoChannelNameExist,
28 doesVideoChannelIdExist,
29 doesVideoChannelNameWithHostExist
30}
31
32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
33 if (!videoChannel) {
34 res.fail({
35 status: HttpStatusCode.NOT_FOUND_404,
36 message: 'Video channel not found'
37 })
38 return false
39 }
40
41 res.locals.videoChannel = videoChannel
42 return true
43}
diff --git a/server/middlewares/validators/shared/video-comments.ts b/server/middlewares/validators/shared/video-comments.ts
new file mode 100644
index 000000000..83ea15c98
--- /dev/null
+++ b/server/middlewares/validators/shared/video-comments.ts
@@ -0,0 +1,73 @@
1import * as express from 'express'
2import { VideoCommentModel } from '@server/models/video/video-comment'
3import { MVideoId } from '@server/types/models'
4import { HttpStatusCode } from '@shared/core-utils'
5
6async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
7 const id = parseInt(idArg + '', 10)
8 const videoComment = await VideoCommentModel.loadById(id)
9
10 if (!videoComment) {
11 res.fail({
12 status: HttpStatusCode.NOT_FOUND_404,
13 message: 'Video comment thread not found'
14 })
15 return false
16 }
17
18 if (videoComment.videoId !== video.id) {
19 res.fail({ message: 'Video comment is not associated to this video.' })
20 return false
21 }
22
23 if (videoComment.inReplyToCommentId !== null) {
24 res.fail({ message: 'Video comment is not a thread.' })
25 return false
26 }
27
28 res.locals.videoCommentThread = videoComment
29 return true
30}
31
32async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
33 const id = parseInt(idArg + '', 10)
34 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
35
36 if (!videoComment) {
37 res.fail({
38 status: HttpStatusCode.NOT_FOUND_404,
39 message: 'Video comment thread not found'
40 })
41 return false
42 }
43
44 if (videoComment.videoId !== video.id) {
45 res.fail({ message: 'Video comment is not associated to this video.' })
46 return false
47 }
48
49 res.locals.videoCommentFull = videoComment
50 return true
51}
52
53async function doesCommentIdExist (idArg: number | string, res: express.Response) {
54 const id = parseInt(idArg + '', 10)
55 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
56
57 if (!videoComment) {
58 res.fail({
59 status: HttpStatusCode.NOT_FOUND_404,
60 message: 'Video comment thread not found'
61 })
62 return false
63 }
64
65 res.locals.videoCommentFull = videoComment
66 return true
67}
68
69export {
70 doesVideoCommentThreadExist,
71 doesVideoCommentExist,
72 doesCommentIdExist
73}
diff --git a/server/middlewares/validators/shared/video-imports.ts b/server/middlewares/validators/shared/video-imports.ts
new file mode 100644
index 000000000..0f984bc17
--- /dev/null
+++ b/server/middlewares/validators/shared/video-imports.ts
@@ -0,0 +1,22 @@
1import * as express from 'express'
2import { VideoImportModel } from '@server/models/video/video-import'
3import { HttpStatusCode } from '@shared/core-utils'
4
5async function doesVideoImportExist (id: number, res: express.Response) {
6 const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
7
8 if (!videoImport) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Video import not found'
12 })
13 return false
14 }
15
16 res.locals.videoImport = videoImport
17 return true
18}
19
20export {
21 doesVideoImportExist
22}
diff --git a/server/middlewares/validators/shared/video-ownerships.ts b/server/middlewares/validators/shared/video-ownerships.ts
new file mode 100644
index 000000000..fc27006ce
--- /dev/null
+++ b/server/middlewares/validators/shared/video-ownerships.ts
@@ -0,0 +1,24 @@
1import * as express from 'express'
2import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
3import { HttpStatusCode } from '@shared/core-utils'
4
5async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) {
6 const id = parseInt(idArg + '', 10)
7 const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
8
9 if (!videoChangeOwnership) {
10 res.fail({
11 status: HttpStatusCode.NOT_FOUND_404,
12 message: 'Video change ownership not found'
13 })
14 return false
15 }
16
17 res.locals.videoChangeOwnership = videoChangeOwnership
18
19 return true
20}
21
22export {
23 doesChangeVideoOwnershipExist
24}
diff --git a/server/middlewares/validators/shared/video-playlists.ts b/server/middlewares/validators/shared/video-playlists.ts
new file mode 100644
index 000000000..d762859a8
--- /dev/null
+++ b/server/middlewares/validators/shared/video-playlists.ts
@@ -0,0 +1,39 @@
1import * as express from 'express'
2import { VideoPlaylistModel } from '@server/models/video/video-playlist'
3import { MVideoPlaylist } from '@server/types/models'
4import { HttpStatusCode } from '@shared/core-utils'
5
6export type VideoPlaylistFetchType = 'summary' | 'all'
7async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
8 if (fetchType === 'summary') {
9 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
10 res.locals.videoPlaylistSummary = videoPlaylist
11
12 return handleVideoPlaylist(videoPlaylist, res)
13 }
14
15 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
16 res.locals.videoPlaylistFull = videoPlaylist
17
18 return handleVideoPlaylist(videoPlaylist, res)
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 doesVideoPlaylistExist
25}
26
27// ---------------------------------------------------------------------------
28
29function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
30 if (!videoPlaylist) {
31 res.fail({
32 status: HttpStatusCode.NOT_FOUND_404,
33 message: 'Video playlist not found'
34 })
35 return false
36 }
37
38 return true
39}
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts
new file mode 100644
index 000000000..2c66c1a3a
--- /dev/null
+++ b/server/middlewares/validators/shared/videos.ts
@@ -0,0 +1,125 @@
1import { Response } from 'express'
2import { loadVideo, VideoLoadType } from '@server/lib/model-loaders'
3import { VideoChannelModel } from '@server/models/video/video-channel'
4import { VideoFileModel } from '@server/models/video/video-file'
5import {
6 MUser,
7 MUserAccountId,
8 MVideoAccountLight,
9 MVideoFormattableDetails,
10 MVideoFullLight,
11 MVideoId,
12 MVideoImmutable,
13 MVideoThumbnail
14} from '@server/types/models'
15import { HttpStatusCode } from '@shared/core-utils'
16import { UserRight } from '@shared/models'
17
18async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
19 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
20
21 const video = await loadVideo(id, fetchType, userId)
22
23 if (video === null) {
24 res.fail({
25 status: HttpStatusCode.NOT_FOUND_404,
26 message: 'Video not found'
27 })
28 return false
29 }
30
31 switch (fetchType) {
32 case 'for-api':
33 res.locals.videoAPI = video as MVideoFormattableDetails
34 break
35
36 case 'all':
37 res.locals.videoAll = video as MVideoFullLight
38 break
39
40 case 'only-immutable-attributes':
41 res.locals.onlyImmutableVideo = video as MVideoImmutable
42 break
43
44 case 'id':
45 res.locals.videoId = video as MVideoId
46 break
47
48 case 'only-video':
49 res.locals.onlyVideo = video as MVideoThumbnail
50 break
51 }
52
53 return true
54}
55
56async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
57 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
58 res.fail({
59 status: HttpStatusCode.NOT_FOUND_404,
60 message: 'VideoFile matching Video not found'
61 })
62 return false
63 }
64
65 return true
66}
67
68async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
69 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
70
71 if (videoChannel === null) {
72 res.fail({ message: 'Unknown video "video channel" for this instance.' })
73 return false
74 }
75
76 // Don't check account id if the user can update any video
77 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
78 res.locals.videoChannel = videoChannel
79 return true
80 }
81
82 if (videoChannel.Account.id !== user.Account.id) {
83 res.fail({
84 message: 'Unknown video "video channel" for this account.'
85 })
86 return false
87 }
88
89 res.locals.videoChannel = videoChannel
90 return true
91}
92
93function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
94 // Retrieve the user who did the request
95 if (onlyOwned && video.isOwned() === false) {
96 res.fail({
97 status: HttpStatusCode.FORBIDDEN_403,
98 message: 'Cannot manage a video of another server.'
99 })
100 return false
101 }
102
103 // Check if the user can delete the video
104 // The user can delete it if he has the right
105 // Or if s/he is the video's account
106 const account = video.VideoChannel.Account
107 if (user.hasRight(right) === false && account.userId !== user.id) {
108 res.fail({
109 status: HttpStatusCode.FORBIDDEN_403,
110 message: 'Cannot manage a video of another user.'
111 })
112 return false
113 }
114
115 return true
116}
117
118// ---------------------------------------------------------------------------
119
120export {
121 doesVideoChannelOfAccountExist,
122 doesVideoExist,
123 doesVideoFileOfVideoExist,
124 checkUserCanManageVideo
125}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index beecc155b..473010460 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -1,5 +1,5 @@
1import { SORTABLE_COLUMNS } from '../../initializers/constants' 1import { SORTABLE_COLUMNS } from '../../initializers/constants'
2import { checkSort, createSortableColumns } from './utils' 2import { checkSort, createSortableColumns } from './shared'
3 3
4// Initialize constants here for better performances 4// Initialize constants here for better performances
5const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) 5const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
@@ -9,6 +9,7 @@ const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES)
9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) 10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) 11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
12const SORTABLE_VIDEO_PLAYLISTS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH)
12const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) 13const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS)
13const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) 14const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
14const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) 15const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
@@ -34,6 +35,7 @@ const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 35const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
35const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) 36const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
36const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) 37const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS)
38const videoPlaylistsSearchSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_SEARCH_COLUMNS)
37const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS) 39const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS)
38const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) 40const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
39const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS) 41const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS)
@@ -75,5 +77,6 @@ export {
75 userNotificationsSortValidator, 77 userNotificationsSortValidator,
76 videoPlaylistsSortValidator, 78 videoPlaylistsSortValidator,
77 videoRedundanciesSortValidator, 79 videoRedundanciesSortValidator,
80 videoPlaylistsSearchSortValidator,
78 pluginsSortValidator 81 pluginsSortValidator
79} 82}
diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts
index a726a567b..d4716257f 100644
--- a/server/middlewares/validators/themes.ts
+++ b/server/middlewares/validators/themes.ts
@@ -1,11 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param } from 'express-validator' 2import { param } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { areValidationErrors } from './utils' 4import { isSafePath } from '../../helpers/custom-validators/misc'
5import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' 5import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
6import { logger } from '../../helpers/logger'
6import { PluginManager } from '../../lib/plugins/plugin-manager' 7import { PluginManager } from '../../lib/plugins/plugin-manager'
7import { isSafePath } from '../../helpers/custom-validators/misc' 8import { areValidationErrors } from './shared'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
9 9
10const serveThemeCSSValidator = [ 10const serveThemeCSSValidator = [
11 param('themeName').custom(isPluginNameValid).withMessage('Should have a valid theme name'), 11 param('themeName').custom(isPluginNameValid).withMessage('Should have a valid theme name'),
@@ -20,11 +20,17 @@ const serveThemeCSSValidator = [
20 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) 20 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
21 21
22 if (!theme || theme.version !== req.params.themeVersion) { 22 if (!theme || theme.version !== req.params.themeVersion) {
23 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 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 })
24 } 27 }
25 28
26 if (theme.css.includes(req.params.staticEndpoint) === false) { 29 if (theme.css.includes(req.params.staticEndpoint) === false) {
27 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 30 return res.fail({
31 status: HttpStatusCode.NOT_FOUND_404,
32 message: 'No static endpoint was found for this theme'
33 })
28 } 34 }
29 35
30 res.locals.registeredPlugin = theme 36 res.locals.registeredPlugin = theme
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts
index 058bf7758..1db0d9b26 100644
--- a/server/middlewares/validators/user-history.ts
+++ b/server/middlewares/validators/user-history.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, query } from 'express-validator' 2import { body, query } from 'express-validator'
3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils'
5import { exists, isDateValid } from '../../helpers/custom-validators/misc' 3import { exists, isDateValid } from '../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './shared'
6 6
7const userHistoryListValidator = [ 7const userHistoryListValidator = [
8 query('search') 8 query('search')
@@ -21,7 +21,7 @@ const userHistoryListValidator = [
21const userHistoryRemoveValidator = [ 21const userHistoryRemoveValidator = [
22 body('beforeDate') 22 body('beforeDate')
23 .optional() 23 .optional()
24 .custom(isDateValid).withMessage('Should have a valid before date'), 24 .custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
25 25
26 (req: express.Request, res: express.Response, next: express.NextFunction) => { 26 (req: express.Request, res: express.Response, next: express.NextFunction) => {
27 logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body }) 27 logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
index 21a7be08d..2f8e7686e 100644
--- a/server/middlewares/validators/user-notifications.ts
+++ b/server/middlewares/validators/user-notifications.ts
@@ -1,9 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, query } from 'express-validator' 2import { body, query } from 'express-validator'
3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils'
5import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
6import { isNotEmptyIntArray, toBooleanOrNull } from '../../helpers/custom-validators/misc' 3import { isNotEmptyIntArray, toBooleanOrNull } from '../../helpers/custom-validators/misc'
4import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
5import { logger } from '../../helpers/logger'
6import { areValidationErrors } from './shared'
7 7
8const listUserNotificationsValidator = [ 8const listUserNotificationsValidator = [
9 query('unread') 9 query('unread')
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts
index 0d0c8ccbf..ab7962923 100644
--- a/server/middlewares/validators/user-subscriptions.ts
+++ b/server/middlewares/validators/user-subscriptions.ts
@@ -1,12 +1,12 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { areValidationErrors } from './utils'
5import { ActorFollowModel } from '../../models/activitypub/actor-follow'
6import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' 4import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
7import { toArray } from '../../helpers/custom-validators/misc' 5import { toArray } from '../../helpers/custom-validators/misc'
6import { logger } from '../../helpers/logger'
8import { WEBSERVER } from '../../initializers/constants' 7import { WEBSERVER } from '../../initializers/constants'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { ActorFollowModel } from '../../models/actor/actor-follow'
9import { areValidationErrors } from './shared'
10 10
11const userSubscriptionListValidator = [ 11const userSubscriptionListValidator = [
12 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 12 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
@@ -61,11 +61,10 @@ const userSubscriptionGetValidator = [
61 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) 61 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host)
62 62
63 if (!subscription || !subscription.ActorFollowing.VideoChannel) { 63 if (!subscription || !subscription.ActorFollowing.VideoChannel) {
64 return res 64 return res.fail({
65 .status(HttpStatusCode.NOT_FOUND_404) 65 status: HttpStatusCode.NOT_FOUND_404,
66 .json({ 66 message: `Subscription ${req.params.uri} not found.`
67 error: `Subscription ${req.params.uri} not found.` 67 })
68 })
69 } 68 }
70 69
71 res.locals.subscription = subscription 70 res.locals.subscription = subscription
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 37119e279..698d7d814 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -7,7 +7,7 @@ import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-code
7import { UserRole } from '../../../shared/models/users' 7import { UserRole } from '../../../shared/models/users'
8import { UserRegister } from '../../../shared/models/users/user-register.model' 8import { UserRegister } from '../../../shared/models/users/user-register.model'
9import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' 9import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
10import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 10import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
11import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 11import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
12import { 12import {
13 isNoInstanceConfigWarningModal, 13 isNoInstanceConfigWarningModal,
@@ -30,13 +30,12 @@ import {
30} from '../../helpers/custom-validators/users' 30} from '../../helpers/custom-validators/users'
31import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' 31import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
32import { logger } from '../../helpers/logger' 32import { logger } from '../../helpers/logger'
33import { doesVideoExist } from '../../helpers/middlewares'
34import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
35import { isThemeRegistered } from '../../lib/plugins/theme-utils' 33import { isThemeRegistered } from '../../lib/plugins/theme-utils'
36import { Redis } from '../../lib/redis' 34import { Redis } from '../../lib/redis'
37import { UserModel } from '../../models/account/user' 35import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
38import { ActorModel } from '../../models/activitypub/actor' 36import { ActorModel } from '../../models/actor/actor'
39import { areValidationErrors } from './utils' 37import { UserModel } from '../../models/user/user'
38import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
40 39
41const usersListValidator = [ 40const usersListValidator = [
42 query('blocked') 41 query('blocked')
@@ -73,23 +72,23 @@ const usersAddValidator = [
73 72
74 const authUser = res.locals.oauth.token.User 73 const authUser = res.locals.oauth.token.User
75 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { 74 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
76 return res 75 return res.fail({
77 .status(HttpStatusCode.FORBIDDEN_403) 76 status: HttpStatusCode.FORBIDDEN_403,
78 .json({ error: 'You can only create users (and not administrators or moderators)' }) 77 message: 'You can only create users (and not administrators or moderators)'
78 })
79 } 79 }
80 80
81 if (req.body.channelName) { 81 if (req.body.channelName) {
82 if (req.body.channelName === req.body.username) { 82 if (req.body.channelName === req.body.username) {
83 return res 83 return res.fail({ message: 'Channel name cannot be the same as user username.' })
84 .status(HttpStatusCode.BAD_REQUEST_400)
85 .json({ error: 'Channel name cannot be the same as user username.' })
86 } 84 }
87 85
88 const existing = await ActorModel.loadLocalByName(req.body.channelName) 86 const existing = await ActorModel.loadLocalByName(req.body.channelName)
89 if (existing) { 87 if (existing) {
90 return res 88 return res.fail({
91 .status(HttpStatusCode.CONFLICT_409) 89 status: HttpStatusCode.CONFLICT_409,
92 .json({ error: `Channel with name ${req.body.channelName} already exists.` }) 90 message: `Channel with name ${req.body.channelName} already exists.`
91 })
93 } 92 }
94 } 93 }
95 94
@@ -121,20 +120,19 @@ const usersRegisterValidator = [
121 const body: UserRegister = req.body 120 const body: UserRegister = req.body
122 if (body.channel) { 121 if (body.channel) {
123 if (!body.channel.name || !body.channel.displayName) { 122 if (!body.channel.name || !body.channel.displayName) {
124 return res 123 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
125 .status(HttpStatusCode.BAD_REQUEST_400)
126 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
127 } 124 }
128 125
129 if (body.channel.name === body.username) { 126 if (body.channel.name === body.username) {
130 return res.status(HttpStatusCode.BAD_REQUEST_400) 127 return res.fail({ message: 'Channel name cannot be the same as user username.' })
131 .json({ error: 'Channel name cannot be the same as user username.' })
132 } 128 }
133 129
134 const existing = await ActorModel.loadLocalByName(body.channel.name) 130 const existing = await ActorModel.loadLocalByName(body.channel.name)
135 if (existing) { 131 if (existing) {
136 return res.status(HttpStatusCode.CONFLICT_409) 132 return res.fail({
137 .json({ error: `Channel with name ${body.channel.name} already exists.` }) 133 status: HttpStatusCode.CONFLICT_409,
134 message: `Channel with name ${body.channel.name} already exists.`
135 })
138 } 136 }
139 } 137 }
140 138
@@ -153,8 +151,7 @@ const usersRemoveValidator = [
153 151
154 const user = res.locals.user 152 const user = res.locals.user
155 if (user.username === 'root') { 153 if (user.username === 'root') {
156 return res.status(HttpStatusCode.BAD_REQUEST_400) 154 return res.fail({ message: 'Cannot remove the root user' })
157 .json({ error: 'Cannot remove the root user' })
158 } 155 }
159 156
160 return next() 157 return next()
@@ -173,8 +170,7 @@ const usersBlockingValidator = [
173 170
174 const user = res.locals.user 171 const user = res.locals.user
175 if (user.username === 'root') { 172 if (user.username === 'root') {
176 return res.status(HttpStatusCode.BAD_REQUEST_400) 173 return res.fail({ message: 'Cannot block the root user' })
177 .json({ error: 'Cannot block the root user' })
178 } 174 }
179 175
180 return next() 176 return next()
@@ -185,9 +181,7 @@ const deleteMeValidator = [
185 (req: express.Request, res: express.Response, next: express.NextFunction) => { 181 (req: express.Request, res: express.Response, next: express.NextFunction) => {
186 const user = res.locals.oauth.token.User 182 const user = res.locals.oauth.token.User
187 if (user.username === 'root') { 183 if (user.username === 'root') {
188 return res.status(HttpStatusCode.BAD_REQUEST_400) 184 return res.fail({ message: 'You cannot delete your root account.' })
189 .json({ error: 'You cannot delete your root account.' })
190 .end()
191 } 185 }
192 186
193 return next() 187 return next()
@@ -217,8 +211,7 @@ const usersUpdateValidator = [
217 211
218 const user = res.locals.user 212 const user = res.locals.user
219 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { 213 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
220 return res.status(HttpStatusCode.BAD_REQUEST_400) 214 return res.fail({ message: 'Cannot change root role.' })
221 .json({ error: 'Cannot change root role.' })
222 } 215 }
223 216
224 return next() 217 return next()
@@ -273,18 +266,18 @@ const usersUpdateMeValidator = [
273 266
274 if (req.body.password || req.body.email) { 267 if (req.body.password || req.body.email) {
275 if (user.pluginAuth !== null) { 268 if (user.pluginAuth !== null) {
276 return res.status(HttpStatusCode.BAD_REQUEST_400) 269 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
277 .json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
278 } 270 }
279 271
280 if (!req.body.currentPassword) { 272 if (!req.body.currentPassword) {
281 return res.status(HttpStatusCode.BAD_REQUEST_400) 273 return res.fail({ message: 'currentPassword parameter is missing.' })
282 .json({ error: 'currentPassword parameter is missing.' })
283 } 274 }
284 275
285 if (await user.isPasswordMatch(req.body.currentPassword) !== true) { 276 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
286 return res.status(HttpStatusCode.UNAUTHORIZED_401) 277 return res.fail({
287 .json({ error: 'currentPassword is invalid.' }) 278 status: HttpStatusCode.UNAUTHORIZED_401,
279 message: 'currentPassword is invalid.'
280 })
288 } 281 }
289 } 282 }
290 283
@@ -309,7 +302,7 @@ const usersGetValidator = [
309] 302]
310 303
311const usersVideoRatingValidator = [ 304const usersVideoRatingValidator = [
312 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 305 isValidVideoIdParam('videoId'),
313 306
314 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 307 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
315 logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) 308 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
@@ -335,8 +328,10 @@ const ensureUserRegistrationAllowed = [
335 ) 328 )
336 329
337 if (allowedResult.allowed === false) { 330 if (allowedResult.allowed === false) {
338 return res.status(HttpStatusCode.FORBIDDEN_403) 331 return res.fail({
339 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' }) 332 status: HttpStatusCode.FORBIDDEN_403,
333 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
334 })
340 } 335 }
341 336
342 return next() 337 return next()
@@ -348,8 +343,10 @@ const ensureUserRegistrationAllowedForIP = [
348 const allowed = isSignupAllowedForCurrentIP(req.ip) 343 const allowed = isSignupAllowedForCurrentIP(req.ip)
349 344
350 if (allowed === false) { 345 if (allowed === false) {
351 return res.status(HttpStatusCode.FORBIDDEN_403) 346 return res.fail({
352 .json({ error: 'You are not on a network authorized for registration.' }) 347 status: HttpStatusCode.FORBIDDEN_403,
348 message: 'You are not on a network authorized for registration.'
349 })
353 } 350 }
354 351
355 return next() 352 return next()
@@ -390,9 +387,10 @@ const usersResetPasswordValidator = [
390 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) 387 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
391 388
392 if (redisVerificationString !== req.body.verificationString) { 389 if (redisVerificationString !== req.body.verificationString) {
393 return res 390 return res.fail({
394 .status(HttpStatusCode.FORBIDDEN_403) 391 status: HttpStatusCode.FORBIDDEN_403,
395 .json({ error: 'Invalid verification string.' }) 392 message: 'Invalid verification string.'
393 })
396 } 394 }
397 395
398 return next() 396 return next()
@@ -437,9 +435,10 @@ const usersVerifyEmailValidator = [
437 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) 435 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
438 436
439 if (redisVerificationString !== req.body.verificationString) { 437 if (redisVerificationString !== req.body.verificationString) {
440 return res 438 return res.fail({
441 .status(HttpStatusCode.FORBIDDEN_403) 439 status: HttpStatusCode.FORBIDDEN_403,
442 .json({ error: 'Invalid verification string.' }) 440 message: 'Invalid verification string.'
441 })
443 } 442 }
444 443
445 return next() 444 return next()
@@ -455,8 +454,10 @@ const ensureAuthUserOwnsAccountValidator = [
455 const user = res.locals.oauth.token.User 454 const user = res.locals.oauth.token.User
456 455
457 if (res.locals.account.id !== user.Account.id) { 456 if (res.locals.account.id !== user.Account.id) {
458 return res.status(HttpStatusCode.FORBIDDEN_403) 457 return res.fail({
459 .json({ error: 'Only owner can access ratings list.' }) 458 status: HttpStatusCode.FORBIDDEN_403,
459 message: 'Only owner can access ratings list.'
460 })
460 } 461 }
461 462
462 return next() 463 return next()
@@ -471,8 +472,10 @@ const ensureCanManageUser = [
471 if (authUser.role === UserRole.ADMINISTRATOR) return next() 472 if (authUser.role === UserRole.ADMINISTRATOR) return next()
472 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() 473 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
473 474
474 return res.status(HttpStatusCode.FORBIDDEN_403) 475 return res.fail({
475 .json({ error: 'A moderator can only manager users.' }) 476 status: HttpStatusCode.FORBIDDEN_403,
477 message: 'A moderator can only manager users.'
478 })
476 } 479 }
477] 480]
478 481
@@ -515,15 +518,19 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
515 const user = await UserModel.loadByUsernameOrEmail(username, email) 518 const user = await UserModel.loadByUsernameOrEmail(username, email)
516 519
517 if (user) { 520 if (user) {
518 res.status(HttpStatusCode.CONFLICT_409) 521 res.fail({
519 .json({ error: 'User with this username or email already exists.' }) 522 status: HttpStatusCode.CONFLICT_409,
523 message: 'User with this username or email already exists.'
524 })
520 return false 525 return false
521 } 526 }
522 527
523 const actor = await ActorModel.loadLocalByName(username) 528 const actor = await ActorModel.loadLocalByName(username)
524 if (actor) { 529 if (actor) {
525 res.status(HttpStatusCode.CONFLICT_409) 530 res.fail({
526 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) 531 status: HttpStatusCode.CONFLICT_409,
532 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
533 })
527 return false 534 return false
528 } 535 }
529 536
@@ -535,14 +542,15 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express
535 542
536 if (!user) { 543 if (!user) {
537 if (abortResponse === true) { 544 if (abortResponse === true) {
538 res.status(HttpStatusCode.NOT_FOUND_404) 545 res.fail({
539 .json({ error: 'User not found' }) 546 status: HttpStatusCode.NOT_FOUND_404,
547 message: 'User not found'
548 })
540 } 549 }
541 550
542 return false 551 return false
543 } 552 }
544 553
545 res.locals.user = user 554 res.locals.user = user
546
547 return true 555 return true
548} 556}
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index 1eabada0a..369c2c9b6 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -3,6 +3,8 @@ export * from './video-captions'
3export * from './video-channels' 3export * from './video-channels'
4export * from './video-comments' 4export * from './video-comments'
5export * from './video-imports' 5export * from './video-imports'
6export * from './video-live'
7export * from './video-ownership-changes'
6export * from './video-watch' 8export * from './video-watch'
7export * from './video-rates' 9export * from './video-rates'
8export * from './video-shares' 10export * from './video-shares'
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 88c788a43..21141d84d 100644
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,14 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, query } from 'express-validator'
3import { isBooleanValid, isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' 3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
4import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
4import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist' 5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist'
5import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
6import { doesVideoBlacklistExist, doesVideoExist } from '../../../helpers/middlewares' 7import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared'
7import { areValidationErrors } from '../utils'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
9 8
10const videosBlacklistRemoveValidator = [ 9const videosBlacklistRemoveValidator = [
11 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 10 isValidVideoIdParam('videoId'),
12 11
13 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 12 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) 13 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
@@ -22,7 +21,8 @@ const videosBlacklistRemoveValidator = [
22] 21]
23 22
24const videosBlacklistAddValidator = [ 23const videosBlacklistAddValidator = [
25 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 24 isValidVideoIdParam('videoId'),
25
26 body('unfederate') 26 body('unfederate')
27 .optional() 27 .optional()
28 .customSanitizer(toBooleanOrNull) 28 .customSanitizer(toBooleanOrNull)
@@ -39,10 +39,10 @@ const videosBlacklistAddValidator = [
39 39
40 const video = res.locals.videoAll 40 const video = res.locals.videoAll
41 if (req.body.unfederate === true && video.remote === true) { 41 if (req.body.unfederate === true && video.remote === true) {
42 return res 42 return res.fail({
43 .status(HttpStatusCode.CONFLICT_409) 43 status: HttpStatusCode.CONFLICT_409,
44 .send({ error: 'You cannot unfederate a remote video.' }) 44 message: 'You cannot unfederate a remote video.'
45 .end() 45 })
46 } 46 }
47 47
48 return next() 48 return next()
@@ -50,7 +50,8 @@ const videosBlacklistAddValidator = [
50] 50]
51 51
52const videosBlacklistUpdateValidator = [ 52const videosBlacklistUpdateValidator = [
53 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 53 isValidVideoIdParam('videoId'),
54
54 body('reason') 55 body('reason')
55 .optional() 56 .optional()
56 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), 57 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 872d9c2ab..2946f3e15 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -1,17 +1,18 @@
1import * as express from 'express' 1import * as express from 'express'
2import { areValidationErrors } from '../utils'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { body, param } from 'express-validator' 2import { body, param } from 'express-validator'
5import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
6import { UserRight } from '../../../../shared' 3import { UserRight } from '../../../../shared'
7import { logger } from '../../../helpers/logger'
8import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' 4import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
9import { cleanUpReqFiles } from '../../../helpers/express-utils' 5import { cleanUpReqFiles } from '../../../helpers/express-utils'
10import { checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist } from '../../../helpers/middlewares' 6import { logger } from '../../../helpers/logger'
7import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
8import { areValidationErrors, checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist, isValidVideoIdParam } from '../shared'
11 9
12const addVideoCaptionValidator = [ 10const addVideoCaptionValidator = [
13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 11 isValidVideoIdParam('videoId'),
14 param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), 12
13 param('captionLanguage')
14 .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'),
15
15 body('captionfile') 16 body('captionfile')
16 .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile')) 17 .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile'))
17 .withMessage( 18 .withMessage(
@@ -35,8 +36,10 @@ const addVideoCaptionValidator = [
35] 36]
36 37
37const deleteVideoCaptionValidator = [ 38const deleteVideoCaptionValidator = [
38 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 39 isValidVideoIdParam('videoId'),
39 param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), 40
41 param('captionLanguage')
42 .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'),
40 43
41 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 44 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params }) 45 logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params })
@@ -54,7 +57,7 @@ const deleteVideoCaptionValidator = [
54] 57]
55 58
56const listVideoCaptionsValidator = [ 59const listVideoCaptionsValidator = [
57 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 60 isValidVideoIdParam('videoId'),
58 61
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 62 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 logger.debug('Checking listVideoCaptions parameters', { parameters: req.params }) 63 logger.debug('Checking listVideoCaptions parameters', { parameters: req.params })
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 2463d281c..e7df185e4 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -3,6 +3,7 @@ import { body, param, query } from 'express-validator'
3import { VIDEO_CHANNELS } from '@server/initializers/constants' 3import { VIDEO_CHANNELS } from '@server/initializers/constants'
4import { MChannelAccountDefault, MUser } from '@server/types/models' 4import { MChannelAccountDefault, MUser } from '@server/types/models'
5import { UserRight } from '../../../../shared' 5import { UserRight } from '../../../../shared'
6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
6import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' 7import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
7import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' 8import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
8import { 9import {
@@ -11,11 +12,9 @@ import {
11 isVideoChannelSupportValid 12 isVideoChannelSupportValid
12} from '../../../helpers/custom-validators/video-channels' 13} from '../../../helpers/custom-validators/video-channels'
13import { logger } from '../../../helpers/logger' 14import { logger } from '../../../helpers/logger'
14import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares' 15import { ActorModel } from '../../../models/actor/actor'
15import { ActorModel } from '../../../models/activitypub/actor'
16import { VideoChannelModel } from '../../../models/video/video-channel' 16import { VideoChannelModel } from '../../../models/video/video-channel'
17import { areValidationErrors } from '../utils' 17import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared'
18import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
19 18
20const videoChannelsAddValidator = [ 19const videoChannelsAddValidator = [
21 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), 20 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
@@ -30,17 +29,16 @@ const videoChannelsAddValidator = [
30 29
31 const actor = await ActorModel.loadLocalByName(req.body.name) 30 const actor = await ActorModel.loadLocalByName(req.body.name)
32 if (actor) { 31 if (actor) {
33 res.status(HttpStatusCode.CONFLICT_409) 32 res.fail({
34 .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) 33 status: HttpStatusCode.CONFLICT_409,
35 .end() 34 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
35 })
36 return false 36 return false
37 } 37 }
38 38
39 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) 39 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
40 if (count >= VIDEO_CHANNELS.MAX_PER_USER) { 40 if (count >= VIDEO_CHANNELS.MAX_PER_USER) {
41 res.status(HttpStatusCode.BAD_REQUEST_400) 41 res.fail({ message: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` })
42 .send({ error: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` })
43 .end()
44 return false 42 return false
45 } 43 }
46 44
@@ -71,13 +69,17 @@ const videoChannelsUpdateValidator = [
71 69
72 // We need to make additional checks 70 // We need to make additional checks
73 if (res.locals.videoChannel.Actor.isOwned() === false) { 71 if (res.locals.videoChannel.Actor.isOwned() === false) {
74 return res.status(HttpStatusCode.FORBIDDEN_403) 72 return res.fail({
75 .json({ error: 'Cannot update video channel of another server' }) 73 status: HttpStatusCode.FORBIDDEN_403,
74 message: 'Cannot update video channel of another server'
75 })
76 } 76 }
77 77
78 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { 78 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
79 return res.status(HttpStatusCode.FORBIDDEN_403) 79 return res.fail({
80 .json({ error: 'Cannot update video channel of another user' }) 80 status: HttpStatusCode.FORBIDDEN_403,
81 message: 'Cannot update video channel of another user'
82 })
81 } 83 }
82 84
83 return next() 85 return next()
@@ -139,6 +141,18 @@ const videoChannelStatsValidator = [
139 } 141 }
140] 142]
141 143
144const videoChannelsListValidator = [
145 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
146
147 (req: express.Request, res: express.Response, next: express.NextFunction) => {
148 logger.debug('Checking video channels search query', { parameters: req.query })
149
150 if (areValidationErrors(req, res)) return
151
152 return next()
153 }
154]
155
142// --------------------------------------------------------------------------- 156// ---------------------------------------------------------------------------
143 157
144export { 158export {
@@ -146,6 +160,7 @@ export {
146 videoChannelsUpdateValidator, 160 videoChannelsUpdateValidator,
147 videoChannelsRemoveValidator, 161 videoChannelsRemoveValidator,
148 videoChannelsNameWithHostValidator, 162 videoChannelsNameWithHostValidator,
163 videoChannelsListValidator,
149 localVideoChannelValidator, 164 localVideoChannelValidator,
150 videoChannelStatsValidator 165 videoChannelStatsValidator
151} 166}
@@ -154,10 +169,10 @@ export {
154 169
155function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { 170function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
156 if (videoChannel.Actor.isOwned() === false) { 171 if (videoChannel.Actor.isOwned() === false) {
157 res.status(HttpStatusCode.FORBIDDEN_403) 172 res.fail({
158 .json({ error: 'Cannot remove video channel of another server.' }) 173 status: HttpStatusCode.FORBIDDEN_403,
159 .end() 174 message: 'Cannot remove video channel of another server.'
160 175 })
161 return false 176 return false
162 } 177 }
163 178
@@ -165,10 +180,10 @@ function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAcco
165 // The user can delete it if s/he is an admin 180 // The user can delete it if s/he is an admin
166 // Or if s/he is the video channel's account 181 // Or if s/he is the video channel's account
167 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { 182 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) {
168 res.status(HttpStatusCode.FORBIDDEN_403) 183 res.fail({
169 .json({ error: 'Cannot remove video channel of another user' }) 184 status: HttpStatusCode.FORBIDDEN_403,
170 .end() 185 message: 'Cannot remove video channel of another user'
171 186 })
172 return false 187 return false
173 } 188 }
174 189
@@ -179,10 +194,10 @@ async function checkVideoChannelIsNotTheLastOne (res: express.Response) {
179 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) 194 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
180 195
181 if (count <= 1) { 196 if (count <= 1) {
182 res.status(HttpStatusCode.CONFLICT_409) 197 res.fail({
183 .json({ error: 'Cannot remove the last channel of this user' }) 198 status: HttpStatusCode.CONFLICT_409,
184 .end() 199 message: 'Cannot remove the last channel of this user'
185 200 })
186 return false 201 return false
187 } 202 }
188 203
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 1afacfed8..885506ebe 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -2,19 +2,14 @@ import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { MUserAccountUrl } from '@server/types/models' 3import { MUserAccountUrl } from '@server/types/models'
4import { UserRight } from '../../../../shared' 4import { UserRight } from '../../../../shared'
5import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' 5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
6import { 6import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
7 doesVideoCommentExist, 7import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
8 doesVideoCommentThreadExist,
9 isValidVideoCommentText
10} from '../../../helpers/custom-validators/video-comments'
11import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
12import { doesVideoExist } from '../../../helpers/middlewares'
13import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' 9import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
14import { Hooks } from '../../../lib/plugins/hooks' 10import { Hooks } from '../../../lib/plugins/hooks'
15import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' 11import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
16import { areValidationErrors } from '../utils' 12import { areValidationErrors, doesVideoCommentExist, doesVideoCommentThreadExist, doesVideoExist, isValidVideoIdParam } from '../shared'
17import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
18 13
19const listVideoCommentsValidator = [ 14const listVideoCommentsValidator = [
20 query('isLocal') 15 query('isLocal')
@@ -45,7 +40,7 @@ const listVideoCommentsValidator = [
45] 40]
46 41
47const listVideoCommentThreadsValidator = [ 42const listVideoCommentThreadsValidator = [
48 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 43 isValidVideoIdParam('videoId'),
49 44
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 45 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params }) 46 logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
@@ -58,8 +53,10 @@ const listVideoCommentThreadsValidator = [
58] 53]
59 54
60const listVideoThreadCommentsValidator = [ 55const listVideoThreadCommentsValidator = [
61 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 56 isValidVideoIdParam('videoId'),
62 param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'), 57
58 param('threadId')
59 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'),
63 60
64 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 61 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
65 logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params }) 62 logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params })
@@ -73,8 +70,10 @@ const listVideoThreadCommentsValidator = [
73] 70]
74 71
75const addVideoCommentThreadValidator = [ 72const addVideoCommentThreadValidator = [
76 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 73 isValidVideoIdParam('videoId'),
77 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), 74
75 body('text')
76 .custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
78 77
79 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 78 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
80 logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body }) 79 logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body })
@@ -89,8 +88,10 @@ const addVideoCommentThreadValidator = [
89] 88]
90 89
91const addVideoCommentReplyValidator = [ 90const addVideoCommentReplyValidator = [
92 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 91 isValidVideoIdParam('videoId'),
92
93 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), 93 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
94
94 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), 95 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
95 96
96 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 97 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -107,8 +108,10 @@ const addVideoCommentReplyValidator = [
107] 108]
108 109
109const videoCommentGetValidator = [ 110const videoCommentGetValidator = [
110 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 111 isValidVideoIdParam('videoId'),
111 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), 112
113 param('commentId')
114 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
112 115
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 116 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) 117 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
@@ -122,7 +125,8 @@ const videoCommentGetValidator = [
122] 125]
123 126
124const removeVideoCommentValidator = [ 127const removeVideoCommentValidator = [
125 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 128 isValidVideoIdParam('videoId'),
129
126 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), 130 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
127 131
128 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 132 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -155,9 +159,10 @@ export {
155 159
156function isVideoCommentsEnabled (video: MVideo, res: express.Response) { 160function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
157 if (video.commentsEnabled !== true) { 161 if (video.commentsEnabled !== true) {
158 res.status(HttpStatusCode.CONFLICT_409) 162 res.fail({
159 .json({ error: 'Video comments are disabled for this video.' }) 163 status: HttpStatusCode.CONFLICT_409,
160 164 message: 'Video comments are disabled for this video.'
165 })
161 return false 166 return false
162 } 167 }
163 168
@@ -166,9 +171,10 @@ function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
166 171
167function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) { 172function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
168 if (videoComment.isDeleted()) { 173 if (videoComment.isDeleted()) {
169 res.status(HttpStatusCode.CONFLICT_409) 174 res.fail({
170 .json({ error: 'This comment is already deleted' }) 175 status: HttpStatusCode.CONFLICT_409,
171 176 message: 'This comment is already deleted'
177 })
172 return false 178 return false
173 } 179 }
174 180
@@ -179,9 +185,10 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC
179 videoComment.accountId !== userAccount.id && // Not the comment owner 185 videoComment.accountId !== userAccount.id && // Not the comment owner
180 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner 186 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
181 ) { 187 ) {
182 res.status(HttpStatusCode.FORBIDDEN_403) 188 res.fail({
183 .json({ error: 'Cannot remove video comment of another user' }) 189 status: HttpStatusCode.FORBIDDEN_403,
184 190 message: 'Cannot remove video comment of another user'
191 })
185 return false 192 return false
186 } 193 }
187 194
@@ -215,9 +222,11 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
215 222
216 if (!acceptedResult || acceptedResult.accepted !== true) { 223 if (!acceptedResult || acceptedResult.accepted !== true) {
217 logger.info('Refused local comment.', { acceptedResult, acceptParameters }) 224 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
218 res.status(HttpStatusCode.FORBIDDEN_403)
219 .json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
220 225
226 res.fail({
227 status: HttpStatusCode.FORBIDDEN_403,
228 message: acceptedResult?.errorMessage || 'Refused local comment'
229 })
221 return false 230 return false
222 } 231 }
223 232
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index c53af3861..85dc647ce 100644
--- a/server/middlewares/validators/videos/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -2,18 +2,17 @@ import * as express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isPreImportVideoAccepted } from '@server/lib/moderation' 3import { isPreImportVideoAccepted } from '@server/lib/moderation'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
5import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' 6import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
6import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' 7import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' 8import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
8import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' 9import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos'
9import { cleanUpReqFiles } from '../../../helpers/express-utils' 10import { cleanUpReqFiles } from '../../../helpers/express-utils'
10import { logger } from '../../../helpers/logger' 11import { logger } from '../../../helpers/logger'
11import { doesVideoChannelOfAccountExist } from '../../../helpers/middlewares'
12import { CONFIG } from '../../../initializers/config' 12import { CONFIG } from '../../../initializers/config'
13import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 13import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
14import { areValidationErrors } from '../utils' 14import { areValidationErrors, doesVideoChannelOfAccountExist } from '../shared'
15import { getCommonVideoEditAttributes } from './videos' 15import { getCommonVideoEditAttributes } from './videos'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17 16
18const videoImportAddValidator = getCommonVideoEditAttributes().concat([ 17const videoImportAddValidator = getCommonVideoEditAttributes().concat([
19 body('channelId') 18 body('channelId')
@@ -33,7 +32,9 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
33 ), 32 ),
34 body('name') 33 body('name')
35 .optional() 34 .optional()
36 .custom(isVideoNameValid).withMessage('Should have a valid name'), 35 .custom(isVideoNameValid).withMessage(
36 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
37 ),
37 38
38 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 39 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
39 logger.debug('Checking videoImportAddValidator parameters', { parameters: req.body }) 40 logger.debug('Checking videoImportAddValidator parameters', { parameters: req.body })
@@ -45,16 +46,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
45 46
46 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { 47 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
47 cleanUpReqFiles(req) 48 cleanUpReqFiles(req)
48 return res.status(HttpStatusCode.CONFLICT_409) 49
49 .json({ error: 'HTTP import is not enabled on this instance.' }) 50 return res.fail({
50 .end() 51 status: HttpStatusCode.CONFLICT_409,
52 message: 'HTTP import is not enabled on this instance.'
53 })
51 } 54 }
52 55
53 if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { 56 if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
54 cleanUpReqFiles(req) 57 cleanUpReqFiles(req)
55 return res.status(HttpStatusCode.CONFLICT_409) 58
56 .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) 59 return res.fail({
57 .end() 60 status: HttpStatusCode.CONFLICT_409,
61 message: 'Torrent/magnet URI import is not enabled on this instance.'
62 })
58 } 63 }
59 64
60 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 65 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -63,9 +68,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
63 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { 68 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
64 cleanUpReqFiles(req) 69 cleanUpReqFiles(req)
65 70
66 return res.status(HttpStatusCode.BAD_REQUEST_400) 71 return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' })
67 .json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
68 .end()
69 } 72 }
70 73
71 if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req) 74 if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
@@ -101,9 +104,11 @@ async function isImportAccepted (req: express.Request, res: express.Response) {
101 104
102 if (!acceptedResult || acceptedResult.accepted !== true) { 105 if (!acceptedResult || acceptedResult.accepted !== true) {
103 logger.info('Refused to import video.', { acceptedResult, acceptParameters }) 106 logger.info('Refused to import video.', { acceptedResult, acceptParameters })
104 res.status(HttpStatusCode.FORBIDDEN_403)
105 .json({ error: acceptedResult.errorMessage || 'Refused to import video' })
106 107
108 res.fail({
109 status: HttpStatusCode.FORBIDDEN_403,
110 message: acceptedResult.errorMessage || 'Refused to import video'
111 })
107 return false 112 return false
108 } 113 }
109 114
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts
index 3a73e1272..7cfb935e3 100644
--- a/server/middlewares/validators/videos/video-live.ts
+++ b/server/middlewares/validators/videos/video-live.ts
@@ -1,22 +1,28 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator' 2import { body } from 'express-validator'
3import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '@server/helpers/middlewares/videos' 3import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
4import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
5import { Hooks } from '@server/lib/plugins/hooks'
6import { VideoModel } from '@server/models/video/video'
4import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
5import { ServerErrorCode, UserRight, VideoState } from '@shared/models' 9import { ServerErrorCode, UserRight, VideoState } from '@shared/models'
6import { isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' 10import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
7import { isVideoNameValid } from '../../../helpers/custom-validators/videos' 11import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
8import { cleanUpReqFiles } from '../../../helpers/express-utils' 12import { cleanUpReqFiles } from '../../../helpers/express-utils'
9import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
10import { CONFIG } from '../../../initializers/config' 14import { CONFIG } from '../../../initializers/config'
11import { areValidationErrors } from '../utils' 15import {
16 areValidationErrors,
17 checkUserCanManageVideo,
18 doesVideoChannelOfAccountExist,
19 doesVideoExist,
20 isValidVideoIdParam
21} from '../shared'
12import { getCommonVideoEditAttributes } from './videos' 22import { getCommonVideoEditAttributes } from './videos'
13import { VideoModel } from '@server/models/video/video'
14import { Hooks } from '@server/lib/plugins/hooks'
15import { isLocalLiveVideoAccepted } from '@server/lib/moderation'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17 23
18const videoLiveGetValidator = [ 24const videoLiveGetValidator = [
19 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 25 isValidVideoIdParam('videoId'),
20 26
21 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 27 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
22 logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username }) 28 logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username })
@@ -29,7 +35,12 @@ const videoLiveGetValidator = [
29 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return 35 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return
30 36
31 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) 37 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
32 if (!videoLive) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 38 if (!videoLive) {
39 return res.fail({
40 status: HttpStatusCode.NOT_FOUND_404,
41 message: 'Live video not found'
42 })
43 }
33 44
34 res.locals.videoLive = videoLive 45 res.locals.videoLive = videoLive
35 46
@@ -43,7 +54,9 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
43 .custom(isIdValid).withMessage('Should have correct video channel id'), 54 .custom(isIdValid).withMessage('Should have correct video channel id'),
44 55
45 body('name') 56 body('name')
46 .custom(isVideoNameValid).withMessage('Should have a valid name'), 57 .custom(isVideoNameValid).withMessage(
58 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
59 ),
47 60
48 body('saveReplay') 61 body('saveReplay')
49 .optional() 62 .optional()
@@ -63,22 +76,27 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
63 if (CONFIG.LIVE.ENABLED !== true) { 76 if (CONFIG.LIVE.ENABLED !== true) {
64 cleanUpReqFiles(req) 77 cleanUpReqFiles(req)
65 78
66 return res.status(HttpStatusCode.FORBIDDEN_403) 79 return res.fail({
67 .json({ error: 'Live is not enabled on this instance' }) 80 status: HttpStatusCode.FORBIDDEN_403,
81 message: 'Live is not enabled on this instance',
82 type: ServerErrorCode.LIVE_NOT_ENABLED
83 })
68 } 84 }
69 85
70 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { 86 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
71 cleanUpReqFiles(req) 87 cleanUpReqFiles(req)
72 88
73 return res.status(HttpStatusCode.FORBIDDEN_403) 89 return res.fail({
74 .json({ error: 'Saving live replay is not allowed instance' }) 90 status: HttpStatusCode.FORBIDDEN_403,
91 message: 'Saving live replay is not enabled on this instance',
92 type: ServerErrorCode.LIVE_NOT_ALLOWING_REPLAY
93 })
75 } 94 }
76 95
77 if (req.body.permanentLive && req.body.saveReplay) { 96 if (req.body.permanentLive && req.body.saveReplay) {
78 cleanUpReqFiles(req) 97 cleanUpReqFiles(req)
79 98
80 return res.status(HttpStatusCode.BAD_REQUEST_400) 99 return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
81 .json({ error: 'Cannot set this live as permanent while saving its replay' })
82 } 100 }
83 101
84 const user = res.locals.oauth.token.User 102 const user = res.locals.oauth.token.User
@@ -90,11 +108,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
90 if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { 108 if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
91 cleanUpReqFiles(req) 109 cleanUpReqFiles(req)
92 110
93 return res.status(HttpStatusCode.FORBIDDEN_403) 111 return res.fail({
94 .json({ 112 status: HttpStatusCode.FORBIDDEN_403,
95 code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED, 113 message: 'Cannot create this live because the max instance lives limit is reached.',
96 error: 'Cannot create this live because the max instance lives limit is reached.' 114 type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED
97 }) 115 })
98 } 116 }
99 } 117 }
100 118
@@ -104,11 +122,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
104 if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) { 122 if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
105 cleanUpReqFiles(req) 123 cleanUpReqFiles(req)
106 124
107 return res.status(HttpStatusCode.FORBIDDEN_403) 125 return res.fail({
108 .json({ 126 status: HttpStatusCode.FORBIDDEN_403,
109 code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED, 127 message: 'Cannot create this live because the max user lives limit is reached.',
110 error: 'Cannot create this live because the max user lives limit is reached.' 128 type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED
111 }) 129 })
112 } 130 }
113 } 131 }
114 132
@@ -130,18 +148,18 @@ const videoLiveUpdateValidator = [
130 if (areValidationErrors(req, res)) return 148 if (areValidationErrors(req, res)) return
131 149
132 if (req.body.permanentLive && req.body.saveReplay) { 150 if (req.body.permanentLive && req.body.saveReplay) {
133 return res.status(HttpStatusCode.BAD_REQUEST_400) 151 return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
134 .json({ error: 'Cannot set this live as permanent while saving its replay' })
135 } 152 }
136 153
137 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { 154 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
138 return res.status(HttpStatusCode.FORBIDDEN_403) 155 return res.fail({
139 .json({ error: 'Saving live replay is not allowed instance' }) 156 status: HttpStatusCode.FORBIDDEN_403,
157 message: 'Saving live replay is not allowed instance'
158 })
140 } 159 }
141 160
142 if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { 161 if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
143 return res.status(HttpStatusCode.BAD_REQUEST_400) 162 return res.fail({ message: 'Cannot update a live that has already started' })
144 .json({ error: 'Cannot update a live that has already started' })
145 } 163 }
146 164
147 // Check the user can manage the live 165 // Check the user can manage the live
@@ -177,9 +195,10 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response)
177 if (!acceptedResult || acceptedResult.accepted !== true) { 195 if (!acceptedResult || acceptedResult.accepted !== true) {
178 logger.info('Refused local live video.', { acceptedResult, acceptParameters }) 196 logger.info('Refused local live video.', { acceptedResult, acceptParameters })
179 197
180 res.status(HttpStatusCode.FORBIDDEN_403) 198 res.fail({
181 .json({ error: acceptedResult.errorMessage || 'Refused local live video' }) 199 status: HttpStatusCode.FORBIDDEN_403,
182 200 message: acceptedResult.errorMessage || 'Refused local live video'
201 })
183 return false 202 return false
184 } 203 }
185 204
diff --git a/server/middlewares/validators/videos/video-ownership-changes.ts b/server/middlewares/validators/videos/video-ownership-changes.ts
new file mode 100644
index 000000000..54ac46c99
--- /dev/null
+++ b/server/middlewares/validators/videos/video-ownership-changes.ts
@@ -0,0 +1,121 @@
1import * as express from 'express'
2import { param } from 'express-validator'
3import { isIdValid } from '@server/helpers/custom-validators/misc'
4import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership'
5import { logger } from '@server/helpers/logger'
6import { isAbleToUploadVideo } from '@server/lib/user'
7import { AccountModel } from '@server/models/account/account'
8import { MVideoWithAllFiles } from '@server/types/models'
9import { HttpStatusCode } from '@shared/core-utils'
10import { ServerErrorCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models'
11import {
12 areValidationErrors,
13 checkUserCanManageVideo,
14 doesChangeVideoOwnershipExist,
15 doesVideoChannelOfAccountExist,
16 doesVideoExist,
17 isValidVideoIdParam
18} from '../shared'
19
20const videosChangeOwnershipValidator = [
21 isValidVideoIdParam('videoId'),
22
23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
24 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
25
26 if (areValidationErrors(req, res)) return
27 if (!await doesVideoExist(req.params.videoId, res)) return
28
29 // Check if the user who did the request is able to change the ownership of the video
30 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
31
32 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
33 if (!nextOwner) {
34 res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
35 return
36 }
37
38 res.locals.nextOwner = nextOwner
39 return next()
40 }
41]
42
43const videosTerminateChangeOwnershipValidator = [
44 param('id')
45 .custom(isIdValid).withMessage('Should have a valid id'),
46
47 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
49
50 if (areValidationErrors(req, res)) return
51 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
52
53 // Check if the user who did the request is able to change the ownership of the video
54 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
55
56 const videoChangeOwnership = res.locals.videoChangeOwnership
57
58 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
59 res.fail({
60 status: HttpStatusCode.FORBIDDEN_403,
61 message: 'Ownership already accepted or refused'
62 })
63 return
64 }
65
66 return next()
67 }
68]
69
70const videosAcceptChangeOwnershipValidator = [
71 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
72 const body = req.body as VideoChangeOwnershipAccept
73 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
74
75 const videoChangeOwnership = res.locals.videoChangeOwnership
76
77 const video = videoChangeOwnership.Video
78
79 if (!await checkCanAccept(video, res)) return
80
81 return next()
82 }
83]
84
85export {
86 videosChangeOwnershipValidator,
87 videosTerminateChangeOwnershipValidator,
88 videosAcceptChangeOwnershipValidator
89}
90
91// ---------------------------------------------------------------------------
92
93async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response): Promise<boolean> {
94 if (video.isLive) {
95
96 if (video.state !== VideoState.WAITING_FOR_LIVE) {
97 res.fail({
98 status: HttpStatusCode.BAD_REQUEST_400,
99 message: 'You can accept an ownership change of a published live.'
100 })
101
102 return false
103 }
104
105 return true
106 }
107
108 const user = res.locals.oauth.token.User
109
110 if (!await isAbleToUploadVideo(user.id, video.getMaxQualityFile().size)) {
111 res.fail({
112 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
113 message: 'The user video quota is exceeded with this video.',
114 type: ServerErrorCode.QUOTA_REACHED
115 })
116
117 return false
118 }
119
120 return true
121}
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index c872d045e..5ee7ee0ce 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -11,6 +11,7 @@ import {
11 isIdOrUUIDValid, 11 isIdOrUUIDValid,
12 isIdValid, 12 isIdValid,
13 isUUIDValid, 13 isUUIDValid,
14 toCompleteUUID,
14 toIntArray, 15 toIntArray,
15 toIntOrNull, 16 toIntOrNull,
16 toValueOrNull 17 toValueOrNull
@@ -25,12 +26,18 @@ import {
25import { isVideoImage } from '../../../helpers/custom-validators/videos' 26import { isVideoImage } from '../../../helpers/custom-validators/videos'
26import { cleanUpReqFiles } from '../../../helpers/express-utils' 27import { cleanUpReqFiles } from '../../../helpers/express-utils'
27import { logger } from '../../../helpers/logger' 28import { logger } from '../../../helpers/logger'
28import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../../../helpers/middlewares'
29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' 30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
31import { MVideoPlaylist } from '../../../types/models/video/video-playlist' 31import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
32import { authenticatePromiseIfNeeded } from '../../auth' 32import { authenticatePromiseIfNeeded } from '../../auth'
33import { areValidationErrors } from '../utils' 33import {
34 areValidationErrors,
35 doesVideoChannelIdExist,
36 doesVideoExist,
37 doesVideoPlaylistExist,
38 isValidPlaylistIdParam,
39 VideoPlaylistFetchType
40} from '../shared'
34 41
35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 42const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
36 body('displayName') 43 body('displayName')
@@ -44,10 +51,13 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
44 const body: VideoPlaylistCreate = req.body 51 const body: VideoPlaylistCreate = req.body
45 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) 52 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
46 53
47 if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) { 54 if (
55 !body.videoChannelId &&
56 (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
57 ) {
48 cleanUpReqFiles(req) 58 cleanUpReqFiles(req)
49 return res.status(HttpStatusCode.BAD_REQUEST_400) 59
50 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) 60 return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
51 } 61 }
52 62
53 return next() 63 return next()
@@ -55,8 +65,7 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
55]) 65])
56 66
57const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ 67const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
58 param('playlistId') 68 isValidPlaylistIdParam('playlistId'),
59 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
60 69
61 body('displayName') 70 body('displayName')
62 .optional() 71 .optional()
@@ -85,14 +94,14 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
85 ) 94 )
86 ) { 95 ) {
87 cleanUpReqFiles(req) 96 cleanUpReqFiles(req)
88 return res.status(HttpStatusCode.BAD_REQUEST_400) 97
89 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) 98 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
90 } 99 }
91 100
92 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { 101 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
93 cleanUpReqFiles(req) 102 cleanUpReqFiles(req)
94 return res.status(HttpStatusCode.BAD_REQUEST_400) 103
95 .json({ error: 'Cannot update a watch later playlist.' }) 104 return res.fail({ message: 'Cannot update a watch later playlist.' })
96 } 105 }
97 106
98 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) 107 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
@@ -102,8 +111,7 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
102]) 111])
103 112
104const videoPlaylistsDeleteValidator = [ 113const videoPlaylistsDeleteValidator = [
105 param('playlistId') 114 isValidPlaylistIdParam('playlistId'),
106 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
107 115
108 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 116 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
109 logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params }) 117 logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params })
@@ -114,8 +122,7 @@ const videoPlaylistsDeleteValidator = [
114 122
115 const videoPlaylist = getPlaylist(res) 123 const videoPlaylist = getPlaylist(res)
116 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { 124 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
117 return res.status(HttpStatusCode.BAD_REQUEST_400) 125 return res.fail({ message: 'Cannot delete a watch later playlist.' })
118 .json({ error: 'Cannot delete a watch later playlist.' })
119 } 126 }
120 127
121 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { 128 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
@@ -128,8 +135,7 @@ const videoPlaylistsDeleteValidator = [
128 135
129const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { 136const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
130 return [ 137 return [
131 param('playlistId') 138 isValidPlaylistIdParam('playlistId'),
132 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
133 139
134 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 140 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
135 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params }) 141 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
@@ -144,7 +150,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
144 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { 150 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
145 if (isUUIDValid(req.params.playlistId)) return next() 151 if (isUUIDValid(req.params.playlistId)) return next()
146 152
147 return res.status(HttpStatusCode.NOT_FOUND_404).end() 153 return res.fail({
154 status: HttpStatusCode.NOT_FOUND_404,
155 message: 'Playlist not found'
156 })
148 } 157 }
149 158
150 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { 159 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
@@ -156,8 +165,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
156 !user || 165 !user ||
157 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST)) 166 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
158 ) { 167 ) {
159 return res.status(HttpStatusCode.FORBIDDEN_403) 168 return res.fail({
160 .json({ error: 'Cannot get this private video playlist.' }) 169 status: HttpStatusCode.FORBIDDEN_403,
170 message: 'Cannot get this private video playlist.'
171 })
161 } 172 }
162 173
163 return next() 174 return next()
@@ -181,9 +192,10 @@ const videoPlaylistsSearchValidator = [
181] 192]
182 193
183const videoPlaylistsAddVideoValidator = [ 194const videoPlaylistsAddVideoValidator = [
184 param('playlistId') 195 isValidPlaylistIdParam('playlistId'),
185 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), 196
186 body('videoId') 197 body('videoId')
198 .customSanitizer(toCompleteUUID)
187 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'), 199 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
188 body('startTimestamp') 200 body('startTimestamp')
189 .optional() 201 .optional()
@@ -211,9 +223,9 @@ const videoPlaylistsAddVideoValidator = [
211] 223]
212 224
213const videoPlaylistsUpdateOrRemoveVideoValidator = [ 225const videoPlaylistsUpdateOrRemoveVideoValidator = [
214 param('playlistId') 226 isValidPlaylistIdParam('playlistId'),
215 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
216 param('playlistElementId') 227 param('playlistElementId')
228 .customSanitizer(toCompleteUUID)
217 .custom(isIdValid).withMessage('Should have an element id/uuid'), 229 .custom(isIdValid).withMessage('Should have an element id/uuid'),
218 body('startTimestamp') 230 body('startTimestamp')
219 .optional() 231 .optional()
@@ -233,10 +245,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
233 245
234 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) 246 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
235 if (!videoPlaylistElement) { 247 if (!videoPlaylistElement) {
236 res.status(HttpStatusCode.NOT_FOUND_404) 248 res.fail({
237 .json({ error: 'Video playlist element not found' }) 249 status: HttpStatusCode.NOT_FOUND_404,
238 .end() 250 message: 'Video playlist element not found'
239 251 })
240 return 252 return
241 } 253 }
242 res.locals.videoPlaylistElement = videoPlaylistElement 254 res.locals.videoPlaylistElement = videoPlaylistElement
@@ -248,8 +260,7 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
248] 260]
249 261
250const videoPlaylistElementAPGetValidator = [ 262const videoPlaylistElementAPGetValidator = [
251 param('playlistId') 263 isValidPlaylistIdParam('playlistId'),
252 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
253 param('playlistElementId') 264 param('playlistElementId')
254 .custom(isIdValid).withMessage('Should have an playlist element id'), 265 .custom(isIdValid).withMessage('Should have an playlist element id'),
255 266
@@ -263,15 +274,18 @@ const videoPlaylistElementAPGetValidator = [
263 274
264 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) 275 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
265 if (!videoPlaylistElement) { 276 if (!videoPlaylistElement) {
266 res.status(HttpStatusCode.NOT_FOUND_404) 277 res.fail({
267 .json({ error: 'Video playlist element not found' }) 278 status: HttpStatusCode.NOT_FOUND_404,
268 .end() 279 message: 'Video playlist element not found'
269 280 })
270 return 281 return
271 } 282 }
272 283
273 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { 284 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
274 return res.status(HttpStatusCode.FORBIDDEN_403).end() 285 return res.fail({
286 status: HttpStatusCode.FORBIDDEN_403,
287 message: 'Cannot get this private video playlist.'
288 })
275 } 289 }
276 290
277 res.locals.videoPlaylistElementAP = videoPlaylistElement 291 res.locals.videoPlaylistElementAP = videoPlaylistElement
@@ -281,8 +295,7 @@ const videoPlaylistElementAPGetValidator = [
281] 295]
282 296
283const videoPlaylistsReorderVideosValidator = [ 297const videoPlaylistsReorderVideosValidator = [
284 param('playlistId') 298 isValidPlaylistIdParam('playlistId'),
285 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
286 body('startPosition') 299 body('startPosition')
287 .isInt({ min: 1 }).withMessage('Should have a valid start position'), 300 .isInt({ min: 1 }).withMessage('Should have a valid start position'),
288 body('insertAfterPosition') 301 body('insertAfterPosition')
@@ -307,18 +320,12 @@ const videoPlaylistsReorderVideosValidator = [
307 const reorderLength: number = req.body.reorderLength 320 const reorderLength: number = req.body.reorderLength
308 321
309 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) { 322 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
310 res.status(HttpStatusCode.BAD_REQUEST_400) 323 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
311 .json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
312 .end()
313
314 return 324 return
315 } 325 }
316 326
317 if (reorderLength && reorderLength + startPosition > nextPosition) { 327 if (reorderLength && reorderLength + startPosition > nextPosition) {
318 res.status(HttpStatusCode.BAD_REQUEST_400) 328 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
319 .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
320 .end()
321
322 return 329 return
323 } 330 }
324 331
@@ -401,10 +408,10 @@ function getCommonPlaylistEditAttributes () {
401 408
402function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) { 409function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
403 if (videoPlaylist.isOwned() === false) { 410 if (videoPlaylist.isOwned() === false) {
404 res.status(HttpStatusCode.FORBIDDEN_403) 411 res.fail({
405 .json({ error: 'Cannot manage video playlist of another server.' }) 412 status: HttpStatusCode.FORBIDDEN_403,
406 .end() 413 message: 'Cannot manage video playlist of another server.'
407 414 })
408 return false 415 return false
409 } 416 }
410 417
@@ -412,10 +419,10 @@ function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: M
412 // The user can delete it if s/he is an admin 419 // The user can delete it if s/he is an admin
413 // Or if s/he is the video playlist's owner 420 // Or if s/he is the video playlist's owner
414 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) { 421 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
415 res.status(HttpStatusCode.FORBIDDEN_403) 422 res.fail({
416 .json({ error: 'Cannot manage video playlist of another user' }) 423 status: HttpStatusCode.FORBIDDEN_403,
417 .end() 424 message: 'Cannot manage video playlist of another user'
418 425 })
419 return false 426 return false
420 } 427 }
421 428
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
index 01bdef25f..5d5dfb222 100644
--- a/server/middlewares/validators/videos/video-rates.ts
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -1,18 +1,18 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
4import { VideoRateType } from '../../../../shared/models/videos'
5import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
6import { isIdValid } from '../../../helpers/custom-validators/misc'
4import { isRatingValid } from '../../../helpers/custom-validators/video-rates' 7import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
5import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' 8import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 10import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoRateType } from '../../../../shared/models/videos' 11import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
10import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
11import { doesVideoExist } from '../../../helpers/middlewares'
12import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
13 12
14const videoUpdateRateValidator = [ 13const videoUpdateRateValidator = [
15 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 14 isValidVideoIdParam('id'),
15
16 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), 16 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
17 17
18 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 18 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -37,8 +37,10 @@ const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) {
37 37
38 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId) 38 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId)
39 if (!rate) { 39 if (!rate) {
40 return res.status(HttpStatusCode.NOT_FOUND_404) 40 return res.fail({
41 .json({ error: 'Video rate not found' }) 41 status: HttpStatusCode.NOT_FOUND_404,
42 message: 'Video rate not found'
43 })
42 } 44 }
43 45
44 res.locals.accountVideoRate = rate 46 res.locals.accountVideoRate = rate
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts
index f0d8e0c36..7e54b6fc0 100644
--- a/server/middlewares/validators/videos/video-shares.ts
+++ b/server/middlewares/validators/videos/video-shares.ts
@@ -1,15 +1,16 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param } from 'express-validator' 2import { param } from 'express-validator'
3import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
4import { isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
5import { VideoShareModel } from '../../../models/video/video-share' 6import { VideoShareModel } from '../../../models/video/video-share'
6import { areValidationErrors } from '../utils' 7import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
7import { doesVideoExist } from '../../../helpers/middlewares'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
9 8
10const videosShareValidator = [ 9const videosShareValidator = [
11 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 10 isValidVideoIdParam('id'),
12 param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), 11
12 param('actorId')
13 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
13 14
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 logger.debug('Checking videoShare parameters', { parameters: req.params }) 16 logger.debug('Checking videoShare parameters', { parameters: req.params })
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
index 29ce0dab6..43306f7cd 100644
--- a/server/middlewares/validators/videos/video-watch.ts
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -1,13 +1,13 @@
1import { body, param } from 'express-validator'
2import * as express from 'express' 1import * as express from 'express'
3import { isIdOrUUIDValid, toIntOrNull } from '../../../helpers/custom-validators/misc' 2import { body } from 'express-validator'
4import { areValidationErrors } from '../utils'
5import { logger } from '../../../helpers/logger'
6import { doesVideoExist } from '../../../helpers/middlewares'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
4import { toIntOrNull } from '../../../helpers/custom-validators/misc'
5import { logger } from '../../../helpers/logger'
6import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
8 7
9const videoWatchingValidator = [ 8const videoWatchingValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 9 isValidVideoIdParam('videoId'),
10
11 body('currentTime') 11 body('currentTime')
12 .customSanitizer(toIntOrNull) 12 .customSanitizer(toIntOrNull)
13 .isInt().withMessage('Should have correct current time'), 13 .isInt().withMessage('Should have correct current time'),
@@ -21,7 +21,10 @@ const videoWatchingValidator = [
21 const user = res.locals.oauth.token.User 21 const user = res.locals.oauth.token.User
22 if (user.videosHistoryEnabled === false) { 22 if (user.videosHistoryEnabled === false) {
23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) 23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
24 return res.status(HttpStatusCode.CONFLICT_409).end() 24 return res.fail({
25 status: HttpStatusCode.CONFLICT_409,
26 message: 'Video history is disabled'
27 })
25 } 28 }
26 29
27 return next() 30 return next()
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index d26bcd4a6..49e10e2b5 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -4,16 +4,14 @@ import { getResumableUploadPath } from '@server/helpers/upload'
4import { isAbleToUploadVideo } from '@server/lib/user' 4import { isAbleToUploadVideo } from '@server/lib/user'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { ExpressPromiseHandler } from '@server/types/express' 6import { ExpressPromiseHandler } from '@server/types/express'
7import { MUserAccountId, MVideoWithRights } from '@server/types/models' 7import { MUserAccountId, MVideoFullLight } from '@server/types/models'
8import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' 8import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
9import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 9import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
10import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
11import { 10import {
12 exists, 11 exists,
13 isBooleanValid, 12 isBooleanValid,
14 isDateValid, 13 isDateValid,
15 isFileFieldValid, 14 isFileFieldValid,
16 isIdOrUUIDValid,
17 isIdValid, 15 isIdValid,
18 isUUIDValid, 16 isUUIDValid,
19 toArray, 17 toArray,
@@ -22,7 +20,6 @@ import {
22 toValueOrNull 20 toValueOrNull
23} from '../../../helpers/custom-validators/misc' 21} from '../../../helpers/custom-validators/misc'
24import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 22import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
25import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
26import { 23import {
27 isScheduleVideoUpdatePrivacyValid, 24 isScheduleVideoUpdatePrivacyValid,
28 isVideoCategoryValid, 25 isVideoCategoryValid,
@@ -42,22 +39,22 @@ import {
42import { cleanUpReqFiles } from '../../../helpers/express-utils' 39import { cleanUpReqFiles } from '../../../helpers/express-utils'
43import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils' 40import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
44import { logger } from '../../../helpers/logger' 41import { logger } from '../../../helpers/logger'
45import {
46 checkUserCanManageVideo,
47 doesVideoChannelOfAccountExist,
48 doesVideoExist,
49 doesVideoFileOfVideoExist
50} from '../../../helpers/middlewares'
51import { deleteFileAndCatch } from '../../../helpers/utils' 42import { deleteFileAndCatch } from '../../../helpers/utils'
52import { getVideoWithAttributes } from '../../../helpers/video' 43import { getVideoWithAttributes } from '../../../helpers/video'
53import { CONFIG } from '../../../initializers/config' 44import { CONFIG } from '../../../initializers/config'
54import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' 45import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
55import { isLocalVideoAccepted } from '../../../lib/moderation' 46import { isLocalVideoAccepted } from '../../../lib/moderation'
56import { Hooks } from '../../../lib/plugins/hooks' 47import { Hooks } from '../../../lib/plugins/hooks'
57import { AccountModel } from '../../../models/account/account'
58import { VideoModel } from '../../../models/video/video' 48import { VideoModel } from '../../../models/video/video'
59import { authenticatePromiseIfNeeded } from '../../auth' 49import { authenticatePromiseIfNeeded } from '../../auth'
60import { areValidationErrors } from '../utils' 50import {
51 areValidationErrors,
52 checkUserCanManageVideo,
53 doesVideoChannelOfAccountExist,
54 doesVideoExist,
55 doesVideoFileOfVideoExist,
56 isValidVideoIdParam
57} from '../shared'
61 58
62const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ 59const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
63 body('videofile') 60 body('videofile')
@@ -65,8 +62,9 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
65 .withMessage('Should have a file'), 62 .withMessage('Should have a file'),
66 body('name') 63 body('name')
67 .trim() 64 .trim()
68 .custom(isVideoNameValid) 65 .custom(isVideoNameValid).withMessage(
69 .withMessage('Should have a valid name'), 66 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
67 ),
70 body('channelId') 68 body('channelId')
71 .customSanitizer(toIntOrNull) 69 .customSanitizer(toIntOrNull)
72 .custom(isIdValid).withMessage('Should have correct video channel id'), 70 .custom(isIdValid).withMessage('Should have correct video channel id'),
@@ -87,9 +85,11 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
87 if (!videoFile.duration) await addDurationToVideo(videoFile) 85 if (!videoFile.duration) await addDurationToVideo(videoFile)
88 } catch (err) { 86 } catch (err) {
89 logger.error('Invalid input file in videosAddLegacyValidator.', { err }) 87 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
90 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
91 .json({ error: 'Video file unreadable.' })
92 88
89 res.fail({
90 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
91 message: 'Video file unreadable.'
92 })
93 return cleanUpReqFiles(req) 93 return cleanUpReqFiles(req)
94 } 94 }
95 95
@@ -117,9 +117,11 @@ const videosAddResumableValidator = [
117 if (!file.duration) await addDurationToVideo(file) 117 if (!file.duration) await addDurationToVideo(file)
118 } catch (err) { 118 } catch (err) {
119 logger.error('Invalid input file in videosAddResumableValidator.', { err }) 119 logger.error('Invalid input file in videosAddResumableValidator.', { err })
120 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
121 .json({ error: 'Video file unreadable.' })
122 120
121 res.fail({
122 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
123 message: 'Video file unreadable.'
124 })
123 return cleanup() 125 return cleanup()
124 } 126 }
125 127
@@ -146,8 +148,9 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
146 .withMessage('Should have a valid filename'), 148 .withMessage('Should have a valid filename'),
147 body('name') 149 body('name')
148 .trim() 150 .trim()
149 .custom(isVideoNameValid) 151 .custom(isVideoNameValid).withMessage(
150 .withMessage('Should have a valid name'), 152 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
153 ),
151 body('channelId') 154 body('channelId')
152 .customSanitizer(toIntOrNull) 155 .customSanitizer(toIntOrNull)
153 .custom(isIdValid).withMessage('Should have correct video channel id'), 156 .custom(isIdValid).withMessage('Should have correct video channel id'),
@@ -192,11 +195,14 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
192]) 195])
193 196
194const videosUpdateValidator = getCommonVideoEditAttributes().concat([ 197const videosUpdateValidator = getCommonVideoEditAttributes().concat([
195 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 198 isValidVideoIdParam('id'),
199
196 body('name') 200 body('name')
197 .optional() 201 .optional()
198 .trim() 202 .trim()
199 .custom(isVideoNameValid).withMessage('Should have a valid name'), 203 .custom(isVideoNameValid).withMessage(
204 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
205 ),
200 body('channelId') 206 body('channelId')
201 .optional() 207 .optional()
202 .customSanitizer(toIntOrNull) 208 .customSanitizer(toIntOrNull)
@@ -238,20 +244,22 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
238 const serverActor = await getServerActor() 244 const serverActor = await getServerActor()
239 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() 245 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
240 246
241 return res.status(HttpStatusCode.FORBIDDEN_403) 247 return res.fail({
242 .json({ 248 status: HttpStatusCode.FORBIDDEN_403,
243 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, 249 message: 'Cannot get this video regarding follow constraints',
244 error: 'Cannot get this video regarding follow constraints.', 250 type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
245 originUrl: video.url 251 data: {
246 }) 252 originUrl: video.url
253 }
254 })
247} 255}
248 256
249const videosCustomGetValidator = ( 257const videosCustomGetValidator = (
250 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes', 258 fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes',
251 authenticateInQuery = false 259 authenticateInQuery = false
252) => { 260) => {
253 return [ 261 return [
254 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 262 isValidVideoIdParam('id'),
255 263
256 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 264 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
257 logger.debug('Checking videosGet parameters', { parameters: req.params }) 265 logger.debug('Checking videosGet parameters', { parameters: req.params })
@@ -262,7 +270,7 @@ const videosCustomGetValidator = (
262 // Controllers does not need to check video rights 270 // Controllers does not need to check video rights
263 if (fetchType === 'only-immutable-attributes') return next() 271 if (fetchType === 'only-immutable-attributes') return next()
264 272
265 const video = getVideoWithAttributes(res) as MVideoWithRights 273 const video = getVideoWithAttributes(res) as MVideoFullLight
266 274
267 // Video private or blacklisted 275 // Video private or blacklisted
268 if (video.requiresAuth()) { 276 if (video.requiresAuth()) {
@@ -270,10 +278,12 @@ const videosCustomGetValidator = (
270 278
271 const user = res.locals.oauth ? res.locals.oauth.token.User : null 279 const user = res.locals.oauth ? res.locals.oauth.token.User : null
272 280
273 // Only the owner or a user that have blacklist rights can see the video 281 // Only the owner or a user that have blocklist rights can see the video
274 if (!user || !user.canGetVideo(video)) { 282 if (!user || !user.canGetVideo(video)) {
275 return res.status(HttpStatusCode.FORBIDDEN_403) 283 return res.fail({
276 .json({ error: 'Cannot get this private/internal or blacklisted video.' }) 284 status: HttpStatusCode.FORBIDDEN_403,
285 message: 'Cannot get this private/internal or blocklisted video'
286 })
277 } 287 }
278 288
279 return next() 289 return next()
@@ -287,7 +297,10 @@ const videosCustomGetValidator = (
287 if (isUUIDValid(req.params.id)) return next() 297 if (isUUIDValid(req.params.id)) return next()
288 298
289 // Don't leak this unlisted video 299 // Don't leak this unlisted video
290 return res.status(HttpStatusCode.NOT_FOUND_404).end() 300 return res.fail({
301 status: HttpStatusCode.NOT_FOUND_404,
302 message: 'Video not found'
303 })
291 } 304 }
292 } 305 }
293 ] 306 ]
@@ -297,8 +310,10 @@ const videosGetValidator = videosCustomGetValidator('all')
297const videosDownloadValidator = videosCustomGetValidator('all', true) 310const videosDownloadValidator = videosCustomGetValidator('all', true)
298 311
299const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ 312const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
300 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 313 isValidVideoIdParam('id'),
301 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), 314
315 param('videoFileId')
316 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
302 317
303 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 318 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
304 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params }) 319 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
@@ -311,7 +326,7 @@ const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
311]) 326])
312 327
313const videosRemoveValidator = [ 328const videosRemoveValidator = [
314 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 329 isValidVideoIdParam('id'),
315 330
316 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 331 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
317 logger.debug('Checking videosRemove parameters', { parameters: req.params }) 332 logger.debug('Checking videosRemove parameters', { parameters: req.params })
@@ -326,74 +341,6 @@ const videosRemoveValidator = [
326 } 341 }
327] 342]
328 343
329const videosChangeOwnershipValidator = [
330 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
331
332 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
333 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
334
335 if (areValidationErrors(req, res)) return
336 if (!await doesVideoExist(req.params.videoId, res)) return
337
338 // Check if the user who did the request is able to change the ownership of the video
339 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
340
341 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
342 if (!nextOwner) {
343 res.status(HttpStatusCode.BAD_REQUEST_400)
344 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
345
346 return
347 }
348 res.locals.nextOwner = nextOwner
349
350 return next()
351 }
352]
353
354const videosTerminateChangeOwnershipValidator = [
355 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
356
357 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
358 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
359
360 if (areValidationErrors(req, res)) return
361 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
362
363 // Check if the user who did the request is able to change the ownership of the video
364 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
365
366 const videoChangeOwnership = res.locals.videoChangeOwnership
367
368 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
369 res.status(HttpStatusCode.FORBIDDEN_403)
370 .json({ error: 'Ownership already accepted or refused' })
371 return
372 }
373
374 return next()
375 }
376]
377
378const videosAcceptChangeOwnershipValidator = [
379 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
380 const body = req.body as VideoChangeOwnershipAccept
381 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
382
383 const user = res.locals.oauth.token.User
384 const videoChangeOwnership = res.locals.videoChangeOwnership
385 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
386 if (isAble === false) {
387 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
388 .json({ error: 'The user video quota is exceeded with this video.' })
389
390 return
391 }
392
393 return next()
394 }
395]
396
397const videosOverviewValidator = [ 344const videosOverviewValidator = [
398 query('page') 345 query('page')
399 .optional() 346 .optional()
@@ -455,7 +402,11 @@ function getCommonVideoEditAttributes () {
455 body('tags') 402 body('tags')
456 .optional() 403 .optional()
457 .customSanitizer(toValueOrNull) 404 .customSanitizer(toValueOrNull)
458 .custom(isVideoTagsValid).withMessage('Should have correct tags'), 405 .custom(isVideoTagsValid)
406 .withMessage(
407 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
408 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
409 ),
459 body('commentsEnabled') 410 body('commentsEnabled')
460 .optional() 411 .optional()
461 .customSanitizer(toBooleanOrNull) 412 .customSanitizer(toBooleanOrNull)
@@ -473,7 +424,7 @@ function getCommonVideoEditAttributes () {
473 .customSanitizer(toValueOrNull), 424 .customSanitizer(toValueOrNull),
474 body('scheduleUpdate.updateAt') 425 body('scheduleUpdate.updateAt')
475 .optional() 426 .optional()
476 .custom(isDateValid).withMessage('Should have a valid schedule update date'), 427 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
477 body('scheduleUpdate.privacy') 428 body('scheduleUpdate.privacy')
478 .optional() 429 .optional()
479 .customSanitizer(toIntOrNull) 430 .customSanitizer(toIntOrNull)
@@ -530,9 +481,10 @@ const commonVideosFiltersValidator = [
530 (req.query.filter === 'all-local' || req.query.filter === 'all') && 481 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
531 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) 482 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
532 ) { 483 ) {
533 res.status(HttpStatusCode.UNAUTHORIZED_401) 484 res.fail({
534 .json({ error: 'You are not allowed to see all local videos.' }) 485 status: HttpStatusCode.UNAUTHORIZED_401,
535 486 message: 'You are not allowed to see all local videos.'
487 })
536 return 488 return
537 } 489 }
538 490
@@ -555,10 +507,6 @@ export {
555 videosCustomGetValidator, 507 videosCustomGetValidator,
556 videosRemoveValidator, 508 videosRemoveValidator,
557 509
558 videosChangeOwnershipValidator,
559 videosTerminateChangeOwnershipValidator,
560 videosAcceptChangeOwnershipValidator,
561
562 getCommonVideoEditAttributes, 510 getCommonVideoEditAttributes,
563 511
564 commonVideosFiltersValidator, 512 commonVideosFiltersValidator,
@@ -573,9 +521,7 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
573 if (!req.body.scheduleUpdate.updateAt) { 521 if (!req.body.scheduleUpdate.updateAt) {
574 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') 522 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
575 523
576 res.status(HttpStatusCode.BAD_REQUEST_400) 524 res.fail({ message: 'Schedule update at is mandatory.' })
577 .json({ error: 'Schedule update at is mandatory.' })
578
579 return true 525 return true
580 } 526 }
581 } 527 }
@@ -597,26 +543,29 @@ async function commonVideoChecksPass (parameters: {
597 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false 543 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
598 544
599 if (!isVideoFileMimeTypeValid(files)) { 545 if (!isVideoFileMimeTypeValid(files)) {
600 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) 546 res.fail({
601 .json({ 547 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
602 error: 'This file is not supported. Please, make sure it is of the following type: ' + 548 message: 'This file is not supported. Please, make sure it is of the following type: ' +
603 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') 549 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
604 }) 550 })
605
606 return false 551 return false
607 } 552 }
608 553
609 if (!isVideoFileSizeValid(videoFileSize.toString())) { 554 if (!isVideoFileSizeValid(videoFileSize.toString())) {
610 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) 555 res.fail({
611 .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) 556 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
612 557 message: 'This file is too large. It exceeds the maximum file size authorized.',
558 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
559 })
613 return false 560 return false
614 } 561 }
615 562
616 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { 563 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
617 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) 564 res.fail({
618 .json({ error: 'The user video quota is exceeded with this video.' }) 565 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
619 566 message: 'The user video quota is exceeded with this video.',
567 type: ServerErrorCode.QUOTA_REACHED
568 })
620 return false 569 return false
621 } 570 }
622 571
@@ -642,9 +591,10 @@ export async function isVideoAccepted (
642 591
643 if (!acceptedResult || acceptedResult.accepted !== true) { 592 if (!acceptedResult || acceptedResult.accepted !== true) {
644 logger.info('Refused local video.', { acceptedResult, acceptParameters }) 593 logger.info('Refused local video.', { acceptedResult, acceptParameters })
645 res.status(HttpStatusCode.FORBIDDEN_403) 594 res.fail({
646 .json({ error: acceptedResult.errorMessage || 'Refused local video' }) 595 status: HttpStatusCode.FORBIDDEN_403,
647 596 message: acceptedResult.errorMessage || 'Refused local video'
597 })
648 return false 598 return false
649 } 599 }
650 600
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts
index a71422ed8..bcdd136c6 100644
--- a/server/middlewares/validators/webfinger.ts
+++ b/server/middlewares/validators/webfinger.ts
@@ -1,11 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { query } from 'express-validator' 2import { query } from 'express-validator'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger' 4import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger'
4import { logger } from '../../helpers/logger'
5import { ActorModel } from '../../models/activitypub/actor'
6import { areValidationErrors } from './utils'
7import { getHostWithPort } from '../../helpers/express-utils' 5import { getHostWithPort } from '../../helpers/express-utils'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 6import { logger } from '../../helpers/logger'
7import { ActorModel } from '../../models/actor/actor'
8import { areValidationErrors } from './shared'
9 9
10const webfingerValidator = [ 10const webfingerValidator = [
11 query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'), 11 query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'),
@@ -21,9 +21,10 @@ const webfingerValidator = [
21 21
22 const actor = await ActorModel.loadLocalUrlByName(name) 22 const actor = await ActorModel.loadLocalUrlByName(name)
23 if (!actor) { 23 if (!actor) {
24 return res.status(HttpStatusCode.NOT_FOUND_404) 24 return res.fail({
25 .send({ error: 'Actor not found' }) 25 status: HttpStatusCode.NOT_FOUND_404,
26 .end() 26 message: 'Actor not found'
27 })
27 } 28 }
28 29
29 res.locals.actorUrl = actor 30 res.locals.actorUrl = actor