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