]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos.ts
Update Janitor Dockerfile
[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'
2efd32f6 5import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, toValueOrNull } from '../../helpers/custom-validators/misc'
b60e5f38 6import {
ac81d1a0
C
7 isVideoAbuseReasonValid,
8 isVideoCategoryValid,
0f320037 9 isVideoChannelOfAccountExist,
ac81d1a0
C
10 isVideoDescriptionValid,
11 isVideoExist,
12 isVideoFile,
13 isVideoImage,
14 isVideoLanguageValid,
15 isVideoLicenceValid,
16 isVideoNameValid,
17 isVideoPrivacyValid,
360329cc
C
18 isVideoRatingTypeValid,
19 isVideoSupportValid,
ac81d1a0 20 isVideoTagsValid
8d468a16 21} from '../../helpers/custom-validators/videos'
da854ddd
C
22import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
23import { logger } from '../../helpers/logger'
f3aaa9a9 24import { CONSTRAINTS_FIELDS } from '../../initializers'
3fd3ab2d
C
25import { UserModel } from '../../models/account/user'
26import { VideoModel } from '../../models/video/video'
3fd3ab2d 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()
2efd32f6 55 .customSanitizer(toValueOrNull)
360329cc
C
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()
2efd32f6 62 .customSanitizer(toValueOrNull)
360329cc
C
63 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
64 body('support')
65 .optional()
2efd32f6 66 .customSanitizer(toValueOrNull)
360329cc
C
67 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
68 body('tags')
69 .optional()
2efd32f6 70 .customSanitizer(toValueOrNull)
360329cc
C
71 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
72 body('commentsEnabled')
73 .toBoolean()
74 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
75 body('privacy')
76 .optional()
77 .toInt()
78 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
0f320037
C
79 body('channelId')
80 .toInt()
81 .custom(isIdValid)
82 .withMessage('Should have correct video channel id'),
b60e5f38 83
a2431b7d 84 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
85 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
86
a2431b7d 87 if (areValidationErrors(req, res)) return
ac81d1a0 88 if (areErrorsInVideoImageFiles(req, res)) return
a2431b7d
C
89
90 const videoFile: Express.Multer.File = req.files['videofile'][0]
91 const user = res.locals.oauth.token.User
b60e5f38 92
0f320037 93 if (!await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
a2431b7d
C
94
95 const isAble = await user.isAbleToUploadVideo(videoFile)
96 if (isAble === false) {
97 res.status(403)
98 .json({ error: 'The user video quota is exceeded with this video.' })
99 .end()
100
101 return
102 }
103
104 let duration: number
105
106 try {
107 duration = await getDurationFromVideoFile(videoFile.path)
108 } catch (err) {
d5b7d911 109 logger.error('Invalid input file in videosAddValidator.', { err })
a2431b7d
C
110 res.status(400)
111 .json({ error: 'Invalid input file.' })
112 .end()
113
114 return
115 }
116
a2431b7d
C
117 videoFile['duration'] = duration
118
119 return next()
b60e5f38
C
120 }
121]
122
123const videosUpdateValidator = [
72c7248b 124 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
ac81d1a0
C
125 body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
126 'This thumbnail file is not supported. Please, make sure it is of the following type : '
127 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
128 ),
129 body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
130 'This preview file is not supported. Please, make sure it is of the following type : '
131 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
132 ),
360329cc
C
133 body('name')
134 .optional()
135 .custom(isVideoNameValid).withMessage('Should have a valid name'),
136 body('category')
137 .optional()
138 .customSanitizer(toIntOrNull)
139 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
140 body('licence')
141 .optional()
142 .customSanitizer(toIntOrNull)
143 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
144 body('language')
145 .optional()
2efd32f6 146 .customSanitizer(toValueOrNull)
360329cc
C
147 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
148 body('nsfw')
149 .optional()
150 .toBoolean()
151 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
152 body('privacy')
153 .optional()
154 .toInt()
155 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
156 body('description')
157 .optional()
2efd32f6 158 .customSanitizer(toValueOrNull)
360329cc
C
159 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
160 body('support')
161 .optional()
2efd32f6 162 .customSanitizer(toValueOrNull)
360329cc
C
163 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
164 body('tags')
165 .optional()
2efd32f6 166 .customSanitizer(toValueOrNull)
360329cc
C
167 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
168 body('commentsEnabled')
169 .optional()
170 .toBoolean()
171 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
0f320037
C
172 body('channelId')
173 .optional()
174 .toInt()
175 .custom(isIdValid).withMessage('Should have correct video channel id'),
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 186 // Check if the user who did the request is able to update the video
0f320037
C
187 const user = res.locals.oauth.token.User
188 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
a2431b7d
C
189
190 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
191 return res.status(409)
192 .json({ error: 'Cannot set "private" a video that was not private anymore.' })
193 .end()
194 }
195
0f320037
C
196 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
197
a2431b7d 198 return next()
b60e5f38
C
199 }
200]
c173e565 201
b60e5f38 202const videosGetValidator = [
72c7248b 203 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 204
a2431b7d 205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 206 logger.debug('Checking videosGet parameters', { parameters: req.params })
7b1f49de 207
a2431b7d
C
208 if (areValidationErrors(req, res)) return
209 if (!await isVideoExist(req.params.id, res)) return
11474c3c 210
a2431b7d 211 const video = res.locals.video
11474c3c 212
81ebea48
C
213 // Video is public, anyone can access it
214 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 215
81ebea48
C
216 // Video is unlisted, check we used the uuid to fetch it
217 if (video.privacy === VideoPrivacy.UNLISTED) {
218 if (isUUIDValid(req.params.id)) return next()
219
220 // Don't leak this unlisted video
221 return res.status(404).end()
222 }
223
224 // Video is private, check the user
a2431b7d
C
225 authenticate(req, res, () => {
226 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
227 return res.status(403)
228 .json({ error: 'Cannot get this private video of another user' })
229 .end()
230 }
231
232 return next()
b60e5f38
C
233 })
234 }
235]
34ca3b52 236
b60e5f38 237const videosRemoveValidator = [
72c7248b 238 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 239
a2431b7d 240 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 241 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 242
a2431b7d
C
243 if (areValidationErrors(req, res)) return
244 if (!await isVideoExist(req.params.id, res)) return
245
246 // Check if the user who did the request is able to delete the video
6221f311 247 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
248
249 return next()
b60e5f38
C
250 }
251]
34ca3b52 252
b60e5f38 253const videosSearchValidator = [
f3aaa9a9 254 query('search').not().isEmpty().withMessage('Should have a valid search'),
c45f7f84 255
b60e5f38
C
256 (req: express.Request, res: express.Response, next: express.NextFunction) => {
257 logger.debug('Checking videosSearch parameters', { parameters: req.params })
c45f7f84 258
a2431b7d
C
259 if (areValidationErrors(req, res)) return
260
261 return next()
b60e5f38
C
262 }
263]
c45f7f84 264
b60e5f38 265const videoAbuseReportValidator = [
72c7248b 266 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 267 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
55fa55a9 268
a2431b7d 269 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 270 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
55fa55a9 271
a2431b7d
C
272 if (areValidationErrors(req, res)) return
273 if (!await isVideoExist(req.params.id, res)) return
274
275 return next()
b60e5f38
C
276 }
277]
55fa55a9 278
b60e5f38 279const videoRateValidator = [
72c7248b 280 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 281 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
d38b8281 282
a2431b7d 283 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 284 logger.debug('Checking videoRate parameters', { parameters: req.body })
d38b8281 285
a2431b7d
C
286 if (areValidationErrors(req, res)) return
287 if (!await isVideoExist(req.params.id, res)) return
288
289 return next()
b60e5f38
C
290 }
291]
d38b8281 292
4e50b6a1
C
293const videosShareValidator = [
294 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
295 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
296
297 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
298 logger.debug('Checking videoShare parameters', { parameters: req.params })
299
300 if (areValidationErrors(req, res)) return
a2431b7d 301 if (!await isVideoExist(req.params.id, res)) return
4e50b6a1 302
3fd3ab2d 303 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
4e50b6a1
C
304 if (!share) {
305 return res.status(404)
306 .end()
307 }
308
309 res.locals.videoShare = share
4e50b6a1
C
310 return next()
311 }
312]
313
9f10b292 314// ---------------------------------------------------------------------------
c45f7f84 315
65fcc311
C
316export {
317 videosAddValidator,
318 videosUpdateValidator,
319 videosGetValidator,
320 videosRemoveValidator,
321 videosSearchValidator,
4e50b6a1 322 videosShareValidator,
65fcc311
C
323
324 videoAbuseReportValidator,
325
35bf0c83 326 videoRateValidator
65fcc311 327}
7b1f49de
C
328
329// ---------------------------------------------------------------------------
330
6221f311 331function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: express.Response) {
198b205c 332 // Retrieve the user who did the request
a2431b7d
C
333 if (video.isOwned() === false) {
334 res.status(403)
60862425 335 .json({ error: 'Cannot remove video of another server, blacklist it' })
11474c3c 336 .end()
a2431b7d 337 return false
11474c3c
C
338 }
339
340 // Check if the user can delete the video
4cb6d457 341 // The user can delete it if he has the right
38fa2065 342 // Or if s/he is the video's account
a2431b7d 343 const account = video.VideoChannel.Account
6221f311 344 if (user.hasRight(right) === false && account.userId !== user.id) {
a2431b7d 345 res.status(403)
11474c3c
C
346 .json({ error: 'Cannot remove video of another user' })
347 .end()
a2431b7d 348 return false
11474c3c
C
349 }
350
a2431b7d 351 return true
198b205c 352}
ac81d1a0
C
353
354function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
355 // Files are optional
356 if (!req.files) return false
357
358 for (const imageField of [ 'thumbnail', 'preview' ]) {
359 if (!req.files[ imageField ]) continue
360
361 const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
362 if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
363 res.status(400)
364 .send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
365 .end()
366 return true
367 }
368 }
369
370 return false
371}