]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
a5e3ed0dcf36f41cc9d37be1a40ca8dca93e92e7
[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 isVideoOriginallyPublishedAtValid,
18 isScheduleVideoUpdatePrivacyValid,
19 isVideoCategoryValid,
20 isVideoChannelOfAccountExist,
21 isVideoDescriptionValid,
22 isVideoExist,
23 isVideoFile,
24 isVideoFilterValid,
25 isVideoImage,
26 isVideoLanguageValid,
27 isVideoLicenceValid,
28 isVideoNameValid,
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 { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
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 { UserModel } from '../../../models/account/user'
41 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
42 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
43 import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
44 import { AccountModel } from '../../../models/account/account'
45 import { VideoFetchType } from '../../../helpers/video'
46 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
47 import { getServerActor } from '../../../helpers/utils'
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 .toInt()
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 = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
68
69 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
70
71 const isAble = await user.isAbleToUploadVideo(videoFile)
72 if (isAble === 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 return next()
94 }
95 ])
96
97 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
98 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
99 body('name')
100 .optional()
101 .custom(isVideoNameValid).withMessage('Should have a valid name'),
102 body('channelId')
103 .optional()
104 .toInt()
105 .custom(isIdValid).withMessage('Should have correct video channel id'),
106
107 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
108 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
109
110 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
111 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
112 if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
113
114 const video = res.locals.video
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 (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
121 cleanUpReqFiles(req)
122 return res.status(409)
123 .json({ error: 'Cannot set "private" a video that was not private.' })
124 }
125
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
127
128 return next()
129 }
130 ])
131
132 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
133 const video: VideoModel = res.locals.video
134
135 // Anybody can watch local videos
136 if (video.isOwned() === true) return next()
137
138 // Logged user
139 if (res.locals.oauth) {
140 // Users can search or watch remote videos
141 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
142 }
143
144 // Anybody can search or watch remote videos
145 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
146
147 // Check our instance follows an actor that shared this video
148 const serverActor = await getServerActor()
149 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
150
151 return res.status(403)
152 .json({
153 error: 'Cannot get this video regarding follow constraints.'
154 })
155 }
156
157 const videosCustomGetValidator = (fetchType: VideoFetchType) => {
158 return [
159 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
160
161 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
162 logger.debug('Checking videosGet parameters', { parameters: req.params })
163
164 if (areValidationErrors(req, res)) return
165 if (!await isVideoExist(req.params.id, res, fetchType)) return
166
167 const video: VideoModel = res.locals.video
168
169 // Video private or blacklisted
170 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
171 await authenticatePromiseIfNeeded(req, res)
172
173 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
174
175 // Only the owner or a user that have blacklist rights can see the video
176 if (
177 !user ||
178 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
179 ) {
180 return res.status(403)
181 .json({ error: 'Cannot get this private or blacklisted video.' })
182 }
183
184 return next()
185 }
186
187 // Video is public, anyone can access it
188 if (video.privacy === VideoPrivacy.PUBLIC) return next()
189
190 // Video is unlisted, check we used the uuid to fetch it
191 if (video.privacy === VideoPrivacy.UNLISTED) {
192 if (isUUIDValid(req.params.id)) return next()
193
194 // Don't leak this unlisted video
195 return res.status(404).end()
196 }
197 }
198 ]
199 }
200
201 const videosGetValidator = videosCustomGetValidator('all')
202
203 const videosRemoveValidator = [
204 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
205
206 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
207 logger.debug('Checking videosRemove parameters', { parameters: req.params })
208
209 if (areValidationErrors(req, res)) return
210 if (!await isVideoExist(req.params.id, res)) return
211
212 // Check if the user who did the request is able to delete the video
213 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
214
215 return next()
216 }
217 ]
218
219 const videosChangeOwnershipValidator = [
220 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
221
222 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
223 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
224
225 if (areValidationErrors(req, res)) return
226 if (!await isVideoExist(req.params.videoId, res)) return
227
228 // Check if the user who did the request is able to change the ownership of the video
229 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
230
231 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
232 if (!nextOwner) {
233 res.status(400)
234 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
235
236 return
237 }
238 res.locals.nextOwner = nextOwner
239
240 return next()
241 }
242 ]
243
244 const videosTerminateChangeOwnershipValidator = [
245 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
246
247 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
248 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
249
250 if (areValidationErrors(req, res)) return
251 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
252
253 // Check if the user who did the request is able to change the ownership of the video
254 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
255
256 return next()
257 },
258 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
259 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
260
261 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
262 return next()
263 } else {
264 res.status(403)
265 .json({ error: 'Ownership already accepted or refused' })
266
267 return
268 }
269 }
270 ]
271
272 const videosAcceptChangeOwnershipValidator = [
273 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
274 const body = req.body as VideoChangeOwnershipAccept
275 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
276
277 const user = res.locals.oauth.token.User
278 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel
279 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
280 if (isAble === false) {
281 res.status(403)
282 .json({ error: 'The user video quota is exceeded with this video.' })
283
284 return
285 }
286
287 return next()
288 }
289 ]
290
291 function getCommonVideoEditAttributes () {
292 return [
293 body('thumbnailfile')
294 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
295 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
296 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
297 ),
298 body('previewfile')
299 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
300 'This preview file is not supported or too large. Please, make sure it is of the following type: '
301 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
302 ),
303
304 body('category')
305 .optional()
306 .customSanitizer(toIntOrNull)
307 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
308 body('licence')
309 .optional()
310 .customSanitizer(toIntOrNull)
311 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
312 body('language')
313 .optional()
314 .customSanitizer(toValueOrNull)
315 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
316 body('nsfw')
317 .optional()
318 .toBoolean()
319 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
320 body('waitTranscoding')
321 .optional()
322 .toBoolean()
323 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
324 body('privacy')
325 .optional()
326 .toInt()
327 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
328 body('description')
329 .optional()
330 .customSanitizer(toValueOrNull)
331 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
332 body('support')
333 .optional()
334 .customSanitizer(toValueOrNull)
335 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
336 body('tags')
337 .optional()
338 .customSanitizer(toValueOrNull)
339 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
340 body('commentsEnabled')
341 .optional()
342 .toBoolean()
343 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
344 body('downloadEnabled')
345 .optional()
346 .toBoolean()
347 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
348 body('originallyPublishedAt')
349 .optional()
350 .customSanitizer(toValueOrNull)
351 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
352 body('scheduleUpdate')
353 .optional()
354 .customSanitizer(toValueOrNull),
355 body('scheduleUpdate.updateAt')
356 .optional()
357 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
358 body('scheduleUpdate.privacy')
359 .optional()
360 .toInt()
361 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
362 ] as (ValidationChain | express.Handler)[]
363 }
364
365 const commonVideosFiltersValidator = [
366 query('categoryOneOf')
367 .optional()
368 .customSanitizer(toArray)
369 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
370 query('licenceOneOf')
371 .optional()
372 .customSanitizer(toArray)
373 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
374 query('languageOneOf')
375 .optional()
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid one of language array'),
378 query('tagsOneOf')
379 .optional()
380 .customSanitizer(toArray)
381 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
382 query('tagsAllOf')
383 .optional()
384 .customSanitizer(toArray)
385 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
386 query('nsfw')
387 .optional()
388 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
389 query('filter')
390 .optional()
391 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
392
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 logger.debug('Checking commons video filters query', { parameters: req.query })
395
396 if (areValidationErrors(req, res)) return
397
398 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
399 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
400 res.status(401)
401 .json({ error: 'You are not allowed to see all local videos.' })
402
403 return
404 }
405
406 return next()
407 }
408 ]
409
410 // ---------------------------------------------------------------------------
411
412 export {
413 videosAddValidator,
414 videosUpdateValidator,
415 videosGetValidator,
416 checkVideoFollowConstraints,
417 videosCustomGetValidator,
418 videosRemoveValidator,
419
420 videosChangeOwnershipValidator,
421 videosTerminateChangeOwnershipValidator,
422 videosAcceptChangeOwnershipValidator,
423
424 getCommonVideoEditAttributes,
425
426 commonVideosFiltersValidator
427 }
428
429 // ---------------------------------------------------------------------------
430
431 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
432 if (req.body.scheduleUpdate) {
433 if (!req.body.scheduleUpdate.updateAt) {
434 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
435
436 res.status(400)
437 .json({ error: 'Schedule update at is mandatory.' })
438
439 return true
440 }
441 }
442
443 return false
444 }