]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/video-comments.ts
Merge branch 'release/3.2.0' into develop
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / video-comments.ts
1 import * as express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { MUserAccountUrl } from '@server/types/models'
4 import { UserRight } from '../../../../shared'
5 import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
6 import {
7 doesVideoCommentExist,
8 doesVideoCommentThreadExist,
9 isValidVideoCommentText
10 } from '../../../helpers/custom-validators/video-comments'
11 import { logger } from '../../../helpers/logger'
12 import { doesVideoExist } from '../../../helpers/middlewares'
13 import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
14 import { Hooks } from '../../../lib/plugins/hooks'
15 import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
16 import { areValidationErrors } from '../utils'
17 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
18
19 const listVideoCommentsValidator = [
20 query('isLocal')
21 .optional()
22 .customSanitizer(toBooleanOrNull)
23 .custom(isBooleanValid)
24 .withMessage('Should have a valid is local boolean'),
25
26 query('search')
27 .optional()
28 .custom(exists).withMessage('Should have a valid search'),
29
30 query('searchAccount')
31 .optional()
32 .custom(exists).withMessage('Should have a valid account search'),
33
34 query('searchVideo')
35 .optional()
36 .custom(exists).withMessage('Should have a valid video search'),
37
38 (req: express.Request, res: express.Response, next: express.NextFunction) => {
39 logger.debug('Checking listVideoCommentsValidator parameters.', { parameters: req.query })
40
41 if (areValidationErrors(req, res)) return
42
43 return next()
44 }
45 ]
46
47 const listVideoCommentThreadsValidator = [
48 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
52
53 if (areValidationErrors(req, res)) return
54 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
55
56 return next()
57 }
58 ]
59
60 const listVideoThreadCommentsValidator = [
61 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
62 param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'),
63
64 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
65 logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params })
66
67 if (areValidationErrors(req, res)) return
68 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
69 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
70
71 return next()
72 }
73 ]
74
75 const addVideoCommentThreadValidator = [
76 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
77 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
78
79 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
80 logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body })
81
82 if (areValidationErrors(req, res)) return
83 if (!await doesVideoExist(req.params.videoId, res)) return
84 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
85 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
86
87 return next()
88 }
89 ]
90
91 const addVideoCommentReplyValidator = [
92 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
93 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
94 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
95
96 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
97 logger.debug('Checking addVideoCommentReply parameters.', { parameters: req.params, body: req.body })
98
99 if (areValidationErrors(req, res)) return
100 if (!await doesVideoExist(req.params.videoId, res)) return
101 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
102 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
103 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
104
105 return next()
106 }
107 ]
108
109 const videoCommentGetValidator = [
110 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
111 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
112
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
115
116 if (areValidationErrors(req, res)) return
117 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
118 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoId, res)) return
119
120 return next()
121 }
122 ]
123
124 const removeVideoCommentValidator = [
125 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
126 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
127
128 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
129 logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params })
130
131 if (areValidationErrors(req, res)) return
132 if (!await doesVideoExist(req.params.videoId, res)) return
133 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
134
135 // Check if the user who did the request is able to delete the video
136 if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
137
138 return next()
139 }
140 ]
141
142 // ---------------------------------------------------------------------------
143
144 export {
145 listVideoCommentThreadsValidator,
146 listVideoThreadCommentsValidator,
147 addVideoCommentThreadValidator,
148 listVideoCommentsValidator,
149 addVideoCommentReplyValidator,
150 videoCommentGetValidator,
151 removeVideoCommentValidator
152 }
153
154 // ---------------------------------------------------------------------------
155
156 function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
157 if (video.commentsEnabled !== true) {
158 res.status(HttpStatusCode.CONFLICT_409)
159 .json({ error: 'Video comments are disabled for this video.' })
160
161 return false
162 }
163
164 return true
165 }
166
167 function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
168 if (videoComment.isDeleted()) {
169 res.status(HttpStatusCode.CONFLICT_409)
170 .json({ error: 'This comment is already deleted' })
171
172 return false
173 }
174
175 const userAccount = user.Account
176
177 if (
178 user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && // Not a moderator
179 videoComment.accountId !== userAccount.id && // Not the comment owner
180 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
181 ) {
182 res.status(HttpStatusCode.FORBIDDEN_403)
183 .json({ error: 'Cannot remove video comment of another user' })
184
185 return false
186 }
187
188 return true
189 }
190
191 async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
192 const acceptParameters = {
193 video,
194 commentBody: req.body,
195 user: res.locals.oauth.token.User
196 }
197
198 let acceptedResult: AcceptResult
199
200 if (isReply) {
201 const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
202
203 acceptedResult = await Hooks.wrapFun(
204 isLocalVideoCommentReplyAccepted,
205 acceptReplyParameters,
206 'filter:api.video-comment-reply.create.accept.result'
207 )
208 } else {
209 acceptedResult = await Hooks.wrapFun(
210 isLocalVideoThreadAccepted,
211 acceptParameters,
212 'filter:api.video-thread.create.accept.result'
213 )
214 }
215
216 if (!acceptedResult || acceptedResult.accepted !== true) {
217 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
218 res.status(HttpStatusCode.FORBIDDEN_403)
219 .json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
220
221 return false
222 }
223
224 return true
225 }