]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - 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
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 import { VideoFetchType } from '../../../helpers/video'
45
46 const videosAddValidator = getCommonVideoAttributes().concat([
47 body('videofile')
48 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
49 'This file is not supported or too large. Please, make sure it is of the following type: '
50 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
51 ),
52 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
53 body('channelId')
54 .toInt()
55 .custom(isIdValid).withMessage('Should have correct video channel id'),
56
57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
58 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
59
60 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
61 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
62
63 const videoFile: Express.Multer.File = req.files['videofile'][0]
64 const user = res.locals.oauth.token.User
65
66 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
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
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
85 return cleanUpReqFiles(req)
86 }
87
88 videoFile['duration'] = duration
89
90 return next()
91 }
92 ])
93
94 const videosUpdateValidator = getCommonVideoAttributes().concat([
95 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
96 body('name')
97 .optional()
98 .custom(isVideoNameValid).withMessage('Should have a valid name'),
99 body('channelId')
100 .optional()
101 .toInt()
102 .custom(isIdValid).withMessage('Should have correct video channel id'),
103
104 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
105 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
106
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)
110
111 const video = res.locals.video
112
113 // Check if the user who did the request is able to update the video
114 const user = res.locals.oauth.token.User
115 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
116
117 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
118 cleanUpReqFiles(req)
119 return res.status(409)
120 .json({ error: 'Cannot set "private" a video that was not private.' })
121 }
122
123 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
124
125 return next()
126 }
127 ])
128
129 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
130 return [
131 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
132
133 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
134 logger.debug('Checking videosGet parameters', { parameters: req.params })
135
136 if (areValidationErrors(req, res)) return
137 if (!await isVideoExist(req.params.id, res, fetchType)) return
138
139 const video: VideoModel = res.locals.video
140
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
145
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.' })
150 }
151
152 return next()
153 })
154 }
155
156 // Video is public, anyone can access it
157 if (video.privacy === VideoPrivacy.PUBLIC) return next()
158
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()
162
163 // Don't leak this unlisted video
164 return res.status(404).end()
165 }
166 }
167 ]
168 }
169
170 const videosGetValidator = videosCustomGetValidator('all')
171
172 const videosRemoveValidator = [
173 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
174
175 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
176 logger.debug('Checking videosRemove parameters', { parameters: req.params })
177
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
182 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
183
184 return next()
185 }
186 ]
187
188 const videoRateValidator = [
189 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
190 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
191
192 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
193 logger.debug('Checking videoRate parameters', { parameters: req.body })
194
195 if (areValidationErrors(req, res)) return
196 if (!await isVideoExist(req.params.id, res)) return
197
198 return next()
199 }
200 ]
201
202 const 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
210 if (!await isVideoExist(req.params.id, res)) return
211
212 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
213 if (!share) {
214 return res.status(404)
215 .end()
216 }
217
218 res.locals.videoShare = share
219 return next()
220 }
221 ]
222
223 const 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)
238 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
239
240 return
241 }
242 res.locals.nextOwner = nextOwner
243
244 return next()
245 }
246 ]
247
248 const 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' })
270
271 return
272 }
273 }
274 ]
275
276 const 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.' })
287
288 return
289 }
290
291 return next()
292 }
293 ]
294
295 function 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 }
361
362 // ---------------------------------------------------------------------------
363
364 export {
365 videosAddValidator,
366 videosUpdateValidator,
367 videosGetValidator,
368 videosCustomGetValidator,
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
389 return true
390 }
391 }
392
393 return false
394 }