]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Move to eslint
[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
eccf70f0 150const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights', authenticateInQuery = false) => {
96f29c0f
C
151 return [
152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 153
96f29c0f
C
154 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
155 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 156
96f29c0f 157 if (areValidationErrors(req, res)) return
0f6acda1 158 if (!await doesVideoExist(req.params.id, res, fetchType)) return
191764f3 159
0283eaac 160 const video = getVideoWithAttributes(res)
453e83ea 161 const videoAll = video as MVideoFullLight
191764f3 162
96f29c0f 163 // Video private or blacklisted
22a73cb8 164 if (videoAll.requiresAuth()) {
eccf70f0 165 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
8d427346 166
dae86118 167 const user = res.locals.oauth ? res.locals.oauth.token.User : null
191764f3 168
8d427346 169 // Only the owner or a user that have blacklist rights can see the video
22a73cb8 170 if (!user || !user.canGetVideo(videoAll)) {
8d427346 171 return res.status(403)
22a73cb8 172 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
8d427346 173 }
191764f3 174
8d427346 175 return next()
96f29c0f 176 }
11474c3c 177
96f29c0f
C
178 // Video is public, anyone can access it
179 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 180
96f29c0f
C
181 // Video is unlisted, check we used the uuid to fetch it
182 if (video.privacy === VideoPrivacy.UNLISTED) {
183 if (isUUIDValid(req.params.id)) return next()
81ebea48 184
96f29c0f
C
185 // Don't leak this unlisted video
186 return res.status(404).end()
187 }
81ebea48 188 }
96f29c0f
C
189 ]
190}
191
192const videosGetValidator = videosCustomGetValidator('all')
eccf70f0 193const videosDownloadValidator = videosCustomGetValidator('all', true)
34ca3b52 194
b60e5f38 195const videosRemoveValidator = [
72c7248b 196 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 197
a2431b7d 198 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 199 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 200
a2431b7d 201 if (areValidationErrors(req, res)) return
0f6acda1 202 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
203
204 // Check if the user who did the request is able to delete the video
453e83ea 205 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
206
207 return next()
b60e5f38
C
208 }
209]
34ca3b52 210
74d63469
GR
211const videosChangeOwnershipValidator = [
212 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
213
214 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
215 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
216
217 if (areValidationErrors(req, res)) return
0f6acda1 218 if (!await doesVideoExist(req.params.videoId, res)) return
74d63469
GR
219
220 // Check if the user who did the request is able to change the ownership of the video
453e83ea 221 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
74d63469
GR
222
223 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
224 if (!nextOwner) {
225 res.status(400)
9ccff238
LD
226 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
227
74d63469
GR
228 return
229 }
230 res.locals.nextOwner = nextOwner
231
232 return next()
233 }
234]
235
236const videosTerminateChangeOwnershipValidator = [
237 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
238
239 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
240 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
241
242 if (areValidationErrors(req, res)) return
243 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
244
245 // Check if the user who did the request is able to change the ownership of the video
246 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
247
dae86118 248 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469 249
a1587156 250 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
74d63469 251 res.status(403)
a1587156 252 .json({ error: 'Ownership already accepted or refused' })
74d63469
GR
253 return
254 }
a1587156
C
255
256 return next()
74d63469
GR
257 }
258]
259
260const videosAcceptChangeOwnershipValidator = [
261 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
262 const body = req.body as VideoChangeOwnershipAccept
0f6acda1 263 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
74d63469
GR
264
265 const user = res.locals.oauth.token.User
dae86118 266 const videoChangeOwnership = res.locals.videoChangeOwnership
d7a25329 267 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
74d63469
GR
268 if (isAble === false) {
269 res.status(403)
270 .json({ error: 'The user video quota is exceeded with this video.' })
9ccff238 271
74d63469
GR
272 return
273 }
274
275 return next()
276 }
277]
278
418d092a 279function getCommonVideoEditAttributes () {
a920fef1
C
280 return [
281 body('thumbnailfile')
282 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
a1587156
C
283 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
284 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
285 ),
a920fef1
C
286 body('previewfile')
287 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
a1587156
C
288 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
289 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
290 ),
a920fef1
C
291
292 body('category')
293 .optional()
294 .customSanitizer(toIntOrNull)
295 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
296 body('licence')
297 .optional()
298 .customSanitizer(toIntOrNull)
299 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
300 body('language')
301 .optional()
302 .customSanitizer(toValueOrNull)
303 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
304 body('nsfw')
305 .optional()
c8861d5d 306 .customSanitizer(toBooleanOrNull)
a920fef1
C
307 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
308 body('waitTranscoding')
309 .optional()
c8861d5d 310 .customSanitizer(toBooleanOrNull)
a920fef1
C
311 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
312 body('privacy')
313 .optional()
c8861d5d 314 .customSanitizer(toValueOrNull)
a920fef1
C
315 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
316 body('description')
317 .optional()
318 .customSanitizer(toValueOrNull)
319 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
320 body('support')
321 .optional()
322 .customSanitizer(toValueOrNull)
323 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
324 body('tags')
325 .optional()
326 .customSanitizer(toValueOrNull)
327 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
328 body('commentsEnabled')
329 .optional()
c8861d5d 330 .customSanitizer(toBooleanOrNull)
a920fef1 331 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
7f2cfe3a 332 body('downloadEnabled')
1e74f19a 333 .optional()
c8861d5d 334 .customSanitizer(toBooleanOrNull)
156c50af 335 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22 336 body('originallyPublishedAt')
c8861d5d
C
337 .optional()
338 .customSanitizer(toValueOrNull)
339 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
340 body('scheduleUpdate')
341 .optional()
342 .customSanitizer(toValueOrNull),
343 body('scheduleUpdate.updateAt')
344 .optional()
345 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
346 body('scheduleUpdate.privacy')
347 .optional()
2b65c4e5 348 .customSanitizer(toIntOrNull)
a920fef1
C
349 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
350 ] as (ValidationChain | express.Handler)[]
351}
fbad87b0 352
1cd3facc
C
353const commonVideosFiltersValidator = [
354 query('categoryOneOf')
355 .optional()
356 .customSanitizer(toArray)
357 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
358 query('licenceOneOf')
359 .optional()
360 .customSanitizer(toArray)
361 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
362 query('languageOneOf')
363 .optional()
364 .customSanitizer(toArray)
365 .custom(isStringArray).withMessage('Should have a valid one of language array'),
366 query('tagsOneOf')
367 .optional()
368 .customSanitizer(toArray)
369 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
370 query('tagsAllOf')
371 .optional()
372 .customSanitizer(toArray)
373 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
374 query('nsfw')
375 .optional()
376 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
377 query('filter')
378 .optional()
379 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
fe987656
C
380 query('skipCount')
381 .optional()
382 .customSanitizer(toBooleanOrNull)
383 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
1cd3facc
C
384
385 (req: express.Request, res: express.Response, next: express.NextFunction) => {
386 logger.debug('Checking commons video filters query', { parameters: req.query })
387
388 if (areValidationErrors(req, res)) return
389
dae86118 390 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
1cd3facc
C
391 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
392 res.status(401)
393 .json({ error: 'You are not allowed to see all local videos.' })
394
395 return
396 }
397
398 return next()
399 }
400]
401
fbad87b0
C
402// ---------------------------------------------------------------------------
403
404export {
405 videosAddValidator,
406 videosUpdateValidator,
407 videosGetValidator,
eccf70f0 408 videosDownloadValidator,
8d427346 409 checkVideoFollowConstraints,
96f29c0f 410 videosCustomGetValidator,
fbad87b0 411 videosRemoveValidator,
fbad87b0 412
74d63469
GR
413 videosChangeOwnershipValidator,
414 videosTerminateChangeOwnershipValidator,
415 videosAcceptChangeOwnershipValidator,
416
418d092a 417 getCommonVideoEditAttributes,
1cd3facc
C
418
419 commonVideosFiltersValidator
fbad87b0
C
420}
421
422// ---------------------------------------------------------------------------
423
424function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
425 if (req.body.scheduleUpdate) {
426 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
427 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
428
fbad87b0
C
429 res.status(400)
430 .json({ error: 'Schedule update at is mandatory.' })
fbad87b0
C
431
432 return true
433 }
434 }
435
436 return false
437}
b4055e1c
C
438
439async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
440 // Check we accept this video
441 const acceptParameters = {
442 videoBody: req.body,
443 videoFile,
444 user: res.locals.oauth.token.User
445 }
89cd1275
C
446 const acceptedResult = await Hooks.wrapFun(
447 isLocalVideoAccepted,
448 acceptParameters,
b4055e1c
C
449 'filter:api.video.upload.accept.result'
450 )
451
452 if (!acceptedResult || acceptedResult.accepted !== true) {
453 logger.info('Refused local video.', { acceptedResult, acceptParameters })
454 res.status(403)
455 .json({ error: acceptedResult.errorMessage || 'Refused local video' })
456
457 return false
458 }
459
460 return true
461}