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