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