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