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