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