]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
e9b036a029b7870209fc3b6fc35675b7f968f48d
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import * as express from 'express'
2 import 'express-validator'
3 import { body, param, query, ValidationChain } from 'express-validator/check'
4 import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5 import {
6 isBooleanValid,
7 isDateValid,
8 isIdOrUUIDValid,
9 isIdValid,
10 isUUIDValid,
11 toArray,
12 toIntOrNull,
13 toValueOrNull
14 } from '../../../helpers/custom-validators/misc'
15 import {
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'
33 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34 import { logger } from '../../../helpers/logger'
35 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
36 import { authenticatePromiseIfNeeded } from '../../oauth'
37 import { areValidationErrors } from '../utils'
38 import { cleanUpReqFiles } from '../../../helpers/express-utils'
39 import { VideoModel } from '../../../models/video/video'
40 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42 import { AccountModel } from '../../../models/account/account'
43 import { VideoFetchType } from '../../../helpers/video'
44 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
45 import { getServerActor } from '../../../helpers/utils'
46 import { CONFIG } from '../../../initializers/config'
47
48 const videosAddValidator = getCommonVideoEditAttributes().concat([
49 body('videofile')
50 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
51 'This file is not supported or too large. Please, make sure it is of the following type: '
52 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
53 ),
54 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
55 body('channelId')
56 .toInt()
57 .custom(isIdValid).withMessage('Should have correct video channel id'),
58
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
61
62 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
63 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
64
65 const videoFile: Express.Multer.File = req.files['videofile'][0]
66 const user = res.locals.oauth.token.User
67
68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
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.' })
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 return next()
93 }
94 ])
95
96 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
97 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
98 body('name')
99 .optional()
100 .custom(isVideoNameValid).withMessage('Should have a valid name'),
101 body('channelId')
102 .optional()
103 .toInt()
104 .custom(isIdValid).withMessage('Should have correct video channel id'),
105
106 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
107 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
108
109 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
110 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
111 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
112
113 const video = res.locals.video
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.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
118
119 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
120 cleanUpReqFiles(req)
121 return res.status(409)
122 .json({ error: 'Cannot set "private" a video that was not private.' })
123 }
124
125 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
126
127 return next()
128 }
129 ])
130
131 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video = res.locals.video
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
156 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
157 return [
158 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
159
160 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
161 logger.debug('Checking videosGet parameters', { parameters: req.params })
162
163 if (areValidationErrors(req, res)) return
164 if (!await doesVideoExist(req.params.id, res, fetchType)) return
165
166 const video = res.locals.video
167
168 // Video private or blacklisted
169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
170 await authenticatePromiseIfNeeded(req, res)
171
172 const user = res.locals.oauth ? res.locals.oauth.token.User : null
173
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 }
182
183 return next()
184 }
185
186 // Video is public, anyone can access it
187 if (video.privacy === VideoPrivacy.PUBLIC) return next()
188
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()
192
193 // Don't leak this unlisted video
194 return res.status(404).end()
195 }
196 }
197 ]
198 }
199
200 const videosGetValidator = videosCustomGetValidator('all')
201
202 const videosRemoveValidator = [
203 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
204
205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
206 logger.debug('Checking videosRemove parameters', { parameters: req.params })
207
208 if (areValidationErrors(req, res)) return
209 if (!await doesVideoExist(req.params.id, res)) return
210
211 // Check if the user who did the request is able to delete the video
212 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
213
214 return next()
215 }
216 ]
217
218 const 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
225 if (!await doesVideoExist(req.params.videoId, res)) return
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)
233 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
234
235 return
236 }
237 res.locals.nextOwner = nextOwner
238
239 return next()
240 }
241 ]
242
243 const 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) => {
258 const videoChangeOwnership = res.locals.videoChangeOwnership
259
260 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
261 return next()
262 } else {
263 res.status(403)
264 .json({ error: 'Ownership already accepted or refused' })
265
266 return
267 }
268 }
269 ]
270
271 const videosAcceptChangeOwnershipValidator = [
272 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
273 const body = req.body as VideoChangeOwnershipAccept
274 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
275
276 const user = res.locals.oauth.token.User
277 const videoChangeOwnership = res.locals.videoChangeOwnership
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.' })
282
283 return
284 }
285
286 return next()
287 }
288 ]
289
290 function getCommonVideoEditAttributes () {
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'),
343 body('downloadEnabled')
344 .optional()
345 .toBoolean()
346 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
347 body('originallyPublishedAt')
348 .optional()
349 .customSanitizer(toValueOrNull)
350 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
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 }
363
364 const 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
397 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
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
409 // ---------------------------------------------------------------------------
410
411 export {
412 videosAddValidator,
413 videosUpdateValidator,
414 videosGetValidator,
415 checkVideoFollowConstraints,
416 videosCustomGetValidator,
417 videosRemoveValidator,
418
419 videosChangeOwnershipValidator,
420 videosTerminateChangeOwnershipValidator,
421 videosAcceptChangeOwnershipValidator,
422
423 getCommonVideoEditAttributes,
424
425 commonVideosFiltersValidator
426 }
427
428 // ---------------------------------------------------------------------------
429
430 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
431 if (req.body.scheduleUpdate) {
432 if (!req.body.scheduleUpdate.updateAt) {
433 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
434
435 res.status(400)
436 .json({ error: 'Schedule update at is mandatory.' })
437
438 return true
439 }
440 }
441
442 return false
443 }