]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Enable video upload and edit
[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,
1e74f19a 17 isVideoOriginallyPublishedAtValid,
2baea0c7 18 isScheduleVideoUpdatePrivacyValid,
ac81d1a0 19 isVideoCategoryValid,
0f320037 20 isVideoChannelOfAccountExist,
ac81d1a0
C
21 isVideoDescriptionValid,
22 isVideoExist,
23 isVideoFile,
1cd3facc 24 isVideoFilterValid,
ac81d1a0
C
25 isVideoImage,
26 isVideoLanguageValid,
27 isVideoLicenceValid,
28 isVideoNameValid,
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'
40import { UserModel } from '../../../models/account/user'
41import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
42import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
43import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
44import { AccountModel } from '../../../models/account/account'
45import { VideoFetchType } from '../../../helpers/video'
1cd3facc 46import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
8d427346 47import { getServerActor } from '../../../helpers/utils'
34ca3b52 48
a920fef1 49const videosAddValidator = getCommonVideoAttributes().concat([
0c237b19
C
50 body('videofile')
51 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
40e87e9e 52 'This file is not supported or too large. Please, make sure it is of the following type: '
0c237b19
C
53 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
54 ),
b60e5f38 55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
56 body('channelId')
57 .toInt()
2baea0c7 58 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 59
a2431b7d 60 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
61 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
62
cf7a61b5
C
63 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
64 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
a2431b7d
C
65
66 const videoFile: Express.Multer.File = req.files['videofile'][0]
67 const user = res.locals.oauth.token.User
b60e5f38 68
cf7a61b5 69 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
a2431b7d
C
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.' })
a2431b7d 75
cf7a61b5 76 return cleanUpReqFiles(req)
a2431b7d
C
77 }
78
79 let duration: number
80
81 try {
82 duration = await getDurationFromVideoFile(videoFile.path)
83 } catch (err) {
d5b7d911 84 logger.error('Invalid input file in videosAddValidator.', { err })
a2431b7d
C
85 res.status(400)
86 .json({ error: 'Invalid input file.' })
a2431b7d 87
cf7a61b5 88 return cleanUpReqFiles(req)
a2431b7d
C
89 }
90
a2431b7d
C
91 videoFile['duration'] = duration
92
93 return next()
b60e5f38 94 }
a920fef1 95])
b60e5f38 96
a920fef1 97const videosUpdateValidator = getCommonVideoAttributes().concat([
72c7248b 98 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
360329cc
C
99 body('name')
100 .optional()
101 .custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
102 body('channelId')
103 .optional()
104 .toInt()
105 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 106
a2431b7d 107 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
108 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
109
cf7a61b5
C
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)
a2431b7d
C
113
114 const video = res.locals.video
115
6221f311 116 // Check if the user who did the request is able to update the video
0f320037 117 const user = res.locals.oauth.token.User
cf7a61b5 118 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d
C
119
120 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
cf7a61b5 121 cleanUpReqFiles(req)
a2431b7d 122 return res.status(409)
bbe0f064 123 .json({ error: 'Cannot set "private" a video that was not private.' })
a2431b7d
C
124 }
125
cf7a61b5 126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 127
a2431b7d 128 return next()
b60e5f38 129 }
a920fef1 130])
c173e565 131
8d427346
C
132async 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
96f29c0f
C
157const videosCustomGetValidator = (fetchType: VideoFetchType) => {
158 return [
159 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
7b1f49de 160
96f29c0f
C
161 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
162 logger.debug('Checking videosGet parameters', { parameters: req.params })
11474c3c 163
96f29c0f
C
164 if (areValidationErrors(req, res)) return
165 if (!await isVideoExist(req.params.id, res, fetchType)) return
191764f3 166
96f29c0f 167 const video: VideoModel = res.locals.video
191764f3 168
96f29c0f
C
169 // Video private or blacklisted
170 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
8d427346
C
171 await authenticatePromiseIfNeeded(req, res)
172
173 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
191764f3 174
8d427346
C
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 }
191764f3 183
8d427346 184 return next()
96f29c0f 185 }
11474c3c 186
96f29c0f
C
187 // Video is public, anyone can access it
188 if (video.privacy === VideoPrivacy.PUBLIC) return next()
11474c3c 189
96f29c0f
C
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()
81ebea48 193
96f29c0f
C
194 // Don't leak this unlisted video
195 return res.status(404).end()
196 }
81ebea48 197 }
96f29c0f
C
198 ]
199}
200
201const videosGetValidator = videosCustomGetValidator('all')
34ca3b52 202
b60e5f38 203const videosRemoveValidator = [
72c7248b 204 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
34ca3b52 205
a2431b7d 206 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 207 logger.debug('Checking videosRemove parameters', { parameters: req.params })
34ca3b52 208
a2431b7d
C
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
6221f311 213 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
214
215 return next()
b60e5f38
C
216 }
217]
34ca3b52 218
74d63469
GR
219const 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)
9ccff238
LD
234 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
235
74d63469
GR
236 return
237 }
238 res.locals.nextOwner = nextOwner
239
240 return next()
241 }
242]
243
244const 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' })
9ccff238 266
74d63469
GR
267 return
268 }
269 }
270]
271
272const 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.' })
9ccff238 283
74d63469
GR
284 return
285 }
286
287 return next()
288 }
289]
290
a920fef1
C
291function getCommonVideoAttributes () {
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'),
1e74f19a 344 body('originallyPublishedAt')
345 .optional()
346 .customSanitizer(toValueOrNull)
347 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
348
349 body('scheduleUpdate')
350 .optional()
351 .customSanitizer(toValueOrNull),
352 body('scheduleUpdate.updateAt')
353 .optional()
354 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
355 body('scheduleUpdate.privacy')
356 .optional()
357 .toInt()
358 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
359 ] as (ValidationChain | express.Handler)[]
360}
fbad87b0 361
1cd3facc
C
362const commonVideosFiltersValidator = [
363 query('categoryOneOf')
364 .optional()
365 .customSanitizer(toArray)
366 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
367 query('licenceOneOf')
368 .optional()
369 .customSanitizer(toArray)
370 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
371 query('languageOneOf')
372 .optional()
373 .customSanitizer(toArray)
374 .custom(isStringArray).withMessage('Should have a valid one of language array'),
375 query('tagsOneOf')
376 .optional()
377 .customSanitizer(toArray)
378 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
379 query('tagsAllOf')
380 .optional()
381 .customSanitizer(toArray)
382 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
383 query('nsfw')
384 .optional()
385 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
386 query('filter')
387 .optional()
388 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
389
390 (req: express.Request, res: express.Response, next: express.NextFunction) => {
391 logger.debug('Checking commons video filters query', { parameters: req.query })
392
393 if (areValidationErrors(req, res)) return
394
395 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
396 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
397 res.status(401)
398 .json({ error: 'You are not allowed to see all local videos.' })
399
400 return
401 }
402
403 return next()
404 }
405]
406
fbad87b0
C
407// ---------------------------------------------------------------------------
408
409export {
410 videosAddValidator,
411 videosUpdateValidator,
412 videosGetValidator,
8d427346 413 checkVideoFollowConstraints,
96f29c0f 414 videosCustomGetValidator,
fbad87b0 415 videosRemoveValidator,
fbad87b0 416
74d63469
GR
417 videosChangeOwnershipValidator,
418 videosTerminateChangeOwnershipValidator,
419 videosAcceptChangeOwnershipValidator,
420
1cd3facc
C
421 getCommonVideoAttributes,
422
423 commonVideosFiltersValidator
fbad87b0
C
424}
425
426// ---------------------------------------------------------------------------
427
428function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
429 if (req.body.scheduleUpdate) {
430 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
431 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
432
fbad87b0
C
433 res.status(400)
434 .json({ error: 'Schedule update at is mandatory.' })
fbad87b0
C
435
436 return true
437 }
438 }
439
440 return false
441}