]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos.ts
a2f64ef8fe3cf9c2f11566e811e88a69424834b2
[github/Chocobozzz/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 (areErrorsInVideoImageFiles(req, res)) return
55 if (areErrorsInScheduleUpdate(req, res)) return
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
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
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
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
104 if (areErrorsInVideoImageFiles(req, res)) return
105 if (areErrorsInScheduleUpdate(req, res)) return
106 if (!await isVideoExist(req.params.id, res)) return
107
108 const video = res.locals.video
109
110 // Check if the user who did the request is able to update the video
111 const user = res.locals.oauth.token.User
112 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
113
114 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
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
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 videosSearchValidator = [
178 query('search').not().isEmpty().withMessage('Should have a valid search'),
179
180 (req: express.Request, res: express.Response, next: express.NextFunction) => {
181 logger.debug('Checking videosSearch parameters', { parameters: req.params })
182
183 if (areValidationErrors(req, res)) return
184
185 return next()
186 }
187 ]
188
189 const videoAbuseReportValidator = [
190 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
191 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
192
193 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
194 logger.debug('Checking videoAbuseReport 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 videoRateValidator = [
204 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
205 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
206
207 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
208 logger.debug('Checking videoRate parameters', { parameters: req.body })
209
210 if (areValidationErrors(req, res)) return
211 if (!await isVideoExist(req.params.id, res)) return
212
213 return next()
214 }
215 ]
216
217 const videosShareValidator = [
218 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
219 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
220
221 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
222 logger.debug('Checking videoShare parameters', { parameters: req.params })
223
224 if (areValidationErrors(req, res)) return
225 if (!await isVideoExist(req.params.id, res)) return
226
227 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
228 if (!share) {
229 return res.status(404)
230 .end()
231 }
232
233 res.locals.videoShare = share
234 return next()
235 }
236 ]
237
238 // ---------------------------------------------------------------------------
239
240 export {
241 videosAddValidator,
242 videosUpdateValidator,
243 videosGetValidator,
244 videosRemoveValidator,
245 videosSearchValidator,
246 videosShareValidator,
247
248 videoAbuseReportValidator,
249
250 videoRateValidator
251 }
252
253 // ---------------------------------------------------------------------------
254
255 function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
256 // Files are optional
257 if (!req.files) return false
258
259 for (const imageField of [ 'thumbnail', 'preview' ]) {
260 if (!req.files[ imageField ]) continue
261
262 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
263 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
264 res.status(400)
265 .json({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
266 .end()
267 return true
268 }
269 }
270
271 return false
272 }
273
274 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
275 if (req.body.scheduleUpdate) {
276 if (!req.body.scheduleUpdate.updateAt) {
277 res.status(400)
278 .json({ error: 'Schedule update at is mandatory.' })
279 .end()
280
281 return true
282 }
283 }
284
285 return false
286 }
287
288 function getCommonVideoAttributes () {
289 return [
290 body('thumbnailfile')
291 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
292 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
293 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
294 ),
295 body('previewfile')
296 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
297 'This preview file is not supported or too large. Please, make sure it is of the following type: '
298 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
299 ),
300
301 body('category')
302 .optional()
303 .customSanitizer(toIntOrNull)
304 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
305 body('licence')
306 .optional()
307 .customSanitizer(toIntOrNull)
308 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
309 body('language')
310 .optional()
311 .customSanitizer(toValueOrNull)
312 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
313 body('nsfw')
314 .optional()
315 .toBoolean()
316 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
317 body('waitTranscoding')
318 .optional()
319 .toBoolean()
320 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
321 body('privacy')
322 .optional()
323 .toInt()
324 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
325 body('description')
326 .optional()
327 .customSanitizer(toValueOrNull)
328 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
329 body('support')
330 .optional()
331 .customSanitizer(toValueOrNull)
332 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
333 body('tags')
334 .optional()
335 .customSanitizer(toValueOrNull)
336 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
337 body('commentsEnabled')
338 .optional()
339 .toBoolean()
340 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
341
342 body('scheduleUpdate')
343 .optional()
344 .customSanitizer(toValueOrNull),
345 body('scheduleUpdate.updateAt')
346 .optional()
347 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
348 body('scheduleUpdate.privacy')
349 .optional()
350 .toInt()
351 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
352 ] as (ValidationChain | express.Handler)[]
353 }