Init commit
This commit is contained in:
20
server/core/middlewares/validators/videos/index.ts
Normal file
20
server/core/middlewares/validators/videos/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export * from './video-blacklist.js'
|
||||
export * from './video-captions.js'
|
||||
export * from './video-channel-sync.js'
|
||||
export * from './video-channels.js'
|
||||
export * from './video-chapters.js'
|
||||
export * from './video-comments.js'
|
||||
export * from './video-files.js'
|
||||
export * from './video-imports.js'
|
||||
export * from './video-live.js'
|
||||
export * from './video-ownership-changes.js'
|
||||
export * from './video-passwords.js'
|
||||
export * from './video-rates.js'
|
||||
export * from './video-shares.js'
|
||||
export * from './video-source.js'
|
||||
export * from './video-stats.js'
|
||||
export * from './video-studio.js'
|
||||
export * from './video-token.js'
|
||||
export * from './video-transcoding.js'
|
||||
export * from './video-view.js'
|
||||
export * from './videos.js'
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './upload.js'
|
||||
export * from './video-validators.js'
|
||||
42
server/core/middlewares/validators/videos/shared/upload.ts
Normal file
42
server/core/middlewares/validators/videos/shared/upload.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import express from 'express'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { ffprobePromise, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
export async function addDurationToVideoFileIfNeeded (options: {
|
||||
res: express.Response
|
||||
videoFile: { path: string, duration?: number }
|
||||
middlewareName: string
|
||||
}) {
|
||||
const { res, middlewareName, videoFile } = options
|
||||
|
||||
try {
|
||||
if (!videoFile.duration) await addDurationToVideo(res, videoFile)
|
||||
} catch (err) {
|
||||
logger.error('Invalid input file in ' + middlewareName, { err })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
|
||||
message: 'Video file unreadable.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addDurationToVideo (res: express.Response, videoFile: { path: string, duration?: number }) {
|
||||
const probe = await ffprobePromise(videoFile.path)
|
||||
res.locals.ffprobe = probe
|
||||
|
||||
const duration = await getVideoStreamDuration(videoFile.path, probe)
|
||||
|
||||
// FFmpeg may not be able to guess video duration
|
||||
// For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
|
||||
if (isNaN(duration)) videoFile.duration = 0
|
||||
else videoFile.duration = duration
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { canVideoFileBeEdited } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode, ServerFilterHookName, VideoState, VideoStateType } from '@peertube/peertube-models'
|
||||
import { isVideoFileMimeTypeValid, isVideoFileSizeValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { CONSTRAINTS_FIELDS, VIDEO_STATES } from '@server/initializers/constants.js'
|
||||
import { isLocalVideoFileAccepted } from '@server/lib/moderation.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { MUserAccountId, MVideo } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { checkUserQuota } from '../../shared/index.js'
|
||||
|
||||
export async function commonVideoFileChecks (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
user: MUserAccountId
|
||||
videoFileSize: number
|
||||
files: express.UploadFilesForCheck
|
||||
}): Promise<boolean> {
|
||||
const { req, res, user, videoFileSize, files } = options
|
||||
|
||||
if (!isVideoFileMimeTypeValid(files)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
|
||||
message: req.t(
|
||||
'This file is not supported. Please, make sure it is of the following type: {types}',
|
||||
{ types: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') }
|
||||
)
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isVideoFileSizeValid(videoFileSize.toString())) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
message: req.t('This file is too large. It exceeds the maximum file size authorized'),
|
||||
type: ServerErrorCode.MAX_FILE_SIZE_REACHED
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (await checkUserQuota({ user, videoFileSize, req, res }) === false) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function isVideoFileAccepted (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
videoFile: express.VideoLegacyUploadFile
|
||||
hook: Extract<ServerFilterHookName, 'filter:api.video.upload.accept.result' | 'filter:api.video.update-file.accept.result'>
|
||||
}) {
|
||||
const { req, res, videoFile, hook } = options
|
||||
|
||||
// Check we accept this video
|
||||
const acceptParameters = {
|
||||
videoBody: req.body,
|
||||
videoFile,
|
||||
user: res.locals.oauth.token.User
|
||||
}
|
||||
const acceptedResult = await Hooks.wrapFun(isLocalVideoFileAccepted, acceptParameters, hook)
|
||||
|
||||
if (acceptedResult?.accepted !== true) {
|
||||
logger.info('Refused local video file.', { acceptedResult, acceptParameters })
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult.errorMessage || req.t('Refused local video file')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkVideoFileCanBeEdited (video: MVideo, req: express.Request, res: express.Response) {
|
||||
if (video.isLive) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot edit a live video')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Cannot edit video that is already waiting for transcoding/edition')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (!canVideoFileBeEdited(video.state)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Video state is not compatible with edition')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkVideoCanBeTranscribed (video: MVideo, req: express.Request, res: express.Response) {
|
||||
if (video.remote) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot run this task on a remote video')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (video.isLive) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot run this task on a live video')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const incompatibleStates = new Set<VideoStateType>([
|
||||
VideoState.TO_IMPORT,
|
||||
VideoState.TO_EDIT,
|
||||
VideoState.TO_MOVE_TO_EXTERNAL_STORAGE,
|
||||
VideoState.TO_MOVE_TO_FILE_SYSTEM,
|
||||
VideoState.TO_IMPORT_FAILED
|
||||
])
|
||||
if (incompatibleStates.has(video.state)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot run this task on a video with "{state}" state', { state: VIDEO_STATES[video.state] })
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
87
server/core/middlewares/validators/videos/video-blacklist.ts
Normal file
87
server/core/middlewares/validators/videos/video-blacklist.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist.js'
|
||||
import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const videosBlacklistRemoveValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosBlacklistAddValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('unfederate')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'),
|
||||
body('reason')
|
||||
.optional()
|
||||
.custom(isVideoBlacklistReasonValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (req.body.unfederate === true && video.remote === true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'You cannot unfederate a remote video.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosBlacklistUpdateValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('reason')
|
||||
.optional()
|
||||
.custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosBlacklistFiltersValidator = [
|
||||
query('type')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
|
||||
query('search')
|
||||
.optional()
|
||||
.not()
|
||||
.isEmpty().withMessage('Should have a valid search'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosBlacklistAddValidator,
|
||||
videosBlacklistRemoveValidator,
|
||||
videosBlacklistUpdateValidator,
|
||||
videosBlacklistFiltersValidator
|
||||
}
|
||||
170
server/core/middlewares/validators/videos/video-captions.ts
Normal file
170
server/core/middlewares/validators/videos/video-captions.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoCaptionGenerate } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanSeeVideo,
|
||||
checkCanManageVideo,
|
||||
doesVideoCaptionExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam,
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
import { checkVideoCanBeTranscribed } from './shared/video-validators.js'
|
||||
|
||||
export const addVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('captionLanguage')
|
||||
.custom(isVideoCaptionLanguageValid).not().isEmpty(),
|
||||
|
||||
body('captionfile')
|
||||
.custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile'))
|
||||
.withMessage(
|
||||
'This caption file is not supported or too large. ' +
|
||||
`Please, make sure it is under ${CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max} bytes ` +
|
||||
'and one of the following mimetypes: ' +
|
||||
Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT).map(key => `${key} (${MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT[key]})`).join(', ')
|
||||
),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (!await doesVideoExist(req.params.videoId, 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 (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.UPDATE_ANY_VIDEO,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const generateVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('forceTranscription')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.ENABLED !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video transcription is disabled on this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkVideoCanBeTranscribed(video, req, res)) return
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await checkCanManageVideo({ user, video, right: UserRight.UPDATE_ANY_VIDEO, req, res, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check the video has not already a caption
|
||||
const captions = await VideoCaptionModel.listVideoCaptions(video.id)
|
||||
if (captions.length !== 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_HAS_CAPTIONS,
|
||||
message: 'This video already has captions'
|
||||
})
|
||||
}
|
||||
|
||||
// Bypass "video is already transcribed" check
|
||||
const body = req.body as VideoCaptionGenerate
|
||||
if (body.forceTranscription === true) {
|
||||
if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only admins can force transcription'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
const info = await VideoJobInfoModel.load(video.id)
|
||||
if (info && info.pendingTranscription > 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCRIBED,
|
||||
message: 'This video is already being transcribed'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('captionLanguage')
|
||||
.custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoCaptionExist(res.locals.videoAll, req.params.captionLanguage, res)) return
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.UPDATE_ANY_VIDEO,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listVideoCaptionsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
const video = res.locals.onlyVideo
|
||||
if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,171 @@
|
||||
import { HttpStatusCode, UserRight, VideoChannelCollaboratorState } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { areValidationErrors, checkCanManageAccount, doesAccountHandleExist, doesChannelHandleExist } from '../shared/index.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
|
||||
export const channelListCollaboratorsValidator = [
|
||||
param('handle').exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (
|
||||
!await doesChannelHandleExist({ handle: req.params.handle, checkCanManage: true, checkIsLocal: true, checkIsOwner: false, req, res })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const channelInviteCollaboratorsValidator = [
|
||||
param('handle').exists(),
|
||||
|
||||
body('accountHandle').exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (
|
||||
!await doesChannelHandleExist({ handle: req.params.handle, checkCanManage: true, checkIsLocal: true, checkIsOwner: true, req, res })
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!await doesAccountHandleExist({ handle: req.body.accountHandle, req, res, checkIsLocal: true, checkCanManage: false })) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (user.Account.id === res.locals.account.id) {
|
||||
res.fail({
|
||||
message: req.t('Cannot invite the account owner of the channel to collaborate'),
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const collaborator = await VideoChannelCollaboratorModel.loadByCollaboratorAccountName({
|
||||
accountName: req.body.accountHandle,
|
||||
channelId: res.locals.videoChannel.id
|
||||
})
|
||||
res.locals.channelCollaborator = collaborator
|
||||
|
||||
if (collaborator && collaborator.state !== VideoChannelCollaboratorState.REJECTED) {
|
||||
res.fail({
|
||||
message: req.t('This account is already a collaborator or has a pending invitation for this channel'),
|
||||
status: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const count = await VideoChannelCollaboratorModel.countByChannel(res.locals.videoChannel.id)
|
||||
if (count >= CONFIG.VIDEO_CHANNELS.MAX_COLLABORATORS_PER_CHANNEL) {
|
||||
res.fail({
|
||||
message: req.t(
|
||||
'The maximum number of collaborators for this channel ({limit}) has been reached',
|
||||
{ limit: CONFIG.VIDEO_CHANNELS.MAX_COLLABORATORS_PER_CHANNEL }
|
||||
),
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const channelAcceptOrRejectInviteCollaboratorsValidator = [
|
||||
param('handle').exists(),
|
||||
param('collaboratorId').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (
|
||||
!await doesChannelHandleExist({ handle: req.params.handle, checkCanManage: false, checkIsLocal: true, checkIsOwner: false, req, res })
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!await doesChannelCollaboratorExist({ collaboratorId: +req.params.collaboratorId, channelHandle: req.params.handle, req, res })) {
|
||||
return
|
||||
}
|
||||
|
||||
const channelCollaborator = res.locals.channelCollaborator
|
||||
|
||||
if (channelCollaborator.state !== VideoChannelCollaboratorState.PENDING) {
|
||||
res.fail({ message: req.t('Collaborator is not in pending state') })
|
||||
return
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (channelCollaborator.accountId !== user.Account.id) {
|
||||
res.fail({ message: req.t('Collaborator is not the current user') })
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const channelDeleteCollaboratorsValidator = [
|
||||
param('handle').exists(),
|
||||
param('collaboratorId').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesChannelCollaboratorExist({ collaboratorId: +req.params.collaboratorId, channelHandle: req.params.handle, req, res })) {
|
||||
return
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const canManageCollaboratorAccount = checkCanManageAccount({
|
||||
user,
|
||||
account: res.locals.channelCollaborator.Account,
|
||||
req,
|
||||
res,
|
||||
specialRight: UserRight.MANAGE_ANY_VIDEO_CHANNEL
|
||||
})
|
||||
|
||||
// Check this is the owner of the channel if the user is not the collaborator account
|
||||
// Only the owner and the collaborator can delete the collaboration
|
||||
const checkIsOwner = !canManageCollaboratorAccount
|
||||
|
||||
if (
|
||||
!await doesChannelHandleExist({ handle: req.params.handle, checkCanManage: true, checkIsLocal: true, checkIsOwner, req, res })
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesChannelCollaboratorExist (options: {
|
||||
collaboratorId: number
|
||||
channelHandle: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { collaboratorId, channelHandle, req, res } = options
|
||||
|
||||
const channelCollaborator = await VideoChannelCollaboratorModel.loadByChannelHandle(collaboratorId, channelHandle)
|
||||
if (!channelCollaborator) {
|
||||
res.fail({
|
||||
message: req.t('Channel collaborator does not exist'),
|
||||
status: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.channelCollaborator = channelCollaborator
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models'
|
||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import * as express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidationErrors, doesChannelIdExist } from '../shared/index.js'
|
||||
import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs.js'
|
||||
|
||||
export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Synchronization is impossible as video channel synchronization is not enabled on the server'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export const videoChannelSyncValidator = [
|
||||
body('externalChannelUrl')
|
||||
.custom(isUrlValid),
|
||||
|
||||
body('videoChannelId')
|
||||
.isInt(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: VideoChannelSyncCreate = req.body
|
||||
if (!await doesChannelIdExist({ id: body.videoChannelId, checkCanManage: true, checkIsOwner: false, checkIsLocal: true, req, res })) {
|
||||
return
|
||||
}
|
||||
|
||||
const count = await VideoChannelSyncModel.countByAccount(res.locals.videoChannel.accountId)
|
||||
if (count >= CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER) {
|
||||
return res.fail({
|
||||
message: `You cannot create more than ${CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER} channel synchronizations`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const ensureSyncExists = [
|
||||
param('id').exists().isInt().withMessage('Should have an sync id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoChannelSyncIdExist(+req.params.id, res)) return
|
||||
if (
|
||||
!await doesChannelIdExist({
|
||||
id: res.locals.videoChannelSync.videoChannelId,
|
||||
checkCanManage: true,
|
||||
checkIsOwner: false,
|
||||
checkIsLocal: true,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listAccountChannelsSyncValidator = [
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
184
server/core/middlewares/validators/videos/video-channels.ts
Normal file
184
server/core/middlewares/validators/videos/video-channels.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { HttpStatusCode, VideosImportInChannelCreate } from '@peertube/peertube-models'
|
||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { MChannelAccountDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isVideoChannelDescriptionValid,
|
||||
isVideoChannelDisplayNameValid,
|
||||
isVideoChannelSupportValid,
|
||||
isVideoChannelUsernameValid
|
||||
} from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { VideoChannelModel } from '../../../models/video/video-channel.js'
|
||||
import { areValidationErrors, checkUserQuota, doesChannelHandleExist } from '../shared/index.js'
|
||||
import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs.js'
|
||||
|
||||
export const videoChannelsAddValidator = [
|
||||
body('name')
|
||||
.custom(isVideoChannelUsernameValid),
|
||||
body('displayName')
|
||||
.custom(isVideoChannelDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isVideoChannelDescriptionValid),
|
||||
body('support')
|
||||
.optional()
|
||||
.custom(isVideoChannelSupportValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const actor = await loadReservedActorName(req.body.name)
|
||||
if (actor) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t(
|
||||
'Another actor (account/channel) with name {name} on this instance already exists or has already existed.',
|
||||
{ name: req.body.name }
|
||||
)
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
|
||||
if (count >= CONFIG.VIDEO_CHANNELS.MAX_PER_USER) {
|
||||
res.fail({ message: req.t('You cannot create more than {count} channels', { count: CONFIG.VIDEO_CHANNELS.MAX_PER_USER }) })
|
||||
return false
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsUpdateValidator = [
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isVideoChannelDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isVideoChannelDescriptionValid),
|
||||
body('support')
|
||||
.optional()
|
||||
.custom(isVideoChannelSupportValid),
|
||||
body('bulkVideosSupportUpdate')
|
||||
.optional()
|
||||
.custom(isBooleanValid).withMessage('Should have a valid bulkVideosSupportUpdate boolean field'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsRemoveValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsHandleValidatorFactory = (options: {
|
||||
checkIsLocal: boolean
|
||||
checkCanManage: boolean
|
||||
checkIsOwner: boolean
|
||||
}) => {
|
||||
const { checkIsLocal, checkCanManage, checkIsOwner } = options
|
||||
|
||||
return [
|
||||
param('handle')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesChannelHandleExist({ handle: req.params.handle, checkCanManage, checkIsLocal, checkIsOwner, req, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const listAccountChannelsValidator = [
|
||||
query('withStats')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsListValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelImportVideosValidator = [
|
||||
body('externalChannelUrl')
|
||||
.custom(isUrlValid),
|
||||
|
||||
body('videoChannelSyncId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: VideosImportInChannelCreate = req.body
|
||||
|
||||
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Channel import is impossible as video upload via HTTP is not enabled on the server')
|
||||
})
|
||||
}
|
||||
|
||||
if (body.videoChannelSyncId && !await doesVideoChannelSyncIdExist(body.videoChannelSyncId, res)) return
|
||||
|
||||
if (res.locals.videoChannelSync && res.locals.videoChannelSync.videoChannelId !== res.locals.videoChannel.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This channel sync is not owned by this channel')
|
||||
})
|
||||
}
|
||||
|
||||
const user = { id: res.locals.videoChannel.Account.userId }
|
||||
if (!await checkUserQuota({ user, videoFileSize: 1, req, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, req: express.Request, res: express.Response) {
|
||||
const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
|
||||
|
||||
if (count <= 1) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Cannot remove the last channel of this user')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
41
server/core/middlewares/validators/videos/video-chapters.ts
Normal file
41
server/core/middlewares/validators/videos/video-chapters.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
import { areVideoChaptersValid } from '@server/helpers/custom-validators/video-chapters.js'
|
||||
|
||||
export const updateVideoChaptersValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('chapters')
|
||||
.custom(areVideoChaptersValid)
|
||||
.withMessage('Chapters must have a valid title and timecode, and each timecode must be unique'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
if (res.locals.videoAll.isLive) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'You cannot add chapters to a live video'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the user who did the request is able to update video chapters (same right as updating the video)
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.UPDATE_ANY_VIDEO,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
393
server/core/middlewares/validators/videos/video-comments.ts
Normal file
393
server/core/middlewares/validators/videos/video-comments.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, VideoCommentPolicy } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { MUserAccountUrl } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
toBooleanOrNull,
|
||||
toCompleteUUID,
|
||||
toIntOrNull
|
||||
} from '../../../helpers/custom-validators/misc.js'
|
||||
import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation.js'
|
||||
import { Hooks } from '../../../lib/plugins/hooks.js'
|
||||
import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video/index.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanManageChannel,
|
||||
checkCanManageVideo,
|
||||
checkCanSeeVideo,
|
||||
doesChannelIdExist,
|
||||
doesVideoCommentExist,
|
||||
doesVideoCommentThreadExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam,
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const listAllVideoCommentsForAdminValidator = [
|
||||
...getCommonVideoCommentsValidators(),
|
||||
|
||||
query('isLocal')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid)
|
||||
.withMessage('Should have a valid isLocal boolean'),
|
||||
|
||||
query('onLocalVideo')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid)
|
||||
.withMessage('Should have a valid onLocalVideo boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.videoId && !await doesVideoExist(req.query.videoId, res, 'unsafe-only-immutable-attributes')) return
|
||||
if (
|
||||
req.query.videoChannelId &&
|
||||
!await doesChannelIdExist({ id: req.query.videoChannelId, checkCanManage: true, checkIsOwner: false, checkIsLocal: true, req, res })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listCommentsOnUserVideosValidator = [
|
||||
...getCommonVideoCommentsValidators(),
|
||||
|
||||
query('isHeldForReview')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.videoId && !await doesVideoExist(req.query.videoId, res, 'all')) return
|
||||
|
||||
if (
|
||||
req.query.videoChannelId &&
|
||||
!await doesChannelIdExist({
|
||||
id: req.query.videoChannelId,
|
||||
checkCanManage: true,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false,
|
||||
req,
|
||||
res,
|
||||
specialRight: UserRight.SEE_ALL_COMMENTS
|
||||
})
|
||||
) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (
|
||||
video &&
|
||||
!await checkCanManageVideo({ user, video, right: UserRight.SEE_ALL_COMMENTS, req, res, checkIsLocal: true, checkIsOwner: false })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const listVideoCommentThreadsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listVideoThreadCommentsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('threadId')
|
||||
.custom(isIdValid),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const addVideoCommentThreadValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('text')
|
||||
.custom(isValidVideoCommentText),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
|
||||
|
||||
if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
|
||||
if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const addVideoCommentReplyValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId').custom(isIdValid),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
body('text').custom(isValidVideoCommentText),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
|
||||
|
||||
if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
|
||||
if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoCommentGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
if (!canVideoBeFederated(res.locals.onlyVideo)) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.onlyVideo, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const removeVideoCommentValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
|
||||
|
||||
if (!await checkCanDeleteVideoComment({ user: res.locals.oauth.token.User, videoComment: res.locals.videoCommentFull, req, res })) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const approveVideoCommentValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
|
||||
|
||||
if (!await checkCanApproveVideoComment({ user: res.locals.oauth.token.User, videoComment: res.locals.videoCommentFull, req, res })) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
|
||||
if (video.commentsPolicy === VideoCommentPolicy.DISABLED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Video comments are disabled for this video.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkCanDeleteVideoComment (options: {
|
||||
user: MUserAccountUrl
|
||||
videoComment: MCommentOwnerVideoReply
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}): Promise<boolean> {
|
||||
const { user, videoComment, req, res } = options
|
||||
|
||||
if (videoComment.isDeleted()) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('This comment is already deleted')
|
||||
})
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
// Owner of the comment
|
||||
if (videoComment.accountId === user.Account.id) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
return checkCanManageCommentsOfVideo(options)
|
||||
}
|
||||
|
||||
function checkCanApproveVideoComment (options: {
|
||||
user: MUserAccountUrl
|
||||
videoComment: MCommentOwnerVideoReply
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}): Promise<boolean> {
|
||||
const { user, videoComment, req, res } = options
|
||||
|
||||
if (videoComment.isDeleted()) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('This comment is deleted')
|
||||
})
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
if (videoComment.heldForReview !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('This comment is not held for review')
|
||||
})
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
return checkCanManageCommentsOfVideo({ user, videoComment, req, res })
|
||||
}
|
||||
|
||||
async function checkCanManageCommentsOfVideo (options: {
|
||||
user: MUserAccountUrl
|
||||
videoComment: MCommentOwnerVideoReply
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, videoComment, req, res } = options
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT)) return true
|
||||
|
||||
const channel = await VideoChannelModel.loadAndPopulateAccount(videoComment.Video.VideoChannel.id)
|
||||
if (await checkCanManageChannel({ channel, user, req, res: null, checkCanManage: true, checkIsOwner: false })) return true
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('User does not have the permission to delete this comment')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
|
||||
const acceptParameters = {
|
||||
video,
|
||||
commentBody: req.body,
|
||||
user: res.locals.oauth.token.User,
|
||||
req
|
||||
}
|
||||
|
||||
let acceptedResult: AcceptResult
|
||||
|
||||
if (isReply) {
|
||||
const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
|
||||
|
||||
acceptedResult = await Hooks.wrapFun(
|
||||
isLocalVideoCommentReplyAccepted,
|
||||
acceptReplyParameters,
|
||||
'filter:api.video-comment-reply.create.accept.result'
|
||||
)
|
||||
} else {
|
||||
acceptedResult = await Hooks.wrapFun(
|
||||
isLocalVideoThreadAccepted,
|
||||
acceptParameters,
|
||||
'filter:api.video-thread.create.accept.result'
|
||||
)
|
||||
}
|
||||
|
||||
if (acceptedResult?.accepted !== true) {
|
||||
logger.info('Refused local comment.', { acceptedResult, acceptParameters })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult?.errorMessage || 'Comment has been rejected.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getCommonVideoCommentsValidators () {
|
||||
return [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('searchAccount')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('searchVideo')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('videoId')
|
||||
.optional()
|
||||
.custom(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
query('videoChannelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
query('autoTagOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid autoTagOneOf array')
|
||||
]
|
||||
}
|
||||
168
server/core/middlewares/validators/videos/video-files.ts
Normal file
168
server/core/middlewares/validators/videos/video-files.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { HttpStatusCode, VideoResolution } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
export const videoFilesDeleteWebVideoValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
if (!video.hasWebVideoFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have Web Video files'
|
||||
})
|
||||
}
|
||||
|
||||
if (!video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete Web Video files since this video does not have HLS playlist'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoFilesDeleteWebVideoFileValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
const files = video.VideoFiles
|
||||
if (!files.find(f => f.id === +req.params.videoFileId)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'This video does not have this Web Video file id'
|
||||
})
|
||||
}
|
||||
|
||||
if (files.length === 1 && !video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete Web Video files since this video does not have HLS playlist'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const videoFilesDeleteHLSValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
if (!video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have HLS files'
|
||||
})
|
||||
}
|
||||
|
||||
if (!video.hasWebVideoFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete HLS playlist since this video does not have Web Video files'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoFilesDeleteHLSFileValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
const hls = video.getHLSPlaylist()
|
||||
|
||||
if (!hls) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have HLS files'
|
||||
})
|
||||
}
|
||||
|
||||
const hlsFiles = hls.VideoFiles
|
||||
const file = hlsFiles.find(f => f.id === +req.params.videoFileId)
|
||||
|
||||
if (!file) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'This HLS playlist does not have this file id'
|
||||
})
|
||||
}
|
||||
|
||||
// Last file to delete
|
||||
if (hlsFiles.length === 1 && !video.hasWebVideoFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete last HLS playlist file since this video does not have Web Video files'
|
||||
})
|
||||
}
|
||||
|
||||
if (hls.hasAudioAndVideoSplitted() && file.resolution === VideoResolution.H_NOVIDEO) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete audio file of HLS playlist with splitted audio/video. Delete all the videos first'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkLocalVideo (video: MVideo, res: express.Response) {
|
||||
if (video.remote) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete files of remote video'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
266
server/core/middlewares/validators/videos/video-imports.ts
Normal file
266
server/core/middlewares/validators/videos/video-imports.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, VideoImportCreate, VideoImportState } from '@peertube/peertube-models'
|
||||
import { isResolvingToUnicastOnly } from '@server/helpers/dns.js'
|
||||
import { isPreImportVideoAccepted } from '@server/lib/moderation.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { MUserAccountId, MVideoImportDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports.js'
|
||||
import { isValidPasswordProtectedPrivacy, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { areValidationErrors, checkCanManageVideo, doesChannelIdExist, doesVideoImportExist } from '../shared/index.js'
|
||||
import { areErrorsInNSFW, getCommonVideoEditAttributes } from './videos.js'
|
||||
|
||||
export const videoImportAddValidator = getCommonVideoEditAttributes().concat([
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
body('targetUrl')
|
||||
.optional()
|
||||
.custom(isVideoImportTargetUrlValid),
|
||||
body('magnetUri')
|
||||
.optional()
|
||||
.custom(isVideoMagnetUriValid),
|
||||
body('torrentfile')
|
||||
.custom((value, { req }) => isVideoImportTorrentFile(req.files))
|
||||
.withMessage(
|
||||
'This torrent file is not supported or too large. Please, make sure it is of the following type: ' +
|
||||
CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
|
||||
),
|
||||
body('name')
|
||||
.optional()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const torrentFile = req.files?.['torrentfile'] ? req.files['torrentfile'][0] : undefined
|
||||
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (areErrorsInNSFW(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('HTTP import is not enabled on this instance')
|
||||
})
|
||||
}
|
||||
|
||||
if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Torrent/magnet URI import is not enabled on this instance')
|
||||
})
|
||||
}
|
||||
|
||||
if (!await doesChannelIdExist({ id: req.body.channelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
// Check we have at least 1 required param
|
||||
if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: req.t('Should have a magnetUri or a targetUrl or a torrent file') })
|
||||
}
|
||||
|
||||
if (req.body.targetUrl) {
|
||||
const hostname = new URL(req.body.targetUrl).hostname
|
||||
|
||||
if (await isResolvingToUnicastOnly(hostname) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot use non unicast IP as targetUrl')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export const listMyVideoImportsValidator = [
|
||||
query('id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('videoId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('videoChannelSyncId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoImportDeleteValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
|
||||
if (!await checkCanManageImport({ user: res.locals.oauth.token.User, videoImport: res.locals.videoImport, req, res })) return
|
||||
|
||||
if (res.locals.videoImport.state === VideoImportState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Cannot delete a pending video import. Cancel it or wait for the end of the import first')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoImportCancelValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoImportExist(forceNumber(req.params.id), res)) return
|
||||
if (!await checkCanManageImport({ user: res.locals.oauth.token.User, videoImport: res.locals.videoImport, req, res })) return
|
||||
|
||||
if (res.locals.videoImport.state !== VideoImportState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Cannot cancel a non pending video import')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoImportRetryValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoImportExist(forceNumber(req.params.id), res)) return
|
||||
if (!await checkCanManageImport({ user: res.locals.oauth.token.User, videoImport: res.locals.videoImport, req, res })) return
|
||||
|
||||
if (res.locals.videoImport.state !== VideoImportState.FAILED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot retry a non failed video import')
|
||||
})
|
||||
}
|
||||
|
||||
if (!res.locals.videoImport.Video) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot retry video import because the associated video metadata has been deleted')
|
||||
})
|
||||
}
|
||||
|
||||
if (res.locals.videoImport.attempts >= CONFIG.IMPORT.VIDEOS.MAX_ATTEMPTS) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot retry video import since it has reached the maximum number of attempts')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function isImportAccepted (req: express.Request, res: express.Response) {
|
||||
const body: VideoImportCreate = req.body
|
||||
const hookName = body.targetUrl
|
||||
? 'filter:api.video.pre-import-url.accept.result'
|
||||
: 'filter:api.video.pre-import-torrent.accept.result'
|
||||
|
||||
// Check we accept this video
|
||||
const acceptParameters = {
|
||||
videoImportBody: body,
|
||||
user: res.locals.oauth.token.User
|
||||
}
|
||||
const acceptedResult = await Hooks.wrapFun(
|
||||
isPreImportVideoAccepted,
|
||||
acceptParameters,
|
||||
hookName
|
||||
)
|
||||
|
||||
if (acceptedResult?.accepted !== true) {
|
||||
logger.info('Refused to import video.', { acceptedResult, acceptParameters })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult.errorMessage || req.t('Refused to import video')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function checkCanManageImport (options: {
|
||||
user: MUserAccountId
|
||||
videoImport: MVideoImportDefault
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, videoImport, req, res } = options
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === true) return true
|
||||
if (videoImport.userId === user.id) return true
|
||||
if (
|
||||
videoImport.Video &&
|
||||
await checkCanManageVideo({
|
||||
user,
|
||||
video: videoImport.Video,
|
||||
req,
|
||||
res,
|
||||
right: UserRight.MANAGE_VIDEO_IMPORTS,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage video import of another user')
|
||||
})
|
||||
return false
|
||||
}
|
||||
350
server/core/middlewares/validators/videos/video-live.ts
Normal file
350
server/core/middlewares/validators/videos/video-live.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
LiveVideoUpdate,
|
||||
ServerErrorCode,
|
||||
UserRight,
|
||||
VideoState
|
||||
} from '@peertube/peertube-models'
|
||||
import { areLiveSchedulesValid, isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import { isLocalLiveVideoAccepted } from '@server/lib/moderation.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session.js'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { areValidationErrors, checkCanManageVideo, doesChannelIdExist, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
import { areErrorsInNSFW, getCommonVideoEditAttributes } from './videos.js'
|
||||
|
||||
export const videoLiveGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return
|
||||
|
||||
const videoLive = await VideoLiveModel.loadByVideoIdFull(res.locals.videoAll.id)
|
||||
if (!videoLive) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Live video not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoLive = videoLive
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
body('name')
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
|
||||
body('saveReplay')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
|
||||
|
||||
body('replaySettings.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoReplayPrivacyValid),
|
||||
|
||||
body('permanentLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid permanentLive boolean'),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isLiveLatencyModeValid),
|
||||
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
body('schedules')
|
||||
.optional()
|
||||
.custom(areLiveSchedulesValid).withMessage('Should have a valid schedules array'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (areErrorsInNSFW(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (CONFIG.LIVE.ENABLED !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Live is not enabled on this instance',
|
||||
type: ServerErrorCode.LIVE_NOT_ENABLED
|
||||
})
|
||||
}
|
||||
|
||||
const body: LiveVideoCreate = req.body
|
||||
|
||||
if (hasValidSaveReplay(body) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Saving live replay is not enabled on this instance',
|
||||
type: ServerErrorCode.LIVE_NOT_ALLOWING_REPLAY
|
||||
})
|
||||
}
|
||||
|
||||
if (hasValidLatencyMode(body) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Custom latency mode is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await doesChannelIdExist({ id: body.channelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
|
||||
const totalInstanceLives = await VideoModel.countLives({ remote: false, mode: 'not-ended' })
|
||||
|
||||
if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot create this live because the max instance lives limit is reached.',
|
||||
type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (CONFIG.LIVE.MAX_USER_LIVES !== -1) {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const totalUserLives = await VideoModel.countLivesOfAccount(user.Account.id)
|
||||
|
||||
if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot create this live because the max user lives limit is reached.',
|
||||
type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!await isLiveVideoAccepted(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export const videoLiveUpdateValidator = [
|
||||
body('saveReplay')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
|
||||
|
||||
body('replaySettings.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoReplayPrivacyValid),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isLiveLatencyModeValid),
|
||||
|
||||
body('schedules')
|
||||
.optional()
|
||||
.custom(areLiveSchedulesValid).withMessage('Should have a valid schedules array'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: LiveVideoUpdate = req.body
|
||||
|
||||
if (hasValidSaveReplay(body) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Saving live replay is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasValidLatencyMode(body) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Custom latency mode is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (!checkLiveSettingsReplayConsistency({ res, body })) return
|
||||
|
||||
if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
return res.fail({ message: 'Cannot update a live that has already started' })
|
||||
}
|
||||
|
||||
// Check the user can manage the live
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.GET_ANY_LIVE,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoLiveListSessionsValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// Check the user can manage the live
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.GET_ANY_LIVE,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoLiveFindReplaySessionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'id')) return
|
||||
|
||||
const session = await VideoLiveSessionModel.findSessionOfReplay(res.locals.videoId.id)
|
||||
if (!session) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No live replay found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoLiveSession = session
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function isLiveVideoAccepted (req: express.Request, res: express.Response) {
|
||||
// Check we accept this video
|
||||
const acceptParameters = {
|
||||
liveVideoBody: req.body,
|
||||
user: res.locals.oauth.token.User
|
||||
}
|
||||
const acceptedResult = await Hooks.wrapFun(
|
||||
isLocalLiveVideoAccepted,
|
||||
acceptParameters,
|
||||
'filter:api.live-video.create.accept.result'
|
||||
)
|
||||
|
||||
if (acceptedResult?.accepted !== true) {
|
||||
logger.info('Refused local live video.', { acceptedResult, acceptParameters })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult.errorMessage || 'Refused local live video'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) {
|
||||
if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
|
||||
if (
|
||||
CONFIG.LIVE.LATENCY_SETTING.ENABLED !== true &&
|
||||
exists(body.latencyMode) &&
|
||||
body.latencyMode !== LiveVideoLatencyMode.DEFAULT
|
||||
) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkLiveSettingsReplayConsistency (options: {
|
||||
res: express.Response
|
||||
body: LiveVideoUpdate
|
||||
}) {
|
||||
const { res, body } = options
|
||||
|
||||
// We now save replays of this live, so replay settings are mandatory
|
||||
if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) {
|
||||
if (!exists(body.replaySettings)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Replay settings are missing now the live replay is saved'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!exists(body.replaySettings.privacy)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Privacy replay setting is missing now the live replay is saved'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Save replay was and is not enabled, so send an error the user if it specified replay settings
|
||||
if ((!exists(body.saveReplay) && res.locals.videoLive.saveReplay === false) || body.saveReplay === false) {
|
||||
if (exists(body.replaySettings)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot save replay settings since live replay is not enabled'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { MUserAccountId, MVideoChangeOwnershipFull, MVideoWithAllFiles } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanManageAccount,
|
||||
checkCanManageVideo,
|
||||
checkUserQuota,
|
||||
doesChangeVideoOwnershipExist,
|
||||
doesChannelIdExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const videosChangeOwnershipValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
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 (
|
||||
!await checkCanManageVideo({
|
||||
user: res.locals.oauth.token.User,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.CHANGE_VIDEO_OWNERSHIP,
|
||||
checkIsOwner: true,
|
||||
checkIsLocal: true,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) return
|
||||
|
||||
const nextOwner = await AccountModel.loadLocalByName(req.body.username)
|
||||
if (!nextOwner) {
|
||||
res.fail({
|
||||
message: req.t('{username} does not exist on {instanceName}', { username: req.body.username, instanceName: CONFIG.INSTANCE.NAME })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res.locals.nextOwner = nextOwner
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videosTerminateChangeOwnershipValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesChangeVideoOwnershipExist(req.params.id, req, res)) return
|
||||
|
||||
// Check if the user who did the request is able to change the ownership of the video
|
||||
if (
|
||||
!checkCanTerminateOwnershipChange({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoChangeOwnership: res.locals.videoChangeOwnership,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) return
|
||||
|
||||
const videoChangeOwnership = res.locals.videoChangeOwnership
|
||||
|
||||
if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Ownership already accepted or refused')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videosAcceptChangeOwnershipValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const body = req.body as VideoChangeOwnershipAccept
|
||||
if (!await doesChannelIdExist({ id: body.channelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: true })) return
|
||||
|
||||
const videoChangeOwnership = res.locals.videoChangeOwnership
|
||||
|
||||
const video = videoChangeOwnership.Video
|
||||
|
||||
if (!await checkCanAccept(video, req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanAccept (video: MVideoWithAllFiles, req: express.Request, res: express.Response): Promise<boolean> {
|
||||
if (video.isLive) {
|
||||
if (video.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('You can accept an ownership change of a published live.')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!await checkUserQuota({ user, videoFileSize: video.getMaxQualityBytes(), req, res })) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkCanTerminateOwnershipChange (options: {
|
||||
user: MUserAccountId
|
||||
videoChangeOwnership: MVideoChangeOwnershipFull
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, videoChangeOwnership, req, res } = options
|
||||
|
||||
if (!checkCanManageAccount({ user, account: videoChangeOwnership.NextOwner, req, res: null, specialRight: UserRight.MANAGE_USERS })) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot terminate an ownership change of another user')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
119
server/core/middlewares/validators/videos/video-passwords.ts
Normal file
119
server/core/middlewares/validators/videos/video-passwords.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanDeleteVideoPassword,
|
||||
checkCanManageVideo,
|
||||
doesVideoExist,
|
||||
doesVideoPasswordExist,
|
||||
isValidVideoIdParam,
|
||||
checkVideoIsPasswordProtected
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const listVideoPasswordValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!checkVideoIsPasswordProtected(req, res)) return
|
||||
|
||||
// Check if the user who did the request is able to access video password list
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.SEE_ALL_VIDEOS,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateVideoPasswordListValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('passwords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await checkAddOrUpdatePasswords(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const addVideoPasswordValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('password')
|
||||
.isString()
|
||||
.withMessage('Video password should be a string')
|
||||
.notEmpty()
|
||||
.withMessage('Password string should not be empty'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkAddOrUpdatePasswords(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const removeVideoPasswordValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('passwordId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!checkVideoIsPasswordProtected(req, res)) return
|
||||
if (!await doesVideoPasswordExist({ id: req.params.passwordId, req, res })) return
|
||||
|
||||
if (!await checkCanDeleteVideoPassword({ user: res.locals.oauth.token.User, video: res.locals.videoAll, req, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkAddOrUpdatePasswords (req: express.Request, res: express.Response) {
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return false
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return false
|
||||
|
||||
// Check if the user who did the request is able to update video passwords
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.UPDATE_ANY_VIDEO,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return false
|
||||
|
||||
return true
|
||||
}
|
||||
551
server/core/middlewares/validators/videos/video-playlists.ts
Normal file
551
server/core/middlewares/validators/videos/video-playlists.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
import { arrayify, forceNumber } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
UserRight,
|
||||
UserRightType,
|
||||
VideoPlaylistCreate,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPlaylistType,
|
||||
VideoPlaylistUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
||||
import { MUserAccountId } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import {
|
||||
isArrayOf,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
isUUIDValid,
|
||||
toBooleanOrNull,
|
||||
toCompleteUUID,
|
||||
toIntArray,
|
||||
toIntOrNull,
|
||||
toValueOrNull
|
||||
} from '../../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isVideoPlaylistDescriptionValid,
|
||||
isVideoPlaylistNameValid,
|
||||
isVideoPlaylistPrivacyValid,
|
||||
isVideoPlaylistTimestampValid,
|
||||
isVideoPlaylistTypeValid
|
||||
} from '../../../helpers/custom-validators/video-playlists.js'
|
||||
import { isVideoImageValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element.js'
|
||||
import { MVideoPlaylistFullSummary } from '../../../types/models/video/video-playlist.js'
|
||||
import { authenticateOrFail } from '../../auth.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanManageAccount,
|
||||
checkCanManageChannel,
|
||||
doesChannelIdExist,
|
||||
doesVideoExist,
|
||||
doesVideoPlaylistExist,
|
||||
isValidPlaylistIdParam,
|
||||
VideoPlaylistFetchType
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
|
||||
body('displayName')
|
||||
.custom(isVideoPlaylistNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const body: VideoPlaylistCreate = req.body
|
||||
if (
|
||||
body.videoChannelId &&
|
||||
!await doesChannelIdExist({ id: body.videoChannelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (
|
||||
!body.videoChannelId &&
|
||||
(body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
|
||||
) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: req.t('Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.') })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!await doesVideoPlaylistExist({ id: req.params.playlistId, req, res, fetchType: 'all' })) return cleanUpReqFiles(req)
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
|
||||
if (
|
||||
!await checkCanManagePlaylist({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoPlaylist,
|
||||
right: UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
const body: VideoPlaylistUpdate = req.body
|
||||
|
||||
const newPrivacy = body.privacy || videoPlaylist.privacy
|
||||
if (
|
||||
newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
|
||||
(
|
||||
(!videoPlaylist.videoChannelId && !body.videoChannelId) ||
|
||||
body.videoChannelId === null
|
||||
)
|
||||
) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: req.t('Cannot set "public" a playlist that is not assigned to a channel.') })
|
||||
}
|
||||
|
||||
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: req.t('Cannot update a watch later playlist.') })
|
||||
}
|
||||
|
||||
if (
|
||||
body.videoChannelId &&
|
||||
!await doesChannelIdExist({ id: body.videoChannelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (res.locals.videoChannel && res.locals.videoChannel.accountId !== videoPlaylist.ownerAccountId) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('The channel must be owned by the same account as the playlist')
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export const videoPlaylistsDeleteValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist({ id: req.params.playlistId, req, res })) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
|
||||
return res.fail({ message: req.t('Cannot delete a watch later playlist.') })
|
||||
}
|
||||
|
||||
if (
|
||||
!await checkCanManagePlaylist({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoPlaylist,
|
||||
right: UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
|
||||
return [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist({ id: req.params.playlistId, req, res, fetchType })) return
|
||||
|
||||
const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
|
||||
|
||||
// Playlist is unlisted, check we used the uuid to fetch it
|
||||
if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
|
||||
if (isUUIDValid(req.params.playlistId)) return next()
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Playlist not found')
|
||||
})
|
||||
}
|
||||
|
||||
if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
|
||||
if (!await authenticateOrFail({ req, res })) return
|
||||
|
||||
if (
|
||||
!await checkCanManagePlaylist({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoPlaylist,
|
||||
right: UserRight.UPDATE_ANY_VIDEO_PLAYLIST,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const videoPlaylistsSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistsAccountValidator = [
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
query('channelNameOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid channelNameOneOf array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistsAddVideoValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
body('videoId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid/short uuid'),
|
||||
body('startTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
body('stopTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist({ id: req.params.playlistId, req, res, fetchType: 'all' })) return
|
||||
if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
|
||||
if (
|
||||
!await checkCanManagePlaylist({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoPlaylist,
|
||||
right: UserRight.UPDATE_ANY_VIDEO_PLAYLIST,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistsUpdateOrRemoveVideoValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
param('playlistElementId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdValid).withMessage('Should have an element id/uuid/short uuid'),
|
||||
|
||||
body('startTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
body('stopTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist({ id: req.params.playlistId, req, res, fetchType: 'all' })) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
|
||||
const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
|
||||
if (!videoPlaylistElement) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video playlist element not found')
|
||||
})
|
||||
return
|
||||
}
|
||||
res.locals.videoPlaylistElement = videoPlaylistElement
|
||||
|
||||
if (
|
||||
!await checkCanManagePlaylist({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoPlaylist,
|
||||
right: UserRight.UPDATE_ANY_VIDEO_PLAYLIST,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistElementAPGetValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
param('playlistElementId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const playlistElementId = forceNumber(req.params.playlistElementId)
|
||||
const playlistId = req.params.playlistId
|
||||
|
||||
const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
|
||||
if (!videoPlaylistElement) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video playlist element not found')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot get this private video playlist.')
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoPlaylistElementAP = videoPlaylistElement
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistsReorderInChannelValidator = [
|
||||
body('startPosition')
|
||||
.isInt({ min: 1 }),
|
||||
body('insertAfterPosition')
|
||||
.isInt({ min: 0 }),
|
||||
body('reorderLength')
|
||||
.optional()
|
||||
.isInt({ min: 1 }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const nextPosition = await VideoPlaylistModel.getNextPositionOf({ videoChannelId: res.locals.videoChannel.id })
|
||||
const startPosition: number = req.body.startPosition
|
||||
const insertAfterPosition: number = req.body.insertAfterPosition
|
||||
const reorderLength: number = req.body.reorderLength
|
||||
|
||||
if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
|
||||
res.fail({
|
||||
message: req.t('Start position or insert after position exceed the channel limits (max: {max})', { max: nextPosition - 1 })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (reorderLength && reorderLength + startPosition > nextPosition) {
|
||||
res.fail({
|
||||
message: req.t(
|
||||
'Reorder length with this start position exceeds the channel limits (max: {max})',
|
||||
{ max: nextPosition - startPosition }
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoPlaylistsReorderVideosValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
body('startPosition')
|
||||
.isInt({ min: 1 }),
|
||||
body('insertAfterPosition')
|
||||
.isInt({ min: 0 }),
|
||||
body('reorderLength')
|
||||
.optional()
|
||||
.isInt({ min: 1 }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist({ id: req.params.playlistId, req, res, fetchType: 'all' })) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
if (
|
||||
!await checkCanManagePlaylist({
|
||||
user: res.locals.oauth.token.User,
|
||||
videoPlaylist,
|
||||
right: UserRight.UPDATE_ANY_VIDEO_PLAYLIST,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) return
|
||||
|
||||
const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
|
||||
const startPosition: number = req.body.startPosition
|
||||
const insertAfterPosition: number = req.body.insertAfterPosition
|
||||
const reorderLength: number = req.body.reorderLength
|
||||
|
||||
if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
|
||||
res.fail({
|
||||
message: req.t('Start position or insert after position exceed the playlist limits (max: {max})', { max: nextPosition - 1 })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (reorderLength && reorderLength + startPosition > nextPosition) {
|
||||
res.fail({
|
||||
message: req.t('Reorder length with this start position exceeds the playlist limits (max: {max})', {
|
||||
max: nextPosition - startPosition
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const commonVideoPlaylistFiltersValidator = [
|
||||
query('playlistType')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTypeValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const doVideosInPlaylistExistValidator = [
|
||||
query('videoIds')
|
||||
.customSanitizer(toIntArray)
|
||||
.custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getCommonPlaylistEditAttributes () {
|
||||
return [
|
||||
body('thumbnailfile')
|
||||
.custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
|
||||
.withMessage(
|
||||
'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
|
||||
CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
body('description')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoPlaylistDescriptionValid),
|
||||
body('privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoPlaylistPrivacyValid),
|
||||
body('videoChannelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
] as (ValidationChain | ExpressPromiseHandler)[]
|
||||
}
|
||||
|
||||
async function checkCanManagePlaylist (options: {
|
||||
user: MUserAccountId
|
||||
videoPlaylist: MVideoPlaylistFullSummary
|
||||
right: UserRightType
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, videoPlaylist, right, res, req } = options
|
||||
|
||||
if (videoPlaylist.isLocal() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage video playlist of another server.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkCanManageAccount({ user, account: videoPlaylist.OwnerAccount, specialRight: right, req, res: null })) return true
|
||||
|
||||
if (videoPlaylist.videoChannelId) {
|
||||
const channel = await VideoChannelModel.loadAndPopulateAccount(videoPlaylist.videoChannelId)
|
||||
|
||||
if (
|
||||
await checkCanManageChannel({
|
||||
channel,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
checkCanManage: true,
|
||||
checkIsOwner: false,
|
||||
specialRight: right
|
||||
})
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage video playlist of another user')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function getPlaylist (res: express.Response) {
|
||||
return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
|
||||
}
|
||||
72
server/core/middlewares/validators/videos/video-rates.ts
Normal file
72
server/core/middlewares/validators/videos/video-rates.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, VideoRateType } from '@peertube/peertube-models'
|
||||
import { isAccountNameValid } from '../../../helpers/custom-validators/accounts.js'
|
||||
import { isIdValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isRatingValid } from '../../../helpers/custom-validators/video-rates.js'
|
||||
import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
|
||||
import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared/index.js'
|
||||
|
||||
const videoUpdateRateValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
body('rating')
|
||||
.custom(isVideoRatingTypeValid),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) {
|
||||
return [
|
||||
param('accountName')
|
||||
.custom(isAccountNameValid),
|
||||
param('videoId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.accountName, +req.params.videoId)
|
||||
if (!rate) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video rate not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.accountVideoRate = rate
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const videoRatingValidator = [
|
||||
query('rating')
|
||||
.optional()
|
||||
.custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoUpdateRateValidator,
|
||||
getAccountVideoRateValidatorFactory,
|
||||
videoRatingValidator
|
||||
}
|
||||
28
server/core/middlewares/validators/videos/video-shares.ts
Normal file
28
server/core/middlewares/validators/videos/video-shares.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isIdValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { VideoShareModel } from '../../../models/video/video-share.js'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
export const videosShareValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('actorId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (!canVideoBeFederated(video)) res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const share = await VideoShareModel.load(req.params.actorId, video.id)
|
||||
if (!share) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
res.locals.videoShare = share
|
||||
return next()
|
||||
}
|
||||
]
|
||||
135
server/core/middlewares/validators/videos/video-source.ts
Normal file
135
server/core/middlewares/validators/videos/video-source.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanAccessVideoSourceFile,
|
||||
checkCanManageVideo,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
||||
|
||||
export const videoSourceGetLatestValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res, 'all')) return
|
||||
|
||||
const video = getVideoWithAttributes(res) as MVideoFullLight
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await checkCanManageVideo({ user, video, right: UserRight.UPDATE_ANY_VIDEO, req, res, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return
|
||||
}
|
||||
|
||||
res.locals.videoSource = await VideoSourceModel.loadLatest(video.id)
|
||||
|
||||
if (!res.locals.videoSource) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video source not found')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const replaceVideoSourceResumableValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const file = buildUploadXFile(req.body as express.CustomUploadXFile<UploadXMetadata>)
|
||||
const cleanup = () => safeUploadXCleanup(file)
|
||||
|
||||
if (!await checkCanUpdateVideoFile({ req, res })) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'updateVideoFileResumableValidator' })) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.update-file.accept.result' })) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
res.locals.updateVideoFileResumable = { ...file, originalname: file.filename }
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const replaceVideoSourceResumableInitValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!await checkCanUpdateVideoFile({ req, res })) return
|
||||
|
||||
const fileMetadata = res.locals.uploadVideoFileResumableMetadata
|
||||
const files = { videofile: [ fileMetadata ] }
|
||||
if (await commonVideoFileChecks({ req, res, user, videoFileSize: fileMetadata.size, files }) === false) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const originalVideoFileDownloadValidator = [
|
||||
param('filename').exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const videoSource = await VideoSourceModel.loadByKeptOriginalFilename(req.params.filename)
|
||||
if (!videoSource) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Original video file not found')
|
||||
})
|
||||
}
|
||||
|
||||
if (!await checkCanAccessVideoSourceFile({ req, res, videoId: videoSource.videoId })) return
|
||||
|
||||
res.locals.videoSource = videoSource
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanUpdateVideoFile (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { req, res } = options
|
||||
|
||||
if (!CONFIG.VIDEO_FILE.UPDATE.ENABLED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Updating the file of an existing video is not allowed on this instance')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!await doesVideoExist(req.params.id, res)) return false
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!await checkCanManageVideo({ user, video, right: UserRight.UPDATE_ANY_VIDEO, req, res, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!checkVideoFileCanBeEdited(video, req, res)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
115
server/core/middlewares/validators/videos/video-stats.ts
Normal file
115
server/core/middlewares/validators/videos/video-stats.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { isDateValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isValidStatTimeserieMetric } from '@server/helpers/custom-validators/video-stats.js'
|
||||
import { STATS_TIMESERIE } from '@server/initializers/constants.js'
|
||||
import { HttpStatusCode, UserRight, VideoStatsTimeserieQuery } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
export const videoOverallOrUserAgentStatsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
query('startDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await commonStatsCheck(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoRetentionStatsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await commonStatsCheck(req, res)) return
|
||||
|
||||
if (res.locals.videoAll.isLive) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot get retention stats of live video'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoTimeseriesStatsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('metric')
|
||||
.custom(isValidStatTimeserieMetric),
|
||||
|
||||
query('startDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await commonStatsCheck(req, res)) return
|
||||
|
||||
const query: VideoStatsTimeserieQuery = req.query
|
||||
if (
|
||||
(query.startDate && !query.endDate) ||
|
||||
(!query.startDate && query.endDate)
|
||||
) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Both start date and end date should be defined if one of them is specified'
|
||||
})
|
||||
}
|
||||
|
||||
if (query.startDate && getIntervalByDays(query.startDate, query.endDate) > STATS_TIMESERIE.MAX_DAYS) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Start date and end date interval is too big'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function commonStatsCheck (req: express.Request, res: express.Response) {
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return false
|
||||
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user: res.locals.oauth.token.User,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.SEE_ALL_VIDEOS,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getIntervalByDays (startDateString: string, endDateString: string) {
|
||||
const startDate = new Date(startDateString)
|
||||
const endDate = new Date(endDateString)
|
||||
|
||||
return (endDate.getTime() - startDate.getTime()) / 1000 / 86400
|
||||
}
|
||||
119
server/core/middlewares/validators/videos/video-studio.ts
Normal file
119
server/core/middlewares/validators/videos/video-studio.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isStudioCutTaskValid,
|
||||
isStudioTaskAddIntroOutroValid,
|
||||
isStudioTaskAddWatermarkValid,
|
||||
isValidStudioTasksArray
|
||||
} from '@server/helpers/custom-validators/video-studio.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio.js'
|
||||
import { isAudioFile } from '@peertube/peertube-ffmpeg'
|
||||
import { HttpStatusCode, UserRight, VideoStudioCreateEdition, VideoStudioTask } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkCanManageVideo, checkUserQuota, doesVideoExist } from '../shared/index.js'
|
||||
import { checkVideoFileCanBeEdited } from './shared/index.js'
|
||||
|
||||
const videoStudioAddEditionValidator = [
|
||||
param('videoId')
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid/short uuid'),
|
||||
|
||||
body('tasks')
|
||||
.custom(isValidStudioTasksArray).withMessage('Should have a valid array of tasks'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (CONFIG.VIDEO_STUDIO.ENABLED !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video studio is disabled on this instance'
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const body: VideoStudioCreateEdition = req.body
|
||||
const files = req.files as Express.Multer.File[]
|
||||
|
||||
const video = res.locals.videoAll
|
||||
const videoIsAudio = video.hasAudio() && !video.hasVideo()
|
||||
|
||||
for (let i = 0; i < body.tasks.length; i++) {
|
||||
const task = body.tasks[i]
|
||||
|
||||
if (!checkTask(req, task, i)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Task ${task.name} is invalid`
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (videoIsAudio) {
|
||||
if (task.name === 'add-intro' || task.name === 'add-outro' || task.name === 'add-watermark') {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Task ${task.name} is invalid: video does not contain a video stream`
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
}
|
||||
|
||||
if (task.name === 'add-intro' || task.name === 'add-outro') {
|
||||
const filePath = getTaskFileFromReq(files, i).path
|
||||
|
||||
// Our concat filter needs a video stream
|
||||
if (await isAudioFile(filePath)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Task ${task.name} is invalid: input file does not contain a video stream`
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!checkVideoFileCanBeEdited(video, req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await checkCanManageVideo({ user, video, right: UserRight.UPDATE_ANY_VIDEO, req, res, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
// Try to make an approximation of bytes added by the intro/outro
|
||||
const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFileFromReq(files, i).path)
|
||||
if (await checkUserQuota({ user, videoFileSize: additionalBytes, req, res }) === false) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoStudioAddEditionValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const taskCheckers: {
|
||||
[id in VideoStudioTask['name']]: (task: VideoStudioTask, indice?: number, files?: Express.Multer.File[]) => boolean
|
||||
} = {
|
||||
'cut': isStudioCutTaskValid,
|
||||
'add-intro': isStudioTaskAddIntroOutroValid,
|
||||
'add-outro': isStudioTaskAddIntroOutroValid,
|
||||
'add-watermark': isStudioTaskAddWatermarkValid
|
||||
}
|
||||
|
||||
function checkTask (req: express.Request, task: VideoStudioTask, indice?: number) {
|
||||
const checker = taskCheckers[task.name]
|
||||
if (!checker) return false
|
||||
|
||||
return checker(task, indice, req.files as Express.Multer.File[])
|
||||
}
|
||||
23
server/core/middlewares/validators/videos/video-token.ts
Normal file
23
server/core/middlewares/validators/videos/video-token.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import express from 'express'
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
|
||||
const videoFileTokenValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const video = res.locals.onlyVideo
|
||||
if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED && !exists(res.locals.oauth.token.User)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: 'Not authenticated'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoFileTokenValidator
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HttpStatusCode, ServerErrorCode, VideoTranscodingCreate } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
import { checkVideoCanBeTranscribed } from './shared/video-validators.js'
|
||||
|
||||
const createTranscodingValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('transcodingType')
|
||||
.custom(isValidCreateTranscodingType),
|
||||
|
||||
body('forceTranscoding')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkVideoCanBeTranscribed(video, req, res)) return
|
||||
|
||||
if (CONFIG.TRANSCODING.ENABLED !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot run transcoding job because transcoding is disabled on this instance'
|
||||
})
|
||||
}
|
||||
|
||||
const body = req.body as VideoTranscodingCreate
|
||||
if (body.forceTranscoding === true) return next()
|
||||
|
||||
const info = await VideoJobInfoModel.load(video.id)
|
||||
if (info && info.pendingTranscode > 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED,
|
||||
message: 'This video is already being transcoded'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
createTranscodingValidator
|
||||
}
|
||||
83
server/core/middlewares/validators/videos/video-view.ts
Normal file
83
server/core/middlewares/validators/videos/video-view.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
isVideoTimeValid,
|
||||
isVideoViewEvent,
|
||||
isVideoViewUAInfo,
|
||||
toVideoViewUADeviceOrNull
|
||||
} from '@server/helpers/custom-validators/video-view.js'
|
||||
import { getCachedVideoDuration } from '@server/lib/video.js'
|
||||
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const tags = [ 'views' ]
|
||||
|
||||
export const getVideoLocalViewerValidator = [
|
||||
param('localViewerId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const localViewer = await LocalVideoViewerModel.loadFullById(+req.params.localViewerId)
|
||||
if (!localViewer) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Local viewer not found',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.localViewerFull = localViewer
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoViewValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('currentTime')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.isInt(),
|
||||
|
||||
body('sessionId')
|
||||
.optional()
|
||||
.isAlphanumeric(undefined, { ignore: '-' }),
|
||||
|
||||
body('viewEvent')
|
||||
.optional()
|
||||
.custom(isVideoViewEvent),
|
||||
|
||||
body('client')
|
||||
.optional()
|
||||
.custom(isVideoViewUAInfo),
|
||||
body('device')
|
||||
.optional()
|
||||
.customSanitizer(toVideoViewUADeviceOrNull),
|
||||
body('operatingSystem')
|
||||
.optional()
|
||||
.custom(isVideoViewUAInfo),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'unsafe-only-immutable-attributes')) return
|
||||
|
||||
const video = res.locals.onlyImmutableVideo
|
||||
const { duration } = await getCachedVideoDuration(video.id)
|
||||
|
||||
const currentTime = req.body.currentTime
|
||||
if (!isVideoTimeValid(currentTime, duration)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Current time ${currentTime} is invalid (video ${video.uuid} duration: ${duration})`,
|
||||
logLevel: 'warn',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
667
server/core/middlewares/validators/videos/videos.ts
Normal file
667
server/core/middlewares/validators/videos/videos.ts
Normal file
@@ -0,0 +1,667 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
ServerErrorCode,
|
||||
UserRight,
|
||||
VideoCreateUpdateCommon,
|
||||
VideosCommonQuery,
|
||||
VideoState
|
||||
} from '@peertube/peertube-models'
|
||||
import { isHostValid } from '@server/helpers/custom-validators/servers.js'
|
||||
import { VideoLoadType } from '@server/lib/model-loaders/video.js'
|
||||
import { Redis } from '@server/lib/redis.js'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
||||
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import {
|
||||
exists,
|
||||
hasArrayLength,
|
||||
isBooleanValid,
|
||||
isDateValid,
|
||||
isFileValid,
|
||||
isIdValid,
|
||||
isNotEmptyIntArray,
|
||||
toBooleanOrNull,
|
||||
toIntArray,
|
||||
toIntOrNull,
|
||||
toValueOrNull
|
||||
} from '../../../helpers/custom-validators/misc.js'
|
||||
import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search.js'
|
||||
import {
|
||||
areVideoTagsValid,
|
||||
isNSFWFlagsValid,
|
||||
isNSFWSummaryValid,
|
||||
isScheduleVideoUpdatePrivacyValid,
|
||||
isValidPasswordProtectedPrivacy,
|
||||
isVideoCategoryValid,
|
||||
isVideoCommentsPolicyValid,
|
||||
isVideoDescriptionValid,
|
||||
isVideoImageValid,
|
||||
isVideoIncludeValid,
|
||||
isVideoLanguageValid,
|
||||
isVideoLicenceValid,
|
||||
isVideoNameValid,
|
||||
isVideoOriginallyPublishedAtValid,
|
||||
isVideoPrivacyValid,
|
||||
isVideoSourceFilenameValid,
|
||||
isVideoSupportValid
|
||||
} from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { getVideoWithAttributes } from '../../../helpers/video.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanAccessVideoStaticFiles,
|
||||
checkCanManageVideo,
|
||||
checkCanSeeVideo,
|
||||
doesChannelIdExist,
|
||||
doesVideoExist,
|
||||
doesVideoFileOfVideoExist,
|
||||
isValidVideoIdParam,
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
||||
|
||||
const getVideoUploadCommonValidator = () => [
|
||||
body('name')
|
||||
.trim()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
body('generateTranscription')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid generateTranscription boolean')
|
||||
]
|
||||
|
||||
export const videosAddLegacyValidator = [
|
||||
...getCommonVideoEditAttributes(),
|
||||
...getVideoUploadCommonValidator(),
|
||||
|
||||
body('videofile')
|
||||
.custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
|
||||
.withMessage('Should have a file'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const videoFile: express.VideoLegacyUploadFile = req.files['videofile'][0]
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (
|
||||
!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) ||
|
||||
!isValidPasswordProtectedPrivacy(req, res) ||
|
||||
!await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddLegacyValidator' }) ||
|
||||
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' })
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Gets called after the last PUT request
|
||||
*/
|
||||
export const videosAddResumableValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const file = buildUploadXFile(req.body as express.CustomUploadXFile<express.UploadNewVideoXFileMetadata>)
|
||||
const cleanup = () => {
|
||||
safeUploadXCleanup(file)
|
||||
|
||||
Redis.Instance.deleteUploadSession(req.query.upload_id)
|
||||
.catch(err => logger.error('Cannot delete upload session', { err }))
|
||||
}
|
||||
|
||||
const uploadId = req.query.upload_id
|
||||
const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId)
|
||||
|
||||
if (sessionExists) {
|
||||
res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
|
||||
message: req.t('The upload is already being processed')
|
||||
})
|
||||
}
|
||||
|
||||
await Redis.Instance.setUploadSession(uploadId)
|
||||
|
||||
if (
|
||||
!await doesChannelIdExist({ id: file.metadata.channelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })
|
||||
) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'videosAddResumableValidator' })) return cleanup()
|
||||
if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.upload.accept.result' })) return cleanup()
|
||||
|
||||
res.locals.uploadVideoFileResumable = { ...file, originalname: file.filename }
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
|
||||
* see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
|
||||
*
|
||||
* Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
|
||||
* see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
|
||||
*/
|
||||
export const videosAddResumableInitValidator = [
|
||||
...getCommonVideoEditAttributes(),
|
||||
...getVideoUploadCommonValidator(),
|
||||
|
||||
body('filename')
|
||||
.custom(isVideoSourceFilenameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
const cleanup = () => cleanUpReqFiles(req)
|
||||
|
||||
logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
|
||||
parameters: req.body,
|
||||
headers: req.headers,
|
||||
files: req.files
|
||||
})
|
||||
|
||||
if (areValidationErrors(req, res, { omitLog: true })) return cleanup()
|
||||
|
||||
const fileMetadata = res.locals.uploadVideoFileResumableMetadata
|
||||
const files = { videofile: [ fileMetadata ] }
|
||||
if (!await commonVideoChecksPass({ req, res, user, videoFileSize: fileMetadata.size, files })) return cleanup()
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup()
|
||||
|
||||
// Multer required unsetting the Content-Type, now we can set it for node-uploadx
|
||||
req.headers['content-type'] = 'application/json; charset=utf-8'
|
||||
|
||||
// Place thumbnail/previewfile in metadata so that uploadx saves it in .META
|
||||
if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile']
|
||||
if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile']
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videosUpdateValidator = getCommonVideoEditAttributes().concat([
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
body('name')
|
||||
.optional()
|
||||
.trim()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
body('channelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
|
||||
if (areErrorsInNSFW(req, res)) return cleanUpReqFiles(req)
|
||||
if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (exists(req.body.privacy) && video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
return res.fail({ message: req.t('Cannot update privacy of a live that has already started') })
|
||||
}
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (
|
||||
!await checkCanManageVideo({
|
||||
user,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.UPDATE_ANY_VIDEO,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (
|
||||
req.body.channelId &&
|
||||
!await doesChannelIdExist({ id: req.body.channelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (res.locals.videoChannel && res.locals.videoChannel.accountId !== video.VideoChannel.accountId) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('The channel must belong to the same account as the original channel')
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const video = getVideoWithAttributes(res)
|
||||
|
||||
// Anybody can watch local videos
|
||||
if (video.isLocal() === 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
|
||||
if (await VideoModel.checkVideoHasInstanceFollow(video.id) === true) return next()
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot get this video regarding follow constraints'),
|
||||
type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
|
||||
data: {
|
||||
originUrl: video.url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type FetchType = Extract<VideoLoadType, 'for-api' | 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes'>
|
||||
export const videosCustomGetValidator = (fetchType: FetchType) => {
|
||||
return [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res, fetchType)) return
|
||||
|
||||
// Controllers does not need to check video rights
|
||||
if (fetchType === 'unsafe-only-immutable-attributes') return next()
|
||||
|
||||
const video = getVideoWithAttributes(res) as MVideoFullLight
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const videosGetValidator = videosCustomGetValidator('all')
|
||||
|
||||
export const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export const videosDownloadValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res, 'all')) return
|
||||
|
||||
const video = getVideoWithAttributes(res)
|
||||
|
||||
if (!await checkCanAccessVideoStaticFiles({ req, res, video, paramId: req.params.id })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videosGenerateDownloadValidator = [
|
||||
query('videoFileIds')
|
||||
.customSanitizer(toIntArray)
|
||||
.custom(isNotEmptyIntArray)
|
||||
.custom(v => hasArrayLength(v, { max: 2 })),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videosRemoveValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
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 (
|
||||
!await checkCanManageVideo({
|
||||
user: res.locals.oauth.token.User,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.REMOVE_ANY_VIDEO,
|
||||
req,
|
||||
res,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videosOverviewValidator = [
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT }),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function getCommonVideoEditAttributes () {
|
||||
return [
|
||||
body('thumbnailfile')
|
||||
.custom((value, { req }) => isVideoImageValid(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 }) => isVideoImageValid(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),
|
||||
body('licence')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoLicenceValid),
|
||||
body('language')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoLanguageValid),
|
||||
body('nsfw')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid nsfw boolean'),
|
||||
body('nsfwFlags')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwSummary')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isNSFWSummaryValid),
|
||||
body('waitTranscoding')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'),
|
||||
body('privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoPrivacyValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoDescriptionValid),
|
||||
body('support')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoSupportValid),
|
||||
body('tags')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(areVideoTagsValid)
|
||||
.withMessage(
|
||||
`Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
|
||||
`${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
|
||||
),
|
||||
body('commentsPolicy')
|
||||
.optional()
|
||||
.custom(isVideoCommentsPolicyValid),
|
||||
|
||||
body('downloadEnabled')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have downloadEnabled boolean'),
|
||||
body('originallyPublishedAt')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoOriginallyPublishedAtValid),
|
||||
body('scheduleUpdate')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull),
|
||||
body('scheduleUpdate.updateAt')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
|
||||
body('scheduleUpdate.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isScheduleVideoUpdatePrivacyValid)
|
||||
] as (ValidationChain | ExpressPromiseHandler)[]
|
||||
}
|
||||
|
||||
export const commonVideosFiltersValidatorFactory = (options: {
|
||||
allowPrivacyFilterForAllUsers?: boolean
|
||||
} = {}) => {
|
||||
return [
|
||||
query('categoryOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid categoryOneOf array'),
|
||||
query('licenceOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid licenceOneOf array'),
|
||||
query('languageOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid languageOneOf array'),
|
||||
query('privacyOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid privacyOneOf array'),
|
||||
query('tagsOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid tagsOneOf array'),
|
||||
query('tagsAllOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid tagsAllOf array'),
|
||||
query('nsfw')
|
||||
.optional()
|
||||
.custom(isBooleanBothQueryValid),
|
||||
query('nsfwFlagsIncluded')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isNSFWFlagsValid),
|
||||
query('nsfwFlagsExcluded')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isNSFWFlagsValid),
|
||||
query('isLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
|
||||
query('includeScheduledLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid includeScheduledLive boolean'),
|
||||
query('include')
|
||||
.optional()
|
||||
.custom(isVideoIncludeValid),
|
||||
query('isLocal')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid isLocal boolean'),
|
||||
query('hasHLSFiles')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'),
|
||||
query('hasWebVideoFiles')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'),
|
||||
query('skipCount')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid skipCount boolean'),
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('excludeAlreadyWatched')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should be a valid excludeAlreadyWatched boolean'),
|
||||
query('autoTagOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid autoTagOneOf array'),
|
||||
query('host')
|
||||
.optional()
|
||||
.custom(isHostValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const query = req.query as VideosCommonQuery
|
||||
|
||||
if (((query.nsfwFlagsExcluded || 0) & (query.nsfwFlagsIncluded || 0)) !== 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot use same flags in nsfwFlagsIncluded and nsfwFlagsExcluded at the same time')
|
||||
})
|
||||
}
|
||||
|
||||
const user = res.locals.oauth?.token.User
|
||||
|
||||
if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
|
||||
if (query.include || (options.allowPrivacyFilterForAllUsers !== true && query.privacyOneOf) || query.autoTagOneOf) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('You are not allowed to see all videos, specify a custom include or auto tags filter')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!user && exists(query.excludeAlreadyWatched)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot use excludeAlreadyWatched parameter when auth token is not provided')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (req.query.filter) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('"filter" query parameter is not supported anymore by PeerTube. Please use "isLocal" and "include" instead')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function areErrorsInNSFW (req: express.Request, res: express.Response) {
|
||||
const body = req.body as VideoCreateUpdateCommon
|
||||
|
||||
if (!body.nsfw) {
|
||||
if (body.nsfwFlags) {
|
||||
res.fail({ message: req.t('Cannot set nsfwFlags if the video is not NSFW') })
|
||||
return true
|
||||
}
|
||||
|
||||
if (body.nsfwSummary) {
|
||||
res.fail({ message: req.t('Cannot set nsfwSummary if the video is not NSFW') })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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.fail({ message: req.t('"scheduleUpdate.updateAt" parameter is mandatory') })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function commonVideoChecksPass (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
user: MUserAccountId
|
||||
videoFileSize: number
|
||||
files: express.UploadFilesForCheck
|
||||
}): Promise<boolean> {
|
||||
const { req, res } = options
|
||||
|
||||
if (areErrorsInScheduleUpdate(req, res)) return false
|
||||
if (areErrorsInNSFW(req, res)) return false
|
||||
|
||||
if (!await doesChannelIdExist({ id: req.body.channelId, req, res, checkCanManage: true, checkIsLocal: true, checkIsOwner: false })) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!await commonVideoFileChecks(options)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user