Add ability to import video with youtube-dl
[oweals/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 }