da2fafb10aaa32d6033244187b2fda2c5a72367a
[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 { UserRight } from '../../../../shared'
4 import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5 import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6 import { logger } from '../../../helpers/logger'
7 import { VideoCommentModel } from '../../../models/video/video-comment'
8 import { areValidationErrors } from '../utils'
9 import { Hooks } from '../../../lib/plugins/hooks'
10 import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
11 import { doesVideoExist } from '../../../helpers/middlewares'
12 import { MCommentOwner, MVideo, MVideoFullLight, MVideoId } from '../../../typings/models/video'
13 import { MUser } from '@server/typings/models'
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: MUser, videoComment: MCommentOwner, 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 account = videoComment.Account
200   if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) {
201     res.status(403)
202       .json({ error: 'Cannot remove video comment of another user' })
203       .end()
204     return false
205   }
206
207   return true
208 }
209
210 async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
211   const acceptParameters = {
212     video,
213     commentBody: req.body,
214     user: res.locals.oauth.token.User
215   }
216
217   let acceptedResult: AcceptResult
218
219   if (isReply) {
220     const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
221
222     acceptedResult = await Hooks.wrapFun(
223       isLocalVideoCommentReplyAccepted,
224       acceptReplyParameters,
225       'filter:api.video-comment-reply.create.accept.result'
226     )
227   } else {
228     acceptedResult = await Hooks.wrapFun(
229       isLocalVideoThreadAccepted,
230       acceptParameters,
231       'filter:api.video-thread.create.accept.result'
232     )
233   }
234
235   if (!acceptedResult || acceptedResult.accepted !== true) {
236     logger.info('Refused local comment.', { acceptedResult, acceptParameters })
237     res.status(403)
238               .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
239
240     return false
241   }
242
243   return true
244 }