Optimize SQL requests of watch page API endpoints
[oweals/peertube.git] / server / middlewares / validators / video-comments.ts
1 import * as express from 'express'
2 import { body, param } from 'express-validator/check'
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 { isVideoExist } from '../../helpers/custom-validators/videos'
7 import { logger } from '../../helpers/logger'
8 import { UserModel } from '../../models/account/user'
9 import { VideoModel } from '../../models/video/video'
10 import { VideoCommentModel } from '../../models/video/video-comment'
11 import { areValidationErrors } from './utils'
12
13 const listVideoCommentThreadsValidator = [
14   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
15
16   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
17     logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
18
19     if (areValidationErrors(req, res)) return
20     if (!await isVideoExist(req.params.videoId, res, 'only-video')) return
21
22     return next()
23   }
24 ]
25
26 const listVideoThreadCommentsValidator = [
27   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
28   param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'),
29
30   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
31     logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params })
32
33     if (areValidationErrors(req, res)) return
34     if (!await isVideoExist(req.params.videoId, res, 'only-video')) return
35     if (!await isVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return
36
37     return next()
38   }
39 ]
40
41 const addVideoCommentThreadValidator = [
42   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
43   body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
44
45   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
46     logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body })
47
48     if (areValidationErrors(req, res)) return
49     if (!await isVideoExist(req.params.videoId, res)) return
50     if (!isVideoCommentsEnabled(res.locals.video, res)) return
51
52     return next()
53   }
54 ]
55
56 const addVideoCommentReplyValidator = [
57   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
58   param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
59   body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
60
61   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
62     logger.debug('Checking addVideoCommentReply parameters.', { parameters: req.params, body: req.body })
63
64     if (areValidationErrors(req, res)) return
65     if (!await isVideoExist(req.params.videoId, res)) return
66     if (!isVideoCommentsEnabled(res.locals.video, res)) return
67     if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return
68
69     return next()
70   }
71 ]
72
73 const videoCommentGetValidator = [
74   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
75   param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
76
77   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
78     logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
79
80     if (areValidationErrors(req, res)) return
81     if (!await isVideoExist(req.params.videoId, res)) return
82     if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return
83
84     return next()
85   }
86 ]
87
88 const removeVideoCommentValidator = [
89   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
90   param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
91
92   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
93     logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params })
94
95     if (areValidationErrors(req, res)) return
96     if (!await isVideoExist(req.params.videoId, res)) return
97     if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return
98
99     // Check if the user who did the request is able to delete the video
100     if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return
101
102     return next()
103   }
104 ]
105
106 // ---------------------------------------------------------------------------
107
108 export {
109   listVideoCommentThreadsValidator,
110   listVideoThreadCommentsValidator,
111   addVideoCommentThreadValidator,
112   addVideoCommentReplyValidator,
113   videoCommentGetValidator,
114   removeVideoCommentValidator
115 }
116
117 // ---------------------------------------------------------------------------
118
119 async function isVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) {
120   const videoComment = await VideoCommentModel.loadById(id)
121
122   if (!videoComment) {
123     res.status(404)
124       .json({ error: 'Video comment thread not found' })
125       .end()
126
127     return false
128   }
129
130   if (videoComment.videoId !== video.id) {
131     res.status(400)
132       .json({ error: 'Video comment is associated to this video.' })
133       .end()
134
135     return false
136   }
137
138   if (videoComment.inReplyToCommentId !== null) {
139     res.status(400)
140       .json({ error: 'Video comment is not a thread.' })
141       .end()
142
143     return false
144   }
145
146   res.locals.videoCommentThread = videoComment
147   return true
148 }
149
150 async function isVideoCommentExist (id: number, video: VideoModel, res: express.Response) {
151   const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
152
153   if (!videoComment) {
154     res.status(404)
155       .json({ error: 'Video comment thread not found' })
156       .end()
157
158     return false
159   }
160
161   if (videoComment.videoId !== video.id) {
162     res.status(400)
163       .json({ error: 'Video comment is associated to this video.' })
164       .end()
165
166     return false
167   }
168
169   res.locals.videoComment = videoComment
170   return true
171 }
172
173 function isVideoCommentsEnabled (video: VideoModel, res: express.Response) {
174   if (video.commentsEnabled !== true) {
175     res.status(409)
176       .json({ error: 'Video comments are disabled for this video.' })
177       .end()
178
179     return false
180   }
181
182   return true
183 }
184
185 function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCommentModel, res: express.Response) {
186   const account = videoComment.Account
187   if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) {
188     res.status(403)
189       .json({ error: 'Cannot remove video comment of another user' })
190       .end()
191     return false
192   }
193
194   return true
195 }