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