]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos.ts
abb23b510f04699f28476fd38ed4090131ddc3cf
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / 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, 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 isVideoAbuseReasonValid,
18 isVideoCategoryValid,
19 isVideoChannelOfAccountExist,
20 isVideoDescriptionValid,
21 isVideoExist,
22 isVideoFile,
23 isVideoImage,
24 isVideoLanguageValid,
25 isVideoLicenceValid,
26 isVideoNameValid,
27 isVideoPrivacyValid,
28 isVideoRatingTypeValid,
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 { VideoShareModel } from '../../models/video/video-share'
36 import { authenticate } from '../oauth'
37 import { areValidationErrors } from './utils'
38
39 const videosAddValidator = getCommonVideoAttributes().concat([
40 body('videofile')
41 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
42 'This file is not supported or too large. Please, make sure it is of the following type: '
43 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
44 ),
45 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
46 body('channelId')
47 .toInt()
48 .custom(isIdValid).withMessage('Should have correct video channel id'),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
52
53 if (areValidationErrors(req, res)) return
54 if (areErrorsInScheduleUpdate(req, res)) return
55
56 const videoFile: Express.Multer.File = req.files['videofile'][0]
57 const user = res.locals.oauth.token.User
58
59 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
60
61 const isAble = await user.isAbleToUploadVideo(videoFile)
62 if (isAble === false) {
63 res.status(403)
64 .json({ error: 'The user video quota is exceeded with this video.' })
65 .end()
66
67 return
68 }
69
70 let duration: number
71
72 try {
73 duration = await getDurationFromVideoFile(videoFile.path)
74 } catch (err) {
75 logger.error('Invalid input file in videosAddValidator.', { err })
76 res.status(400)
77 .json({ error: 'Invalid input file.' })
78 .end()
79
80 return
81 }
82
83 videoFile['duration'] = duration
84
85 return next()
86 }
87 ])
88
89 const videosUpdateValidator = getCommonVideoAttributes().concat([
90 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
91 body('name')
92 .optional()
93 .custom(isVideoNameValid).withMessage('Should have a valid name'),
94 body('channelId')
95 .optional()
96 .toInt()
97 .custom(isIdValid).withMessage('Should have correct video channel id'),
98
99 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
100 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
101
102 if (areValidationErrors(req, res)) return
103 if (areErrorsInScheduleUpdate(req, res)) return
104 if (!await isVideoExist(req.params.id, res)) return
105
106 const video = res.locals.video
107
108 // Check if the user who did the request is able to update the video
109 const user = res.locals.oauth.token.User
110 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
111
112 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
113 return res.status(409)
114 .json({ error: 'Cannot set "private" a video that was not private.' })
115 .end()
116 }
117
118 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return
119
120 return next()
121 }
122 ])
123
124 const videosGetValidator = [
125 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
126
127 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
128 logger.debug('Checking videosGet parameters', { parameters: req.params })
129
130 if (areValidationErrors(req, res)) return
131 if (!await isVideoExist(req.params.id, res)) return
132
133 const video = res.locals.video
134
135 // Video is public, anyone can access it
136 if (video.privacy === VideoPrivacy.PUBLIC) return next()
137
138 // Video is unlisted, check we used the uuid to fetch it
139 if (video.privacy === VideoPrivacy.UNLISTED) {
140 if (isUUIDValid(req.params.id)) return next()
141
142 // Don't leak this unlisted video
143 return res.status(404).end()
144 }
145
146 // Video is private, check the user
147 authenticate(req, res, () => {
148 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
149 return res.status(403)
150 .json({ error: 'Cannot get this private video of another user' })
151 .end()
152 }
153
154 return next()
155 })
156 }
157 ]
158
159 const videosRemoveValidator = [
160 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
161
162 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
163 logger.debug('Checking videosRemove parameters', { parameters: req.params })
164
165 if (areValidationErrors(req, res)) return
166 if (!await isVideoExist(req.params.id, res)) return
167
168 // Check if the user who did the request is able to delete the video
169 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
170
171 return next()
172 }
173 ]
174
175 const videosSearchValidator = [
176 query('search').not().isEmpty().withMessage('Should have a valid search'),
177
178 (req: express.Request, res: express.Response, next: express.NextFunction) => {
179 logger.debug('Checking videosSearch parameters', { parameters: req.params })
180
181 if (areValidationErrors(req, res)) return
182
183 return next()
184 }
185 ]
186
187 const videoAbuseReportValidator = [
188 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
189 body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
190
191 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
192 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
193
194 if (areValidationErrors(req, res)) return
195 if (!await isVideoExist(req.params.id, res)) return
196
197 return next()
198 }
199 ]
200
201 const videoRateValidator = [
202 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
203 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
204
205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
206 logger.debug('Checking videoRate parameters', { parameters: req.body })
207
208 if (areValidationErrors(req, res)) return
209 if (!await isVideoExist(req.params.id, res)) return
210
211 return next()
212 }
213 ]
214
215 const videosShareValidator = [
216 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
217 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
218
219 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
220 logger.debug('Checking videoShare parameters', { parameters: req.params })
221
222 if (areValidationErrors(req, res)) return
223 if (!await isVideoExist(req.params.id, res)) return
224
225 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
226 if (!share) {
227 return res.status(404)
228 .end()
229 }
230
231 res.locals.videoShare = share
232 return next()
233 }
234 ]
235
236 // ---------------------------------------------------------------------------
237
238 export {
239 videosAddValidator,
240 videosUpdateValidator,
241 videosGetValidator,
242 videosRemoveValidator,
243 videosSearchValidator,
244 videosShareValidator,
245
246 videoAbuseReportValidator,
247
248 videoRateValidator
249 }
250
251 // ---------------------------------------------------------------------------
252
253 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
254 if (req.body.scheduleUpdate) {
255 if (!req.body.scheduleUpdate.updateAt) {
256 res.status(400)
257 .json({ error: 'Schedule update at is mandatory.' })
258 .end()
259
260 return true
261 }
262 }
263
264 return false
265 }
266
267 function getCommonVideoAttributes () {
268 return [
269 body('thumbnailfile')
270 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
271 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
272 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
273 ),
274 body('previewfile')
275 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
276 'This preview file is not supported or too large. Please, make sure it is of the following type: '
277 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
278 ),
279
280 body('category')
281 .optional()
282 .customSanitizer(toIntOrNull)
283 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
284 body('licence')
285 .optional()
286 .customSanitizer(toIntOrNull)
287 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
288 body('language')
289 .optional()
290 .customSanitizer(toValueOrNull)
291 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
292 body('nsfw')
293 .optional()
294 .toBoolean()
295 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
296 body('waitTranscoding')
297 .optional()
298 .toBoolean()
299 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
300 body('privacy')
301 .optional()
302 .toInt()
303 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
304 body('description')
305 .optional()
306 .customSanitizer(toValueOrNull)
307 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
308 body('support')
309 .optional()
310 .customSanitizer(toValueOrNull)
311 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
312 body('tags')
313 .optional()
314 .customSanitizer(toValueOrNull)
315 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
316 body('commentsEnabled')
317 .optional()
318 .toBoolean()
319 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
320
321 body('scheduleUpdate')
322 .optional()
323 .customSanitizer(toValueOrNull),
324 body('scheduleUpdate.updateAt')
325 .optional()
326 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
327 body('scheduleUpdate.privacy')
328 .optional()
329 .toInt()
330 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
331 ] as (ValidationChain | express.Handler)[]
332 }