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