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