1 import * as express from 'express'
2 import { body, param, query, ValidationChain } from 'express-validator'
3 import { getServerActor } from '@server/models/application/application'
4 import { MVideoFullLight } from '@server/types/models'
5 import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
6 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
17 } from '../../../helpers/custom-validators/misc'
18 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
19 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
21 isScheduleVideoUpdatePrivacyValid,
23 isVideoDescriptionValid,
30 isVideoOriginallyPublishedAtValid,
34 } from '../../../helpers/custom-validators/videos'
35 import { cleanUpReqFiles } from '../../../helpers/express-utils'
36 import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
37 import { logger } from '../../../helpers/logger'
39 checkUserCanManageVideo,
40 doesVideoChannelOfAccountExist,
42 doesVideoFileOfVideoExist
43 } from '../../../helpers/middlewares'
44 import { getVideoWithAttributes } from '../../../helpers/video'
45 import { CONFIG } from '../../../initializers/config'
46 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
47 import { isLocalVideoAccepted } from '../../../lib/moderation'
48 import { Hooks } from '../../../lib/plugins/hooks'
49 import { AccountModel } from '../../../models/account/account'
50 import { VideoModel } from '../../../models/video/video'
51 import { authenticatePromiseIfNeeded } from '../../oauth'
52 import { areValidationErrors } from '../utils'
54 const videosAddValidator = getCommonVideoEditAttributes().concat([
56 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
57 'This file is not supported or too large. Please, make sure it is of the following type: ' +
58 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
60 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
62 .customSanitizer(toIntOrNull)
63 .custom(isIdValid).withMessage('Should have correct video channel id'),
65 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
66 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
68 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
69 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
71 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
72 const user = res.locals.oauth.token.User
74 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
76 if (await user.isAbleToUploadVideo(videoFile) === false) {
78 .json({ error: 'The user video quota is exceeded with this video.' })
80 return cleanUpReqFiles(req)
86 duration = await getDurationFromVideoFile(videoFile.path)
88 logger.error('Invalid input file in videosAddValidator.', { err })
90 .json({ error: 'Invalid input file.' })
92 return cleanUpReqFiles(req)
95 videoFile.duration = duration
97 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
103 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
104 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
107 .custom(isVideoNameValid).withMessage('Should have a valid name'),
110 .customSanitizer(toIntOrNull)
111 .custom(isIdValid).withMessage('Should have correct video channel id'),
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
116 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
117 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
118 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
120 // Check if the user who did the request is able to update the video
121 const user = res.locals.oauth.token.User
122 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
124 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
130 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
131 const video = getVideoWithAttributes(res)
133 // Anybody can watch local videos
134 if (video.isOwned() === true) return next()
137 if (res.locals.oauth) {
138 // Users can search or watch remote videos
139 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
142 // Anybody can search or watch remote videos
143 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
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()
149 return res.status(403)
151 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
152 error: 'Cannot get this video regarding follow constraints.',
157 const videosCustomGetValidator = (
158 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
159 authenticateInQuery = false
162 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
164 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
165 logger.debug('Checking videosGet parameters', { parameters: req.params })
167 if (areValidationErrors(req, res)) return
168 if (!await doesVideoExist(req.params.id, res, fetchType)) return
170 // Controllers does not need to check video rights
171 if (fetchType === 'only-immutable-attributes') return next()
173 const video = getVideoWithAttributes(res)
174 const videoAll = video as MVideoFullLight
176 // Video private or blacklisted
177 if (videoAll.requiresAuth()) {
178 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
180 const user = res.locals.oauth ? res.locals.oauth.token.User : null
182 // Only the owner or a user that have blacklist rights can see the video
183 if (!user || !user.canGetVideo(videoAll)) {
184 return res.status(403)
185 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
191 // Video is public, anyone can access it
192 if (video.privacy === VideoPrivacy.PUBLIC) return next()
194 // Video is unlisted, check we used the uuid to fetch it
195 if (video.privacy === VideoPrivacy.UNLISTED) {
196 if (isUUIDValid(req.params.id)) return next()
198 // Don't leak this unlisted video
199 return res.status(404).end()
205 const videosGetValidator = videosCustomGetValidator('all')
206 const videosDownloadValidator = videosCustomGetValidator('all', true)
208 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
209 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
210 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
212 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
213 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
215 if (areValidationErrors(req, res)) return
216 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
222 const videosRemoveValidator = [
223 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
225 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
226 logger.debug('Checking videosRemove parameters', { parameters: req.params })
228 if (areValidationErrors(req, res)) return
229 if (!await doesVideoExist(req.params.id, res)) return
231 // Check if the user who did the request is able to delete the video
232 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
238 const videosChangeOwnershipValidator = [
239 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
241 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
242 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
244 if (areValidationErrors(req, res)) return
245 if (!await doesVideoExist(req.params.videoId, res)) return
247 // Check if the user who did the request is able to change the ownership of the video
248 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
250 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
253 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
257 res.locals.nextOwner = nextOwner
263 const videosTerminateChangeOwnershipValidator = [
264 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
269 if (areValidationErrors(req, res)) return
270 if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
272 // Check if the user who did the request is able to change the ownership of the video
273 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
275 const videoChangeOwnership = res.locals.videoChangeOwnership
277 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
279 .json({ error: 'Ownership already accepted or refused' })
287 const videosAcceptChangeOwnershipValidator = [
288 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
289 const body = req.body as VideoChangeOwnershipAccept
290 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
292 const user = res.locals.oauth.token.User
293 const videoChangeOwnership = res.locals.videoChangeOwnership
294 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
295 if (isAble === false) {
297 .json({ error: 'The user video quota is exceeded with this video.' })
306 const videosOverviewValidator = [
309 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
310 .withMessage('Should have a valid pagination'),
312 (req: express.Request, res: express.Response, next: express.NextFunction) => {
313 if (areValidationErrors(req, res)) return
319 function getCommonVideoEditAttributes () {
321 body('thumbnailfile')
322 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
323 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
324 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
327 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
328 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
329 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
334 .customSanitizer(toIntOrNull)
335 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
338 .customSanitizer(toIntOrNull)
339 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
342 .customSanitizer(toValueOrNull)
343 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
346 .customSanitizer(toBooleanOrNull)
347 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
348 body('waitTranscoding')
350 .customSanitizer(toBooleanOrNull)
351 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
354 .customSanitizer(toValueOrNull)
355 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
358 .customSanitizer(toValueOrNull)
359 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
362 .customSanitizer(toValueOrNull)
363 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
366 .customSanitizer(toValueOrNull)
367 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
368 body('commentsEnabled')
370 .customSanitizer(toBooleanOrNull)
371 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
372 body('downloadEnabled')
374 .customSanitizer(toBooleanOrNull)
375 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
376 body('originallyPublishedAt')
378 .customSanitizer(toValueOrNull)
379 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
380 body('scheduleUpdate')
382 .customSanitizer(toValueOrNull),
383 body('scheduleUpdate.updateAt')
385 .custom(isDateValid).withMessage('Should have a valid schedule update date'),
386 body('scheduleUpdate.privacy')
388 .customSanitizer(toIntOrNull)
389 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
390 ] as (ValidationChain | express.Handler)[]
393 const commonVideosFiltersValidator = [
394 query('categoryOneOf')
396 .customSanitizer(toArray)
397 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
398 query('licenceOneOf')
400 .customSanitizer(toArray)
401 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
402 query('languageOneOf')
404 .customSanitizer(toArray)
405 .custom(isStringArray).withMessage('Should have a valid one of language array'),
408 .customSanitizer(toArray)
409 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
412 .customSanitizer(toArray)
413 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
416 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
419 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
422 .customSanitizer(toBooleanOrNull)
423 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
425 (req: express.Request, res: express.Response, next: express.NextFunction) => {
426 logger.debug('Checking commons video filters query', { parameters: req.query })
428 if (areValidationErrors(req, res)) return
430 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
431 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
433 .json({ error: 'You are not allowed to see all local videos.' })
442 // ---------------------------------------------------------------------------
446 videosUpdateValidator,
448 videoFileMetadataGetValidator,
449 videosDownloadValidator,
450 checkVideoFollowConstraints,
451 videosCustomGetValidator,
452 videosRemoveValidator,
454 videosChangeOwnershipValidator,
455 videosTerminateChangeOwnershipValidator,
456 videosAcceptChangeOwnershipValidator,
458 getCommonVideoEditAttributes,
460 commonVideosFiltersValidator,
462 videosOverviewValidator
465 // ---------------------------------------------------------------------------
467 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
468 if (req.body.scheduleUpdate) {
469 if (!req.body.scheduleUpdate.updateAt) {
470 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
473 .json({ error: 'Schedule update at is mandatory.' })
482 async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
483 // Check we accept this video
484 const acceptParameters = {
487 user: res.locals.oauth.token.User
489 const acceptedResult = await Hooks.wrapFun(
490 isLocalVideoAccepted,
492 'filter:api.video.upload.accept.result'
495 if (!acceptedResult || acceptedResult.accepted !== true) {
496 logger.info('Refused local video.', { acceptedResult, acceptParameters })
498 .json({ error: acceptedResult.errorMessage || 'Refused local video' })