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