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