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