]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos.ts
Cache AP video route for 5 seconds
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos.ts
CommitLineData
69818c93 1import * as express from 'express'
3fd3ab2d 2import 'express-validator'
8d468a16
C
3import { body, param, query } from 'express-validator/check'
4import { UserRight, VideoPrivacy } from '../../../shared'
360329cc 5import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, toStringOrNull } from '../../helpers/custom-validators/misc'
b60e5f38 6import {
ac81d1a0
C
7 isVideoAbuseReasonValid,
8 isVideoCategoryValid,
9 isVideoDescriptionValid,
10 isVideoExist,
11 isVideoFile,
12 isVideoImage,
13 isVideoLanguageValid,
14 isVideoLicenceValid,
15 isVideoNameValid,
16 isVideoPrivacyValid,
360329cc
C
17 isVideoRatingTypeValid,
18 isVideoSupportValid,
ac81d1a0 19 isVideoTagsValid
8d468a16 20} from '../../helpers/custom-validators/videos'
da854ddd
C
21import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
22import { logger } from '../../helpers/logger'
f3aaa9a9 23import { CONSTRAINTS_FIELDS } from '../../initializers'
3fd3ab2d
C
24import { UserModel } from '../../models/account/user'
25import { VideoModel } from '../../models/video/video'
26import { VideoChannelModel } from '../../models/video/video-channel'
27import { VideoShareModel } from '../../models/video/video-share'
11474c3c 28import { authenticate } from '../oauth'
a2431b7d 29import { areValidationErrors } from './utils'
34ca3b52 30
b60e5f38 31const videosAddValidator = [
8376734e 32 body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
10db166b
C
33 'This file is not supported. Please, make sure it is of the following type : '
34 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
8376734e 35 ),
ac81d1a0
C
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 ),
b60e5f38 44 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
360329cc
C
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'),
72c7248b 78 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 79
a2431b7d 80 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
81 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
82
a2431b7d 83 if (areValidationErrors(req, res)) return
ac81d1a0 84 if (areErrorsInVideoImageFiles(req, res)) return
a2431b7d
C
85
86 const videoFile: Express.Multer.File = req.files['videofile'][0]
87 const user = res.locals.oauth.token.User
b60e5f38 88
3fd3ab2d 89 const videoChannel = await VideoChannelModel.loadByIdAndAccount(req.body.channelId, user.Account.id)
a2431b7d
C
90 if (!videoChannel) {
91 res.status(400)
92 .json({ error: 'Unknown video video channel for this account.' })
93 .end()
72c7248b 94
a2431b7d
C
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) {
d5b7d911 114 logger.error('Invalid input file in videosAddValidator.', { err })
a2431b7d
C
115 res.status(400)
116 .json({ error: 'Invalid input file.' })
117 .end()
118
119 return
120 }
121
a2431b7d
C
122 videoFile['duration'] = duration
123
124 return next()
b60e5f38
C
125 }
126]
127
128const videosUpdateValidator = [
72c7248b 129 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
ac81d1a0
C
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 ),
360329cc
C
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'),
b60e5f38 176
a2431b7d 177 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
178 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
179
a2431b7d 180 if (areValidationErrors(req, res)) return
ac81d1a0 181 if (areErrorsInVideoImageFiles(req, res)) return
a2431b7d
C
182 if (!await isVideoExist(req.params.id, res)) return
183
184 const video = res.locals.video
185
6221f311
C
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
a2431b7d
C
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()
b60e5f38
C
196 }
197]
c173e565 198
b60e5f38 199const videosGetValidator = [
72c7248b 200 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 201
a2431b7d 202 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 203 logger.debug('Checking videosGet parameters', { parameters: req.params })
7b1f49de 204
a2431b7d
C
205 if (areValidationErrors(req, res)) return
206 if (!await isVideoExist(req.params.id, res)) return
11474c3c 207
a2431b7d 208 const video = res.locals.video
11474c3c 209
81ebea48
C
210 // Video is public, anyone can access it
211 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 212
81ebea48
C
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
a2431b7d
C
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()
b60e5f38
C
230 })
231 }
232]
34ca3b52 233
b60e5f38 234const videosRemoveValidator = [
72c7248b 235 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 236
a2431b7d 237 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 238 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 239
a2431b7d
C
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
6221f311 244 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
245
246 return next()
b60e5f38
C
247 }
248]
34ca3b52 249
b60e5f38 250const videosSearchValidator = [
f3aaa9a9 251 query('search').not().isEmpty().withMessage('Should have a valid search'),
c45f7f84 252
b60e5f38
C
253 (req: express.Request, res: express.Response, next: express.NextFunction) => {
254 logger.debug('Checking videosSearch parameters', { parameters: req.params })
c45f7f84 255
a2431b7d
C
256 if (areValidationErrors(req, res)) return
257
258 return next()
b60e5f38
C
259 }
260]
c45f7f84 261
b60e5f38 262const videoAbuseReportValidator = [
72c7248b 263 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 264 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
55fa55a9 265
a2431b7d 266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 267 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
55fa55a9 268
a2431b7d
C
269 if (areValidationErrors(req, res)) return
270 if (!await isVideoExist(req.params.id, res)) return
271
272 return next()
b60e5f38
C
273 }
274]
55fa55a9 275
b60e5f38 276const videoRateValidator = [
72c7248b 277 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 278 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
d38b8281 279
a2431b7d 280 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 281 logger.debug('Checking videoRate parameters', { parameters: req.body })
d38b8281 282
a2431b7d
C
283 if (areValidationErrors(req, res)) return
284 if (!await isVideoExist(req.params.id, res)) return
285
286 return next()
b60e5f38
C
287 }
288]
d38b8281 289
4e50b6a1
C
290const 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
a2431b7d 298 if (!await isVideoExist(req.params.id, res)) return
4e50b6a1 299
3fd3ab2d 300 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
4e50b6a1
C
301 if (!share) {
302 return res.status(404)
303 .end()
304 }
305
306 res.locals.videoShare = share
4e50b6a1
C
307 return next()
308 }
309]
310
9f10b292 311// ---------------------------------------------------------------------------
c45f7f84 312
65fcc311
C
313export {
314 videosAddValidator,
315 videosUpdateValidator,
316 videosGetValidator,
317 videosRemoveValidator,
318 videosSearchValidator,
4e50b6a1 319 videosShareValidator,
65fcc311
C
320
321 videoAbuseReportValidator,
322
35bf0c83 323 videoRateValidator
65fcc311 324}
7b1f49de
C
325
326// ---------------------------------------------------------------------------
327
6221f311 328function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
198b205c 329 // Retrieve the user who did the request
a2431b7d
C
330 if (video.isOwned() === false) {
331 res.status(403)
60862425 332 .json({ error: 'Cannot remove video of another server, blacklist it' })
11474c3c 333 .end()
a2431b7d 334 return false
11474c3c
C
335 }
336
337 // Check if the user can delete the video
4cb6d457 338 // The user can delete it if he has the right
38fa2065 339 // Or if s/he is the video's account
a2431b7d 340 const account = video.VideoChannel.Account
6221f311 341 if (user.hasRight(right) === false && account.userId !== user.id) {
a2431b7d 342 res.status(403)
11474c3c
C
343 .json({ error: 'Cannot remove video of another user' })
344 .end()
a2431b7d 345 return false
11474c3c
C
346 }
347
a2431b7d 348 return true
198b205c 349}
ac81d1a0
C
350
351function 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}