]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/middlewares/validators/videos/videos.ts
Add server hooks
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
... / ...
CommitLineData
1import * as express from 'express'
2import 'express-validator'
3import { body, param, query, ValidationChain } from 'express-validator/check'
4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5import {
6 isBooleanValid,
7 isDateValid,
8 isIdOrUUIDValid,
9 isIdValid,
10 isUUIDValid,
11 toArray,
12 toIntOrNull,
13 toValueOrNull
14} from '../../../helpers/custom-validators/misc'
15import {
16 checkUserCanManageVideo,
17 doesVideoChannelOfAccountExist,
18 doesVideoExist,
19 isScheduleVideoUpdatePrivacyValid,
20 isVideoCategoryValid,
21 isVideoDescriptionValid,
22 isVideoFile,
23 isVideoFilterValid,
24 isVideoImage,
25 isVideoLanguageValid,
26 isVideoLicenceValid,
27 isVideoNameValid,
28 isVideoOriginallyPublishedAtValid,
29 isVideoPrivacyValid,
30 isVideoSupportValid,
31 isVideoTagsValid
32} from '../../../helpers/custom-validators/videos'
33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34import { logger } from '../../../helpers/logger'
35import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
36import { authenticate, authenticatePromiseIfNeeded } from '../../oauth'
37import { areValidationErrors } from '../utils'
38import { cleanUpReqFiles } from '../../../helpers/express-utils'
39import { VideoModel } from '../../../models/video/video'
40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42import { AccountModel } from '../../../models/account/account'
43import { VideoFetchType } from '../../../helpers/video'
44import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
45import { getServerActor } from '../../../helpers/utils'
46import { CONFIG } from '../../../initializers/config'
47import { isLocalVideoAccepted } from '../../../lib/moderation'
48import { Hooks } from '../../../lib/plugins/hooks'
49
50const videosAddValidator = getCommonVideoEditAttributes().concat([
51 body('videofile')
52 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
53 'This file is not supported or too large. Please, make sure it is of the following type: '
54 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
55 ),
56 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
57 body('channelId')
58 .toInt()
59 .custom(isIdValid).withMessage('Should have correct video channel id'),
60
61 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
62 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
63
64 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
65 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
66
67 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
68 const user = res.locals.oauth.token.User
69
70 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
71
72 if (await user.isAbleToUploadVideo(videoFile) === false) {
73 res.status(403)
74 .json({ error: 'The user video quota is exceeded with this video.' })
75
76 return cleanUpReqFiles(req)
77 }
78
79 let duration: number
80
81 try {
82 duration = await getDurationFromVideoFile(videoFile.path)
83 } catch (err) {
84 logger.error('Invalid input file in videosAddValidator.', { err })
85 res.status(400)
86 .json({ error: 'Invalid input file.' })
87
88 return cleanUpReqFiles(req)
89 }
90
91 videoFile.duration = duration
92
93 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
94
95 return next()
96 }
97])
98
99const videosUpdateValidator = getCommonVideoEditAttributes().concat([
100 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
101 body('name')
102 .optional()
103 .custom(isVideoNameValid).withMessage('Should have a valid name'),
104 body('channelId')
105 .optional()
106 .toInt()
107 .custom(isIdValid).withMessage('Should have correct video channel id'),
108
109 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
110 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
111
112 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
113 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
114 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
115
116 // Check if the user who did the request is able to update the video
117 const user = res.locals.oauth.token.User
118 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
119
120 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
121
122 return next()
123 }
124])
125
126async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
127 const video = res.locals.video
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
151const videosCustomGetValidator = (fetchType: VideoFetchType) => {
152 return [
153 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
154
155 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
156 logger.debug('Checking videosGet parameters', { parameters: req.params })
157
158 if (areValidationErrors(req, res)) return
159 if (!await doesVideoExist(req.params.id, res, fetchType)) return
160
161 const video = res.locals.video
162
163 // Video private or blacklisted
164 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
165 await authenticatePromiseIfNeeded(req, res)
166
167 const user = res.locals.oauth ? res.locals.oauth.token.User : null
168
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 }
177
178 return next()
179 }
180
181 // Video is public, anyone can access it
182 if (video.privacy === VideoPrivacy.PUBLIC) return next()
183
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()
187
188 // Don't leak this unlisted video
189 return res.status(404).end()
190 }
191 }
192 ]
193}
194
195const videosGetValidator = videosCustomGetValidator('all')
196
197const videosRemoveValidator = [
198 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
199
200 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
201 logger.debug('Checking videosRemove parameters', { parameters: req.params })
202
203 if (areValidationErrors(req, res)) return
204 if (!await doesVideoExist(req.params.id, res)) return
205
206 // Check if the user who did the request is able to delete the video
207 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
208
209 return next()
210 }
211]
212
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
220 if (!await doesVideoExist(req.params.videoId, res)) return
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)
228 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
229
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) => {
253 const videoChangeOwnership = res.locals.videoChangeOwnership
254
255 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
256 return next()
257 } else {
258 res.status(403)
259 .json({ error: 'Ownership already accepted or refused' })
260
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
269 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
270
271 const user = res.locals.oauth.token.User
272 const videoChangeOwnership = res.locals.videoChangeOwnership
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.' })
277
278 return
279 }
280
281 return next()
282 }
283]
284
285function getCommonVideoEditAttributes () {
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'),
338 body('downloadEnabled')
339 .optional()
340 .toBoolean()
341 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
342 body('originallyPublishedAt')
343 .optional()
344 .customSanitizer(toValueOrNull)
345 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
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}
358
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
392 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
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
404// ---------------------------------------------------------------------------
405
406export {
407 videosAddValidator,
408 videosUpdateValidator,
409 videosGetValidator,
410 checkVideoFollowConstraints,
411 videosCustomGetValidator,
412 videosRemoveValidator,
413
414 videosChangeOwnershipValidator,
415 videosTerminateChangeOwnershipValidator,
416 videosAcceptChangeOwnershipValidator,
417
418 getCommonVideoEditAttributes,
419
420 commonVideosFiltersValidator
421}
422
423// ---------------------------------------------------------------------------
424
425function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
426 if (req.body.scheduleUpdate) {
427 if (!req.body.scheduleUpdate.updateAt) {
428 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
429
430 res.status(400)
431 .json({ error: 'Schedule update at is mandatory.' })
432
433 return true
434 }
435 }
436
437 return false
438}
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 }
447 const acceptedResult = await Hooks.wrapObject(
448 isLocalVideoAccepted(acceptParameters),
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}