]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
Add user history and resume videos
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, ValidationChain } from 'express-validator/check'
4 import { UserRight, VideoChangeOwnershipStatus, 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 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42 import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43 import { AccountModel } from '../../../models/account/account'
44 import { VideoFetchType } from '../../../helpers/video'
45
46 const videosAddValidator = getCommonVideoAttributes().concat([
47 body('videofile')
48 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
49 'This file is not supported or too large. Please, make sure it is of the following type: '
50 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
51 ),
52 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
53 body('channelId')
54 .toInt()
55 .custom(isIdValid).withMessage('Should have correct video channel id'),
56
57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
58 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
59
60 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
61 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
62
63 const videoFile: Express.Multer.File = req.files['videofile'][0]
64 const user = res.locals.oauth.token.User
65
66 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
67
68 const isAble = await user.isAbleToUploadVideo(videoFile)
69 if (isAble === false) {
70 res.status(403)
71 .json({ error: 'The user video quota is exceeded with this video.' })
72 .end()
73
74 return cleanUpReqFiles(req)
75 }
76
77 let duration: number
78
79 try {
80 duration = await getDurationFromVideoFile(videoFile.path)
81 } catch (err) {
82 logger.error('Invalid input file in videosAddValidator.', { err })
83 res.status(400)
84 .json({ error: 'Invalid input file.' })
85 .end()
86
87 return cleanUpReqFiles(req)
88 }
89
90 videoFile['duration'] = duration
91
92 return next()
93 }
94 ])
95
96 const videosUpdateValidator = getCommonVideoAttributes().concat([
97 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
98 body('name')
99 .optional()
100 .custom(isVideoNameValid).withMessage('Should have a valid name'),
101 body('channelId')
102 .optional()
103 .toInt()
104 .custom(isIdValid).withMessage('Should have correct video channel id'),
105
106 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
107 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
108
109 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
110 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
111 if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
112
113 const video = res.locals.video
114
115 // Check if the user who did the request is able to update the video
116 const user = res.locals.oauth.token.User
117 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
118
119 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
120 cleanUpReqFiles(req)
121 return res.status(409)
122 .json({ error: 'Cannot set "private" a video that was not private.' })
123 .end()
124 }
125
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
127
128 return next()
129 }
130 ])
131
132 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
133 return [
134 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
135
136 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
137 logger.debug('Checking videosGet parameters', { parameters: req.params })
138
139 if (areValidationErrors(req, res)) return
140 if (!await isVideoExist(req.params.id, res, fetchType)) return
141
142 const video: VideoModel = res.locals.video
143
144 // Video private or blacklisted
145 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
146 return authenticate(req, res, () => {
147 const user: UserModel = res.locals.oauth.token.User
148
149 // Only the owner or a user that have blacklist rights can see the video
150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
151 return res.status(403)
152 .json({ error: 'Cannot get this private or blacklisted video.' })
153 .end()
154 }
155
156 return next()
157 })
158 }
159
160 // Video is public, anyone can access it
161 if (video.privacy === VideoPrivacy.PUBLIC) return next()
162
163 // Video is unlisted, check we used the uuid to fetch it
164 if (video.privacy === VideoPrivacy.UNLISTED) {
165 if (isUUIDValid(req.params.id)) return next()
166
167 // Don't leak this unlisted video
168 return res.status(404).end()
169 }
170 }
171 ]
172 }
173
174 const videosGetValidator = videosCustomGetValidator('all')
175
176 const videosRemoveValidator = [
177 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
178
179 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
180 logger.debug('Checking videosRemove parameters', { parameters: req.params })
181
182 if (areValidationErrors(req, res)) return
183 if (!await isVideoExist(req.params.id, res)) return
184
185 // Check if the user who did the request is able to delete the video
186 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
187
188 return next()
189 }
190 ]
191
192 const videoRateValidator = [
193 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
194 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
195
196 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
197 logger.debug('Checking videoRate parameters', { parameters: req.body })
198
199 if (areValidationErrors(req, res)) return
200 if (!await isVideoExist(req.params.id, res)) return
201
202 return next()
203 }
204 ]
205
206 const videosShareValidator = [
207 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
208 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
209
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videoShare parameters', { parameters: req.params })
212
213 if (areValidationErrors(req, res)) return
214 if (!await isVideoExist(req.params.id, res)) return
215
216 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
217 if (!share) {
218 return res.status(404)
219 .end()
220 }
221
222 res.locals.videoShare = share
223 return next()
224 }
225 ]
226
227 const videosChangeOwnershipValidator = [
228 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
229
230 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
231 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
232
233 if (areValidationErrors(req, res)) return
234 if (!await isVideoExist(req.params.videoId, res)) return
235
236 // Check if the user who did the request is able to change the ownership of the video
237 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
238
239 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
240 if (!nextOwner) {
241 res.status(400)
242 .type('json')
243 .end()
244 return
245 }
246 res.locals.nextOwner = nextOwner
247
248 return next()
249 }
250 ]
251
252 const videosTerminateChangeOwnershipValidator = [
253 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
254
255 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
256 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
257
258 if (areValidationErrors(req, res)) return
259 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
260
261 // Check if the user who did the request is able to change the ownership of the video
262 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
263
264 return next()
265 },
266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
268
269 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
270 return next()
271 } else {
272 res.status(403)
273 .json({ error: 'Ownership already accepted or refused' })
274 .end()
275 return
276 }
277 }
278 ]
279
280 const videosAcceptChangeOwnershipValidator = [
281 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
282 const body = req.body as VideoChangeOwnershipAccept
283 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
284
285 const user = res.locals.oauth.token.User
286 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
287 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
288 if (isAble === false) {
289 res.status(403)
290 .json({ error: 'The user video quota is exceeded with this video.' })
291 .end()
292 return
293 }
294
295 return next()
296 }
297 ]
298
299 function getCommonVideoAttributes () {
300 return [
301 body('thumbnailfile')
302 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
303 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
304 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
305 ),
306 body('previewfile')
307 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
308 'This preview file is not supported or too large. Please, make sure it is of the following type: '
309 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
310 ),
311
312 body('category')
313 .optional()
314 .customSanitizer(toIntOrNull)
315 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
316 body('licence')
317 .optional()
318 .customSanitizer(toIntOrNull)
319 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
320 body('language')
321 .optional()
322 .customSanitizer(toValueOrNull)
323 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
324 body('nsfw')
325 .optional()
326 .toBoolean()
327 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
328 body('waitTranscoding')
329 .optional()
330 .toBoolean()
331 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
332 body('privacy')
333 .optional()
334 .toInt()
335 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
336 body('description')
337 .optional()
338 .customSanitizer(toValueOrNull)
339 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
340 body('support')
341 .optional()
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
344 body('tags')
345 .optional()
346 .customSanitizer(toValueOrNull)
347 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
348 body('commentsEnabled')
349 .optional()
350 .toBoolean()
351 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
352
353 body('scheduleUpdate')
354 .optional()
355 .customSanitizer(toValueOrNull),
356 body('scheduleUpdate.updateAt')
357 .optional()
358 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
359 body('scheduleUpdate.privacy')
360 .optional()
361 .toInt()
362 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
363 ] as (ValidationChain | express.Handler)[]
364 }
365
366 // ---------------------------------------------------------------------------
367
368 export {
369 videosAddValidator,
370 videosUpdateValidator,
371 videosGetValidator,
372 videosCustomGetValidator,
373 videosRemoveValidator,
374 videosShareValidator,
375
376 videoRateValidator,
377
378 videosChangeOwnershipValidator,
379 videosTerminateChangeOwnershipValidator,
380 videosAcceptChangeOwnershipValidator,
381
382 getCommonVideoAttributes
383 }
384
385 // ---------------------------------------------------------------------------
386
387 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
388 if (req.body.scheduleUpdate) {
389 if (!req.body.scheduleUpdate.updateAt) {
390 res.status(400)
391 .json({ error: 'Schedule update at is mandatory.' })
392 .end()
393
394 return true
395 }
396 }
397
398 return false
399 }