Fix error logging
[oweals/peertube.git] / server / middlewares / validators / videos.ts
1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query } from 'express-validator/check'
4 import { UserRight, VideoPrivacy } from '../../../shared'
5 import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid } from '../../helpers/custom-validators/misc'
6 import {
7   isVideoAbuseReasonValid,
8   isVideoCategoryValid,
9   isVideoDescriptionValid,
10   isVideoExist,
11   isVideoFile,
12   isVideoImage,
13   isVideoLanguageValid,
14   isVideoLicenceValid,
15   isVideoNameValid,
16   isVideoPrivacyValid,
17   isVideoRatingTypeValid, isVideoSupportValid,
18   isVideoTagsValid
19 } from '../../helpers/custom-validators/videos'
20 import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
21 import { logger } from '../../helpers/logger'
22 import { CONSTRAINTS_FIELDS } from '../../initializers'
23 import { UserModel } from '../../models/account/user'
24 import { VideoModel } from '../../models/video/video'
25 import { VideoChannelModel } from '../../models/video/video-channel'
26 import { VideoShareModel } from '../../models/video/video-share'
27 import { authenticate } from '../oauth'
28 import { areValidationErrors } from './utils'
29
30 const videosAddValidator = [
31   body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
32     'This file is not supported. Please, make sure it is of the following type : '
33     + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
34   ),
35   body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
36     'This thumbnail file is not supported. Please, make sure it is of the following type : '
37     + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
38   ),
39   body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
40     'This preview file is not supported. Please, make sure it is of the following type : '
41     + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
42   ),
43   body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
44   body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
45   body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
46   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
47   body('nsfw').custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
48   body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
49   body('support').optional().custom(isVideoSupportValid).withMessage('Should have a valid support text'),
50   body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
51   body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
52   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
53   body('commentsEnabled').custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
54
55   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
56     logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
57
58     if (areValidationErrors(req, res)) return
59     if (areErrorsInVideoImageFiles(req, res)) return
60
61     const videoFile: Express.Multer.File = req.files['videofile'][0]
62     const user = res.locals.oauth.token.User
63
64     const videoChannel = await VideoChannelModel.loadByIdAndAccount(req.body.channelId, user.Account.id)
65     if (!videoChannel) {
66       res.status(400)
67         .json({ error: 'Unknown video video channel for this account.' })
68         .end()
69
70       return
71     }
72
73     res.locals.videoChannel = videoChannel
74
75     const isAble = await user.isAbleToUploadVideo(videoFile)
76     if (isAble === false) {
77       res.status(403)
78          .json({ error: 'The user video quota is exceeded with this video.' })
79          .end()
80
81       return
82     }
83
84     let duration: number
85
86     try {
87       duration = await getDurationFromVideoFile(videoFile.path)
88     } catch (err) {
89       logger.error('Invalid input file in videosAddValidator.', { err })
90       res.status(400)
91          .json({ error: 'Invalid input file.' })
92          .end()
93
94       return
95     }
96
97     videoFile['duration'] = duration
98
99     return next()
100   }
101 ]
102
103 const videosUpdateValidator = [
104   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
105   body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
106     'This thumbnail file is not supported. Please, make sure it is of the following type : '
107     + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
108   ),
109   body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
110     'This preview file is not supported. Please, make sure it is of the following type : '
111     + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
112   ),
113   body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'),
114   body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
115   body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
116   body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'),
117   body('nsfw').optional().custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
118   body('privacy').optional().custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
119   body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
120   body('support').optional().custom(isVideoSupportValid).withMessage('Should have a valid support text'),
121   body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'),
122   body('commentsEnabled').optional().custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
123
124   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
125     logger.debug('Checking videosUpdate parameters', { parameters: req.body })
126
127     if (areValidationErrors(req, res)) return
128     if (areErrorsInVideoImageFiles(req, res)) return
129     if (!await isVideoExist(req.params.id, res)) return
130
131     const video = res.locals.video
132
133     // Check if the user who did the request is able to update the video
134     if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
135
136     if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
137       return res.status(409)
138         .json({ error: 'Cannot set "private" a video that was not private anymore.' })
139         .end()
140     }
141
142     return next()
143   }
144 ]
145
146 const videosGetValidator = [
147   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
148
149   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
150     logger.debug('Checking videosGet parameters', { parameters: req.params })
151
152     if (areValidationErrors(req, res)) return
153     if (!await isVideoExist(req.params.id, res)) return
154
155     const video = res.locals.video
156
157     // Video is public, anyone can access it
158     if (video.privacy === VideoPrivacy.PUBLIC) return next()
159
160     // Video is unlisted, check we used the uuid to fetch it
161     if (video.privacy === VideoPrivacy.UNLISTED) {
162       if (isUUIDValid(req.params.id)) return next()
163
164       // Don't leak this unlisted video
165       return res.status(404).end()
166     }
167
168     // Video is private, check the user
169     authenticate(req, res, () => {
170       if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
171         return res.status(403)
172           .json({ error: 'Cannot get this private video of another user' })
173           .end()
174       }
175
176       return next()
177     })
178   }
179 ]
180
181 const videosRemoveValidator = [
182   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
183
184   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
185     logger.debug('Checking videosRemove parameters', { parameters: req.params })
186
187     if (areValidationErrors(req, res)) return
188     if (!await isVideoExist(req.params.id, res)) return
189
190     // Check if the user who did the request is able to delete the video
191     if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
192
193     return next()
194   }
195 ]
196
197 const videosSearchValidator = [
198   query('search').not().isEmpty().withMessage('Should have a valid search'),
199
200   (req: express.Request, res: express.Response, next: express.NextFunction) => {
201     logger.debug('Checking videosSearch parameters', { parameters: req.params })
202
203     if (areValidationErrors(req, res)) return
204
205     return next()
206   }
207 ]
208
209 const videoAbuseReportValidator = [
210   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
211   body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
212
213   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
214     logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
215
216     if (areValidationErrors(req, res)) return
217     if (!await isVideoExist(req.params.id, res)) return
218
219     return next()
220   }
221 ]
222
223 const videoRateValidator = [
224   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
225   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
226
227   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
228     logger.debug('Checking videoRate parameters', { parameters: req.body })
229
230     if (areValidationErrors(req, res)) return
231     if (!await isVideoExist(req.params.id, res)) return
232
233     return next()
234   }
235 ]
236
237 const videosShareValidator = [
238   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
239   param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
240
241   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
242     logger.debug('Checking videoShare parameters', { parameters: req.params })
243
244     if (areValidationErrors(req, res)) return
245     if (!await isVideoExist(req.params.id, res)) return
246
247     const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
248     if (!share) {
249       return res.status(404)
250         .end()
251     }
252
253     res.locals.videoShare = share
254     return next()
255   }
256 ]
257
258 // ---------------------------------------------------------------------------
259
260 export {
261   videosAddValidator,
262   videosUpdateValidator,
263   videosGetValidator,
264   videosRemoveValidator,
265   videosSearchValidator,
266   videosShareValidator,
267
268   videoAbuseReportValidator,
269
270   videoRateValidator
271 }
272
273 // ---------------------------------------------------------------------------
274
275 function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
276   // Retrieve the user who did the request
277   if (video.isOwned() === false) {
278     res.status(403)
279               .json({ error: 'Cannot remove video of another server, blacklist it' })
280               .end()
281     return false
282   }
283
284   // Check if the user can delete the video
285   // The user can delete it if he has the right
286   // Or if s/he is the video's account
287   const account = video.VideoChannel.Account
288   if (user.hasRight(right) === false && account.userId !== user.id) {
289     res.status(403)
290               .json({ error: 'Cannot remove video of another user' })
291               .end()
292     return false
293   }
294
295   return true
296 }
297
298 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
299   // Files are optional
300   if (!req.files) return false
301
302   for (const imageField of [ 'thumbnail', 'preview' ]) {
303     if (!req.files[ imageField ]) continue
304
305     const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
306     if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
307       res.status(400)
308         .send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
309         .end()
310       return true
311     }
312   }
313
314   return false
315 }