]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
Server: Bulk update videos support field
[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'
74dc3bca 35import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
8d427346 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'
6dd9de95 46import { CONFIG } from '../../../initializers/config'
34ca3b52 47
418d092a 48const videosAddValidator = getCommonVideoEditAttributes().concat([
0c237b19
C
49 body('videofile')
50 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
40e87e9e 51 'This file is not supported or too large. Please, make sure it is of the following type: '
0c237b19
C
52 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
53 ),
b60e5f38 54 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
0f320037
C
55 body('channelId')
56 .toInt()
2baea0c7 57 .custom(isIdValid).withMessage('Should have correct video channel id'),
b60e5f38 58
a2431b7d 59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38
C
60 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
61
cf7a61b5
C
62 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
63 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
a2431b7d
C
64
65 const videoFile: Express.Multer.File = req.files['videofile'][0]
66 const user = res.locals.oauth.token.User
b60e5f38 67
0f6acda1 68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
a2431b7d
C
69
70 const isAble = await user.isAbleToUploadVideo(videoFile)
3acc5084 71
a2431b7d
C
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
418d092a 97const videosUpdateValidator = getCommonVideoEditAttributes().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)
0f6acda1 112 if (!await doesVideoExist(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
0f6acda1 126 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 127
a2431b7d 128 return next()
b60e5f38 129 }
a920fef1 130])
c173e565 131
8d427346 132async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
dae86118 133 const video = res.locals.video
8d427346
C
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 164 if (areValidationErrors(req, res)) return
0f6acda1 165 if (!await doesVideoExist(req.params.id, res, fetchType)) return
191764f3 166
dae86118 167 const video = 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
dae86118 173 const user = 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 209 if (areValidationErrors(req, res)) return
0f6acda1 210 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
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
0f6acda1 226 if (!await doesVideoExist(req.params.videoId, res)) return
74d63469
GR
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) => {
dae86118 259 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469
GR
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
0f6acda1 275 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
74d63469
GR
276
277 const user = res.locals.oauth.token.User
dae86118 278 const videoChangeOwnership = res.locals.videoChangeOwnership
74d63469
GR
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
418d092a 291function getCommonVideoEditAttributes () {
a920fef1
C
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'),
7f2cfe3a 344 body('downloadEnabled')
1e74f19a 345 .optional()
156c50af
LD
346 .toBoolean()
347 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
b718fd22
C
348 body('originallyPublishedAt')
349 .optional()
350 .customSanitizer(toValueOrNull)
351 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
a920fef1
C
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}
fbad87b0 364
1cd3facc
C
365const 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
dae86118 398 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
1cd3facc
C
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
fbad87b0
C
410// ---------------------------------------------------------------------------
411
412export {
413 videosAddValidator,
414 videosUpdateValidator,
415 videosGetValidator,
8d427346 416 checkVideoFollowConstraints,
96f29c0f 417 videosCustomGetValidator,
fbad87b0 418 videosRemoveValidator,
fbad87b0 419
74d63469
GR
420 videosChangeOwnershipValidator,
421 videosTerminateChangeOwnershipValidator,
422 videosAcceptChangeOwnershipValidator,
423
418d092a 424 getCommonVideoEditAttributes,
1cd3facc
C
425
426 commonVideosFiltersValidator
fbad87b0
C
427}
428
429// ---------------------------------------------------------------------------
430
431function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
432 if (req.body.scheduleUpdate) {
433 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
434 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
435
fbad87b0
C
436 res.status(400)
437 .json({ error: 'Schedule update at is mandatory.' })
fbad87b0
C
438
439 return true
440 }
441 }
442
443 return false
444}