123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- import * as express from 'express'
- import 'express-validator'
- import { body, param, query, ValidationChain } from 'express-validator/check'
- import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
- import {
- isBooleanValid,
- isDateValid,
- isIdOrUUIDValid,
- isIdValid,
- isUUIDValid,
- toArray,
- toIntOrNull,
- toValueOrNull
- } from '../../../helpers/custom-validators/misc'
- import {
- checkUserCanManageVideo,
- doesVideoChannelOfAccountExist,
- doesVideoExist,
- isScheduleVideoUpdatePrivacyValid,
- isVideoCategoryValid,
- isVideoDescriptionValid,
- isVideoFile,
- isVideoFilterValid,
- isVideoImage,
- isVideoLanguageValid,
- isVideoLicenceValid,
- isVideoNameValid,
- isVideoOriginallyPublishedAtValid,
- isVideoPrivacyValid,
- isVideoSupportValid,
- isVideoTagsValid
- } from '../../../helpers/custom-validators/videos'
- import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
- import { logger } from '../../../helpers/logger'
- import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
- import { authenticatePromiseIfNeeded } from '../../oauth'
- import { areValidationErrors } from '../utils'
- import { cleanUpReqFiles } from '../../../helpers/express-utils'
- import { VideoModel } from '../../../models/video/video'
- import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
- import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
- import { AccountModel } from '../../../models/account/account'
- import { VideoFetchType } from '../../../helpers/video'
- import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
- import { getServerActor } from '../../../helpers/utils'
- import { CONFIG } from '../../../initializers/config'
- const videosAddValidator = getCommonVideoEditAttributes().concat([
- body('videofile')
- .custom((value, { req }) => isVideoFile(req.files)).withMessage(
- 'This file is not supported or too large. Please, make sure it is of the following type: '
- + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
- ),
- body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
- body('channelId')
- .toInt()
- .custom(isIdValid).withMessage('Should have correct video channel id'),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
- if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
- if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
- const videoFile: Express.Multer.File = req.files['videofile'][0]
- const user = res.locals.oauth.token.User
- if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
- const isAble = await user.isAbleToUploadVideo(videoFile)
- if (isAble === false) {
- res.status(403)
- .json({ error: 'The user video quota is exceeded with this video.' })
- return cleanUpReqFiles(req)
- }
- let duration: number
- try {
- duration = await getDurationFromVideoFile(videoFile.path)
- } catch (err) {
- logger.error('Invalid input file in videosAddValidator.', { err })
- res.status(400)
- .json({ error: 'Invalid input file.' })
- return cleanUpReqFiles(req)
- }
- videoFile['duration'] = duration
- return next()
- }
- ])
- const videosUpdateValidator = getCommonVideoEditAttributes().concat([
- param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
- body('name')
- .optional()
- .custom(isVideoNameValid).withMessage('Should have a valid name'),
- body('channelId')
- .optional()
- .toInt()
- .custom(isIdValid).withMessage('Should have correct video channel id'),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videosUpdate parameters', { parameters: req.body })
- if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
- if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
- if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
- // Check if the user who did the request is able to update the video
- const user = res.locals.oauth.token.User
- if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
- if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
- return next()
- }
- ])
- async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
- const video = res.locals.video
- // Anybody can watch local videos
- if (video.isOwned() === true) return next()
- // Logged user
- if (res.locals.oauth) {
- // Users can search or watch remote videos
- if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
- }
- // Anybody can search or watch remote videos
- if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
- // Check our instance follows an actor that shared this video
- const serverActor = await getServerActor()
- if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
- return res.status(403)
- .json({
- error: 'Cannot get this video regarding follow constraints.'
- })
- }
- const videosCustomGetValidator = (fetchType: VideoFetchType) => {
- return [
- param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videosGet parameters', { parameters: req.params })
- if (areValidationErrors(req, res)) return
- if (!await doesVideoExist(req.params.id, res, fetchType)) return
- const video = res.locals.video
- // Video private or blacklisted
- if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
- await authenticatePromiseIfNeeded(req, res)
- const user = res.locals.oauth ? res.locals.oauth.token.User : null
- // Only the owner or a user that have blacklist rights can see the video
- if (
- !user ||
- (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
- ) {
- return res.status(403)
- .json({ error: 'Cannot get this private or blacklisted video.' })
- }
- return next()
- }
- // Video is public, anyone can access it
- if (video.privacy === VideoPrivacy.PUBLIC) return next()
- // Video is unlisted, check we used the uuid to fetch it
- if (video.privacy === VideoPrivacy.UNLISTED) {
- if (isUUIDValid(req.params.id)) return next()
- // Don't leak this unlisted video
- return res.status(404).end()
- }
- }
- ]
- }
- const videosGetValidator = videosCustomGetValidator('all')
- const videosRemoveValidator = [
- param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videosRemove parameters', { parameters: req.params })
- if (areValidationErrors(req, res)) return
- if (!await doesVideoExist(req.params.id, res)) return
- // Check if the user who did the request is able to delete the video
- if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
- return next()
- }
- ]
- const videosChangeOwnershipValidator = [
- param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking changeOwnership parameters', { parameters: req.params })
- if (areValidationErrors(req, res)) return
- if (!await doesVideoExist(req.params.videoId, res)) return
- // Check if the user who did the request is able to change the ownership of the video
- if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
- const nextOwner = await AccountModel.loadLocalByName(req.body.username)
- if (!nextOwner) {
- res.status(400)
- .json({ error: 'Changing video ownership to a remote account is not supported yet' })
- return
- }
- res.locals.nextOwner = nextOwner
- return next()
- }
- ]
- const videosTerminateChangeOwnershipValidator = [
- param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking changeOwnership parameters', { parameters: req.params })
- if (areValidationErrors(req, res)) return
- if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
- // Check if the user who did the request is able to change the ownership of the video
- if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
- return next()
- },
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- const videoChangeOwnership = res.locals.videoChangeOwnership
- if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
- return next()
- } else {
- res.status(403)
- .json({ error: 'Ownership already accepted or refused' })
- return
- }
- }
- ]
- const videosAcceptChangeOwnershipValidator = [
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- const body = req.body as VideoChangeOwnershipAccept
- if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
- const user = res.locals.oauth.token.User
- const videoChangeOwnership = res.locals.videoChangeOwnership
- const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
- if (isAble === false) {
- res.status(403)
- .json({ error: 'The user video quota is exceeded with this video.' })
- return
- }
- return next()
- }
- ]
- function getCommonVideoEditAttributes () {
- return [
- body('thumbnailfile')
- .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
- 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
- + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
- ),
- body('previewfile')
- .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
- 'This preview file is not supported or too large. Please, make sure it is of the following type: '
- + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
- ),
- body('category')
- .optional()
- .customSanitizer(toIntOrNull)
- .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
- body('licence')
- .optional()
- .customSanitizer(toIntOrNull)
- .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
- body('language')
- .optional()
- .customSanitizer(toValueOrNull)
- .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
- body('nsfw')
- .optional()
- .toBoolean()
- .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
- body('waitTranscoding')
- .optional()
- .toBoolean()
- .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
- body('privacy')
- .optional()
- .toInt()
- .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
- body('description')
- .optional()
- .customSanitizer(toValueOrNull)
- .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
- body('support')
- .optional()
- .customSanitizer(toValueOrNull)
- .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
- body('tags')
- .optional()
- .customSanitizer(toValueOrNull)
- .custom(isVideoTagsValid).withMessage('Should have correct tags'),
- body('commentsEnabled')
- .optional()
- .toBoolean()
- .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
- body('downloadEnabled')
- .optional()
- .toBoolean()
- .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
- body('originallyPublishedAt')
- .optional()
- .customSanitizer(toValueOrNull)
- .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
- body('scheduleUpdate')
- .optional()
- .customSanitizer(toValueOrNull),
- body('scheduleUpdate.updateAt')
- .optional()
- .custom(isDateValid).withMessage('Should have a valid schedule update date'),
- body('scheduleUpdate.privacy')
- .optional()
- .toInt()
- .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
- ] as (ValidationChain | express.Handler)[]
- }
- const commonVideosFiltersValidator = [
- query('categoryOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isNumberArray).withMessage('Should have a valid one of category array'),
- query('licenceOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
- query('languageOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isStringArray).withMessage('Should have a valid one of language array'),
- query('tagsOneOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isStringArray).withMessage('Should have a valid one of tags array'),
- query('tagsAllOf')
- .optional()
- .customSanitizer(toArray)
- .custom(isStringArray).withMessage('Should have a valid all of tags array'),
- query('nsfw')
- .optional()
- .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
- query('filter')
- .optional()
- .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking commons video filters query', { parameters: req.query })
- if (areValidationErrors(req, res)) return
- const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
- if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
- res.status(401)
- .json({ error: 'You are not allowed to see all local videos.' })
- return
- }
- return next()
- }
- ]
- // ---------------------------------------------------------------------------
- export {
- videosAddValidator,
- videosUpdateValidator,
- videosGetValidator,
- checkVideoFollowConstraints,
- videosCustomGetValidator,
- videosRemoveValidator,
- videosChangeOwnershipValidator,
- videosTerminateChangeOwnershipValidator,
- videosAcceptChangeOwnershipValidator,
- getCommonVideoEditAttributes,
- commonVideosFiltersValidator
- }
- // ---------------------------------------------------------------------------
- function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
- if (req.body.scheduleUpdate) {
- if (!req.body.scheduleUpdate.updateAt) {
- logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
- res.status(400)
- .json({ error: 'Schedule update at is mandatory.' })
- return true
- }
- }
- return false
- }
|