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