4846a5e9e391f20b636a693ed44baa0a05779664
[oweals/peertube.git] / server / middlewares / validators / videos / video-comments.ts
1 import * as express from 'express'
2 import { body, param } from 'express-validator'
3 import { MUserAccountUrl } from '@server/typings/models'
4 import { UserRight } from '../../../../shared'
5 import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
6 import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
7 import { logger } from '../../../helpers/logger'
8 import { doesVideoExist } from '../../../helpers/middlewares'
9 import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
10 import { Hooks } from '../../../lib/plugins/hooks'
11 import { VideoCommentModel } from '../../../models/video/video-comment'
12 import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../typings/models/video'
13 import { areValidationErrors } from '../utils'
14
15 const listVideoCommentThreadsValidator = [
16   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
17
18   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
19     logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
20
21     if (areValidationErrors(req, res)) return
22     if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
23
24     return next()
25   }
26 ]
27
28 const listVideoThreadCommentsValidator = [
29   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
30   param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'),
31
32   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
33     logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params })
34
35     if (areValidationErrors(req, res)) return
36     if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
37     if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
38
39     return next()
40   }
41 ]
42
43 const addVideoCommentThreadValidator = [
44   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
45   body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
46
47   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
48     logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body })
49
50     if (areValidationErrors(req, res)) return
51     if (!await doesVideoExist(req.params.videoId, res)) return
52     if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
53     if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
54
55     return next()
56   }
57 ]
58
59 const addVideoCommentReplyValidator = [
60   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
61   param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
62   body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
63
64   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
65     logger.debug('Checking addVideoCommentReply parameters.', { parameters: req.params, body: req.body })
66
67     if (areValidationErrors(req, res)) return
68     if (!await doesVideoExist(req.params.videoId, res)) return
69     if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
70     if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
71     if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
72
73     return next()
74   }
75 ]
76
77 const videoCommentGetValidator = [
78   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
79   param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
80
81   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
82     logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
83
84     if (areValidationErrors(req, res)) return
85     if (!await doesVideoExist(req.params.videoId, res, 'id')) return
86     if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoId, res)) return
87
88     return next()
89   }
90 ]
91
92 const removeVideoCommentValidator = [
93   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
94   param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
95
96   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
97     logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params })
98
99     if (areValidationErrors(req, res)) return
100     if (!await doesVideoExist(req.params.videoId, res)) return
101     if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
102
103     // Check if the user who did the request is able to delete the video
104     if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
105
106     return next()
107   }
108 ]
109
110 // ---------------------------------------------------------------------------
111
112 export {
113   listVideoCommentThreadsValidator,
114   listVideoThreadCommentsValidator,
115   addVideoCommentThreadValidator,
116   addVideoCommentReplyValidator,
117   videoCommentGetValidator,
118   removeVideoCommentValidator
119 }
120
121 // ---------------------------------------------------------------------------
122
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) {
180   if (video.commentsEnabled !== true) {
181     res.status(409)
182       .json({ error: 'Video comments are disabled for this video.' })
183       .end()
184
185     return false
186   }
187
188   return true
189 }
190
191 function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
192   if (videoComment.isDeleted()) {
193     res.status(409)
194       .json({ error: 'This comment is already deleted' })
195       .end()
196     return false
197   }
198
199   const userAccount = user.Account
200
201   if (
202     user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && // Not a moderator
203     videoComment.accountId !== userAccount.id && // Not the comment owner
204     videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
205   ) {
206     res.status(403)
207       .json({ error: 'Cannot remove video comment of another user' })
208
209     return false
210   }
211
212   return true
213 }
214
215 async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
216   const acceptParameters = {
217     video,
218     commentBody: req.body,
219     user: res.locals.oauth.token.User
220   }
221
222   let acceptedResult: AcceptResult
223
224   if (isReply) {
225     const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
226
227     acceptedResult = await Hooks.wrapFun(
228       isLocalVideoCommentReplyAccepted,
229       acceptReplyParameters,
230       'filter:api.video-comment-reply.create.accept.result'
231     )
232   } else {
233     acceptedResult = await Hooks.wrapFun(
234       isLocalVideoThreadAccepted,
235       acceptParameters,
236       'filter:api.video-thread.create.accept.result'
237     )
238   }
239
240   if (!acceptedResult || acceptedResult.accepted !== true) {
241     logger.info('Refused local comment.', { acceptedResult, acceptParameters })
242     res.status(403)
243               .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
244
245     return false
246   }
247
248   return true
249 }