Add state and moderationComment for abuses on server side
[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/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 cleanUpReqFiles(req)
54     if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
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 cleanUpReqFiles(req)
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 cleanUpReqFiles(req)
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 cleanUpReqFiles(req)
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 cleanUpReqFiles(req)
103     if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
104     if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
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 cleanUpReqFiles(req)
111
112     if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
113       cleanUpReqFiles(req)
114       return res.status(409)
115         .json({ error: 'Cannot set "private" a video that was not private.' })
116         .end()
117     }
118
119     if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
120
121     return next()
122   }
123 ])
124
125 const videosGetValidator = [
126   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
127
128   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
129     logger.debug('Checking videosGet parameters', { parameters: req.params })
130
131     if (areValidationErrors(req, res)) return
132     if (!await isVideoExist(req.params.id, res)) return
133
134     const video = res.locals.video
135
136     // Video is public, anyone can access it
137     if (video.privacy === VideoPrivacy.PUBLIC) return next()
138
139     // Video is unlisted, check we used the uuid to fetch it
140     if (video.privacy === VideoPrivacy.UNLISTED) {
141       if (isUUIDValid(req.params.id)) return next()
142
143       // Don't leak this unlisted video
144       return res.status(404).end()
145     }
146
147     // Video is private, check the user
148     authenticate(req, res, () => {
149       if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
150         return res.status(403)
151           .json({ error: 'Cannot get this private video of another user' })
152           .end()
153       }
154
155       return next()
156     })
157   }
158 ]
159
160 const videosRemoveValidator = [
161   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
162
163   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
164     logger.debug('Checking videosRemove parameters', { parameters: req.params })
165
166     if (areValidationErrors(req, res)) return
167     if (!await isVideoExist(req.params.id, res)) return
168
169     // Check if the user who did the request is able to delete the video
170     if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
171
172     return next()
173   }
174 ]
175
176 const videoRateValidator = [
177   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
178   body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
179
180   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
181     logger.debug('Checking videoRate parameters', { parameters: req.body })
182
183     if (areValidationErrors(req, res)) return
184     if (!await isVideoExist(req.params.id, res)) return
185
186     return next()
187   }
188 ]
189
190 const videosShareValidator = [
191   param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
192   param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
193
194   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
195     logger.debug('Checking videoShare parameters', { parameters: req.params })
196
197     if (areValidationErrors(req, res)) return
198     if (!await isVideoExist(req.params.id, res)) return
199
200     const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
201     if (!share) {
202       return res.status(404)
203         .end()
204     }
205
206     res.locals.videoShare = share
207     return next()
208   }
209 ]
210
211 function getCommonVideoAttributes () {
212   return [
213     body('thumbnailfile')
214       .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
215       'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
216       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
217     ),
218     body('previewfile')
219       .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
220       'This preview file is not supported or too large. Please, make sure it is of the following type: '
221       + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
222     ),
223
224     body('category')
225       .optional()
226       .customSanitizer(toIntOrNull)
227       .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
228     body('licence')
229       .optional()
230       .customSanitizer(toIntOrNull)
231       .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
232     body('language')
233       .optional()
234       .customSanitizer(toValueOrNull)
235       .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
236     body('nsfw')
237       .optional()
238       .toBoolean()
239       .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
240     body('waitTranscoding')
241       .optional()
242       .toBoolean()
243       .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
244     body('privacy')
245       .optional()
246       .toInt()
247       .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
248     body('description')
249       .optional()
250       .customSanitizer(toValueOrNull)
251       .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
252     body('support')
253       .optional()
254       .customSanitizer(toValueOrNull)
255       .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
256     body('tags')
257       .optional()
258       .customSanitizer(toValueOrNull)
259       .custom(isVideoTagsValid).withMessage('Should have correct tags'),
260     body('commentsEnabled')
261       .optional()
262       .toBoolean()
263       .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
264
265     body('scheduleUpdate')
266       .optional()
267       .customSanitizer(toValueOrNull),
268     body('scheduleUpdate.updateAt')
269       .optional()
270       .custom(isDateValid).withMessage('Should have a valid schedule update date'),
271     body('scheduleUpdate.privacy')
272       .optional()
273       .toInt()
274       .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
275   ] as (ValidationChain | express.Handler)[]
276 }
277
278 // ---------------------------------------------------------------------------
279
280 export {
281   videosAddValidator,
282   videosUpdateValidator,
283   videosGetValidator,
284   videosRemoveValidator,
285   videosShareValidator,
286
287   videoRateValidator,
288
289   getCommonVideoAttributes
290 }
291
292 // ---------------------------------------------------------------------------
293
294 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
295   if (req.body.scheduleUpdate) {
296     if (!req.body.scheduleUpdate.updateAt) {
297       res.status(400)
298          .json({ error: 'Schedule update at is mandatory.' })
299          .end()
300
301       return true
302     }
303   }
304
305   return false
306 }