Remove unnecessary image check in video upload
[oweals/peertube.git] / server / middlewares / validators / videos.ts
1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query, 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 videosSearchValidator = [
176   query('search').not().isEmpty().withMessage('Should have a valid search'),
177
178   (req: express.Request, res: express.Response, next: express.NextFunction) => {
179     logger.debug('Checking videosSearch parameters', { parameters: req.params })
180
181     if (areValidationErrors(req, res)) return
182
183     return next()
184   }
185 ]
186
187 const videoAbuseReportValidator = [
188   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
189   body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
190
191   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
192     logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
193
194     if (areValidationErrors(req, res)) return
195     if (!await isVideoExist(req.params.id, res)) return
196
197     return next()
198   }
199 ]
200
201 const videoRateValidator = [
202   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
203   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
204
205   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
206     logger.debug('Checking videoRate parameters', { parameters: req.body })
207
208     if (areValidationErrors(req, res)) return
209     if (!await isVideoExist(req.params.id, res)) return
210
211     return next()
212   }
213 ]
214
215 const videosShareValidator = [
216   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
217   param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
218
219   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
220     logger.debug('Checking videoShare parameters', { parameters: req.params })
221
222     if (areValidationErrors(req, res)) return
223     if (!await isVideoExist(req.params.id, res)) return
224
225     const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
226     if (!share) {
227       return res.status(404)
228         .end()
229     }
230
231     res.locals.videoShare = share
232     return next()
233   }
234 ]
235
236 // ---------------------------------------------------------------------------
237
238 export {
239   videosAddValidator,
240   videosUpdateValidator,
241   videosGetValidator,
242   videosRemoveValidator,
243   videosSearchValidator,
244   videosShareValidator,
245
246   videoAbuseReportValidator,
247
248   videoRateValidator
249 }
250
251 // ---------------------------------------------------------------------------
252
253 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
254   if (req.body.scheduleUpdate) {
255     if (!req.body.scheduleUpdate.updateAt) {
256       res.status(400)
257          .json({ error: 'Schedule update at is mandatory.' })
258          .end()
259
260       return true
261     }
262   }
263
264   return false
265 }
266
267 function getCommonVideoAttributes () {
268   return [
269     body('thumbnailfile')
270       .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
271       'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
272       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
273     ),
274     body('previewfile')
275       .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
276       'This preview file is not supported or too large. Please, make sure it is of the following type: '
277       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
278     ),
279
280     body('category')
281       .optional()
282       .customSanitizer(toIntOrNull)
283       .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
284     body('licence')
285       .optional()
286       .customSanitizer(toIntOrNull)
287       .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
288     body('language')
289       .optional()
290       .customSanitizer(toValueOrNull)
291       .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
292     body('nsfw')
293       .optional()
294       .toBoolean()
295       .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
296     body('waitTranscoding')
297       .optional()
298       .toBoolean()
299       .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
300     body('privacy')
301       .optional()
302       .toInt()
303       .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
304     body('description')
305       .optional()
306       .customSanitizer(toValueOrNull)
307       .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
308     body('support')
309       .optional()
310       .customSanitizer(toValueOrNull)
311       .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
312     body('tags')
313       .optional()
314       .customSanitizer(toValueOrNull)
315       .custom(isVideoTagsValid).withMessage('Should have correct tags'),
316     body('commentsEnabled')
317       .optional()
318       .toBoolean()
319       .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
320
321     body('scheduleUpdate')
322       .optional()
323       .customSanitizer(toValueOrNull),
324     body('scheduleUpdate.updateAt')
325       .optional()
326       .custom(isDateValid).withMessage('Should have a valid schedule update date'),
327     body('scheduleUpdate.privacy')
328       .optional()
329       .toInt()
330       .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
331   ] as (ValidationChain | express.Handler)[]
332 }