]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos.ts
Optimize SQL requests of watch page API endpoints
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos.ts
CommitLineData
69818c93 1import * as express from 'express'
3fd3ab2d 2import 'express-validator'
57c36b27 3import { body, param, ValidationChain } from 'express-validator/check'
74d63469 4import { UserRight, VideoChangeOwnershipStatus, 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 {
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,
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 34import { VideoShareModel } from '../../models/video/video-share'
11474c3c 35import { authenticate } from '../oauth'
a2431b7d 36import { areValidationErrors } from './utils'
06215f15 37import { cleanUpReqFiles } from '../../helpers/express-utils'
191764f3
C
38import { VideoModel } from '../../models/video/video'
39import { UserModel } from '../../models/account/user'
74d63469
GR
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'
34ca3b52 44
a920fef1 45const videosAddValidator = getCommonVideoAttributes().concat([
0c237b19
C
46 body('videofile')
47 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
40e87e9e 48 'This file is not supported or too large. Please, make sure it is of the following type: '
0c237b19
C
49 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
50 ),
b60e5f38 51 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
52 body('channelId')
53 .toInt()
2baea0c7 54 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 55
a2431b7d 56 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
57 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
58
cf7a61b5
C
59 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
60 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
a2431b7d
C
61
62 const videoFile: Express.Multer.File = req.files['videofile'][0]
63 const user = res.locals.oauth.token.User
b60e5f38 64
cf7a61b5 65 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
a2431b7d
C
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
cf7a61b5 73 return cleanUpReqFiles(req)
a2431b7d
C
74 }
75
76 let duration: number
77
78 try {
79 duration = await getDurationFromVideoFile(videoFile.path)
80 } catch (err) {
d5b7d911 81 logger.error('Invalid input file in videosAddValidator.', { err })
a2431b7d
C
82 res.status(400)
83 .json({ error: 'Invalid input file.' })
84 .end()
85
cf7a61b5 86 return cleanUpReqFiles(req)
a2431b7d
C
87 }
88
a2431b7d
C
89 videoFile['duration'] = duration
90
91 return next()
b60e5f38 92 }
a920fef1 93])
b60e5f38 94
a920fef1 95const videosUpdateValidator = getCommonVideoAttributes().concat([
72c7248b 96 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
97 body('name')
98 .optional()
99 .custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
100 body('channelId')
101 .optional()
102 .toInt()
103 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 104
a2431b7d 105 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
106 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
107
cf7a61b5
C
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)
a2431b7d
C
111
112 const video = res.locals.video
113
6221f311 114 // Check if the user who did the request is able to update the video
0f320037 115 const user = res.locals.oauth.token.User
cf7a61b5 116 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d
C
117
118 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
cf7a61b5 119 cleanUpReqFiles(req)
a2431b7d 120 return res.status(409)
bbe0f064 121 .json({ error: 'Cannot set "private" a video that was not private.' })
a2431b7d
C
122 .end()
123 }
124
cf7a61b5 125 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 126
a2431b7d 127 return next()
b60e5f38 128 }
a920fef1 129])
c173e565 130
b60e5f38 131const videosGetValidator = [
72c7248b 132 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 133
a2431b7d 134 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 135 logger.debug('Checking videosGet parameters', { parameters: req.params })
7b1f49de 136
a2431b7d
C
137 if (areValidationErrors(req, res)) return
138 if (!await isVideoExist(req.params.id, res)) return
11474c3c 139
191764f3
C
140 const video: VideoModel = res.locals.video
141
142 // Video private or blacklisted
143 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
99492dbc 144 return authenticate(req, res, () => {
191764f3
C
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 }
11474c3c 159
81ebea48
C
160 // Video is public, anyone can access it
161 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 162
81ebea48
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()
166
167 // Don't leak this unlisted video
168 return res.status(404).end()
169 }
b60e5f38
C
170 }
171]
34ca3b52 172
b60e5f38 173const videosRemoveValidator = [
72c7248b 174 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 175
a2431b7d 176 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 177 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 178
a2431b7d
C
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
6221f311 183 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
184
185 return next()
b60e5f38
C
186 }
187]
34ca3b52 188
b60e5f38 189const videoRateValidator = [
72c7248b 190 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 191 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
d38b8281 192
a2431b7d 193 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 194 logger.debug('Checking videoRate parameters', { parameters: req.body })
d38b8281 195
a2431b7d
C
196 if (areValidationErrors(req, res)) return
197 if (!await isVideoExist(req.params.id, res)) return
198
199 return next()
b60e5f38
C
200 }
201]
d38b8281 202
4e50b6a1
C
203const 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
a2431b7d 211 if (!await isVideoExist(req.params.id, res)) return
4e50b6a1 212
3fd3ab2d 213 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
4e50b6a1
C
214 if (!share) {
215 return res.status(404)
216 .end()
217 }
218
219 res.locals.videoShare = share
4e50b6a1
C
220 return next()
221 }
222]
223
74d63469
GR
224const 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
249const 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
277const 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
a920fef1
C
296function 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}
fbad87b0
C
362
363// ---------------------------------------------------------------------------
364
365export {
366 videosAddValidator,
367 videosUpdateValidator,
368 videosGetValidator,
369 videosRemoveValidator,
370 videosShareValidator,
371
fbad87b0
C
372 videoRateValidator,
373
74d63469
GR
374 videosChangeOwnershipValidator,
375 videosTerminateChangeOwnershipValidator,
376 videosAcceptChangeOwnershipValidator,
377
fbad87b0
C
378 getCommonVideoAttributes
379}
380
381// ---------------------------------------------------------------------------
382
383function 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}