diff options
author | Chocobozzz <me@florianbigard.com> | 2020-07-07 10:57:04 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-07-10 14:02:41 +0200 |
commit | 57f6896f67cfc570cf3605dd94b0778101b2d9b9 (patch) | |
tree | b82d879c46868ce75ff76c3e4d4eed590a87f6c4 /server | |
parent | d95d15598847c7f020aa056e7e6e0c02d2bbf732 (diff) | |
download | PeerTube-57f6896f67cfc570cf3605dd94b0778101b2d9b9.tar.gz PeerTube-57f6896f67cfc570cf3605dd94b0778101b2d9b9.tar.zst PeerTube-57f6896f67cfc570cf3605dd94b0778101b2d9b9.zip |
Implement abuses check params
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/abuse.ts | 10 | ||||
-rw-r--r-- | server/helpers/custom-validators/abuses.ts | 17 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-comments.ts | 81 | ||||
-rw-r--r-- | server/helpers/middlewares/abuses.ts | 13 | ||||
-rw-r--r-- | server/helpers/middlewares/accounts.ts | 4 | ||||
-rw-r--r-- | server/middlewares/validators/abuse.ts | 74 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-comments.ts | 70 | ||||
-rw-r--r-- | server/models/abuse/abuse.ts | 91 | ||||
-rw-r--r-- | server/models/abuse/video-comment-abuse.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 65 | ||||
-rw-r--r-- | server/models/video/video.ts | 4 | ||||
-rw-r--r-- | server/tests/api/check-params/abuses.ts | 271 | ||||
-rw-r--r-- | server/tests/api/check-params/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/check-params/video-abuses.ts | 6 | ||||
-rw-r--r-- | server/types/models/moderation/abuse.ts | 9 | ||||
-rw-r--r-- | server/typings/express/index.d.ts | 1 |
16 files changed, 588 insertions, 131 deletions
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts index ee046cb3a..38808021d 100644 --- a/server/controllers/api/abuse.ts +++ b/server/controllers/api/abuse.ts | |||
@@ -23,7 +23,7 @@ import { AccountModel } from '../../models/account/account' | |||
23 | 23 | ||
24 | const abuseRouter = express.Router() | 24 | const abuseRouter = express.Router() |
25 | 25 | ||
26 | abuseRouter.get('/abuse', | 26 | abuseRouter.get('/', |
27 | authenticate, | 27 | authenticate, |
28 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | 28 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
29 | paginationValidator, | 29 | paginationValidator, |
@@ -33,18 +33,18 @@ abuseRouter.get('/abuse', | |||
33 | abuseListValidator, | 33 | abuseListValidator, |
34 | asyncMiddleware(listAbuses) | 34 | asyncMiddleware(listAbuses) |
35 | ) | 35 | ) |
36 | abuseRouter.put('/:videoId/abuse/:id', | 36 | abuseRouter.put('/:id', |
37 | authenticate, | 37 | authenticate, |
38 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | 38 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
39 | asyncMiddleware(abuseUpdateValidator), | 39 | asyncMiddleware(abuseUpdateValidator), |
40 | asyncRetryTransactionMiddleware(updateAbuse) | 40 | asyncRetryTransactionMiddleware(updateAbuse) |
41 | ) | 41 | ) |
42 | abuseRouter.post('/:videoId/abuse', | 42 | abuseRouter.post('/', |
43 | authenticate, | 43 | authenticate, |
44 | asyncMiddleware(abuseReportValidator), | 44 | asyncMiddleware(abuseReportValidator), |
45 | asyncRetryTransactionMiddleware(reportAbuse) | 45 | asyncRetryTransactionMiddleware(reportAbuse) |
46 | ) | 46 | ) |
47 | abuseRouter.delete('/:videoId/abuse/:id', | 47 | abuseRouter.delete('/:id', |
48 | authenticate, | 48 | authenticate, |
49 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | 49 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
50 | asyncMiddleware(abuseGetValidator), | 50 | asyncMiddleware(abuseGetValidator), |
@@ -74,7 +74,7 @@ async function listAbuses (req: express.Request, res: express.Response) { | |||
74 | count: req.query.count, | 74 | count: req.query.count, |
75 | sort: req.query.sort, | 75 | sort: req.query.sort, |
76 | id: req.query.id, | 76 | id: req.query.id, |
77 | filter: 'video', | 77 | filter: req.query.filter, |
78 | predefinedReason: req.query.predefinedReason, | 78 | predefinedReason: req.query.predefinedReason, |
79 | search: req.query.search, | 79 | search: req.query.search, |
80 | state: req.query.state, | 80 | state: req.query.state, |
diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts index a6a895c65..c21468caa 100644 --- a/server/helpers/custom-validators/abuses.ts +++ b/server/helpers/custom-validators/abuses.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models' | 2 | import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs, AbuseCreate } from '@shared/models' |
3 | import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants' | 3 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
4 | import { exists, isArray } from './misc' | 4 | import { exists, isArray } from './misc' |
5 | 5 | ||
6 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES | 6 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES |
@@ -13,7 +13,11 @@ function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) { | |||
13 | return exists(value) && value in abusePredefinedReasonsMap | 13 | return exists(value) && value in abusePredefinedReasonsMap |
14 | } | 14 | } |
15 | 15 | ||
16 | function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) { | 16 | function isAbuseFilterValid (value: AbuseFilter) { |
17 | return value === 'video' || value === 'comment' || value === 'account' | ||
18 | } | ||
19 | |||
20 | function areAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) { | ||
17 | return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap) | 21 | return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap) |
18 | } | 22 | } |
19 | 23 | ||
@@ -22,7 +26,9 @@ function isAbuseTimestampValid (value: number) { | |||
22 | } | 26 | } |
23 | 27 | ||
24 | function isAbuseTimestampCoherent (endAt: number, { req }) { | 28 | function isAbuseTimestampCoherent (endAt: number, { req }) { |
25 | return exists(req.body.startAt) && endAt > req.body.startAt | 29 | const startAt = (req.body as AbuseCreate).video.startAt |
30 | |||
31 | return exists(startAt) && endAt > startAt | ||
26 | } | 32 | } |
27 | 33 | ||
28 | function isAbuseModerationCommentValid (value: string) { | 34 | function isAbuseModerationCommentValid (value: string) { |
@@ -44,8 +50,9 @@ function isAbuseVideoIsValid (value: AbuseVideoIs) { | |||
44 | 50 | ||
45 | export { | 51 | export { |
46 | isAbuseReasonValid, | 52 | isAbuseReasonValid, |
53 | isAbuseFilterValid, | ||
47 | isAbusePredefinedReasonValid, | 54 | isAbusePredefinedReasonValid, |
48 | isAbusePredefinedReasonsValid, | 55 | areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid, |
49 | isAbuseTimestampValid, | 56 | isAbuseTimestampValid, |
50 | isAbuseTimestampCoherent, | 57 | isAbuseTimestampCoherent, |
51 | isAbuseModerationCommentValid, | 58 | isAbuseModerationCommentValid, |
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts index 846f28b17..a01680cbe 100644 --- a/server/helpers/custom-validators/video-comments.ts +++ b/server/helpers/custom-validators/video-comments.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import 'multer' | 1 | import * as express from 'express' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
3 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
5 | import { MVideoId } from '@server/types/models' | ||
4 | 6 | ||
5 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS | 7 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS |
6 | 8 | ||
@@ -8,8 +10,83 @@ function isValidVideoCommentText (value: string) { | |||
8 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) | 10 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) |
9 | } | 11 | } |
10 | 12 | ||
13 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
14 | const id = parseInt(idArg + '', 10) | ||
15 | const videoComment = await VideoCommentModel.loadById(id) | ||
16 | |||
17 | if (!videoComment) { | ||
18 | res.status(404) | ||
19 | .json({ error: 'Video comment thread not found' }) | ||
20 | .end() | ||
21 | |||
22 | return false | ||
23 | } | ||
24 | |||
25 | if (videoComment.videoId !== video.id) { | ||
26 | res.status(400) | ||
27 | .json({ error: 'Video comment is not associated to this video.' }) | ||
28 | .end() | ||
29 | |||
30 | return false | ||
31 | } | ||
32 | |||
33 | if (videoComment.inReplyToCommentId !== null) { | ||
34 | res.status(400) | ||
35 | .json({ error: 'Video comment is not a thread.' }) | ||
36 | .end() | ||
37 | |||
38 | return false | ||
39 | } | ||
40 | |||
41 | res.locals.videoCommentThread = videoComment | ||
42 | return true | ||
43 | } | ||
44 | |||
45 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
46 | const id = parseInt(idArg + '', 10) | ||
47 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
48 | |||
49 | if (!videoComment) { | ||
50 | res.status(404) | ||
51 | .json({ error: 'Video comment thread not found' }) | ||
52 | .end() | ||
53 | |||
54 | return false | ||
55 | } | ||
56 | |||
57 | if (videoComment.videoId !== video.id) { | ||
58 | res.status(400) | ||
59 | .json({ error: 'Video comment is not associated to this video.' }) | ||
60 | .end() | ||
61 | |||
62 | return false | ||
63 | } | ||
64 | |||
65 | res.locals.videoCommentFull = videoComment | ||
66 | return true | ||
67 | } | ||
68 | |||
69 | async function doesCommentIdExist (idArg: number | string, res: express.Response) { | ||
70 | const id = parseInt(idArg + '', 10) | ||
71 | const videoComment = await VideoCommentModel.loadById(id) | ||
72 | |||
73 | if (!videoComment) { | ||
74 | res.status(404) | ||
75 | .json({ error: 'Video comment thread not found' }) | ||
76 | |||
77 | return false | ||
78 | } | ||
79 | |||
80 | res.locals.videoComment = videoComment | ||
81 | |||
82 | return true | ||
83 | } | ||
84 | |||
11 | // --------------------------------------------------------------------------- | 85 | // --------------------------------------------------------------------------- |
12 | 86 | ||
13 | export { | 87 | export { |
14 | isValidVideoCommentText | 88 | isValidVideoCommentText, |
89 | doesVideoCommentThreadExist, | ||
90 | doesVideoCommentExist, | ||
91 | doesCommentIdExist | ||
15 | } | 92 | } |
diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts index 3906f6760..b102273a2 100644 --- a/server/helpers/middlewares/abuses.ts +++ b/server/helpers/middlewares/abuses.ts | |||
@@ -17,7 +17,6 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri | |||
17 | if (abuse === null) { | 17 | if (abuse === null) { |
18 | res.status(404) | 18 | res.status(404) |
19 | .json({ error: 'Video abuse not found' }) | 19 | .json({ error: 'Video abuse not found' }) |
20 | .end() | ||
21 | 20 | ||
22 | return false | 21 | return false |
23 | } | 22 | } |
@@ -26,8 +25,18 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri | |||
26 | return true | 25 | return true |
27 | } | 26 | } |
28 | 27 | ||
29 | async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { | 28 | async function doesAbuseExist (abuseId: number | string, res: Response) { |
29 | const abuse = await AbuseModel.loadById(parseInt(abuseId + '', 10)) | ||
30 | 30 | ||
31 | if (!abuse) { | ||
32 | res.status(404) | ||
33 | .json({ error: 'Video abuse not found' }) | ||
34 | |||
35 | return false | ||
36 | } | ||
37 | |||
38 | res.locals.abuse = abuse | ||
39 | return true | ||
31 | } | 40 | } |
32 | 41 | ||
33 | // --------------------------------------------------------------------------- | 42 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts index bddea7eaa..29b4ed1a6 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/helpers/middlewares/accounts.ts | |||
@@ -3,8 +3,8 @@ import { AccountModel } from '../../models/account/account' | |||
3 | import * as Bluebird from 'bluebird' | 3 | import * as Bluebird from 'bluebird' |
4 | import { MAccountDefault } from '../../types/models' | 4 | import { MAccountDefault } from '../../types/models' |
5 | 5 | ||
6 | function doesAccountIdExist (id: number, res: Response, sendNotFound = true) { | 6 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { |
7 | const promise = AccountModel.load(id) | 7 | const promise = AccountModel.load(parseInt(id + '', 10)) |
8 | 8 | ||
9 | return doesAccountExist(promise, res, sendNotFound) | 9 | return doesAccountExist(promise, res, sendNotFound) |
10 | } | 10 | } |
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index f098e2ff9..048dbead0 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { | 3 | import { |
4 | isAbuseFilterValid, | ||
4 | isAbuseModerationCommentValid, | 5 | isAbuseModerationCommentValid, |
5 | isAbusePredefinedReasonsValid, | 6 | isAbusePredefinedReasonsValid, |
6 | isAbusePredefinedReasonValid, | 7 | isAbusePredefinedReasonValid, |
@@ -11,29 +12,28 @@ import { | |||
11 | isAbuseVideoIsValid | 12 | isAbuseVideoIsValid |
12 | } from '@server/helpers/custom-validators/abuses' | 13 | } from '@server/helpers/custom-validators/abuses' |
13 | import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' | 14 | import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' |
15 | import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments' | ||
14 | import { logger } from '@server/helpers/logger' | 16 | import { logger } from '@server/helpers/logger' |
15 | import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares' | 17 | import { doesAbuseExist, doesAccountIdExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares' |
18 | import { AbuseCreate } from '@shared/models' | ||
16 | import { areValidationErrors } from './utils' | 19 | import { areValidationErrors } from './utils' |
17 | 20 | ||
18 | const abuseReportValidator = [ | 21 | const abuseReportValidator = [ |
19 | param('videoId') | 22 | body('account.id') |
23 | .optional() | ||
24 | .custom(isIdValid) | ||
25 | .withMessage('Should have a valid accountId'), | ||
26 | |||
27 | body('video.id') | ||
28 | .optional() | ||
20 | .custom(isIdOrUUIDValid) | 29 | .custom(isIdOrUUIDValid) |
21 | .not() | ||
22 | .isEmpty() | ||
23 | .withMessage('Should have a valid videoId'), | 30 | .withMessage('Should have a valid videoId'), |
24 | body('reason') | 31 | body('video.startAt') |
25 | .custom(isAbuseReasonValid) | ||
26 | .withMessage('Should have a valid reason'), | ||
27 | body('predefinedReasons') | ||
28 | .optional() | ||
29 | .custom(isAbusePredefinedReasonsValid) | ||
30 | .withMessage('Should have a valid list of predefined reasons'), | ||
31 | body('startAt') | ||
32 | .optional() | 32 | .optional() |
33 | .customSanitizer(toIntOrNull) | 33 | .customSanitizer(toIntOrNull) |
34 | .custom(isAbuseTimestampValid) | 34 | .custom(isAbuseTimestampValid) |
35 | .withMessage('Should have valid starting time value'), | 35 | .withMessage('Should have valid starting time value'), |
36 | body('endAt') | 36 | body('video.endAt') |
37 | .optional() | 37 | .optional() |
38 | .customSanitizer(toIntOrNull) | 38 | .customSanitizer(toIntOrNull) |
39 | .custom(isAbuseTimestampValid) | 39 | .custom(isAbuseTimestampValid) |
@@ -42,47 +42,70 @@ const abuseReportValidator = [ | |||
42 | .custom(isAbuseTimestampCoherent) | 42 | .custom(isAbuseTimestampCoherent) |
43 | .withMessage('Should have a startAt timestamp beginning before endAt'), | 43 | .withMessage('Should have a startAt timestamp beginning before endAt'), |
44 | 44 | ||
45 | body('comment.id') | ||
46 | .optional() | ||
47 | .custom(isIdValid) | ||
48 | .withMessage('Should have a valid commentId'), | ||
49 | |||
50 | body('reason') | ||
51 | .custom(isAbuseReasonValid) | ||
52 | .withMessage('Should have a valid reason'), | ||
53 | |||
54 | body('predefinedReasons') | ||
55 | .optional() | ||
56 | .custom(isAbusePredefinedReasonsValid) | ||
57 | .withMessage('Should have a valid list of predefined reasons'), | ||
58 | |||
45 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
46 | logger.debug('Checking abuseReport parameters', { parameters: req.body }) | 60 | logger.debug('Checking abuseReport parameters', { parameters: req.body }) |
47 | 61 | ||
48 | if (areValidationErrors(req, res)) return | 62 | if (areValidationErrors(req, res)) return |
49 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
50 | 63 | ||
51 | // TODO: check comment or video (exlusive) | 64 | const body: AbuseCreate = req.body |
65 | |||
66 | if (body.video?.id && !await doesVideoExist(body.video.id, res)) return | ||
67 | if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return | ||
68 | if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return | ||
69 | |||
70 | if (!body.video?.id && !body.account?.id && !body.comment?.id) { | ||
71 | res.status(400) | ||
72 | .json({ error: 'video id or account id or comment id is required.' }) | ||
73 | |||
74 | return | ||
75 | } | ||
52 | 76 | ||
53 | return next() | 77 | return next() |
54 | } | 78 | } |
55 | ] | 79 | ] |
56 | 80 | ||
57 | const abuseGetValidator = [ | 81 | const abuseGetValidator = [ |
58 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
59 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | 82 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), |
60 | 83 | ||
61 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 84 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
62 | logger.debug('Checking abuseGetValidator parameters', { parameters: req.body }) | 85 | logger.debug('Checking abuseGetValidator parameters', { parameters: req.body }) |
63 | 86 | ||
64 | if (areValidationErrors(req, res)) return | 87 | if (areValidationErrors(req, res)) return |
65 | // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return | 88 | if (!await doesAbuseExist(req.params.id, res)) return |
66 | 89 | ||
67 | return next() | 90 | return next() |
68 | } | 91 | } |
69 | ] | 92 | ] |
70 | 93 | ||
71 | const abuseUpdateValidator = [ | 94 | const abuseUpdateValidator = [ |
72 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
73 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), | 95 | param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'), |
96 | |||
74 | body('state') | 97 | body('state') |
75 | .optional() | 98 | .optional() |
76 | .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), | 99 | .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'), |
77 | body('moderationComment') | 100 | body('moderationComment') |
78 | .optional() | 101 | .optional() |
79 | .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'), | 102 | .custom(isAbuseModerationCommentValid).withMessage('Should have a valid moderation comment'), |
80 | 103 | ||
81 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 104 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
82 | logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body }) | 105 | logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body }) |
83 | 106 | ||
84 | if (areValidationErrors(req, res)) return | 107 | if (areValidationErrors(req, res)) return |
85 | // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return | 108 | if (!await doesAbuseExist(req.params.id, res)) return |
86 | 109 | ||
87 | return next() | 110 | return next() |
88 | } | 111 | } |
@@ -92,6 +115,10 @@ const abuseListValidator = [ | |||
92 | query('id') | 115 | query('id') |
93 | .optional() | 116 | .optional() |
94 | .custom(isIdValid).withMessage('Should have a valid id'), | 117 | .custom(isIdValid).withMessage('Should have a valid id'), |
118 | query('filter') | ||
119 | .optional() | ||
120 | .custom(isAbuseFilterValid) | ||
121 | .withMessage('Should have a valid filter'), | ||
95 | query('predefinedReason') | 122 | query('predefinedReason') |
96 | .optional() | 123 | .optional() |
97 | .custom(isAbusePredefinedReasonValid) | 124 | .custom(isAbusePredefinedReasonValid) |
@@ -151,10 +178,7 @@ const videoAbuseReportValidator = [ | |||
151 | .optional() | 178 | .optional() |
152 | .customSanitizer(toIntOrNull) | 179 | .customSanitizer(toIntOrNull) |
153 | .custom(isAbuseTimestampValid) | 180 | .custom(isAbuseTimestampValid) |
154 | .withMessage('Should have valid ending time value') | 181 | .withMessage('Should have valid ending time value'), |
155 | .bail() | ||
156 | .custom(isAbuseTimestampCoherent) | ||
157 | .withMessage('Should have a startAt timestamp beginning before endAt'), | ||
158 | 182 | ||
159 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 183 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
160 | logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) | 184 | logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) |
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index ef019fcf9..77f5c6ff3 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -3,13 +3,16 @@ import { body, param } from 'express-validator' | |||
3 | import { MUserAccountUrl } from '@server/types/models' | 3 | import { MUserAccountUrl } from '@server/types/models' |
4 | import { UserRight } from '../../../../shared' | 4 | import { UserRight } from '../../../../shared' |
5 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | 5 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' |
6 | import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' | 6 | import { |
7 | doesVideoCommentExist, | ||
8 | doesVideoCommentThreadExist, | ||
9 | isValidVideoCommentText | ||
10 | } from '../../../helpers/custom-validators/video-comments' | ||
7 | import { logger } from '../../../helpers/logger' | 11 | import { logger } from '../../../helpers/logger' |
8 | import { doesVideoExist } from '../../../helpers/middlewares' | 12 | import { doesVideoExist } from '../../../helpers/middlewares' |
9 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' | 13 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' |
10 | import { Hooks } from '../../../lib/plugins/hooks' | 14 | import { Hooks } from '../../../lib/plugins/hooks' |
11 | import { VideoCommentModel } from '../../../models/video/video-comment' | 15 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' |
12 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../types/models/video' | ||
13 | import { areValidationErrors } from '../utils' | 16 | import { areValidationErrors } from '../utils' |
14 | 17 | ||
15 | const listVideoCommentThreadsValidator = [ | 18 | const listVideoCommentThreadsValidator = [ |
@@ -120,67 +123,10 @@ export { | |||
120 | 123 | ||
121 | // --------------------------------------------------------------------------- | 124 | // --------------------------------------------------------------------------- |
122 | 125 | ||
123 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
124 | const id = parseInt(idArg + '', 10) | ||
125 | const videoComment = await VideoCommentModel.loadById(id) | ||
126 | |||
127 | if (!videoComment) { | ||
128 | res.status(404) | ||
129 | .json({ error: 'Video comment thread not found' }) | ||
130 | .end() | ||
131 | |||
132 | return false | ||
133 | } | ||
134 | |||
135 | if (videoComment.videoId !== video.id) { | ||
136 | res.status(400) | ||
137 | .json({ error: 'Video comment is not associated to this video.' }) | ||
138 | .end() | ||
139 | |||
140 | return false | ||
141 | } | ||
142 | |||
143 | if (videoComment.inReplyToCommentId !== null) { | ||
144 | res.status(400) | ||
145 | .json({ error: 'Video comment is not a thread.' }) | ||
146 | .end() | ||
147 | |||
148 | return false | ||
149 | } | ||
150 | |||
151 | res.locals.videoCommentThread = videoComment | ||
152 | return true | ||
153 | } | ||
154 | |||
155 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
156 | const id = parseInt(idArg + '', 10) | ||
157 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
158 | |||
159 | if (!videoComment) { | ||
160 | res.status(404) | ||
161 | .json({ error: 'Video comment thread not found' }) | ||
162 | .end() | ||
163 | |||
164 | return false | ||
165 | } | ||
166 | |||
167 | if (videoComment.videoId !== video.id) { | ||
168 | res.status(400) | ||
169 | .json({ error: 'Video comment is not associated to this video.' }) | ||
170 | .end() | ||
171 | |||
172 | return false | ||
173 | } | ||
174 | |||
175 | res.locals.videoCommentFull = videoComment | ||
176 | return true | ||
177 | } | ||
178 | |||
179 | function isVideoCommentsEnabled (video: MVideo, res: express.Response) { | 126 | function isVideoCommentsEnabled (video: MVideo, res: express.Response) { |
180 | if (video.commentsEnabled !== true) { | 127 | if (video.commentsEnabled !== true) { |
181 | res.status(409) | 128 | res.status(409) |
182 | .json({ error: 'Video comments are disabled for this video.' }) | 129 | .json({ error: 'Video comments are disabled for this video.' }) |
183 | .end() | ||
184 | 130 | ||
185 | return false | 131 | return false |
186 | } | 132 | } |
@@ -192,7 +138,7 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC | |||
192 | if (videoComment.isDeleted()) { | 138 | if (videoComment.isDeleted()) { |
193 | res.status(409) | 139 | res.status(409) |
194 | .json({ error: 'This comment is already deleted' }) | 140 | .json({ error: 'This comment is already deleted' }) |
195 | .end() | 141 | |
196 | return false | 142 | return false |
197 | } | 143 | } |
198 | 144 | ||
@@ -240,7 +186,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon | |||
240 | if (!acceptedResult || acceptedResult.accepted !== true) { | 186 | if (!acceptedResult || acceptedResult.accepted !== true) { |
241 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) | 187 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) |
242 | res.status(403) | 188 | res.status(403) |
243 | .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) | 189 | .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) |
244 | 190 | ||
245 | return false | 191 | return false |
246 | } | 192 | } |
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 4f99f9c9b..087c77bd3 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -19,16 +19,17 @@ import { | |||
19 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' | 19 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' |
20 | import { | 20 | import { |
21 | Abuse, | 21 | Abuse, |
22 | AbuseFilter, | ||
22 | AbuseObject, | 23 | AbuseObject, |
23 | AbusePredefinedReasons, | 24 | AbusePredefinedReasons, |
24 | abusePredefinedReasonsMap, | 25 | abusePredefinedReasonsMap, |
25 | AbusePredefinedReasonsString, | 26 | AbusePredefinedReasonsString, |
26 | AbuseState, | 27 | AbuseState, |
27 | AbuseVideoIs, | 28 | AbuseVideoIs, |
28 | VideoAbuse | 29 | VideoAbuse, |
30 | VideoCommentAbuse | ||
29 | } from '@shared/models' | 31 | } from '@shared/models' |
30 | import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter' | 32 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
31 | import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants' | ||
32 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' | 33 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' |
33 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 34 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
34 | import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' | 35 | import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' |
@@ -38,6 +39,7 @@ import { VideoBlacklistModel } from '../video/video-blacklist' | |||
38 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | 39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
39 | import { VideoAbuseModel } from './video-abuse' | 40 | import { VideoAbuseModel } from './video-abuse' |
40 | import { VideoCommentAbuseModel } from './video-comment-abuse' | 41 | import { VideoCommentAbuseModel } from './video-comment-abuse' |
42 | import { VideoCommentModel } from '../video/video-comment' | ||
41 | 43 | ||
42 | export enum ScopeNames { | 44 | export enum ScopeNames { |
43 | FOR_API = 'FOR_API' | 45 | FOR_API = 'FOR_API' |
@@ -66,19 +68,18 @@ export enum ScopeNames { | |||
66 | serverAccountId: number | 68 | serverAccountId: number |
67 | userAccountId: number | 69 | userAccountId: number |
68 | }) => { | 70 | }) => { |
69 | const onlyBlacklisted = options.videoIs === 'blacklisted' | 71 | const whereAnd: WhereOptions[] = [] |
70 | const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel) | ||
71 | 72 | ||
72 | const where = { | 73 | whereAnd.push({ |
73 | reporterAccountId: { | 74 | reporterAccountId: { |
74 | [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') | 75 | [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') |
75 | } | 76 | } |
76 | } | 77 | }) |
77 | 78 | ||
78 | if (options.search) { | 79 | if (options.search) { |
79 | const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%') | 80 | const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%') |
80 | 81 | ||
81 | Object.assign(where, { | 82 | whereAnd.push({ |
82 | [Op.or]: [ | 83 | [Op.or]: [ |
83 | { | 84 | { |
84 | [Op.and]: [ | 85 | [Op.and]: [ |
@@ -110,11 +111,11 @@ export enum ScopeNames { | |||
110 | }) | 111 | }) |
111 | } | 112 | } |
112 | 113 | ||
113 | if (options.id) Object.assign(where, { id: options.id }) | 114 | if (options.id) whereAnd.push({ id: options.id }) |
114 | if (options.state) Object.assign(where, { state: options.state }) | 115 | if (options.state) whereAnd.push({ state: options.state }) |
115 | 116 | ||
116 | if (options.videoIs === 'deleted') { | 117 | if (options.videoIs === 'deleted') { |
117 | Object.assign(where, { | 118 | whereAnd.push({ |
118 | '$VideoAbuse.deletedVideo$': { | 119 | '$VideoAbuse.deletedVideo$': { |
119 | [Op.not]: null | 120 | [Op.not]: null |
120 | } | 121 | } |
@@ -122,13 +123,23 @@ export enum ScopeNames { | |||
122 | } | 123 | } |
123 | 124 | ||
124 | if (options.predefinedReasonId) { | 125 | if (options.predefinedReasonId) { |
125 | Object.assign(where, { | 126 | whereAnd.push({ |
126 | predefinedReasons: { | 127 | predefinedReasons: { |
127 | [Op.contains]: [ options.predefinedReasonId ] | 128 | [Op.contains]: [ options.predefinedReasonId ] |
128 | } | 129 | } |
129 | }) | 130 | }) |
130 | } | 131 | } |
131 | 132 | ||
133 | if (options.filter === 'account') { | ||
134 | whereAnd.push({ | ||
135 | videoId: null, | ||
136 | commentId: null | ||
137 | }) | ||
138 | } | ||
139 | |||
140 | const onlyBlacklisted = options.videoIs === 'blacklisted' | ||
141 | const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel) | ||
142 | |||
132 | return { | 143 | return { |
133 | attributes: { | 144 | attributes: { |
134 | include: [ | 145 | include: [ |
@@ -223,6 +234,23 @@ export enum ScopeNames { | |||
223 | where: searchAttribute(options.searchReportee, 'name') | 234 | where: searchAttribute(options.searchReportee, 'name') |
224 | }, | 235 | }, |
225 | { | 236 | { |
237 | model: VideoCommentAbuseModel.unscoped(), | ||
238 | required: options.filter === 'comment', | ||
239 | include: [ | ||
240 | { | ||
241 | model: VideoCommentModel.unscoped(), | ||
242 | required: false, | ||
243 | include: [ | ||
244 | { | ||
245 | model: VideoModel.unscoped(), | ||
246 | attributes: [ 'name', 'id', 'uuid' ], | ||
247 | required: true | ||
248 | } | ||
249 | ] | ||
250 | } | ||
251 | ] | ||
252 | }, | ||
253 | { | ||
226 | model: VideoAbuseModel, | 254 | model: VideoAbuseModel, |
227 | required: options.filter === 'video' || !!options.videoIs || videoRequired, | 255 | required: options.filter === 'video' || !!options.videoIs || videoRequired, |
228 | include: [ | 256 | include: [ |
@@ -241,8 +269,7 @@ export enum ScopeNames { | |||
241 | include: [ | 269 | include: [ |
242 | { | 270 | { |
243 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | 271 | model: AccountModel.scope(AccountScopeNames.SUMMARY), |
244 | required: true, | 272 | required: true |
245 | where: searchAttribute(options.searchReportee, 'name') | ||
246 | } | 273 | } |
247 | ] | 274 | ] |
248 | }, | 275 | }, |
@@ -256,7 +283,9 @@ export enum ScopeNames { | |||
256 | ] | 283 | ] |
257 | } | 284 | } |
258 | ], | 285 | ], |
259 | where | 286 | where: { |
287 | [Op.and]: whereAnd | ||
288 | } | ||
260 | } | 289 | } |
261 | } | 290 | } |
262 | })) | 291 | })) |
@@ -348,6 +377,7 @@ export class AbuseModel extends Model<AbuseModel> { | |||
348 | }) | 377 | }) |
349 | VideoAbuse: VideoAbuseModel | 378 | VideoAbuse: VideoAbuseModel |
350 | 379 | ||
380 | // FIXME: deprecated in 2.3. Remove these validators | ||
351 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> { | 381 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> { |
352 | const videoWhere: WhereOptions = {} | 382 | const videoWhere: WhereOptions = {} |
353 | 383 | ||
@@ -369,6 +399,16 @@ export class AbuseModel extends Model<AbuseModel> { | |||
369 | return AbuseModel.findOne(query) | 399 | return AbuseModel.findOne(query) |
370 | } | 400 | } |
371 | 401 | ||
402 | static loadById (id: number): Bluebird<MAbuse> { | ||
403 | const query = { | ||
404 | where: { | ||
405 | id | ||
406 | } | ||
407 | } | ||
408 | |||
409 | return AbuseModel.findOne(query) | ||
410 | } | ||
411 | |||
372 | static listForApi (parameters: { | 412 | static listForApi (parameters: { |
373 | start: number | 413 | start: number |
374 | count: number | 414 | count: number |
@@ -454,6 +494,7 @@ export class AbuseModel extends Model<AbuseModel> { | |||
454 | const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number | 494 | const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number |
455 | 495 | ||
456 | let video: VideoAbuse | 496 | let video: VideoAbuse |
497 | let comment: VideoCommentAbuse | ||
457 | 498 | ||
458 | if (this.VideoAbuse) { | 499 | if (this.VideoAbuse) { |
459 | const abuseModel = this.VideoAbuse | 500 | const abuseModel = this.VideoAbuse |
@@ -475,6 +516,24 @@ export class AbuseModel extends Model<AbuseModel> { | |||
475 | } | 516 | } |
476 | } | 517 | } |
477 | 518 | ||
519 | if (this.VideoCommentAbuse) { | ||
520 | const abuseModel = this.VideoCommentAbuse | ||
521 | const entity = abuseModel.VideoComment || abuseModel.deletedComment | ||
522 | |||
523 | comment = { | ||
524 | id: entity.id, | ||
525 | text: entity.text, | ||
526 | |||
527 | deleted: !abuseModel.VideoComment, | ||
528 | |||
529 | video: { | ||
530 | id: entity.Video.id, | ||
531 | name: entity.Video.name, | ||
532 | uuid: entity.Video.uuid | ||
533 | } | ||
534 | } | ||
535 | } | ||
536 | |||
478 | return { | 537 | return { |
479 | id: this.id, | 538 | id: this.id, |
480 | reason: this.reason, | 539 | reason: this.reason, |
@@ -490,7 +549,7 @@ export class AbuseModel extends Model<AbuseModel> { | |||
490 | moderationComment: this.moderationComment, | 549 | moderationComment: this.moderationComment, |
491 | 550 | ||
492 | video, | 551 | video, |
493 | comment: null, | 552 | comment, |
494 | 553 | ||
495 | createdAt: this.createdAt, | 554 | createdAt: this.createdAt, |
496 | updatedAt: this.updatedAt, | 555 | updatedAt: this.updatedAt, |
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts index b4cc2762e..de9f4d5fd 100644 --- a/server/models/abuse/video-comment-abuse.ts +++ b/server/models/abuse/video-comment-abuse.ts | |||
@@ -25,7 +25,7 @@ export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> { | |||
25 | @AllowNull(true) | 25 | @AllowNull(true) |
26 | @Default(null) | 26 | @Default(null) |
27 | @Column(DataType.JSONB) | 27 | @Column(DataType.JSONB) |
28 | deletedComment: VideoComment | 28 | deletedComment: VideoComment & { Video: { name: string, id: number, uuid: string }} |
29 | 29 | ||
30 | @ForeignKey(() => AbuseModel) | 30 | @ForeignKey(() => AbuseModel) |
31 | @Column | 31 | @Column |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 90625d987..fb6078ed8 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,7 +1,22 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { uniq } from 'lodash' | 2 | import { uniq } from 'lodash' |
3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | 3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { |
5 | AllowNull, | ||
6 | BeforeDestroy, | ||
7 | BelongsTo, | ||
8 | Column, | ||
9 | CreatedAt, | ||
10 | DataType, | ||
11 | ForeignKey, | ||
12 | HasMany, | ||
13 | Is, | ||
14 | Model, | ||
15 | Scopes, | ||
16 | Table, | ||
17 | UpdatedAt | ||
18 | } from 'sequelize-typescript' | ||
19 | import { logger } from '@server/helpers/logger' | ||
5 | import { getServerActor } from '@server/models/application/application' | 20 | import { getServerActor } from '@server/models/application/application' |
6 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 21 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
7 | import { VideoPrivacy } from '@shared/models' | 22 | import { VideoPrivacy } from '@shared/models' |
@@ -24,6 +39,7 @@ import { | |||
24 | MCommentOwnerVideoReply, | 39 | MCommentOwnerVideoReply, |
25 | MVideoImmutable | 40 | MVideoImmutable |
26 | } from '../../types/models/video' | 41 | } from '../../types/models/video' |
42 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
27 | import { AccountModel } from '../account/account' | 43 | import { AccountModel } from '../account/account' |
28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 44 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
29 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' | 45 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' |
@@ -224,6 +240,53 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
224 | }) | 240 | }) |
225 | Account: AccountModel | 241 | Account: AccountModel |
226 | 242 | ||
243 | @HasMany(() => VideoCommentAbuseModel, { | ||
244 | foreignKey: { | ||
245 | name: 'commentId', | ||
246 | allowNull: true | ||
247 | }, | ||
248 | onDelete: 'set null' | ||
249 | }) | ||
250 | CommentAbuses: VideoCommentAbuseModel[] | ||
251 | |||
252 | @BeforeDestroy | ||
253 | static async saveEssentialDataToAbuses (instance: VideoCommentModel, options) { | ||
254 | const tasks: Promise<any>[] = [] | ||
255 | |||
256 | if (!Array.isArray(instance.CommentAbuses)) { | ||
257 | instance.CommentAbuses = await instance.$get('CommentAbuses') | ||
258 | |||
259 | if (instance.CommentAbuses.length === 0) return undefined | ||
260 | } | ||
261 | |||
262 | if (!instance.Video) { | ||
263 | instance.Video = await instance.$get('Video') | ||
264 | } | ||
265 | |||
266 | logger.info('Saving video comment %s for abuse.', instance.url) | ||
267 | |||
268 | const details = Object.assign(instance.toFormattedJSON(), { | ||
269 | Video: { | ||
270 | id: instance.Video.id, | ||
271 | name: instance.Video.name, | ||
272 | uuid: instance.Video.uuid | ||
273 | } | ||
274 | }) | ||
275 | |||
276 | for (const abuse of instance.CommentAbuses) { | ||
277 | abuse.deletedComment = details | ||
278 | |||
279 | tasks.push(abuse.save({ transaction: options.transaction })) | ||
280 | } | ||
281 | |||
282 | Promise.all(tasks) | ||
283 | .catch(err => { | ||
284 | logger.error('Some errors when saving details of comment %s in its abuses before destroy hook.', instance.url, { err }) | ||
285 | }) | ||
286 | |||
287 | return undefined | ||
288 | } | ||
289 | |||
227 | static loadById (id: number, t?: Transaction): Bluebird<MComment> { | 290 | static loadById (id: number, t?: Transaction): Bluebird<MComment> { |
228 | const query: FindOptions = { | 291 | const query: FindOptions = { |
229 | where: { | 292 | where: { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 272bba0e1..43609587c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> { | |||
803 | static async saveEssentialDataToAbuses (instance: VideoModel, options) { | 803 | static async saveEssentialDataToAbuses (instance: VideoModel, options) { |
804 | const tasks: Promise<any>[] = [] | 804 | const tasks: Promise<any>[] = [] |
805 | 805 | ||
806 | logger.info('Saving video abuses details of video %s.', instance.url) | ||
807 | |||
808 | if (!Array.isArray(instance.VideoAbuses)) { | 806 | if (!Array.isArray(instance.VideoAbuses)) { |
809 | instance.VideoAbuses = await instance.$get('VideoAbuses') | 807 | instance.VideoAbuses = await instance.$get('VideoAbuses') |
810 | 808 | ||
811 | if (instance.VideoAbuses.length === 0) return undefined | 809 | if (instance.VideoAbuses.length === 0) return undefined |
812 | } | 810 | } |
813 | 811 | ||
812 | logger.info('Saving video abuses details of video %s.', instance.url) | ||
813 | |||
814 | const details = instance.toFormattedDetailsJSON() | 814 | const details = instance.toFormattedDetailsJSON() |
815 | 815 | ||
816 | for (const abuse of instance.VideoAbuses) { | 816 | for (const abuse of instance.VideoAbuses) { |
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts new file mode 100644 index 000000000..ba7c0833f --- /dev/null +++ b/server/tests/api/check-params/abuses.ts | |||
@@ -0,0 +1,271 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { AbuseCreate, AbuseState } from '@shared/models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createUser, | ||
8 | deleteAbuse, | ||
9 | flushAndRunServer, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | updateAbuse, | ||
15 | uploadVideo, | ||
16 | userLogin | ||
17 | } from '../../../../shared/extra-utils' | ||
18 | import { | ||
19 | checkBadCountPagination, | ||
20 | checkBadSortPagination, | ||
21 | checkBadStartPagination | ||
22 | } from '../../../../shared/extra-utils/requests/check-api-params' | ||
23 | |||
24 | // FIXME: deprecated in 2.3. Remove this controller | ||
25 | |||
26 | describe('Test video abuses API validators', function () { | ||
27 | const basePath = '/api/v1/abuses/' | ||
28 | |||
29 | let server: ServerInfo | ||
30 | let userAccessToken = '' | ||
31 | let abuseId: number | ||
32 | |||
33 | // --------------------------------------------------------------- | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(30000) | ||
37 | |||
38 | server = await flushAndRunServer(1) | ||
39 | |||
40 | await setAccessTokensToServers([ server ]) | ||
41 | |||
42 | const username = 'user1' | ||
43 | const password = 'my super password' | ||
44 | await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password }) | ||
45 | userAccessToken = await userLogin(server, { username, password }) | ||
46 | |||
47 | const res = await uploadVideo(server.url, server.accessToken, {}) | ||
48 | server.video = res.body.video | ||
49 | }) | ||
50 | |||
51 | describe('When listing abuses', function () { | ||
52 | const path = basePath | ||
53 | |||
54 | it('Should fail with a bad start pagination', async function () { | ||
55 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
56 | }) | ||
57 | |||
58 | it('Should fail with a bad count pagination', async function () { | ||
59 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
60 | }) | ||
61 | |||
62 | it('Should fail with an incorrect sort', async function () { | ||
63 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
64 | }) | ||
65 | |||
66 | it('Should fail with a non authenticated user', async function () { | ||
67 | await makeGetRequest({ | ||
68 | url: server.url, | ||
69 | path, | ||
70 | statusCodeExpected: 401 | ||
71 | }) | ||
72 | }) | ||
73 | |||
74 | it('Should fail with a non admin user', async function () { | ||
75 | await makeGetRequest({ | ||
76 | url: server.url, | ||
77 | path, | ||
78 | token: userAccessToken, | ||
79 | statusCodeExpected: 403 | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | it('Should fail with a bad id filter', async function () { | ||
84 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } }) | ||
85 | }) | ||
86 | |||
87 | it('Should fail with a bad filter', async function () { | ||
88 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } }) | ||
89 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } }) | ||
90 | }) | ||
91 | |||
92 | it('Should fail with bad predefined reason', async function () { | ||
93 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } }) | ||
94 | }) | ||
95 | |||
96 | it('Should fail with a bad state filter', async function () { | ||
97 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } }) | ||
98 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } }) | ||
99 | }) | ||
100 | |||
101 | it('Should fail with a bad videoIs filter', async function () { | ||
102 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } }) | ||
103 | }) | ||
104 | |||
105 | it('Should succeed with the correct params', async function () { | ||
106 | const query = { | ||
107 | id: 13, | ||
108 | predefinedReason: 'violentOrRepulsive', | ||
109 | filter: 'comment', | ||
110 | state: 2, | ||
111 | videoIs: 'deleted' | ||
112 | } | ||
113 | |||
114 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query, statusCodeExpected: 200 }) | ||
115 | }) | ||
116 | }) | ||
117 | |||
118 | describe('When reporting an abuse', function () { | ||
119 | const path = basePath | ||
120 | |||
121 | it('Should fail with nothing', async function () { | ||
122 | const fields = {} | ||
123 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
124 | }) | ||
125 | |||
126 | it('Should fail with a wrong video', async function () { | ||
127 | const fields = { video: { id: 'blabla' }, reason: 'my super reason' } | ||
128 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields }) | ||
129 | }) | ||
130 | |||
131 | it('Should fail with an unknown video', async function () { | ||
132 | const fields = { video: { id: 42 }, reason: 'my super reason' } | ||
133 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 }) | ||
134 | }) | ||
135 | |||
136 | it('Should fail with a wrong comment', async function () { | ||
137 | const fields = { comment: { id: 'blabla' }, reason: 'my super reason' } | ||
138 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields }) | ||
139 | }) | ||
140 | |||
141 | it('Should fail with an unknown comment', async function () { | ||
142 | const fields = { comment: { id: 42 }, reason: 'my super reason' } | ||
143 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 }) | ||
144 | }) | ||
145 | |||
146 | it('Should fail with a wrong account', async function () { | ||
147 | const fields = { account: { id: 'blabla' }, reason: 'my super reason' } | ||
148 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields }) | ||
149 | }) | ||
150 | |||
151 | it('Should fail with an unknown account', async function () { | ||
152 | const fields = { account: { id: 42 }, reason: 'my super reason' } | ||
153 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 }) | ||
154 | }) | ||
155 | |||
156 | it('Should fail with not account, comment or video', async function () { | ||
157 | const fields = { reason: 'my super reason' } | ||
158 | await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 400 }) | ||
159 | }) | ||
160 | |||
161 | it('Should fail with a non authenticated user', async function () { | ||
162 | const fields = { video: { id: server.video.id }, reason: 'my super reason' } | ||
163 | |||
164 | await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) | ||
165 | }) | ||
166 | |||
167 | it('Should fail with a reason too short', async function () { | ||
168 | const fields = { video: { id: server.video.id }, reason: 'h' } | ||
169 | |||
170 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
171 | }) | ||
172 | |||
173 | it('Should fail with a too big reason', async function () { | ||
174 | const fields = { video: { id: server.video.id }, reason: 'super'.repeat(605) } | ||
175 | |||
176 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
177 | }) | ||
178 | |||
179 | it('Should succeed with the correct parameters (basic)', async function () { | ||
180 | const fields: AbuseCreate = { video: { id: server.video.id }, reason: 'my super reason' } | ||
181 | |||
182 | const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) | ||
183 | abuseId = res.body.abuse.id | ||
184 | }) | ||
185 | |||
186 | it('Should fail with a wrong predefined reason', async function () { | ||
187 | const fields = { video: { id: server.video.id }, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] } | ||
188 | |||
189 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
190 | }) | ||
191 | |||
192 | it('Should fail with negative timestamps', async function () { | ||
193 | const fields = { video: { id: server.video.id, startAt: -1 }, reason: 'my super reason' } | ||
194 | |||
195 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
196 | }) | ||
197 | |||
198 | it('Should fail mith misordered startAt/endAt', async function () { | ||
199 | const fields = { video: { id: server.video.id, startAt: 5, endAt: 1 }, reason: 'my super reason' } | ||
200 | |||
201 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
202 | }) | ||
203 | |||
204 | it('Should succeed with the corret parameters (advanced)', async function () { | ||
205 | const fields: AbuseCreate = { | ||
206 | video: { | ||
207 | id: server.video.id, | ||
208 | startAt: 1, | ||
209 | endAt: 5 | ||
210 | }, | ||
211 | reason: 'my super reason', | ||
212 | predefinedReasons: [ 'serverRules' ] | ||
213 | } | ||
214 | |||
215 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) | ||
216 | }) | ||
217 | }) | ||
218 | |||
219 | describe('When updating an abuse', function () { | ||
220 | |||
221 | it('Should fail with a non authenticated user', async function () { | ||
222 | await updateAbuse(server.url, 'blabla', abuseId, {}, 401) | ||
223 | }) | ||
224 | |||
225 | it('Should fail with a non admin user', async function () { | ||
226 | await updateAbuse(server.url, userAccessToken, abuseId, {}, 403) | ||
227 | }) | ||
228 | |||
229 | it('Should fail with a bad abuse id', async function () { | ||
230 | await updateAbuse(server.url, server.accessToken, 45, {}, 404) | ||
231 | }) | ||
232 | |||
233 | it('Should fail with a bad state', async function () { | ||
234 | const body = { state: 5 } | ||
235 | await updateAbuse(server.url, server.accessToken, abuseId, body, 400) | ||
236 | }) | ||
237 | |||
238 | it('Should fail with a bad moderation comment', async function () { | ||
239 | const body = { moderationComment: 'b'.repeat(3001) } | ||
240 | await updateAbuse(server.url, server.accessToken, abuseId, body, 400) | ||
241 | }) | ||
242 | |||
243 | it('Should succeed with the correct params', async function () { | ||
244 | const body = { state: AbuseState.ACCEPTED } | ||
245 | await updateAbuse(server.url, server.accessToken, abuseId, body) | ||
246 | }) | ||
247 | }) | ||
248 | |||
249 | describe('When deleting a video abuse', function () { | ||
250 | |||
251 | it('Should fail with a non authenticated user', async function () { | ||
252 | await deleteAbuse(server.url, 'blabla', abuseId, 401) | ||
253 | }) | ||
254 | |||
255 | it('Should fail with a non admin user', async function () { | ||
256 | await deleteAbuse(server.url, userAccessToken, abuseId, 403) | ||
257 | }) | ||
258 | |||
259 | it('Should fail with a bad abuse id', async function () { | ||
260 | await deleteAbuse(server.url, server.accessToken, 45, 404) | ||
261 | }) | ||
262 | |||
263 | it('Should succeed with the correct params', async function () { | ||
264 | await deleteAbuse(server.url, server.accessToken, abuseId) | ||
265 | }) | ||
266 | }) | ||
267 | |||
268 | after(async function () { | ||
269 | await cleanupTests([ server ]) | ||
270 | }) | ||
271 | }) | ||
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 93ffd98b1..0ee1f27aa 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './abuses' | ||
1 | import './accounts' | 2 | import './accounts' |
2 | import './blocklist' | 3 | import './blocklist' |
3 | import './bulk' | 4 | import './bulk' |
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts index f122baef4..3b361ca79 100644 --- a/server/tests/api/check-params/video-abuses.ts +++ b/server/tests/api/check-params/video-abuses.ts | |||
@@ -152,12 +152,6 @@ describe('Test video abuses API validators', function () { | |||
152 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 152 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
153 | }) | 153 | }) |
154 | 154 | ||
155 | it('Should fail mith misordered startAt/endAt', async function () { | ||
156 | const fields = { reason: 'my super reason', startAt: 5, endAt: 1 } | ||
157 | |||
158 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
159 | }) | ||
160 | |||
161 | it('Should succeed with the corret parameters (advanced)', async function () { | 155 | it('Should succeed with the corret parameters (advanced)', async function () { |
162 | const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 } | 156 | const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 } |
163 | 157 | ||
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts index abbc93d6f..8e12be874 100644 --- a/server/types/models/moderation/abuse.ts +++ b/server/types/models/moderation/abuse.ts | |||
@@ -3,7 +3,7 @@ import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse | |||
3 | import { PickWith } from '@shared/core-utils' | 3 | import { PickWith } from '@shared/core-utils' |
4 | import { AbuseModel } from '../../../models/abuse/abuse' | 4 | import { AbuseModel } from '../../../models/abuse/abuse' |
5 | import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' | 5 | import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' |
6 | import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo } from '../video' | 6 | import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video' |
7 | import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' | 7 | import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' |
8 | 8 | ||
9 | type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> | 9 | type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M> |
@@ -51,6 +51,10 @@ export type MCommentAbuseUrl = | |||
51 | MCommentAbuse & | 51 | MCommentAbuse & |
52 | UseCommentAbuse<'VideoComment', MCommentUrl> | 52 | UseCommentAbuse<'VideoComment', MCommentUrl> |
53 | 53 | ||
54 | export type MCommentAbuseFormattable = | ||
55 | MCommentAbuse & | ||
56 | UseCommentAbuse<'VideoComment', MComment & PickWith<MCommentVideo, 'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>>> | ||
57 | |||
54 | // ############################################################################ | 58 | // ############################################################################ |
55 | 59 | ||
56 | export type MAbuseId = Pick<AbuseModel, 'id'> | 60 | export type MAbuseId = Pick<AbuseModel, 'id'> |
@@ -94,4 +98,5 @@ export type MAbuseFull = | |||
94 | export type MAbuseFormattable = | 98 | export type MAbuseFormattable = |
95 | MAbuse & | 99 | MAbuse & |
96 | Use<'ReporterAccount', MAccountFormattable> & | 100 | Use<'ReporterAccount', MAccountFormattable> & |
97 | Use<'VideoAbuse', MVideoAbuseFormattable> | 101 | Use<'VideoAbuse', MVideoAbuseFormattable> & |
102 | Use<'VideoCommentAbuse', MCommentAbuseFormattable> | ||
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 7595e6d86..b1afffcd4 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -91,6 +91,7 @@ declare module 'express' { | |||
91 | 91 | ||
92 | accountVideoRate?: MAccountVideoRateAccountVideo | 92 | accountVideoRate?: MAccountVideoRateAccountVideo |
93 | 93 | ||
94 | videoComment?: MComment | ||
94 | videoCommentFull?: MCommentOwnerVideoReply | 95 | videoCommentFull?: MCommentOwnerVideoReply |
95 | videoCommentThread?: MComment | 96 | videoCommentThread?: MComment |
96 | 97 | ||