]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
Check live duration and size
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { isAbleToUploadVideo } from '@server/lib/user'
4 import { getServerActor } from '@server/models/application/application'
5 import { MVideoFullLight } from '@server/types/models'
6 import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
7 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
8 import {
9 isBooleanValid,
10 isDateValid,
11 isIdOrUUIDValid,
12 isIdValid,
13 isUUIDValid,
14 toArray,
15 toBooleanOrNull,
16 toIntOrNull,
17 toValueOrNull
18 } from '../../../helpers/custom-validators/misc'
19 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
20 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
21 import {
22 isScheduleVideoUpdatePrivacyValid,
23 isVideoCategoryValid,
24 isVideoDescriptionValid,
25 isVideoFile,
26 isVideoFilterValid,
27 isVideoImage,
28 isVideoLanguageValid,
29 isVideoLicenceValid,
30 isVideoNameValid,
31 isVideoOriginallyPublishedAtValid,
32 isVideoPrivacyValid,
33 isVideoSupportValid,
34 isVideoTagsValid
35 } from '../../../helpers/custom-validators/videos'
36 import { cleanUpReqFiles } from '../../../helpers/express-utils'
37 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
38 import { logger } from '../../../helpers/logger'
39 import {
40 checkUserCanManageVideo,
41 doesVideoChannelOfAccountExist,
42 doesVideoExist,
43 doesVideoFileOfVideoExist
44 } from '../../../helpers/middlewares'
45 import { getVideoWithAttributes } from '../../../helpers/video'
46 import { CONFIG } from '../../../initializers/config'
47 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
48 import { isLocalVideoAccepted } from '../../../lib/moderation'
49 import { Hooks } from '../../../lib/plugins/hooks'
50 import { AccountModel } from '../../../models/account/account'
51 import { VideoModel } from '../../../models/video/video'
52 import { authenticatePromiseIfNeeded } from '../../oauth'
53 import { areValidationErrors } from '../utils'
54
55 const videosAddValidator = getCommonVideoEditAttributes().concat([
56 body('videofile')
57 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
58 'This file is not supported or too large. Please, make sure it is of the following type: ' +
59 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
60 ),
61 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
62 body('channelId')
63 .customSanitizer(toIntOrNull)
64 .custom(isIdValid).withMessage('Should have correct video channel id'),
65
66 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
67 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
68
69 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
70 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
71
72 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
73 const user = res.locals.oauth.token.User
74
75 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
76
77 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
78 res.status(403)
79 .json({ error: 'The user video quota is exceeded with this video.' })
80
81 return cleanUpReqFiles(req)
82 }
83
84 let duration: number
85
86 try {
87 duration = await getDurationFromVideoFile(videoFile.path)
88 } catch (err) {
89 logger.error('Invalid input file in videosAddValidator.', { err })
90 res.status(400)
91 .json({ error: 'Invalid input file.' })
92
93 return cleanUpReqFiles(req)
94 }
95
96 videoFile.duration = duration
97
98 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
99
100 return next()
101 }
102 ])
103
104 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
105 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
106 body('name')
107 .optional()
108 .custom(isVideoNameValid).withMessage('Should have a valid name'),
109 body('channelId')
110 .optional()
111 .customSanitizer(toIntOrNull)
112 .custom(isIdValid).withMessage('Should have correct video channel id'),
113
114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
116
117 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
118 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
119 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
120
121 // Check if the user who did the request is able to update the video
122 const user = res.locals.oauth.token.User
123 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
124
125 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
126
127 return next()
128 }
129 ])
130
131 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video = getVideoWithAttributes(res)
133
134 // Anybody can watch local videos
135 if (video.isOwned() === true) return next()
136
137 // Logged user
138 if (res.locals.oauth) {
139 // Users can search or watch remote videos
140 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
141 }
142
143 // Anybody can search or watch remote videos
144 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
145
146 // Check our instance follows an actor that shared this video
147 const serverActor = await getServerActor()
148 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
149
150 return res.status(403)
151 .json({
152 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
153 error: 'Cannot get this video regarding follow constraints.',
154 originUrl: video.url
155 })
156 }
157
158 const videosCustomGetValidator = (
159 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
160 authenticateInQuery = false
161 ) => {
162 return [
163 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
164
165 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
166 logger.debug('Checking videosGet parameters', { parameters: req.params })
167
168 if (areValidationErrors(req, res)) return
169 if (!await doesVideoExist(req.params.id, res, fetchType)) return
170
171 // Controllers does not need to check video rights
172 if (fetchType === 'only-immutable-attributes') return next()
173
174 const video = getVideoWithAttributes(res)
175 const videoAll = video as MVideoFullLight
176
177 // Video private or blacklisted
178 if (videoAll.requiresAuth()) {
179 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
180
181 const user = res.locals.oauth ? res.locals.oauth.token.User : null
182
183 // Only the owner or a user that have blacklist rights can see the video
184 if (!user || !user.canGetVideo(videoAll)) {
185 return res.status(403)
186 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
187 }
188
189 return next()
190 }
191
192 // Video is public, anyone can access it
193 if (video.privacy === VideoPrivacy.PUBLIC) return next()
194
195 // Video is unlisted, check we used the uuid to fetch it
196 if (video.privacy === VideoPrivacy.UNLISTED) {
197 if (isUUIDValid(req.params.id)) return next()
198
199 // Don't leak this unlisted video
200 return res.status(404).end()
201 }
202 }
203 ]
204 }
205
206 const videosGetValidator = videosCustomGetValidator('all')
207 const videosDownloadValidator = videosCustomGetValidator('all', true)
208
209 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
210 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
211 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
212
213 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
214 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
215
216 if (areValidationErrors(req, res)) return
217 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
218
219 return next()
220 }
221 ])
222
223 const videosRemoveValidator = [
224 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
225
226 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
227 logger.debug('Checking videosRemove parameters', { parameters: req.params })
228
229 if (areValidationErrors(req, res)) return
230 if (!await doesVideoExist(req.params.id, res)) return
231
232 // Check if the user who did the request is able to delete the video
233 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
234
235 return next()
236 }
237 ]
238
239 const videosChangeOwnershipValidator = [
240 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
241
242 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
243 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
244
245 if (areValidationErrors(req, res)) return
246 if (!await doesVideoExist(req.params.videoId, res)) return
247
248 // Check if the user who did the request is able to change the ownership of the video
249 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
250
251 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
252 if (!nextOwner) {
253 res.status(400)
254 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
255
256 return
257 }
258 res.locals.nextOwner = nextOwner
259
260 return next()
261 }
262 ]
263
264 const videosTerminateChangeOwnershipValidator = [
265 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
266
267 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
268 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
269
270 if (areValidationErrors(req, res)) return
271 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
272
273 // Check if the user who did the request is able to change the ownership of the video
274 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
275
276 const videoChangeOwnership = res.locals.videoChangeOwnership
277
278 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
279 res.status(403)
280 .json({ error: 'Ownership already accepted or refused' })
281 return
282 }
283
284 return next()
285 }
286 ]
287
288 const videosAcceptChangeOwnershipValidator = [
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 const body = req.body as VideoChangeOwnershipAccept
291 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
292
293 const user = res.locals.oauth.token.User
294 const videoChangeOwnership = res.locals.videoChangeOwnership
295 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
296 if (isAble === false) {
297 res.status(403)
298 .json({ error: 'The user video quota is exceeded with this video.' })
299
300 return
301 }
302
303 return next()
304 }
305 ]
306
307 const videosOverviewValidator = [
308 query('page')
309 .optional()
310 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
311 .withMessage('Should have a valid pagination'),
312
313 (req: express.Request, res: express.Response, next: express.NextFunction) => {
314 if (areValidationErrors(req, res)) return
315
316 return next()
317 }
318 ]
319
320 function getCommonVideoEditAttributes () {
321 return [
322 body('thumbnailfile')
323 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
324 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
325 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
326 ),
327 body('previewfile')
328 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
329 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
330 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
331 ),
332
333 body('category')
334 .optional()
335 .customSanitizer(toIntOrNull)
336 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
337 body('licence')
338 .optional()
339 .customSanitizer(toIntOrNull)
340 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
341 body('language')
342 .optional()
343 .customSanitizer(toValueOrNull)
344 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
345 body('nsfw')
346 .optional()
347 .customSanitizer(toBooleanOrNull)
348 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
349 body('waitTranscoding')
350 .optional()
351 .customSanitizer(toBooleanOrNull)
352 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
353 body('privacy')
354 .optional()
355 .customSanitizer(toValueOrNull)
356 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
357 body('description')
358 .optional()
359 .customSanitizer(toValueOrNull)
360 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
361 body('support')
362 .optional()
363 .customSanitizer(toValueOrNull)
364 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
365 body('tags')
366 .optional()
367 .customSanitizer(toValueOrNull)
368 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
369 body('commentsEnabled')
370 .optional()
371 .customSanitizer(toBooleanOrNull)
372 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
373 body('downloadEnabled')
374 .optional()
375 .customSanitizer(toBooleanOrNull)
376 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
377 body('originallyPublishedAt')
378 .optional()
379 .customSanitizer(toValueOrNull)
380 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
381 body('scheduleUpdate')
382 .optional()
383 .customSanitizer(toValueOrNull),
384 body('scheduleUpdate.updateAt')
385 .optional()
386 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
387 body('scheduleUpdate.privacy')
388 .optional()
389 .customSanitizer(toIntOrNull)
390 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
391 ] as (ValidationChain | express.Handler)[]
392 }
393
394 const commonVideosFiltersValidator = [
395 query('categoryOneOf')
396 .optional()
397 .customSanitizer(toArray)
398 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
399 query('licenceOneOf')
400 .optional()
401 .customSanitizer(toArray)
402 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
403 query('languageOneOf')
404 .optional()
405 .customSanitizer(toArray)
406 .custom(isStringArray).withMessage('Should have a valid one of language array'),
407 query('tagsOneOf')
408 .optional()
409 .customSanitizer(toArray)
410 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
411 query('tagsAllOf')
412 .optional()
413 .customSanitizer(toArray)
414 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
415 query('nsfw')
416 .optional()
417 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
418 query('filter')
419 .optional()
420 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
421 query('skipCount')
422 .optional()
423 .customSanitizer(toBooleanOrNull)
424 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
425
426 (req: express.Request, res: express.Response, next: express.NextFunction) => {
427 logger.debug('Checking commons video filters query', { parameters: req.query })
428
429 if (areValidationErrors(req, res)) return
430
431 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
432 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
433 res.status(401)
434 .json({ error: 'You are not allowed to see all local videos.' })
435
436 return
437 }
438
439 return next()
440 }
441 ]
442
443 // ---------------------------------------------------------------------------
444
445 export {
446 videosAddValidator,
447 videosUpdateValidator,
448 videosGetValidator,
449 videoFileMetadataGetValidator,
450 videosDownloadValidator,
451 checkVideoFollowConstraints,
452 videosCustomGetValidator,
453 videosRemoveValidator,
454
455 videosChangeOwnershipValidator,
456 videosTerminateChangeOwnershipValidator,
457 videosAcceptChangeOwnershipValidator,
458
459 getCommonVideoEditAttributes,
460
461 commonVideosFiltersValidator,
462
463 videosOverviewValidator
464 }
465
466 // ---------------------------------------------------------------------------
467
468 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
469 if (req.body.scheduleUpdate) {
470 if (!req.body.scheduleUpdate.updateAt) {
471 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
472
473 res.status(400)
474 .json({ error: 'Schedule update at is mandatory.' })
475
476 return true
477 }
478 }
479
480 return false
481 }
482
483 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
484 // Check we accept this video
485 const acceptParameters = {
486 videoBody: req.body,
487 videoFile,
488 user: res.locals.oauth.token.User
489 }
490 const acceptedResult = await Hooks.wrapFun(
491 isLocalVideoAccepted,
492 acceptParameters,
493 'filter:api.video.upload.accept.result'
494 )
495
496 if (!acceptedResult || acceptedResult.accepted !== true) {
497 logger.info('Refused local video.', { acceptedResult, acceptParameters })
498 res.status(403)
499 .json({ error: acceptedResult.errorMessage || 'Refused local video' })
500
501 return false
502 }
503
504 return true
505 }