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