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