diff options
Diffstat (limited to 'server/middlewares/validators')
-rw-r--r-- | server/middlewares/validators/activitypub/signature.ts | 16 | ||||
-rw-r--r-- | server/middlewares/validators/blocklist.ts | 172 | ||||
-rw-r--r-- | server/middlewares/validators/config.ts | 19 | ||||
-rw-r--r-- | server/middlewares/validators/index.ts | 3 | ||||
-rw-r--r-- | server/middlewares/validators/redundancy.ts | 33 | ||||
-rw-r--r-- | server/middlewares/validators/search.ts | 38 | ||||
-rw-r--r-- | server/middlewares/validators/server.ts | 78 | ||||
-rw-r--r-- | server/middlewares/validators/sort.ts | 11 | ||||
-rw-r--r-- | server/middlewares/validators/user-history.ts | 26 | ||||
-rw-r--r-- | server/middlewares/validators/user-notifications.ts | 63 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 14 | ||||
-rw-r--r-- | server/middlewares/validators/videos/index.ts | 2 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-blacklist.ts | 15 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-rates.ts | 55 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-shares.ts | 38 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-watch.ts | 7 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 165 |
17 files changed, 638 insertions, 117 deletions
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts index 4efe9aafa..be14e92ea 100644 --- a/server/middlewares/validators/activitypub/signature.ts +++ b/server/middlewares/validators/activitypub/signature.ts | |||
@@ -9,10 +9,18 @@ import { logger } from '../../../helpers/logger' | |||
9 | import { areValidationErrors } from '../utils' | 9 | import { areValidationErrors } from '../utils' |
10 | 10 | ||
11 | const signatureValidator = [ | 11 | const signatureValidator = [ |
12 | body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), | 12 | body('signature.type') |
13 | body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'), | 13 | .optional() |
14 | body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), | 14 | .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), |
15 | body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'), | 15 | body('signature.created') |
16 | .optional() | ||
17 | .custom(isDateValid).withMessage('Should have a valid signature created date'), | ||
18 | body('signature.creator') | ||
19 | .optional() | ||
20 | .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), | ||
21 | body('signature.signatureValue') | ||
22 | .optional() | ||
23 | .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), | ||
16 | 24 | ||
17 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 25 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
18 | logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) | 26 | logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) |
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts new file mode 100644 index 000000000..109276c63 --- /dev/null +++ b/server/middlewares/validators/blocklist.ts | |||
@@ -0,0 +1,172 @@ | |||
1 | import { body, param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' | ||
6 | import { UserModel } from '../../models/account/user' | ||
7 | import { AccountBlocklistModel } from '../../models/account/account-blocklist' | ||
8 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
9 | import { ServerBlocklistModel } from '../../models/server/server-blocklist' | ||
10 | import { ServerModel } from '../../models/server/server' | ||
11 | import { CONFIG } from '../../initializers' | ||
12 | import { getServerActor } from '../../helpers/utils' | ||
13 | |||
14 | const blockAccountValidator = [ | ||
15 | body('accountName').exists().withMessage('Should have an account name with host'), | ||
16 | |||
17 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
18 | logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body }) | ||
19 | |||
20 | if (areValidationErrors(req, res)) return | ||
21 | if (!await isAccountNameWithHostExist(req.body.accountName, res)) return | ||
22 | |||
23 | const user = res.locals.oauth.token.User as UserModel | ||
24 | const accountToBlock = res.locals.account | ||
25 | |||
26 | if (user.Account.id === accountToBlock.id) { | ||
27 | res.status(409) | ||
28 | .send({ error: 'You cannot block yourself.' }) | ||
29 | .end() | ||
30 | |||
31 | return | ||
32 | } | ||
33 | |||
34 | return next() | ||
35 | } | ||
36 | ] | ||
37 | |||
38 | const unblockAccountByAccountValidator = [ | ||
39 | param('accountName').exists().withMessage('Should have an account name with host'), | ||
40 | |||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params }) | ||
43 | |||
44 | if (areValidationErrors(req, res)) return | ||
45 | if (!await isAccountNameWithHostExist(req.params.accountName, res)) return | ||
46 | |||
47 | const user = res.locals.oauth.token.User as UserModel | ||
48 | const targetAccount = res.locals.account | ||
49 | if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return | ||
50 | |||
51 | return next() | ||
52 | } | ||
53 | ] | ||
54 | |||
55 | const unblockAccountByServerValidator = [ | ||
56 | param('accountName').exists().withMessage('Should have an account name with host'), | ||
57 | |||
58 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
59 | logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params }) | ||
60 | |||
61 | if (areValidationErrors(req, res)) return | ||
62 | if (!await isAccountNameWithHostExist(req.params.accountName, res)) return | ||
63 | |||
64 | const serverActor = await getServerActor() | ||
65 | const targetAccount = res.locals.account | ||
66 | if (!await isUnblockAccountExists(serverActor.Account.id, targetAccount.id, res)) return | ||
67 | |||
68 | return next() | ||
69 | } | ||
70 | ] | ||
71 | |||
72 | const blockServerValidator = [ | ||
73 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | ||
74 | |||
75 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
76 | logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) | ||
77 | |||
78 | if (areValidationErrors(req, res)) return | ||
79 | |||
80 | const host: string = req.body.host | ||
81 | |||
82 | if (host === CONFIG.WEBSERVER.HOST) { | ||
83 | return res.status(409) | ||
84 | .send({ error: 'You cannot block your own server.' }) | ||
85 | .end() | ||
86 | } | ||
87 | |||
88 | const server = await ServerModel.loadByHost(host) | ||
89 | if (!server) { | ||
90 | return res.status(404) | ||
91 | .send({ error: 'Server host not found.' }) | ||
92 | .end() | ||
93 | } | ||
94 | |||
95 | res.locals.server = server | ||
96 | |||
97 | return next() | ||
98 | } | ||
99 | ] | ||
100 | |||
101 | const unblockServerByAccountValidator = [ | ||
102 | param('host').custom(isHostValid).withMessage('Should have an account name with host'), | ||
103 | |||
104 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
105 | logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params }) | ||
106 | |||
107 | if (areValidationErrors(req, res)) return | ||
108 | |||
109 | const user = res.locals.oauth.token.User as UserModel | ||
110 | if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return | ||
111 | |||
112 | return next() | ||
113 | } | ||
114 | ] | ||
115 | |||
116 | const unblockServerByServerValidator = [ | ||
117 | param('host').custom(isHostValid).withMessage('Should have an account name with host'), | ||
118 | |||
119 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
120 | logger.debug('Checking unblockServerByServerValidator parameters', { parameters: req.params }) | ||
121 | |||
122 | if (areValidationErrors(req, res)) return | ||
123 | |||
124 | const serverActor = await getServerActor() | ||
125 | if (!await isUnblockServerExists(serverActor.Account.id, req.params.host, res)) return | ||
126 | |||
127 | return next() | ||
128 | } | ||
129 | ] | ||
130 | |||
131 | // --------------------------------------------------------------------------- | ||
132 | |||
133 | export { | ||
134 | blockServerValidator, | ||
135 | blockAccountValidator, | ||
136 | unblockAccountByAccountValidator, | ||
137 | unblockServerByAccountValidator, | ||
138 | unblockAccountByServerValidator, | ||
139 | unblockServerByServerValidator | ||
140 | } | ||
141 | |||
142 | // --------------------------------------------------------------------------- | ||
143 | |||
144 | async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) { | ||
145 | const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) | ||
146 | if (!accountBlock) { | ||
147 | res.status(404) | ||
148 | .send({ error: 'Account block entry not found.' }) | ||
149 | .end() | ||
150 | |||
151 | return false | ||
152 | } | ||
153 | |||
154 | res.locals.accountBlock = accountBlock | ||
155 | |||
156 | return true | ||
157 | } | ||
158 | |||
159 | async function isUnblockServerExists (accountId: number, host: string, res: express.Response) { | ||
160 | const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) | ||
161 | if (!serverBlock) { | ||
162 | res.status(404) | ||
163 | .send({ error: 'Server block entry not found.' }) | ||
164 | .end() | ||
165 | |||
166 | return false | ||
167 | } | ||
168 | |||
169 | res.locals.serverBlock = serverBlock | ||
170 | |||
171 | return true | ||
172 | } | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index f3f257d57..90108fa82 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -1,29 +1,44 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator/check' | 2 | import { body } from 'express-validator/check' |
3 | import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' | 3 | import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | 6 | ||
7 | const customConfigUpdateValidator = [ | 7 | const customConfigUpdateValidator = [ |
8 | body('instance.name').exists().withMessage('Should have a valid instance name'), | 8 | body('instance.name').exists().withMessage('Should have a valid instance name'), |
9 | body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'), | ||
9 | body('instance.description').exists().withMessage('Should have a valid instance description'), | 10 | body('instance.description').exists().withMessage('Should have a valid instance description'), |
10 | body('instance.terms').exists().withMessage('Should have a valid instance terms'), | 11 | body('instance.terms').exists().withMessage('Should have a valid instance terms'), |
11 | body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), | 12 | body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), |
12 | body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), | 13 | body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), |
13 | body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), | 14 | body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), |
14 | body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), | 15 | body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), |
15 | body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), | 16 | |
17 | body('services.twitter.username').exists().withMessage('Should have a valid twitter username'), | ||
18 | body('services.twitter.whitelisted').isBoolean().withMessage('Should have a valid twitter whitelisted boolean'), | ||
19 | |||
20 | body('cache.previews.size').isInt().withMessage('Should have a valid previews cache size'), | ||
21 | body('cache.captions.size').isInt().withMessage('Should have a valid captions cache size'), | ||
22 | |||
16 | body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), | 23 | body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), |
17 | body('signup.limit').isInt().withMessage('Should have a valid signup limit'), | 24 | body('signup.limit').isInt().withMessage('Should have a valid signup limit'), |
25 | body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'), | ||
26 | |||
18 | body('admin.email').isEmail().withMessage('Should have a valid administrator email'), | 27 | body('admin.email').isEmail().withMessage('Should have a valid administrator email'), |
28 | body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'), | ||
29 | |||
19 | body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'), | 30 | body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'), |
31 | body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily video quota'), | ||
32 | |||
20 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), | 33 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), |
34 | body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), | ||
21 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), | 35 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), |
22 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), | 36 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), |
23 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), | 37 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), |
24 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), | 38 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), |
25 | body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), | 39 | body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), |
26 | body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), | 40 | body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), |
41 | |||
27 | body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), | 42 | body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), |
28 | body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), | 43 | body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), |
29 | 44 | ||
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 17226614c..65dd00335 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './blocklist' | ||
2 | export * from './oembed' | 3 | export * from './oembed' |
3 | export * from './activitypub' | 4 | export * from './activitypub' |
4 | export * from './pagination' | 5 | export * from './pagination' |
@@ -10,3 +11,5 @@ export * from './user-subscriptions' | |||
10 | export * from './videos' | 11 | export * from './videos' |
11 | export * from './webfinger' | 12 | export * from './webfinger' |
12 | export * from './search' | 13 | export * from './search' |
14 | export * from './server' | ||
15 | export * from './user-history' | ||
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index c72ab78b2..329322509 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -13,7 +13,7 @@ import { ActorFollowModel } from '../../models/activitypub/actor-follow' | |||
13 | import { SERVER_ACTOR_NAME } from '../../initializers' | 13 | import { SERVER_ACTOR_NAME } from '../../initializers' |
14 | import { ServerModel } from '../../models/server/server' | 14 | import { ServerModel } from '../../models/server/server' |
15 | 15 | ||
16 | const videoRedundancyGetValidator = [ | 16 | const videoFileRedundancyGetValidator = [ |
17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), |
18 | param('resolution') | 18 | param('resolution') |
19 | .customSanitizer(toIntOrNull) | 19 | .customSanitizer(toIntOrNull) |
@@ -24,7 +24,7 @@ const videoRedundancyGetValidator = [ | |||
24 | .custom(exists).withMessage('Should have a valid fps'), | 24 | .custom(exists).withMessage('Should have a valid fps'), |
25 | 25 | ||
26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
27 | logger.debug('Checking videoRedundancyGetValidator parameters', { parameters: req.params }) | 27 | logger.debug('Checking videoFileRedundancyGetValidator parameters', { parameters: req.params }) |
28 | 28 | ||
29 | if (areValidationErrors(req, res)) return | 29 | if (areValidationErrors(req, res)) return |
30 | if (!await isVideoExist(req.params.videoId, res)) return | 30 | if (!await isVideoExist(req.params.videoId, res)) return |
@@ -38,7 +38,31 @@ const videoRedundancyGetValidator = [ | |||
38 | res.locals.videoFile = videoFile | 38 | res.locals.videoFile = videoFile |
39 | 39 | ||
40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) | 40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) |
41 | if (!videoRedundancy)return res.status(404).json({ error: 'Video redundancy not found.' }) | 41 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) |
42 | res.locals.videoRedundancy = videoRedundancy | ||
43 | |||
44 | return next() | ||
45 | } | ||
46 | ] | ||
47 | |||
48 | const videoPlaylistRedundancyGetValidator = [ | ||
49 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | ||
50 | param('streamingPlaylistType').custom(exists).withMessage('Should have a valid streaming playlist type'), | ||
51 | |||
52 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
53 | logger.debug('Checking videoPlaylistRedundancyGetValidator parameters', { parameters: req.params }) | ||
54 | |||
55 | if (areValidationErrors(req, res)) return | ||
56 | if (!await isVideoExist(req.params.videoId, res)) return | ||
57 | |||
58 | const video: VideoModel = res.locals.video | ||
59 | const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType) | ||
60 | |||
61 | if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' }) | ||
62 | res.locals.videoStreamingPlaylist = videoStreamingPlaylist | ||
63 | |||
64 | const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) | ||
65 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) | ||
42 | res.locals.videoRedundancy = videoRedundancy | 66 | res.locals.videoRedundancy = videoRedundancy |
43 | 67 | ||
44 | return next() | 68 | return next() |
@@ -75,6 +99,7 @@ const updateServerRedundancyValidator = [ | |||
75 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
76 | 100 | ||
77 | export { | 101 | export { |
78 | videoRedundancyGetValidator, | 102 | videoFileRedundancyGetValidator, |
103 | videoPlaylistRedundancyGetValidator, | ||
79 | updateServerRedundancyValidator | 104 | updateServerRedundancyValidator |
80 | } | 105 | } |
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index 8baf643a5..6a95d6095 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -2,8 +2,7 @@ import * as express from 'express' | |||
2 | import { areValidationErrors } from './utils' | 2 | import { areValidationErrors } from './utils' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { query } from 'express-validator/check' | 4 | import { query } from 'express-validator/check' |
5 | import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' | 5 | import { isDateValid } from '../../helpers/custom-validators/misc' |
6 | import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc' | ||
7 | 6 | ||
8 | const videosSearchValidator = [ | 7 | const videosSearchValidator = [ |
9 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 8 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), |
@@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [ | |||
35 | } | 34 | } |
36 | ] | 35 | ] |
37 | 36 | ||
38 | const commonVideosFiltersValidator = [ | ||
39 | query('categoryOneOf') | ||
40 | .optional() | ||
41 | .customSanitizer(toArray) | ||
42 | .custom(isNumberArray).withMessage('Should have a valid one of category array'), | ||
43 | query('licenceOneOf') | ||
44 | .optional() | ||
45 | .customSanitizer(toArray) | ||
46 | .custom(isNumberArray).withMessage('Should have a valid one of licence array'), | ||
47 | query('languageOneOf') | ||
48 | .optional() | ||
49 | .customSanitizer(toArray) | ||
50 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | ||
51 | query('tagsOneOf') | ||
52 | .optional() | ||
53 | .customSanitizer(toArray) | ||
54 | .custom(isStringArray).withMessage('Should have a valid one of tags array'), | ||
55 | query('tagsAllOf') | ||
56 | .optional() | ||
57 | .customSanitizer(toArray) | ||
58 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | ||
59 | query('nsfw') | ||
60 | .optional() | ||
61 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | ||
62 | |||
63 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
64 | logger.debug('Checking commons video filters query', { parameters: req.query }) | ||
65 | |||
66 | if (areValidationErrors(req, res)) return | ||
67 | |||
68 | return next() | ||
69 | } | ||
70 | ] | ||
71 | |||
72 | // --------------------------------------------------------------------------- | 37 | // --------------------------------------------------------------------------- |
73 | 38 | ||
74 | export { | 39 | export { |
75 | commonVideosFiltersValidator, | ||
76 | videoChannelsSearchValidator, | 40 | videoChannelsSearchValidator, |
77 | videosSearchValidator | 41 | videosSearchValidator |
78 | } | 42 | } |
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts new file mode 100644 index 000000000..d85afc2ff --- /dev/null +++ b/server/middlewares/validators/server.ts | |||
@@ -0,0 +1,78 @@ | |||
1 | import * as express from 'express' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | import { areValidationErrors } from './utils' | ||
4 | import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers' | ||
5 | import { ServerModel } from '../../models/server/server' | ||
6 | import { body } from 'express-validator/check' | ||
7 | import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' | ||
8 | import { Emailer } from '../../lib/emailer' | ||
9 | import { Redis } from '../../lib/redis' | ||
10 | import { CONFIG } from '../../initializers/constants' | ||
11 | |||
12 | const serverGetValidator = [ | ||
13 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | ||
14 | |||
15 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return | ||
19 | |||
20 | const server = await ServerModel.loadByHost(req.body.host) | ||
21 | if (!server) { | ||
22 | return res.status(404) | ||
23 | .send({ error: 'Server host not found.' }) | ||
24 | .end() | ||
25 | } | ||
26 | |||
27 | res.locals.server = server | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | ] | ||
32 | |||
33 | const contactAdministratorValidator = [ | ||
34 | body('fromName') | ||
35 | .custom(isUserDisplayNameValid).withMessage('Should have a valid name'), | ||
36 | body('fromEmail') | ||
37 | .isEmail().withMessage('Should have a valid email'), | ||
38 | body('body') | ||
39 | .custom(isValidContactBody).withMessage('Should have a valid body'), | ||
40 | |||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | logger.debug('Checking contactAdministratorValidator parameters', { parameters: req.body }) | ||
43 | |||
44 | if (areValidationErrors(req, res)) return | ||
45 | |||
46 | if (CONFIG.CONTACT_FORM.ENABLED === false) { | ||
47 | return res | ||
48 | .status(409) | ||
49 | .send({ error: 'Contact form is not enabled on this instance.' }) | ||
50 | .end() | ||
51 | } | ||
52 | |||
53 | if (Emailer.isEnabled() === false) { | ||
54 | return res | ||
55 | .status(409) | ||
56 | .send({ error: 'Emailer is not enabled on this instance.' }) | ||
57 | .end() | ||
58 | } | ||
59 | |||
60 | if (await Redis.Instance.isContactFormIpExists(req.ip)) { | ||
61 | logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) | ||
62 | |||
63 | return res | ||
64 | .status(403) | ||
65 | .send({ error: 'You already sent a contact form recently.' }) | ||
66 | .end() | ||
67 | } | ||
68 | |||
69 | return next() | ||
70 | } | ||
71 | ] | ||
72 | |||
73 | // --------------------------------------------------------------------------- | ||
74 | |||
75 | export { | ||
76 | serverGetValidator, | ||
77 | contactAdministratorValidator | ||
78 | } | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 08dcc2680..5ceda845f 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -16,6 +16,9 @@ const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.V | |||
16 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) | 16 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) |
17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) | 17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) |
18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | 18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) |
19 | const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | ||
20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | ||
21 | const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
19 | 22 | ||
20 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 23 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
21 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 24 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -31,6 +34,9 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) | |||
31 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) | 34 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) |
32 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) | 35 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) |
33 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) | 36 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) |
37 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) | ||
38 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) | ||
39 | const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) | ||
34 | 40 | ||
35 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
36 | 42 | ||
@@ -48,5 +54,8 @@ export { | |||
48 | jobsSortValidator, | 54 | jobsSortValidator, |
49 | videoCommentThreadsSortValidator, | 55 | videoCommentThreadsSortValidator, |
50 | userSubscriptionsSortValidator, | 56 | userSubscriptionsSortValidator, |
51 | videoChannelsSearchSortValidator | 57 | videoChannelsSearchSortValidator, |
58 | accountsBlocklistSortValidator, | ||
59 | serversBlocklistSortValidator, | ||
60 | userNotificationsSortValidator | ||
52 | } | 61 | } |
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts new file mode 100644 index 000000000..418313d09 --- /dev/null +++ b/server/middlewares/validators/user-history.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body } from 'express-validator/check' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
7 | |||
8 | const userHistoryRemoveValidator = [ | ||
9 | body('beforeDate') | ||
10 | .optional() | ||
11 | .custom(isDateValid).withMessage('Should have a valid before date'), | ||
12 | |||
13 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
14 | logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body }) | ||
15 | |||
16 | if (areValidationErrors(req, res)) return | ||
17 | |||
18 | return next() | ||
19 | } | ||
20 | ] | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | userHistoryRemoveValidator | ||
26 | } | ||
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts new file mode 100644 index 000000000..46486e081 --- /dev/null +++ b/server/middlewares/validators/user-notifications.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body, query } from 'express-validator/check' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
7 | import { isNotEmptyIntArray } from '../../helpers/custom-validators/misc' | ||
8 | |||
9 | const listUserNotificationsValidator = [ | ||
10 | query('unread') | ||
11 | .optional() | ||
12 | .toBoolean() | ||
13 | .isBoolean().withMessage('Should have a valid unread boolean'), | ||
14 | |||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking listUserNotificationsValidator parameters', { parameters: req.query }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | const updateNotificationSettingsValidator = [ | ||
25 | body('newVideoFromSubscription') | ||
26 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), | ||
27 | body('newCommentOnMyVideo') | ||
28 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), | ||
29 | body('videoAbuseAsModerator') | ||
30 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), | ||
31 | body('blacklistOnMyVideo') | ||
32 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new blacklist on my video notification setting'), | ||
33 | |||
34 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
35 | logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) | ||
36 | |||
37 | if (areValidationErrors(req, res)) return | ||
38 | |||
39 | return next() | ||
40 | } | ||
41 | ] | ||
42 | |||
43 | const markAsReadUserNotificationsValidator = [ | ||
44 | body('ids') | ||
45 | .optional() | ||
46 | .custom(isNotEmptyIntArray).withMessage('Should have a valid notification ids to mark as read'), | ||
47 | |||
48 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
49 | logger.debug('Checking markAsReadUserNotificationsValidator parameters', { parameters: req.body }) | ||
50 | |||
51 | if (areValidationErrors(req, res)) return | ||
52 | |||
53 | return next() | ||
54 | } | ||
55 | ] | ||
56 | |||
57 | // --------------------------------------------------------------------------- | ||
58 | |||
59 | export { | ||
60 | listUserNotificationsValidator, | ||
61 | updateNotificationSettingsValidator, | ||
62 | markAsReadUserNotificationsValidator | ||
63 | } | ||
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 61297120a..a52e3060a 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -5,15 +5,16 @@ import { body, param } from 'express-validator/check' | |||
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 6 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' |
7 | import { | 7 | import { |
8 | isUserAutoPlayVideoValid, isUserBlockedReasonValid, | 8 | isUserAutoPlayVideoValid, |
9 | isUserBlockedReasonValid, | ||
9 | isUserDescriptionValid, | 10 | isUserDescriptionValid, |
10 | isUserDisplayNameValid, | 11 | isUserDisplayNameValid, |
11 | isUserNSFWPolicyValid, | 12 | isUserNSFWPolicyValid, |
12 | isUserPasswordValid, | 13 | isUserPasswordValid, |
13 | isUserRoleValid, | 14 | isUserRoleValid, |
14 | isUserUsernameValid, | 15 | isUserUsernameValid, |
15 | isUserVideoQuotaValid, | 16 | isUserVideoQuotaDailyValid, |
16 | isUserVideoQuotaDailyValid | 17 | isUserVideoQuotaValid, isUserVideosHistoryEnabledValid |
17 | } from '../../helpers/custom-validators/users' | 18 | } from '../../helpers/custom-validators/users' |
18 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 19 | import { isVideoExist } from '../../helpers/custom-validators/videos' |
19 | import { logger } from '../../helpers/logger' | 20 | import { logger } from '../../helpers/logger' |
@@ -22,7 +23,6 @@ import { Redis } from '../../lib/redis' | |||
22 | import { UserModel } from '../../models/account/user' | 23 | import { UserModel } from '../../models/account/user' |
23 | import { areValidationErrors } from './utils' | 24 | import { areValidationErrors } from './utils' |
24 | import { ActorModel } from '../../models/activitypub/actor' | 25 | import { ActorModel } from '../../models/activitypub/actor' |
25 | import { comparePassword } from '../../helpers/peertube-crypto' | ||
26 | 26 | ||
27 | const usersAddValidator = [ | 27 | const usersAddValidator = [ |
28 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), | 28 | body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), |
@@ -113,7 +113,9 @@ const deleteMeValidator = [ | |||
113 | 113 | ||
114 | const usersUpdateValidator = [ | 114 | const usersUpdateValidator = [ |
115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
116 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), | ||
116 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 117 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
118 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), | ||
117 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 119 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
118 | body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), | 120 | body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), |
119 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), | 121 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), |
@@ -143,6 +145,9 @@ const usersUpdateMeValidator = [ | |||
143 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 145 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
144 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), | 146 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), |
145 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), | 147 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), |
148 | body('videosHistoryEnabled') | ||
149 | .optional() | ||
150 | .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), | ||
146 | 151 | ||
147 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 152 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
148 | logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) | 153 | logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) |
@@ -229,6 +234,7 @@ const usersAskResetPasswordValidator = [ | |||
229 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) | 234 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) |
230 | 235 | ||
231 | if (areValidationErrors(req, res)) return | 236 | if (areValidationErrors(req, res)) return |
237 | |||
232 | const exists = await checkUserEmailExist(req.body.email, res, false) | 238 | const exists = await checkUserEmailExist(req.body.email, res, false) |
233 | if (!exists) { | 239 | if (!exists) { |
234 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) | 240 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index 294783d85..a0d585b93 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -5,4 +5,6 @@ export * from './video-channels' | |||
5 | export * from './video-comments' | 5 | export * from './video-comments' |
6 | export * from './video-imports' | 6 | export * from './video-imports' |
7 | export * from './video-watch' | 7 | export * from './video-watch' |
8 | export * from './video-rates' | ||
9 | export * from './video-shares' | ||
8 | export * from './videos' | 10 | export * from './videos' |
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 13da7acff..2688f63ae 100644 --- a/server/middlewares/validators/videos/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | 3 | import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { areValidationErrors } from '../utils' | 6 | import { areValidationErrors } from '../utils' |
7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' | 7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' |
8 | import { VideoModel } from '../../../models/video/video' | ||
8 | 9 | ||
9 | const videosBlacklistRemoveValidator = [ | 10 | const videosBlacklistRemoveValidator = [ |
10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 11 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
@@ -22,6 +23,10 @@ const videosBlacklistRemoveValidator = [ | |||
22 | 23 | ||
23 | const videosBlacklistAddValidator = [ | 24 | const videosBlacklistAddValidator = [ |
24 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 25 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
26 | body('unfederate') | ||
27 | .optional() | ||
28 | .toBoolean() | ||
29 | .custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'), | ||
25 | body('reason') | 30 | body('reason') |
26 | .optional() | 31 | .optional() |
27 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), | 32 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), |
@@ -32,6 +37,14 @@ const videosBlacklistAddValidator = [ | |||
32 | if (areValidationErrors(req, res)) return | 37 | if (areValidationErrors(req, res)) return |
33 | if (!await isVideoExist(req.params.videoId, res)) return | 38 | if (!await isVideoExist(req.params.videoId, res)) return |
34 | 39 | ||
40 | const video: VideoModel = res.locals.video | ||
41 | if (req.body.unfederate === true && video.remote === true) { | ||
42 | return res | ||
43 | .status(409) | ||
44 | .send({ error: 'You cannot unfederate a remote video.' }) | ||
45 | .end() | ||
46 | } | ||
47 | |||
35 | return next() | 48 | return next() |
36 | } | 49 | } |
37 | ] | 50 | ] |
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts new file mode 100644 index 000000000..793354520 --- /dev/null +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body, param } from 'express-validator/check' | ||
4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | ||
5 | import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { areValidationErrors } from '../utils' | ||
8 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
9 | import { VideoRateType } from '../../../../shared/models/videos' | ||
10 | import { isAccountNameValid } from '../../../helpers/custom-validators/accounts' | ||
11 | |||
12 | const videoUpdateRateValidator = [ | ||
13 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
14 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), | ||
15 | |||
16 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
17 | logger.debug('Checking videoRate parameters', { parameters: req.body }) | ||
18 | |||
19 | if (areValidationErrors(req, res)) return | ||
20 | if (!await isVideoExist(req.params.id, res)) return | ||
21 | |||
22 | return next() | ||
23 | } | ||
24 | ] | ||
25 | |||
26 | const getAccountVideoRateValidator = function (rateType: VideoRateType) { | ||
27 | return [ | ||
28 | param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), | ||
29 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
30 | |||
31 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
32 | logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) | ||
33 | |||
34 | if (areValidationErrors(req, res)) return | ||
35 | |||
36 | const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId) | ||
37 | if (!rate) { | ||
38 | return res.status(404) | ||
39 | .json({ error: 'Video rate not found' }) | ||
40 | .end() | ||
41 | } | ||
42 | |||
43 | res.locals.accountVideoRate = rate | ||
44 | |||
45 | return next() | ||
46 | } | ||
47 | ] | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | ||
51 | |||
52 | export { | ||
53 | videoUpdateRateValidator, | ||
54 | getAccountVideoRateValidator | ||
55 | } | ||
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts new file mode 100644 index 000000000..646d7acb1 --- /dev/null +++ b/server/middlewares/validators/videos/video-shares.ts | |||
@@ -0,0 +1,38 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { param } from 'express-validator/check' | ||
4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | ||
5 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { VideoShareModel } from '../../../models/video/video-share' | ||
8 | import { areValidationErrors } from '../utils' | ||
9 | import { VideoModel } from '../../../models/video/video' | ||
10 | |||
11 | const videosShareValidator = [ | ||
12 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
13 | param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), | ||
14 | |||
15 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking videoShare parameters', { parameters: req.params }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return | ||
19 | if (!await isVideoExist(req.params.id, res)) return | ||
20 | |||
21 | const video: VideoModel = res.locals.video | ||
22 | |||
23 | const share = await VideoShareModel.load(req.params.actorId, video.id) | ||
24 | if (!share) { | ||
25 | return res.status(404) | ||
26 | .end() | ||
27 | } | ||
28 | |||
29 | res.locals.videoShare = share | ||
30 | return next() | ||
31 | } | ||
32 | ] | ||
33 | |||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | export { | ||
37 | videosShareValidator | ||
38 | } | ||
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts index bca64662f..c38ad8a10 100644 --- a/server/middlewares/validators/videos/video-watch.ts +++ b/server/middlewares/validators/videos/video-watch.ts | |||
@@ -4,6 +4,7 @@ import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | |||
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { areValidationErrors } from '../utils' | 5 | import { areValidationErrors } from '../utils' |
6 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
7 | import { UserModel } from '../../../models/account/user' | ||
7 | 8 | ||
8 | const videoWatchingValidator = [ | 9 | const videoWatchingValidator = [ |
9 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
@@ -17,6 +18,12 @@ const videoWatchingValidator = [ | |||
17 | if (areValidationErrors(req, res)) return | 18 | if (areValidationErrors(req, res)) return |
18 | if (!await isVideoExist(req.params.videoId, res, 'id')) return | 19 | if (!await isVideoExist(req.params.videoId, res, 'id')) return |
19 | 20 | ||
21 | const user = res.locals.oauth.token.User as UserModel | ||
22 | if (user.videosHistoryEnabled === false) { | ||
23 | logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) | ||
24 | return res.status(409).end() | ||
25 | } | ||
26 | |||
20 | return next() | 27 | return next() |
21 | } | 28 | } |
22 | ] | 29 | ] |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 27e8a7449..d9626929c 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param, ValidationChain } from 'express-validator/check' | 3 | import { body, param, query, ValidationChain } from 'express-validator/check' |
4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' | 4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' |
5 | import { | 5 | import { |
6 | isBooleanValid, | 6 | isBooleanValid, |
@@ -8,6 +8,7 @@ import { | |||
8 | isIdOrUUIDValid, | 8 | isIdOrUUIDValid, |
9 | isIdValid, | 9 | isIdValid, |
10 | isUUIDValid, | 10 | isUUIDValid, |
11 | toArray, | ||
11 | toIntOrNull, | 12 | toIntOrNull, |
12 | toValueOrNull | 13 | toValueOrNull |
13 | } from '../../../helpers/custom-validators/misc' | 14 | } from '../../../helpers/custom-validators/misc' |
@@ -19,20 +20,19 @@ import { | |||
19 | isVideoDescriptionValid, | 20 | isVideoDescriptionValid, |
20 | isVideoExist, | 21 | isVideoExist, |
21 | isVideoFile, | 22 | isVideoFile, |
23 | isVideoFilterValid, | ||
22 | isVideoImage, | 24 | isVideoImage, |
23 | isVideoLanguageValid, | 25 | isVideoLanguageValid, |
24 | isVideoLicenceValid, | 26 | isVideoLicenceValid, |
25 | isVideoNameValid, | 27 | isVideoNameValid, |
26 | isVideoPrivacyValid, | 28 | isVideoPrivacyValid, |
27 | isVideoRatingTypeValid, | ||
28 | isVideoSupportValid, | 29 | isVideoSupportValid, |
29 | isVideoTagsValid | 30 | isVideoTagsValid |
30 | } from '../../../helpers/custom-validators/videos' | 31 | } from '../../../helpers/custom-validators/videos' |
31 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' | 32 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' |
32 | import { logger } from '../../../helpers/logger' | 33 | import { logger } from '../../../helpers/logger' |
33 | import { CONSTRAINTS_FIELDS } from '../../../initializers' | 34 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers' |
34 | import { VideoShareModel } from '../../../models/video/video-share' | 35 | import { authenticatePromiseIfNeeded } from '../../oauth' |
35 | import { authenticate } from '../../oauth' | ||
36 | import { areValidationErrors } from '../utils' | 36 | import { areValidationErrors } from '../utils' |
37 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 37 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
38 | import { VideoModel } from '../../../models/video/video' | 38 | import { VideoModel } from '../../../models/video/video' |
@@ -42,6 +42,8 @@ import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/vid | |||
42 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' | 42 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' |
43 | import { AccountModel } from '../../../models/account/account' | 43 | import { AccountModel } from '../../../models/account/account' |
44 | import { VideoFetchType } from '../../../helpers/video' | 44 | import { VideoFetchType } from '../../../helpers/video' |
45 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | ||
46 | import { getServerActor } from '../../../helpers/utils' | ||
45 | 47 | ||
46 | const videosAddValidator = getCommonVideoAttributes().concat([ | 48 | const videosAddValidator = getCommonVideoAttributes().concat([ |
47 | body('videofile') | 49 | body('videofile') |
@@ -69,7 +71,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([ | |||
69 | if (isAble === false) { | 71 | if (isAble === false) { |
70 | res.status(403) | 72 | res.status(403) |
71 | .json({ error: 'The user video quota is exceeded with this video.' }) | 73 | .json({ error: 'The user video quota is exceeded with this video.' }) |
72 | .end() | ||
73 | 74 | ||
74 | return cleanUpReqFiles(req) | 75 | return cleanUpReqFiles(req) |
75 | } | 76 | } |
@@ -82,7 +83,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([ | |||
82 | logger.error('Invalid input file in videosAddValidator.', { err }) | 83 | logger.error('Invalid input file in videosAddValidator.', { err }) |
83 | res.status(400) | 84 | res.status(400) |
84 | .json({ error: 'Invalid input file.' }) | 85 | .json({ error: 'Invalid input file.' }) |
85 | .end() | ||
86 | 86 | ||
87 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
88 | } | 88 | } |
@@ -120,7 +120,6 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([ | |||
120 | cleanUpReqFiles(req) | 120 | cleanUpReqFiles(req) |
121 | return res.status(409) | 121 | return res.status(409) |
122 | .json({ error: 'Cannot set "private" a video that was not private.' }) | 122 | .json({ error: 'Cannot set "private" a video that was not private.' }) |
123 | .end() | ||
124 | } | 123 | } |
125 | 124 | ||
126 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 125 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) |
@@ -129,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([ | |||
129 | } | 128 | } |
130 | ]) | 129 | ]) |
131 | 130 | ||
131 | async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
132 | const video: VideoModel = res.locals.video | ||
133 | |||
134 | // Anybody can watch local videos | ||
135 | if (video.isOwned() === true) return next() | ||
136 | |||
137 | // Logged user | ||
138 | if (res.locals.oauth) { | ||
139 | // Users can search or watch remote videos | ||
140 | if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next() | ||
141 | } | ||
142 | |||
143 | // Anybody can search or watch remote videos | ||
144 | if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next() | ||
145 | |||
146 | // Check our instance follows an actor that shared this video | ||
147 | const serverActor = await getServerActor() | ||
148 | if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() | ||
149 | |||
150 | return res.status(403) | ||
151 | .json({ | ||
152 | error: 'Cannot get this video regarding follow constraints.' | ||
153 | }) | ||
154 | } | ||
155 | |||
132 | const videosCustomGetValidator = (fetchType: VideoFetchType) => { | 156 | const videosCustomGetValidator = (fetchType: VideoFetchType) => { |
133 | return [ | 157 | return [ |
134 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 158 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
@@ -143,18 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => { | |||
143 | 167 | ||
144 | // Video private or blacklisted | 168 | // Video private or blacklisted |
145 | if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { | 169 | if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { |
146 | return authenticate(req, res, () => { | 170 | await authenticatePromiseIfNeeded(req, res) |
147 | const user: UserModel = res.locals.oauth.token.User | 171 | |
148 | 172 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null | |
149 | // Only the owner or a user that have blacklist rights can see the video | 173 | |
150 | if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { | 174 | // Only the owner or a user that have blacklist rights can see the video |
151 | return res.status(403) | 175 | if ( |
152 | .json({ error: 'Cannot get this private or blacklisted video.' }) | 176 | !user || |
153 | .end() | 177 | (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) |
154 | } | 178 | ) { |
155 | 179 | return res.status(403) | |
156 | return next() | 180 | .json({ error: 'Cannot get this private or blacklisted video.' }) |
157 | }) | 181 | } |
182 | |||
183 | return next() | ||
158 | } | 184 | } |
159 | 185 | ||
160 | // Video is public, anyone can access it | 186 | // Video is public, anyone can access it |
@@ -189,41 +215,6 @@ const videosRemoveValidator = [ | |||
189 | } | 215 | } |
190 | ] | 216 | ] |
191 | 217 | ||
192 | const videoRateValidator = [ | ||
193 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
194 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), | ||
195 | |||
196 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
197 | logger.debug('Checking videoRate parameters', { parameters: req.body }) | ||
198 | |||
199 | if (areValidationErrors(req, res)) return | ||
200 | if (!await isVideoExist(req.params.id, res)) return | ||
201 | |||
202 | return next() | ||
203 | } | ||
204 | ] | ||
205 | |||
206 | const videosShareValidator = [ | ||
207 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
208 | param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'), | ||
209 | |||
210 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
211 | logger.debug('Checking videoShare parameters', { parameters: req.params }) | ||
212 | |||
213 | if (areValidationErrors(req, res)) return | ||
214 | if (!await isVideoExist(req.params.id, res)) return | ||
215 | |||
216 | const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined) | ||
217 | if (!share) { | ||
218 | return res.status(404) | ||
219 | .end() | ||
220 | } | ||
221 | |||
222 | res.locals.videoShare = share | ||
223 | return next() | ||
224 | } | ||
225 | ] | ||
226 | |||
227 | const videosChangeOwnershipValidator = [ | 218 | const videosChangeOwnershipValidator = [ |
228 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 219 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), |
229 | 220 | ||
@@ -239,8 +230,8 @@ const videosChangeOwnershipValidator = [ | |||
239 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) | 230 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) |
240 | if (!nextOwner) { | 231 | if (!nextOwner) { |
241 | res.status(400) | 232 | res.status(400) |
242 | .type('json') | 233 | .json({ error: 'Changing video ownership to a remote account is not supported yet' }) |
243 | .end() | 234 | |
244 | return | 235 | return |
245 | } | 236 | } |
246 | res.locals.nextOwner = nextOwner | 237 | res.locals.nextOwner = nextOwner |
@@ -271,7 +262,7 @@ const videosTerminateChangeOwnershipValidator = [ | |||
271 | } else { | 262 | } else { |
272 | res.status(403) | 263 | res.status(403) |
273 | .json({ error: 'Ownership already accepted or refused' }) | 264 | .json({ error: 'Ownership already accepted or refused' }) |
274 | .end() | 265 | |
275 | return | 266 | return |
276 | } | 267 | } |
277 | } | 268 | } |
@@ -288,7 +279,7 @@ const videosAcceptChangeOwnershipValidator = [ | |||
288 | if (isAble === false) { | 279 | if (isAble === false) { |
289 | res.status(403) | 280 | res.status(403) |
290 | .json({ error: 'The user video quota is exceeded with this video.' }) | 281 | .json({ error: 'The user video quota is exceeded with this video.' }) |
291 | .end() | 282 | |
292 | return | 283 | return |
293 | } | 284 | } |
294 | 285 | ||
@@ -367,23 +358,68 @@ function getCommonVideoAttributes () { | |||
367 | ] as (ValidationChain | express.Handler)[] | 358 | ] as (ValidationChain | express.Handler)[] |
368 | } | 359 | } |
369 | 360 | ||
361 | const commonVideosFiltersValidator = [ | ||
362 | query('categoryOneOf') | ||
363 | .optional() | ||
364 | .customSanitizer(toArray) | ||
365 | .custom(isNumberArray).withMessage('Should have a valid one of category array'), | ||
366 | query('licenceOneOf') | ||
367 | .optional() | ||
368 | .customSanitizer(toArray) | ||
369 | .custom(isNumberArray).withMessage('Should have a valid one of licence array'), | ||
370 | query('languageOneOf') | ||
371 | .optional() | ||
372 | .customSanitizer(toArray) | ||
373 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | ||
374 | query('tagsOneOf') | ||
375 | .optional() | ||
376 | .customSanitizer(toArray) | ||
377 | .custom(isStringArray).withMessage('Should have a valid one of tags array'), | ||
378 | query('tagsAllOf') | ||
379 | .optional() | ||
380 | .customSanitizer(toArray) | ||
381 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | ||
382 | query('nsfw') | ||
383 | .optional() | ||
384 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | ||
385 | query('filter') | ||
386 | .optional() | ||
387 | .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), | ||
388 | |||
389 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
390 | logger.debug('Checking commons video filters query', { parameters: req.query }) | ||
391 | |||
392 | if (areValidationErrors(req, res)) return | ||
393 | |||
394 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
395 | if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) { | ||
396 | res.status(401) | ||
397 | .json({ error: 'You are not allowed to see all local videos.' }) | ||
398 | |||
399 | return | ||
400 | } | ||
401 | |||
402 | return next() | ||
403 | } | ||
404 | ] | ||
405 | |||
370 | // --------------------------------------------------------------------------- | 406 | // --------------------------------------------------------------------------- |
371 | 407 | ||
372 | export { | 408 | export { |
373 | videosAddValidator, | 409 | videosAddValidator, |
374 | videosUpdateValidator, | 410 | videosUpdateValidator, |
375 | videosGetValidator, | 411 | videosGetValidator, |
412 | checkVideoFollowConstraints, | ||
376 | videosCustomGetValidator, | 413 | videosCustomGetValidator, |
377 | videosRemoveValidator, | 414 | videosRemoveValidator, |
378 | videosShareValidator, | ||
379 | |||
380 | videoRateValidator, | ||
381 | 415 | ||
382 | videosChangeOwnershipValidator, | 416 | videosChangeOwnershipValidator, |
383 | videosTerminateChangeOwnershipValidator, | 417 | videosTerminateChangeOwnershipValidator, |
384 | videosAcceptChangeOwnershipValidator, | 418 | videosAcceptChangeOwnershipValidator, |
385 | 419 | ||
386 | getCommonVideoAttributes | 420 | getCommonVideoAttributes, |
421 | |||
422 | commonVideosFiltersValidator | ||
387 | } | 423 | } |
388 | 424 | ||
389 | // --------------------------------------------------------------------------- | 425 | // --------------------------------------------------------------------------- |
@@ -391,9 +427,10 @@ export { | |||
391 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { | 427 | function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { |
392 | if (req.body.scheduleUpdate) { | 428 | if (req.body.scheduleUpdate) { |
393 | if (!req.body.scheduleUpdate.updateAt) { | 429 | if (!req.body.scheduleUpdate.updateAt) { |
430 | logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') | ||
431 | |||
394 | res.status(400) | 432 | res.status(400) |
395 | .json({ error: 'Schedule update at is mandatory.' }) | 433 | .json({ error: 'Schedule update at is mandatory.' }) |
396 | .end() | ||
397 | 434 | ||
398 | return true | 435 | return true |
399 | } | 436 | } |