]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
656d161d8ab5b14d731aa76e9f3326b696ed3557
[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 isVideoSupportValid,
30 isVideoTagsValid
31 } from '../../../helpers/custom-validators/videos'
32 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
33 import { logger } from '../../../helpers/logger'
34 import { CONSTRAINTS_FIELDS } from '../../../initializers'
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 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
46
47 const videosAddValidator = getCommonVideoAttributes().concat([
48 body('videofile')
49 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
50 'This file is not supported or too large. Please, make sure it is of the following type: '
51 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
52 ),
53 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
54 body('channelId')
55 .toInt()
56 .custom(isIdValid).withMessage('Should have correct video channel id'),
57
58 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
60
61 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
62 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
63
64 const videoFile: Express.Multer.File = req.files['videofile'][0]
65 const user = res.locals.oauth.token.User
66
67 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
68
69 const isAble = await user.isAbleToUploadVideo(videoFile)
70 if (isAble === false) {
71 res.status(403)
72 .json({ error: 'The user video quota is exceeded with this video.' })
73
74 return cleanUpReqFiles(req)
75 }
76
77 let duration: number
78
79 try {
80 duration = await getDurationFromVideoFile(videoFile.path)
81 } catch (err) {
82 logger.error('Invalid input file in videosAddValidator.', { err })
83 res.status(400)
84 .json({ error: 'Invalid input file.' })
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 }
123
124 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
125
126 return next()
127 }
128 ])
129
130 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
131 return [
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, fetchType)) 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 }
152
153 return next()
154 })
155 }
156
157 // Video is public, anyone can access it
158 if (video.privacy === VideoPrivacy.PUBLIC) return next()
159
160 // Video is unlisted, check we used the uuid to fetch it
161 if (video.privacy === VideoPrivacy.UNLISTED) {
162 if (isUUIDValid(req.params.id)) return next()
163
164 // Don't leak this unlisted video
165 return res.status(404).end()
166 }
167 }
168 ]
169 }
170
171 const videosGetValidator = videosCustomGetValidator('all')
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 videosChangeOwnershipValidator = [
190 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
191
192 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
193 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
194
195 if (areValidationErrors(req, res)) return
196 if (!await isVideoExist(req.params.videoId, res)) return
197
198 // Check if the user who did the request is able to change the ownership of the video
199 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
200
201 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
202 if (!nextOwner) {
203 res.status(400)
204 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
205
206 return
207 }
208 res.locals.nextOwner = nextOwner
209
210 return next()
211 }
212 ]
213
214 const videosTerminateChangeOwnershipValidator = [
215 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
216
217 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
218 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
219
220 if (areValidationErrors(req, res)) return
221 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
222
223 // Check if the user who did the request is able to change the ownership of the video
224 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
225
226 return next()
227 },
228 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
229 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
230
231 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
232 return next()
233 } else {
234 res.status(403)
235 .json({ error: 'Ownership already accepted or refused' })
236
237 return
238 }
239 }
240 ]
241
242 const videosAcceptChangeOwnershipValidator = [
243 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
244 const body = req.body as VideoChangeOwnershipAccept
245 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
246
247 const user = res.locals.oauth.token.User
248 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
249 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
250 if (isAble === false) {
251 res.status(403)
252 .json({ error: 'The user video quota is exceeded with this video.' })
253
254 return
255 }
256
257 return next()
258 }
259 ]
260
261 function getCommonVideoAttributes () {
262 return [
263 body('thumbnailfile')
264 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
265 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
266 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
267 ),
268 body('previewfile')
269 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
270 'This preview file is not supported or too large. Please, make sure it is of the following type: '
271 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
272 ),
273
274 body('category')
275 .optional()
276 .customSanitizer(toIntOrNull)
277 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
278 body('licence')
279 .optional()
280 .customSanitizer(toIntOrNull)
281 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
282 body('language')
283 .optional()
284 .customSanitizer(toValueOrNull)
285 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
286 body('nsfw')
287 .optional()
288 .toBoolean()
289 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
290 body('waitTranscoding')
291 .optional()
292 .toBoolean()
293 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
294 body('privacy')
295 .optional()
296 .toInt()
297 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
298 body('description')
299 .optional()
300 .customSanitizer(toValueOrNull)
301 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
302 body('support')
303 .optional()
304 .customSanitizer(toValueOrNull)
305 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
306 body('tags')
307 .optional()
308 .customSanitizer(toValueOrNull)
309 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
310 body('commentsEnabled')
311 .optional()
312 .toBoolean()
313 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
314
315 body('scheduleUpdate')
316 .optional()
317 .customSanitizer(toValueOrNull),
318 body('scheduleUpdate.updateAt')
319 .optional()
320 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
321 body('scheduleUpdate.privacy')
322 .optional()
323 .toInt()
324 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
325 ] as (ValidationChain | express.Handler)[]
326 }
327
328 const commonVideosFiltersValidator = [
329 query('categoryOneOf')
330 .optional()
331 .customSanitizer(toArray)
332 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
333 query('licenceOneOf')
334 .optional()
335 .customSanitizer(toArray)
336 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
337 query('languageOneOf')
338 .optional()
339 .customSanitizer(toArray)
340 .custom(isStringArray).withMessage('Should have a valid one of language array'),
341 query('tagsOneOf')
342 .optional()
343 .customSanitizer(toArray)
344 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
345 query('tagsAllOf')
346 .optional()
347 .customSanitizer(toArray)
348 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
349 query('nsfw')
350 .optional()
351 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
352 query('filter')
353 .optional()
354 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
355
356 (req: express.Request, res: express.Response, next: express.NextFunction) => {
357 logger.debug('Checking commons video filters query', { parameters: req.query })
358
359 if (areValidationErrors(req, res)) return
360
361 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
362 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
363 res.status(401)
364 .json({ error: 'You are not allowed to see all local videos.' })
365
366 return
367 }
368
369 return next()
370 }
371 ]
372
373 // ---------------------------------------------------------------------------
374
375 export {
376 videosAddValidator,
377 videosUpdateValidator,
378 videosGetValidator,
379 videosCustomGetValidator,
380 videosRemoveValidator,
381
382 videosChangeOwnershipValidator,
383 videosTerminateChangeOwnershipValidator,
384 videosAcceptChangeOwnershipValidator,
385
386 getCommonVideoAttributes,
387
388 commonVideosFiltersValidator
389 }
390
391 // ---------------------------------------------------------------------------
392
393 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
394 if (req.body.scheduleUpdate) {
395 if (!req.body.scheduleUpdate.updateAt) {
396 res.status(400)
397 .json({ error: 'Schedule update at is mandatory.' })
398
399 return true
400 }
401 }
402
403 return false
404 }