]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Move config in its own file
[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'
6dd9de95 35import { CONSTRAINTS_FIELDS } from '../../../initializers'
8d427346 36import { 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'
34ca3b52 47
418d092a 48const videosAddValidator = getCommonVideoEditAttributes().concat([
0c237b19
C
49 body('videofile')
50 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
40e87e9e 51 'This file is not supported or too large. Please, make sure it is of the following type: '
0c237b19
C
52 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
53 ),
b60e5f38 54 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
55 body('channelId')
56 .toInt()
2baea0c7 57 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 58
a2431b7d 59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
60 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
61
cf7a61b5
C
62 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
63 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
a2431b7d
C
64
65 const videoFile: Express.Multer.File = req.files['videofile'][0]
66 const user = res.locals.oauth.token.User
b60e5f38 67
0f6acda1 68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
a2431b7d
C
69
70 const isAble = await user.isAbleToUploadVideo(videoFile)
71 if (isAble === false) {
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 })
a2431b7d
C
84 res.status(400)
85 .json({ error: 'Invalid input file.' })
a2431b7d 86
cf7a61b5 87 return cleanUpReqFiles(req)
a2431b7d
C
88 }
89
a2431b7d
C
90 videoFile['duration'] = duration
91
92 return next()
b60e5f38 93 }
a920fef1 94])
b60e5f38 95
418d092a 96const videosUpdateValidator = getCommonVideoEditAttributes().concat([
72c7248b 97 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
98 body('name')
99 .optional()
100 .custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
101 body('channelId')
102 .optional()
103 .toInt()
104 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 105
a2431b7d 106 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
107 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
108
cf7a61b5
C
109 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
110 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
0f6acda1 111 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
a2431b7d
C
112
113 const video = res.locals.video
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
cf7a61b5 117 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d
C
118
119 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
cf7a61b5 120 cleanUpReqFiles(req)
a2431b7d 121 return res.status(409)
bbe0f064 122 .json({ error: 'Cannot set "private" a video that was not private.' })
a2431b7d
C
123 }
124
0f6acda1 125 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 126
a2431b7d 127 return next()
b60e5f38 128 }
a920fef1 129])
c173e565 130
8d427346 131async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
dae86118 132 const video = res.locals.video
8d427346
C
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 error: 'Cannot get this video regarding follow constraints.'
153 })
154}
155
96f29c0f
C
156const videosCustomGetValidator = (fetchType: VideoFetchType) => {
157 return [
158 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 159
96f29c0f
C
160 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
161 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 162
96f29c0f 163 if (areValidationErrors(req, res)) return
0f6acda1 164 if (!await doesVideoExist(req.params.id, res, fetchType)) return
191764f3 165
dae86118 166 const video = res.locals.video
191764f3 167
96f29c0f
C
168 // Video private or blacklisted
169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
8d427346
C
170 await authenticatePromiseIfNeeded(req, res)
171
dae86118 172 const user = res.locals.oauth ? res.locals.oauth.token.User : null
191764f3 173
8d427346
C
174 // Only the owner or a user that have blacklist rights can see the video
175 if (
176 !user ||
177 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
178 ) {
179 return res.status(403)
180 .json({ error: 'Cannot get this private or blacklisted video.' })
181 }
191764f3 182
8d427346 183 return next()
96f29c0f 184 }
11474c3c 185
96f29c0f
C
186 // Video is public, anyone can access it
187 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 188
96f29c0f
C
189 // Video is unlisted, check we used the uuid to fetch it
190 if (video.privacy === VideoPrivacy.UNLISTED) {
191 if (isUUIDValid(req.params.id)) return next()
81ebea48 192
96f29c0f
C
193 // Don't leak this unlisted video
194 return res.status(404).end()
195 }
81ebea48 196 }
96f29c0f
C
197 ]
198}
199
200const videosGetValidator = videosCustomGetValidator('all')
34ca3b52 201
b60e5f38 202const videosRemoveValidator = [
72c7248b 203 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 204
a2431b7d 205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 206 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 207
a2431b7d 208 if (areValidationErrors(req, res)) return
0f6acda1 209 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
210
211 // Check if the user who did the request is able to delete the video
6221f311 212 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
213
214 return next()
b60e5f38
C
215 }
216]
34ca3b52 217
74d63469
GR
218const videosChangeOwnershipValidator = [
219 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
220
221 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
222 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
223
224 if (areValidationErrors(req, res)) return
0f6acda1 225 if (!await doesVideoExist(req.params.videoId, res)) return
74d63469
GR
226
227 // Check if the user who did the request is able to change the ownership of the video
228 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
229
230 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
231 if (!nextOwner) {
232 res.status(400)
9ccff238
LD
233 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
234
74d63469
GR
235 return
236 }
237 res.locals.nextOwner = nextOwner
238
239 return next()
240 }
241]
242
243const videosTerminateChangeOwnershipValidator = [
244 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
245
246 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
247 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
248
249 if (areValidationErrors(req, res)) return
250 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
251
252 // Check if the user who did the request is able to change the ownership of the video
253 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
254
255 return next()
256 },
257 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
dae86118 258 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469
GR
259
260 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
261 return next()
262 } else {
263 res.status(403)
264 .json({ error: 'Ownership already accepted or refused' })
9ccff238 265
74d63469
GR
266 return
267 }
268 }
269]
270
271const videosAcceptChangeOwnershipValidator = [
272 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
273 const body = req.body as VideoChangeOwnershipAccept
0f6acda1 274 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
74d63469
GR
275
276 const user = res.locals.oauth.token.User
dae86118 277 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469
GR
278 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
279 if (isAble === false) {
280 res.status(403)
281 .json({ error: 'The user video quota is exceeded with this video.' })
9ccff238 282
74d63469
GR
283 return
284 }
285
286 return next()
287 }
288]
289
418d092a 290function getCommonVideoEditAttributes () {
a920fef1
C
291 return [
292 body('thumbnailfile')
293 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
294 'This thumbnail 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 body('previewfile')
298 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
299 'This preview file is not supported or too large. Please, make sure it is of the following type: '
300 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
301 ),
302
303 body('category')
304 .optional()
305 .customSanitizer(toIntOrNull)
306 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
307 body('licence')
308 .optional()
309 .customSanitizer(toIntOrNull)
310 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
311 body('language')
312 .optional()
313 .customSanitizer(toValueOrNull)
314 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
315 body('nsfw')
316 .optional()
317 .toBoolean()
318 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
319 body('waitTranscoding')
320 .optional()
321 .toBoolean()
322 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
323 body('privacy')
324 .optional()
325 .toInt()
326 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
327 body('description')
328 .optional()
329 .customSanitizer(toValueOrNull)
330 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
331 body('support')
332 .optional()
333 .customSanitizer(toValueOrNull)
334 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
335 body('tags')
336 .optional()
337 .customSanitizer(toValueOrNull)
338 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
339 body('commentsEnabled')
340 .optional()
341 .toBoolean()
342 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
7f2cfe3a 343 body('downloadEnabled')
1e74f19a 344 .optional()
156c50af
LD
345 .toBoolean()
346 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22
C
347 body('originallyPublishedAt')
348 .optional()
349 .customSanitizer(toValueOrNull)
350 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
351 body('scheduleUpdate')
352 .optional()
353 .customSanitizer(toValueOrNull),
354 body('scheduleUpdate.updateAt')
355 .optional()
356 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
357 body('scheduleUpdate.privacy')
358 .optional()
359 .toInt()
360 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
361 ] as (ValidationChain | express.Handler)[]
362}
fbad87b0 363
1cd3facc
C
364const commonVideosFiltersValidator = [
365 query('categoryOneOf')
366 .optional()
367 .customSanitizer(toArray)
368 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
369 query('licenceOneOf')
370 .optional()
371 .customSanitizer(toArray)
372 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
373 query('languageOneOf')
374 .optional()
375 .customSanitizer(toArray)
376 .custom(isStringArray).withMessage('Should have a valid one of language array'),
377 query('tagsOneOf')
378 .optional()
379 .customSanitizer(toArray)
380 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
381 query('tagsAllOf')
382 .optional()
383 .customSanitizer(toArray)
384 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
385 query('nsfw')
386 .optional()
387 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
388 query('filter')
389 .optional()
390 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
391
392 (req: express.Request, res: express.Response, next: express.NextFunction) => {
393 logger.debug('Checking commons video filters query', { parameters: req.query })
394
395 if (areValidationErrors(req, res)) return
396
dae86118 397 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
1cd3facc
C
398 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
399 res.status(401)
400 .json({ error: 'You are not allowed to see all local videos.' })
401
402 return
403 }
404
405 return next()
406 }
407]
408
fbad87b0
C
409// ---------------------------------------------------------------------------
410
411export {
412 videosAddValidator,
413 videosUpdateValidator,
414 videosGetValidator,
8d427346 415 checkVideoFollowConstraints,
96f29c0f 416 videosCustomGetValidator,
fbad87b0 417 videosRemoveValidator,
fbad87b0 418
74d63469
GR
419 videosChangeOwnershipValidator,
420 videosTerminateChangeOwnershipValidator,
421 videosAcceptChangeOwnershipValidator,
422
418d092a 423 getCommonVideoEditAttributes,
1cd3facc
C
424
425 commonVideosFiltersValidator
fbad87b0
C
426}
427
428// ---------------------------------------------------------------------------
429
430function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
431 if (req.body.scheduleUpdate) {
432 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
433 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
434
fbad87b0
C
435 res.status(400)
436 .json({ error: 'Schedule update at is mandatory.' })
fbad87b0
C
437
438 return true
439 }
440 }
441
442 return false
443}