]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
Add ability to disable webtorrent
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
4 import {
5 isBooleanValid,
6 isDateValid,
7 isIdOrUUIDValid,
8 isIdValid,
9 isUUIDValid,
10 toArray,
11 toBooleanOrNull,
12 toIntOrNull,
13 toValueOrNull
14 } from '../../../helpers/custom-validators/misc'
15 import {
16 isScheduleVideoUpdatePrivacyValid,
17 isVideoCategoryValid,
18 isVideoDescriptionValid,
19 isVideoFile,
20 isVideoFilterValid,
21 isVideoImage,
22 isVideoLanguageValid,
23 isVideoLicenceValid,
24 isVideoNameValid,
25 isVideoOriginallyPublishedAtValid,
26 isVideoPrivacyValid,
27 isVideoSupportValid,
28 isVideoTagsValid
29 } from '../../../helpers/custom-validators/videos'
30 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
31 import { logger } from '../../../helpers/logger'
32 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
33 import { authenticatePromiseIfNeeded } from '../../oauth'
34 import { areValidationErrors } from '../utils'
35 import { cleanUpReqFiles } from '../../../helpers/express-utils'
36 import { VideoModel } from '../../../models/video/video'
37 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
38 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
39 import { AccountModel } from '../../../models/account/account'
40 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
41 import { getServerActor } from '../../../helpers/utils'
42 import { CONFIG } from '../../../initializers/config'
43 import { isLocalVideoAccepted } from '../../../lib/moderation'
44 import { Hooks } from '../../../lib/plugins/hooks'
45 import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares'
46 import { MVideoFullLight } from '@server/typings/models'
47 import { getVideoWithAttributes } from '../../../helpers/video'
48
49 const videosAddValidator = getCommonVideoEditAttributes().concat([
50 body('videofile')
51 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
52 'This file is not supported or too large. Please, make sure it is of the following type: '
53 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
54 ),
55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
56 body('channelId')
57 .customSanitizer(toIntOrNull)
58 .custom(isIdValid).withMessage('Should have correct video channel id'),
59
60 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
61 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
62
63 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
64 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
65
66 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
68
69 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
70
71 if (await user.isAbleToUploadVideo(videoFile) === false) {
72 res.status(403)
73 .json({ error: 'The user video quota is exceeded with this video.' })
74
75 return cleanUpReqFiles(req)
76 }
77
78 let duration: number
79
80 try {
81 duration = await getDurationFromVideoFile(videoFile.path)
82 } catch (err) {
83 logger.error('Invalid input file in videosAddValidator.', { err })
84 res.status(400)
85 .json({ error: 'Invalid input file.' })
86
87 return cleanUpReqFiles(req)
88 }
89
90 videoFile.duration = duration
91
92 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
93
94 return next()
95 }
96 ])
97
98 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
99 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
100 body('name')
101 .optional()
102 .custom(isVideoNameValid).withMessage('Should have a valid name'),
103 body('channelId')
104 .optional()
105 .customSanitizer(toIntOrNull)
106 .custom(isIdValid).withMessage('Should have correct video channel id'),
107
108 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
109 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
110
111 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
112 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
113 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
114
115 // Check if the user who did the request is able to update the video
116 const user = res.locals.oauth.token.User
117 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
118
119 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
120
121 return next()
122 }
123 ])
124
125 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
126 const video = getVideoWithAttributes(res)
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
150 const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights') => {
151 return [
152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
153
154 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
155 logger.debug('Checking videosGet parameters', { parameters: req.params })
156
157 if (areValidationErrors(req, res)) return
158 if (!await doesVideoExist(req.params.id, res, fetchType)) return
159
160 const video = getVideoWithAttributes(res)
161 const videoAll = video as MVideoFullLight
162
163 // Video private or blacklisted
164 if (video.privacy === VideoPrivacy.PRIVATE || videoAll.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 (videoAll.VideoChannel && videoAll.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
195 const videosGetValidator = videosCustomGetValidator('all')
196
197 const 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.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
208
209 return next()
210 }
211 ]
212
213 const 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.videoAll, 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
238 const 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
266 const 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.getMaxQualityFile())
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
285 function 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 .customSanitizer(toBooleanOrNull)
313 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
314 body('waitTranscoding')
315 .optional()
316 .customSanitizer(toBooleanOrNull)
317 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
318 body('privacy')
319 .optional()
320 .customSanitizer(toValueOrNull)
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 .customSanitizer(toBooleanOrNull)
337 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
338 body('downloadEnabled')
339 .optional()
340 .customSanitizer(toBooleanOrNull)
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 .customSanitizer(toIntOrNull)
355 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
356 ] as (ValidationChain | express.Handler)[]
357 }
358
359 const 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
406 export {
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
425 function 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
440 async 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.wrapFun(
448 isLocalVideoAccepted,
449 acceptParameters,
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 }