]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Add explicit error message that changing video ownership only works with local accou...
[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.' })
a2431b7d 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.' })
a2431b7d 84
cf7a61b5 85 return cleanUpReqFiles(req)
a2431b7d
C
86 }
87
a2431b7d
C
88 videoFile['duration'] = duration
89
90 return next()
b60e5f38 91 }
a920fef1 92])
b60e5f38 93
a920fef1 94const videosUpdateValidator = getCommonVideoAttributes().concat([
72c7248b 95 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
96 body('name')
97 .optional()
98 .custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
99 body('channelId')
100 .optional()
101 .toInt()
102 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 103
a2431b7d 104 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
105 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
106
cf7a61b5
C
107 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
108 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
109 if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
a2431b7d
C
110
111 const video = res.locals.video
112
6221f311 113 // Check if the user who did the request is able to update the video
0f320037 114 const user = res.locals.oauth.token.User
cf7a61b5 115 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d
C
116
117 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
cf7a61b5 118 cleanUpReqFiles(req)
a2431b7d 119 return res.status(409)
bbe0f064 120 .json({ error: 'Cannot set "private" a video that was not private.' })
a2431b7d
C
121 }
122
cf7a61b5 123 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 124
a2431b7d 125 return next()
b60e5f38 126 }
a920fef1 127])
c173e565 128
96f29c0f
C
129const videosCustomGetValidator = (fetchType: VideoFetchType) => {
130 return [
131 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 132
96f29c0f
C
133 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
134 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 135
96f29c0f
C
136 if (areValidationErrors(req, res)) return
137 if (!await isVideoExist(req.params.id, res, fetchType)) return
191764f3 138
96f29c0f 139 const video: VideoModel = res.locals.video
191764f3 140
96f29c0f
C
141 // Video private or blacklisted
142 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
143 return authenticate(req, res, () => {
144 const user: UserModel = res.locals.oauth.token.User
191764f3 145
96f29c0f
C
146 // Only the owner or a user that have blacklist rights can see the video
147 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
148 return res.status(403)
149 .json({ error: 'Cannot get this private or blacklisted video.' })
96f29c0f 150 }
191764f3 151
96f29c0f
C
152 return next()
153 })
154 }
11474c3c 155
96f29c0f
C
156 // Video is public, anyone can access it
157 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 158
96f29c0f
C
159 // Video is unlisted, check we used the uuid to fetch it
160 if (video.privacy === VideoPrivacy.UNLISTED) {
161 if (isUUIDValid(req.params.id)) return next()
81ebea48 162
96f29c0f
C
163 // Don't leak this unlisted video
164 return res.status(404).end()
165 }
81ebea48 166 }
96f29c0f
C
167 ]
168}
169
170const videosGetValidator = videosCustomGetValidator('all')
34ca3b52 171
b60e5f38 172const videosRemoveValidator = [
72c7248b 173 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 174
a2431b7d 175 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 176 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 177
a2431b7d
C
178 if (areValidationErrors(req, res)) return
179 if (!await isVideoExist(req.params.id, res)) return
180
181 // Check if the user who did the request is able to delete the video
6221f311 182 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
183
184 return next()
b60e5f38
C
185 }
186]
34ca3b52 187
b60e5f38 188const videoRateValidator = [
72c7248b 189 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
b60e5f38 190 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
d38b8281 191
a2431b7d 192 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 193 logger.debug('Checking videoRate parameters', { parameters: req.body })
d38b8281 194
a2431b7d
C
195 if (areValidationErrors(req, res)) return
196 if (!await isVideoExist(req.params.id, res)) return
197
198 return next()
b60e5f38
C
199 }
200]
d38b8281 201
4e50b6a1
C
202const videosShareValidator = [
203 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
204 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
205
206 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
207 logger.debug('Checking videoShare parameters', { parameters: req.params })
208
209 if (areValidationErrors(req, res)) return
a2431b7d 210 if (!await isVideoExist(req.params.id, res)) return
4e50b6a1 211
3fd3ab2d 212 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
4e50b6a1
C
213 if (!share) {
214 return res.status(404)
215 .end()
216 }
217
218 res.locals.videoShare = share
4e50b6a1
C
219 return next()
220 }
221]
222
74d63469
GR
223const videosChangeOwnershipValidator = [
224 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
225
226 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
227 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
228
229 if (areValidationErrors(req, res)) return
230 if (!await isVideoExist(req.params.videoId, res)) return
231
232 // Check if the user who did the request is able to change the ownership of the video
233 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
234
235 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
236 if (!nextOwner) {
237 res.status(400)
9ccff238
LD
238 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
239
74d63469
GR
240 return
241 }
242 res.locals.nextOwner = nextOwner
243
244 return next()
245 }
246]
247
248const videosTerminateChangeOwnershipValidator = [
249 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
250
251 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
252 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
253
254 if (areValidationErrors(req, res)) return
255 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
256
257 // Check if the user who did the request is able to change the ownership of the video
258 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
259
260 return next()
261 },
262 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
263 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
264
265 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
266 return next()
267 } else {
268 res.status(403)
269 .json({ error: 'Ownership already accepted or refused' })
9ccff238 270
74d63469
GR
271 return
272 }
273 }
274]
275
276const videosAcceptChangeOwnershipValidator = [
277 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
278 const body = req.body as VideoChangeOwnershipAccept
279 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
280
281 const user = res.locals.oauth.token.User
282 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
283 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
284 if (isAble === false) {
285 res.status(403)
286 .json({ error: 'The user video quota is exceeded with this video.' })
9ccff238 287
74d63469
GR
288 return
289 }
290
291 return next()
292 }
293]
294
a920fef1
C
295function getCommonVideoAttributes () {
296 return [
297 body('thumbnailfile')
298 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
299 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
300 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
301 ),
302 body('previewfile')
303 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
304 'This preview file is not supported or too large. Please, make sure it is of the following type: '
305 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
306 ),
307
308 body('category')
309 .optional()
310 .customSanitizer(toIntOrNull)
311 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
312 body('licence')
313 .optional()
314 .customSanitizer(toIntOrNull)
315 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
316 body('language')
317 .optional()
318 .customSanitizer(toValueOrNull)
319 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
320 body('nsfw')
321 .optional()
322 .toBoolean()
323 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
324 body('waitTranscoding')
325 .optional()
326 .toBoolean()
327 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
328 body('privacy')
329 .optional()
330 .toInt()
331 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
332 body('description')
333 .optional()
334 .customSanitizer(toValueOrNull)
335 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
336 body('support')
337 .optional()
338 .customSanitizer(toValueOrNull)
339 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
340 body('tags')
341 .optional()
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
344 body('commentsEnabled')
345 .optional()
346 .toBoolean()
347 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
348
349 body('scheduleUpdate')
350 .optional()
351 .customSanitizer(toValueOrNull),
352 body('scheduleUpdate.updateAt')
353 .optional()
354 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
355 body('scheduleUpdate.privacy')
356 .optional()
357 .toInt()
358 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
359 ] as (ValidationChain | express.Handler)[]
360}
fbad87b0
C
361
362// ---------------------------------------------------------------------------
363
364export {
365 videosAddValidator,
366 videosUpdateValidator,
367 videosGetValidator,
96f29c0f 368 videosCustomGetValidator,
fbad87b0
C
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.' })
fbad87b0
C
388
389 return true
390 }
391 }
392
393 return false
394}