Init commit
This commit is contained in:
254
server/core/middlewares/validators/abuse.ts
Normal file
254
server/core/middlewares/validators/abuse.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { AbuseCreate, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import {
|
||||
areAbusePredefinedReasonsValid,
|
||||
isAbuseFilterValid,
|
||||
isAbuseMessageValid,
|
||||
isAbuseModerationCommentValid,
|
||||
isAbusePredefinedReasonValid,
|
||||
isAbuseReasonValid,
|
||||
isAbuseStateValid,
|
||||
isAbuseTimestampCoherent,
|
||||
isAbuseTimestampValid,
|
||||
isAbuseVideoIsValid
|
||||
} from '@server/helpers/custom-validators/abuses.js'
|
||||
import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { AbuseMessageModel } from '@server/models/abuse/abuse-message.js'
|
||||
import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared/index.js'
|
||||
|
||||
const abuseReportValidator = [
|
||||
body('account.id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
body('video.id')
|
||||
.optional()
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
body('video.startAt')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isAbuseTimestampValid),
|
||||
body('video.endAt')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isAbuseTimestampValid)
|
||||
.bail()
|
||||
.custom(isAbuseTimestampCoherent)
|
||||
.withMessage('Should have a startAt timestamp beginning before endAt'),
|
||||
|
||||
body('comment.id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
body('reason')
|
||||
.custom(isAbuseReasonValid),
|
||||
|
||||
body('predefinedReasons')
|
||||
.optional()
|
||||
.custom(areAbusePredefinedReasonsValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: AbuseCreate = req.body
|
||||
|
||||
if (body.video?.id && !await doesVideoExist(body.video.id, res)) return
|
||||
if (body.account?.id && !await doesAccountIdExist({ id: body.account.id, req, res, checkIsLocal: false, checkCanManage: false })) return
|
||||
if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
|
||||
|
||||
if (!body.video?.id && !body.account?.id && !body.comment?.id) {
|
||||
res.fail({ message: 'video id or account id or comment id is required.' })
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseGetValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAbuseExist(req.params.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseUpdateValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
body('state')
|
||||
.optional()
|
||||
.custom(isAbuseStateValid),
|
||||
body('moderationComment')
|
||||
.optional()
|
||||
.custom(isAbuseModerationCommentValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAbuseExist(req.params.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseListForAdminsValidator = [
|
||||
query('id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
query('filter')
|
||||
.optional()
|
||||
.custom(isAbuseFilterValid),
|
||||
query('predefinedReason')
|
||||
.optional()
|
||||
.custom(isAbusePredefinedReasonValid),
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('state')
|
||||
.optional()
|
||||
.custom(isAbuseStateValid),
|
||||
query('videoIs')
|
||||
.optional()
|
||||
.custom(isAbuseVideoIsValid),
|
||||
query('searchReporter')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('searchReportee')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('searchVideo')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('searchVideoChannel')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseListForUserValidator = [
|
||||
query('id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('state')
|
||||
.optional()
|
||||
.custom(isAbuseStateValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getAbuseValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAbuseExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.user
|
||||
const abuse = res.locals.abuse
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuse.reporterAccountId !== user.Account.id) {
|
||||
const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
|
||||
logger.warn(message)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const checkAbuseValidForMessagesValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const abuse = res.locals.abuse
|
||||
if (abuse.ReporterAccount.isLocal() === false) {
|
||||
return res.fail({ message: 'This abuse was created by a user of your instance.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const addAbuseMessageValidator = [
|
||||
body('message')
|
||||
.custom(isAbuseMessageValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const deleteAbuseMessageValidator = [
|
||||
param('messageId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.user
|
||||
const abuse = res.locals.abuse
|
||||
|
||||
const messageId = forceNumber(req.params.messageId)
|
||||
const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
|
||||
|
||||
if (!abuseMessage) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Abuse message not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot delete this abuse message'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.abuseMessage = abuseMessage
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
abuseListForAdminsValidator,
|
||||
abuseReportValidator,
|
||||
abuseGetValidator,
|
||||
addAbuseMessageValidator,
|
||||
checkAbuseValidForMessagesValidator,
|
||||
abuseUpdateValidator,
|
||||
deleteAbuseMessageValidator,
|
||||
abuseListForUserValidator,
|
||||
getAbuseValidator
|
||||
}
|
||||
31
server/core/middlewares/validators/account.ts
Normal file
31
server/core/middlewares/validators/account.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { areValidationErrors, checkCanManageAccount, doesAccountHandleExist } from './shared/index.js'
|
||||
|
||||
export const accountHandleGetValidatorFactory = (options: {
|
||||
checkCanManage: boolean
|
||||
checkIsLocal: boolean
|
||||
}) => {
|
||||
const { checkCanManage, checkIsLocal } = options
|
||||
|
||||
return [
|
||||
param('handle')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.params.handle, req, res, checkIsLocal, checkCanManage })) return
|
||||
|
||||
if (checkCanManage) {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!checkCanManageAccount({ account: res.locals.account, user, req, res, specialRight: UserRight.MANAGE_USERS })) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
29
server/core/middlewares/validators/activitypub/activity.ts
Normal file
29
server/core/middlewares/validators/activitypub/activity.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import express from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
|
||||
async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
logger.debug('Checking activity pub parameters')
|
||||
|
||||
if (!isRootActivityValid(req.body)) {
|
||||
logger.warn('Incorrect activity parameters.', { activity: req.body })
|
||||
return res.fail({ message: 'Incorrect activity' })
|
||||
}
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
const remoteActor = res.locals.signature.actor
|
||||
if (serverActor.id === remoteActor.id || remoteActor.serverId === null) {
|
||||
logger.error('Receiving request in INBOX by ourselves!', req.body)
|
||||
return res.status(HttpStatusCode.CONFLICT_409).end()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
activityPubValidator
|
||||
}
|
||||
3
server/core/middlewares/validators/activitypub/index.ts
Normal file
3
server/core/middlewares/validators/activitypub/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './activity.js'
|
||||
export * from './signature.js'
|
||||
export * from './pagination.js'
|
||||
25
server/core/middlewares/validators/activitypub/pagination.ts
Normal file
25
server/core/middlewares/validators/activitypub/pagination.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { PAGINATION } from '@server/initializers/constants.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const apPaginationValidator = [
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1 }),
|
||||
query('size')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: PAGINATION.OUTBOX.COUNT.MAX }).withMessage(`Should have a valid page size (max: ${PAGINATION.OUTBOX.COUNT.MAX})`),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
apPaginationValidator
|
||||
}
|
||||
39
server/core/middlewares/validators/activitypub/signature.ts
Normal file
39
server/core/middlewares/validators/activitypub/signature.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import {
|
||||
isSignatureCreatorValid,
|
||||
isSignatureTypeValid,
|
||||
isSignatureValueValid
|
||||
} from '../../../helpers/custom-validators/activitypub/signature.js'
|
||||
import { isDateValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const signatureValidator = [
|
||||
body('signature.type')
|
||||
.optional()
|
||||
.custom(isSignatureTypeValid),
|
||||
body('signature.created')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a signature created date that conforms to ISO 8601'),
|
||||
body('signature.creator')
|
||||
.optional()
|
||||
.custom(isSignatureCreatorValid),
|
||||
body('signature.signatureValue')
|
||||
.optional()
|
||||
.custom(isSignatureValueValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
|
||||
|
||||
if (areValidationErrors(req, res, { omitLog: true })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
signatureValidator
|
||||
}
|
||||
4
server/core/middlewares/validators/actor-image.ts
Normal file
4
server/core/middlewares/validators/actor-image.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { updateActorImageValidatorFactory } from './shared/images.js'
|
||||
|
||||
export const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
|
||||
export const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
|
||||
43
server/core/middlewares/validators/automatic-tags.ts
Normal file
43
server/core/middlewares/validators/automatic-tags.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CommentAutomaticTagPoliciesUpdate } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { AutomaticTagger } from '@server/lib/automatic-tags/automatic-tagger.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { doesAccountHandleExist } from './shared/accounts.js'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
export const manageAccountAutomaticTagsValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.params.accountName, req, res, checkIsLocal: true, checkCanManage: true })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateAutomaticTagPoliciesValidator = [
|
||||
...manageAccountAutomaticTagsValidator,
|
||||
|
||||
body('review')
|
||||
.custom(isStringArray).withMessage('Should have a valid review array'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body = req.body as CommentAutomaticTagPoliciesUpdate
|
||||
|
||||
const tagsObj = await AutomaticTagger.getAutomaticTagAvailable(res.locals.account)
|
||||
const available = new Set(tagsObj.available.map(({ name }) => name))
|
||||
|
||||
for (const name of body.review) {
|
||||
if (!available.has(name)) {
|
||||
return res.fail({ message: `${name} is not an available automatic tag` })
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
179
server/core/middlewares/validators/blocklist.ts
Normal file
179
server/core/middlewares/validators/blocklist.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { WEBSERVER } from '../../initializers/constants.js'
|
||||
import { AccountBlocklistModel } from '../../models/account/account-blocklist.js'
|
||||
import { ServerBlocklistModel } from '../../models/server/server-blocklist.js'
|
||||
import { ServerModel } from '../../models/server/server.js'
|
||||
import { areValidationErrors, doesAccountHandleExist } from './shared/index.js'
|
||||
|
||||
const blockAccountValidator = [
|
||||
body('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.body.accountName, req, res, checkIsLocal: false, checkCanManage: false })) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const accountToBlock = res.locals.account
|
||||
|
||||
if (user.Account.id === accountToBlock.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'You cannot block yourself.'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockAccountByAccountValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.params.accountName, req, res, checkIsLocal: false, checkCanManage: false })) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const targetAccount = res.locals.account
|
||||
if (!await doesUnblockAccountExist(user.Account.id, targetAccount.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockAccountByServerValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.params.accountName, req, res, checkIsLocal: false, checkCanManage: false })) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
const targetAccount = res.locals.account
|
||||
if (!await doesUnblockAccountExist(serverActor.Account.id, targetAccount.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const blockServerValidator = [
|
||||
body('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const host: string = req.body.host
|
||||
|
||||
if (host === WEBSERVER.HOST) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'You cannot block your own server.'
|
||||
})
|
||||
}
|
||||
|
||||
const server = await ServerModel.loadOrCreateByHost(host)
|
||||
|
||||
res.locals.server = server
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockServerByAccountValidator = [
|
||||
param('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await doesUnblockServerExist(user.Account.id, req.params.host, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockServerByServerValidator = [
|
||||
param('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
if (!await doesUnblockServerExist(serverActor.Account.id, req.params.host, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const blocklistStatusValidator = [
|
||||
query('hosts')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
|
||||
|
||||
query('accounts')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
blockServerValidator,
|
||||
blockAccountValidator,
|
||||
unblockAccountByAccountValidator,
|
||||
unblockServerByAccountValidator,
|
||||
unblockAccountByServerValidator,
|
||||
unblockServerByServerValidator,
|
||||
blocklistStatusValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
|
||||
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
|
||||
if (!accountBlock) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Account block entry not found.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.accountBlock = accountBlock
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
|
||||
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
|
||||
if (!serverBlock) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Server block entry not found.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.serverBlock = serverBlock
|
||||
return true
|
||||
}
|
||||
29
server/core/middlewares/validators/bulk.ts
Normal file
29
server/core/middlewares/validators/bulk.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { BulkRemoveCommentsOfBody, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk.js'
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { areValidationErrors, doesAccountHandleExist } from './shared/index.js'
|
||||
|
||||
export const bulkRemoveCommentsOfValidator = [
|
||||
body('accountName')
|
||||
.exists(),
|
||||
body('scope')
|
||||
.custom(isBulkRemoveCommentsOfScopeValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.body.accountName, req, res, checkIsLocal: false, checkCanManage: false })) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const body = req.body as BulkRemoveCommentsOfBody
|
||||
|
||||
if (body.scope === 'instance' && user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'User cannot remove any comments of this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
295
server/core/middlewares/validators/config.ts
Normal file
295
server/core/middlewares/validators/config.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isConfigLogoTypeValid, isLogoImageFile } from '@server/helpers/custom-validators/config.js'
|
||||
import { isIntOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isPlayerThemeValid } from '@server/helpers/custom-validators/player-settings.js'
|
||||
import { isNumberArray, isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { isVideoCommentsPolicyValid, isVideoLicenceValid, isVideoPrivacyValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { guessLanguageFromReq } from '@server/helpers/i18n.js'
|
||||
import { CONFIG, isEmailEnabled } from '@server/initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { getBrowseVideosDefaultScopeError, getBrowseVideosDefaultSortError } from '../../helpers/custom-validators/browse-videos.js'
|
||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins.js'
|
||||
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users.js'
|
||||
import { isThemeRegistered } from '../../lib/plugins/theme-utils.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
export const customConfigUpdateValidator = [
|
||||
body('instance.name').exists(),
|
||||
body('instance.shortDescription').exists(),
|
||||
body('instance.description').exists(),
|
||||
body('instance.terms').exists(),
|
||||
|
||||
body('instance.codeOfConduct').exists(),
|
||||
body('instance.creationReason').exists(),
|
||||
body('instance.moderationInformation').exists(),
|
||||
body('instance.administrator').exists(),
|
||||
body('instance.maintenanceLifetime').exists(),
|
||||
body('instance.businessModel').exists(),
|
||||
body('instance.hardwareInformation').exists(),
|
||||
body('instance.serverCountry').exists(),
|
||||
body('instance.support.text').exists(),
|
||||
body('instance.social.externalLink').exists(),
|
||||
body('instance.social.mastodonLink').exists(),
|
||||
body('instance.social.blueskyLink').exists(),
|
||||
body('instance.defaultLanguage').exists(),
|
||||
body('instance.social.xLink').exists(),
|
||||
|
||||
body('instance.isNSFW').isBoolean(),
|
||||
body('instance.languages').custom(isStringArray),
|
||||
body('instance.categories').custom(isNumberArray),
|
||||
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid),
|
||||
body('instance.defaultClientRoute').exists(),
|
||||
body('instance.customizations.css').exists(),
|
||||
body('instance.customizations.javascript').exists(),
|
||||
|
||||
body('services.twitter.username').exists(),
|
||||
|
||||
body('cache.previews.size').isInt(),
|
||||
body('cache.captions.size').isInt(),
|
||||
body('cache.torrents.size').isInt(),
|
||||
body('cache.storyboards.size').isInt(),
|
||||
|
||||
body('signup.enabled').isBoolean(),
|
||||
body('signup.limit').isInt(),
|
||||
body('signup.requiresEmailVerification').isBoolean(),
|
||||
body('signup.requiresApproval').isBoolean(),
|
||||
body('signup.minimumAge').isInt(),
|
||||
|
||||
body('admin.email').isEmail(),
|
||||
body('contactForm.enabled').isBoolean(),
|
||||
|
||||
body('user.history.videos.enabled').isBoolean(),
|
||||
body('user.videoQuota').custom(isUserVideoQuotaValid),
|
||||
body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid),
|
||||
|
||||
body('videoChannels.maxPerUser').isInt(),
|
||||
|
||||
body('transcoding.enabled').isBoolean(),
|
||||
body('transcoding.originalFile.keep').isBoolean(),
|
||||
body('transcoding.allowAdditionalExtensions').isBoolean(),
|
||||
body('transcoding.threads').isInt(),
|
||||
body('transcoding.concurrency').isInt({ min: 1 }),
|
||||
body('transcoding.resolutions.0p').isBoolean(),
|
||||
body('transcoding.resolutions.144p').isBoolean(),
|
||||
body('transcoding.resolutions.240p').isBoolean(),
|
||||
body('transcoding.resolutions.360p').isBoolean(),
|
||||
body('transcoding.resolutions.480p').isBoolean(),
|
||||
body('transcoding.resolutions.720p').isBoolean(),
|
||||
body('transcoding.resolutions.1080p').isBoolean(),
|
||||
body('transcoding.resolutions.1440p').isBoolean(),
|
||||
body('transcoding.resolutions.2160p').isBoolean(),
|
||||
body('transcoding.remoteRunners.enabled').isBoolean(),
|
||||
|
||||
body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
||||
body('transcoding.fps.max').custom(isIntOrNull),
|
||||
|
||||
body('transcoding.webVideos.enabled').isBoolean(),
|
||||
body('transcoding.hls.enabled').isBoolean(),
|
||||
|
||||
body('videoStudio.enabled').isBoolean(),
|
||||
body('videoStudio.remoteRunners.enabled').isBoolean(),
|
||||
|
||||
body('videoFile.update.enabled').isBoolean(),
|
||||
|
||||
body('import.videos.concurrency').isInt({ min: 0 }),
|
||||
body('import.videos.http.enabled').isBoolean(),
|
||||
body('import.videos.torrent.enabled').isBoolean(),
|
||||
|
||||
body('import.videoChannelSynchronization.enabled').isBoolean(),
|
||||
body('import.users.enabled').isBoolean(),
|
||||
|
||||
body('export.users.enabled').isBoolean(),
|
||||
body('export.users.maxUserVideoQuota').exists(),
|
||||
body('export.users.exportExpiration').exists(),
|
||||
|
||||
body('trending.videos.algorithms.default').exists(),
|
||||
body('trending.videos.algorithms.enabled').exists(),
|
||||
|
||||
body('followers.instance.enabled').isBoolean(),
|
||||
body('followers.instance.manualApproval').isBoolean(),
|
||||
body('followers.channels.enabled').isBoolean(),
|
||||
|
||||
body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
|
||||
|
||||
body('broadcastMessage.enabled').isBoolean(),
|
||||
body('broadcastMessage.message').exists(),
|
||||
body('broadcastMessage.level').exists(),
|
||||
body('broadcastMessage.dismissable').isBoolean(),
|
||||
|
||||
body('live.enabled').isBoolean(),
|
||||
body('live.allowReplay').isBoolean(),
|
||||
body('live.maxDuration').isInt(),
|
||||
body('live.maxInstanceLives').custom(isIntOrNull),
|
||||
body('live.maxUserLives').custom(isIntOrNull),
|
||||
body('live.transcoding.enabled').isBoolean(),
|
||||
body('live.transcoding.threads').isInt(),
|
||||
body('live.transcoding.resolutions.144p').isBoolean(),
|
||||
body('live.transcoding.resolutions.240p').isBoolean(),
|
||||
body('live.transcoding.resolutions.360p').isBoolean(),
|
||||
body('live.transcoding.resolutions.480p').isBoolean(),
|
||||
body('live.transcoding.resolutions.720p').isBoolean(),
|
||||
body('live.transcoding.resolutions.1080p').isBoolean(),
|
||||
body('live.transcoding.resolutions.1440p').isBoolean(),
|
||||
body('live.transcoding.resolutions.2160p').isBoolean(),
|
||||
body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
||||
body('live.transcoding.fps.max').custom(isIntOrNull),
|
||||
body('live.transcoding.remoteRunners.enabled').isBoolean(),
|
||||
|
||||
body('search.remoteUri.users').isBoolean(),
|
||||
body('search.remoteUri.anonymous').isBoolean(),
|
||||
body('search.searchIndex.enabled').isBoolean(),
|
||||
body('search.searchIndex.url').exists(),
|
||||
body('search.searchIndex.disableLocalSearch').isBoolean(),
|
||||
body('search.searchIndex.isDefaultSearch').isBoolean(),
|
||||
|
||||
body('defaults.publish.commentsPolicy').custom(isVideoCommentsPolicyValid),
|
||||
body('defaults.publish.privacy').custom(isVideoPrivacyValid),
|
||||
body('defaults.publish.licence').optional().custom(isVideoLicenceValid),
|
||||
body('defaults.p2p.webapp.enabled').isBoolean(),
|
||||
body('defaults.p2p.embed.enabled').isBoolean(),
|
||||
body('defaults.player.autoPlay').isBoolean(),
|
||||
body('defaults.player.theme').custom(isPlayerThemeValid),
|
||||
|
||||
body('email.body.signature').exists(),
|
||||
body('email.subject.prefix').exists(),
|
||||
|
||||
body('videoComments.acceptRemoteComments').isBoolean(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!checkInvalidConfigIfEmailDisabled(req.body, req, res)) return
|
||||
if (!checkInvalidTranscodingConfig(req.body, req, res)) return
|
||||
if (!checkInvalidSynchronizationConfig(req.body, req, res)) return
|
||||
if (!checkInvalidLiveConfig(req.body, req, res)) return
|
||||
if (!checkInvalidVideoStudioConfig(req.body, req, res)) return
|
||||
if (!checkInvalidSearchConfig(req.body, req, res)) return
|
||||
if (!checkInvalidBrowseVideosConfig(req.body, req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function ensureConfigIsEditable (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (!CONFIG.WEBADMIN.CONFIGURATION.EDITION.ALLOWED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.METHOD_NOT_ALLOWED_405,
|
||||
message: req.t('Server configuration is static and cannot be edited')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export const updateOrDeleteLogoValidator = [
|
||||
param('logoType')
|
||||
.custom(isConfigLogoTypeValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateInstanceLogoValidator = [
|
||||
body('logofile').custom((value, { req }) => isLogoImageFile(req.files, 'logofile')).withMessage(
|
||||
'This file is not supported or too large. Please, make sure it is of the following type : ' +
|
||||
CONSTRAINTS_FIELDS.LOGO.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
if (isEmailEnabled()) return true
|
||||
|
||||
if (customConfig.signup.requiresEmailVerification === true) {
|
||||
res.fail({ message: req.t('SMTP is not configured but you require signup email verification.') })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidTranscodingConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
if (customConfig.transcoding.enabled === false) return true
|
||||
|
||||
if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) {
|
||||
res.fail({ message: req.t('You need to enable at least web_videos transcoding or hls transcoding') })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidSynchronizationConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
if (customConfig.import.videoChannelSynchronization.enabled && !customConfig.import.videos.http.enabled) {
|
||||
res.fail({ message: req.t('You need to enable HTTP video import in order to enable channel synchronization') })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidLiveConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
if (customConfig.live.enabled === false) return true
|
||||
|
||||
if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
|
||||
res.fail({ message: req.t('You cannot allow live replay if transcoding is not enabled') })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidVideoStudioConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
if (customConfig.videoStudio.enabled === false) return true
|
||||
|
||||
if (customConfig.videoStudio.enabled === true && customConfig.transcoding.enabled === false) {
|
||||
res.fail({ message: req.t('You cannot enable video studio if transcoding is not enabled') })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidSearchConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
if (customConfig.search.searchIndex.enabled === false) return true
|
||||
|
||||
if (customConfig.search.searchIndex.enabled === true && customConfig.search.remoteUri.users === false) {
|
||||
res.fail({ message: req.t('You cannot enable search index without enabling remote URI search for users.') })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidBrowseVideosConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||
const sortError = getBrowseVideosDefaultSortError(
|
||||
customConfig.client.browseVideos.defaultSort,
|
||||
customConfig.trending.videos.algorithms.enabled,
|
||||
guessLanguageFromReq(req, res)
|
||||
)
|
||||
if (sortError) {
|
||||
res.fail({ message: sortError })
|
||||
return false
|
||||
}
|
||||
|
||||
const scopeError = getBrowseVideosDefaultScopeError(customConfig.client.browseVideos.defaultScope, guessLanguageFromReq(req, res))
|
||||
if (scopeError) {
|
||||
res.fail({ message: scopeError })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
282
server/core/middlewares/validators/event-registrations.ts
Normal file
282
server/core/middlewares/validators/event-registrations.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { EventRegistrationModel } from '@server/models/video/event-registration.js'
|
||||
import { EventModel } from '@server/models/video/event.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
const phoneRegexp = /^[0-9+()\-\s]{7,20}$/
|
||||
|
||||
const requiredRegistrationFields = [
|
||||
body('firstName')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('First name is required'),
|
||||
|
||||
body('lastName')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Last name is required'),
|
||||
|
||||
body('email')
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage('Email must be valid')
|
||||
.bail()
|
||||
.normalizeEmail(),
|
||||
|
||||
body('phone')
|
||||
.trim()
|
||||
.matches(phoneRegexp)
|
||||
.withMessage('Phone must be valid'),
|
||||
|
||||
body('companyName')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Company name is required'),
|
||||
|
||||
body('jobTitle')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Job title is required'),
|
||||
|
||||
body('country')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('Country must be between 2 and 100 characters')
|
||||
]
|
||||
|
||||
const optionalRegistrationFields = [
|
||||
body('firstName')
|
||||
.optional()
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('First name cannot be empty'),
|
||||
|
||||
body('lastName')
|
||||
.optional()
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Last name cannot be empty'),
|
||||
|
||||
body('email')
|
||||
.optional()
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage('Email must be valid')
|
||||
.bail()
|
||||
.normalizeEmail(),
|
||||
|
||||
body('phone')
|
||||
.optional()
|
||||
.trim()
|
||||
.matches(phoneRegexp)
|
||||
.withMessage('Phone must be valid'),
|
||||
|
||||
body('companyName')
|
||||
.optional()
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Company name cannot be empty'),
|
||||
|
||||
body('jobTitle')
|
||||
.optional()
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Job title cannot be empty'),
|
||||
|
||||
body('country')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('Country must be between 2 and 100 characters')
|
||||
]
|
||||
|
||||
async function loadEventWithChannel (eventId: number) {
|
||||
return EventModel.findByPk(eventId, {
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.scope([ 'WITH_ACCOUNT' ]),
|
||||
required: true
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
export const createEventRegistrationValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
...requiredRegistrationFields,
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const eventId = parseInt(req.params.eventId)
|
||||
const event = await loadEventWithChannel(eventId)
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.event = event
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listEventRegistrationsValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const event = await loadEventWithChannel(parseInt(req.params.eventId))
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.event = event
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const getEventRegistrationValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
param('registrationId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid registration id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const eventId = parseInt(req.params.eventId)
|
||||
const registrationId = parseInt(req.params.registrationId)
|
||||
const [ event, registration ] = await Promise.all([
|
||||
loadEventWithChannel(eventId),
|
||||
EventRegistrationModel.loadByIdAndEventId(registrationId, eventId)
|
||||
])
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (!registration) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.event = event
|
||||
resLocals.eventRegistration = registration
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateEventRegistrationValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
param('registrationId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid registration id'),
|
||||
|
||||
...optionalRegistrationFields,
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const eventId = parseInt(req.params.eventId)
|
||||
const registrationId = parseInt(req.params.registrationId)
|
||||
const [ event, registration ] = await Promise.all([
|
||||
loadEventWithChannel(eventId),
|
||||
EventRegistrationModel.loadByIdAndEventId(registrationId, eventId)
|
||||
])
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (!registration) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.event = event
|
||||
resLocals.eventRegistration = registration
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteEventRegistrationValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
param('registrationId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid registration id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const eventId = parseInt(req.params.eventId)
|
||||
const registrationId = parseInt(req.params.registrationId)
|
||||
const [ event, registration ] = await Promise.all([
|
||||
loadEventWithChannel(eventId),
|
||||
EventRegistrationModel.loadByIdAndEventId(registrationId, eventId)
|
||||
])
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (!registration) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.event = event
|
||||
resLocals.eventRegistration = registration
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
254
server/core/middlewares/validators/events.ts
Normal file
254
server/core/middlewares/validators/events.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { EventModel } from '@server/models/video/event.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
export const createEventValidator = [
|
||||
body('title')
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Event title must be between 1 and 255 characters'),
|
||||
|
||||
body('description')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 5000 })
|
||||
.withMessage('Event description must not exceed 5000 characters'),
|
||||
|
||||
body('startTime')
|
||||
.isISO8601()
|
||||
.withMessage('Start time must be a valid ISO 8601 date'),
|
||||
|
||||
body('endTime')
|
||||
.isISO8601()
|
||||
.withMessage('End time must be a valid ISO 8601 date'),
|
||||
|
||||
body('state')
|
||||
.optional()
|
||||
.isIn([ 'active', 'disabled' ])
|
||||
.withMessage('Invalid event state'),
|
||||
|
||||
body('liveUrl')
|
||||
.optional({ checkFalsy: true})
|
||||
.isLength({ max: 2048 })
|
||||
.withMessage('Live URL must not exceed 2048 characters')
|
||||
.bail()
|
||||
.isURL()
|
||||
.withMessage('Live URL must be a valid URL'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
// Validate end time is after start time
|
||||
const startTime = new Date(req.body.startTime)
|
||||
const endTime = new Date(req.body.endTime)
|
||||
|
||||
if (endTime <= startTime) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
|
||||
message: 'End time must be after start time'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate channel exists
|
||||
const { channelId } = req.params
|
||||
const channel = await VideoChannelModel.scope([ 'WITH_ACCOUNT' ]).findByPk(channelId)
|
||||
|
||||
if (!channel) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Channel not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.channel = channel
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateEventValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
body('title')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Event title must be between 1 and 255 characters'),
|
||||
|
||||
body('description')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 5000 })
|
||||
.withMessage('Event description must not exceed 5000 characters'),
|
||||
|
||||
body('startTime')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Start time must be a valid ISO 8601 date'),
|
||||
|
||||
body('endTime')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('End time must be a valid ISO 8601 date'),
|
||||
|
||||
body('state')
|
||||
.optional()
|
||||
.isIn([ 'active', 'disabled' ])
|
||||
.withMessage('Invalid event state'),
|
||||
|
||||
body('liveUrl')
|
||||
.optional({ checkFalsy: true})
|
||||
.isLength({ max: 2048 })
|
||||
.withMessage('Live URL must not exceed 2048 characters')
|
||||
.bail()
|
||||
.isURL()
|
||||
.withMessage('Live URL must be a valid URL'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
// Validate time range if both provided
|
||||
if (req.body.startTime && req.body.endTime) {
|
||||
const startTime = new Date(req.body.startTime)
|
||||
const endTime = new Date(req.body.endTime)
|
||||
|
||||
if (endTime <= startTime) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
|
||||
message: 'End time must be after start time'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate channel exists
|
||||
const { channelId, eventId } = req.params
|
||||
const channel = await VideoChannelModel.scope([ 'WITH_ACCOUNT' ]).findByPk(channelId)
|
||||
|
||||
if (!channel) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Channel not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate event exists and belongs to channel
|
||||
const event = await EventModel.findByIdAndChannelId(parseInt(eventId), parseInt(channelId))
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.channel = channel
|
||||
resLocals.event = event
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteEventValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { channelId, eventId } = req.params
|
||||
const channel = await VideoChannelModel.scope([ 'WITH_ACCOUNT' ]).findByPk(channelId)
|
||||
|
||||
if (!channel) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Channel not found'
|
||||
})
|
||||
}
|
||||
|
||||
const event = await EventModel.findByIdAndChannelId(parseInt(eventId), parseInt(channelId))
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.channel = channel
|
||||
resLocals.event = event
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateEventStateValidator = [
|
||||
param('eventId')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
body('state')
|
||||
.isIn([ 'active', 'disabled' ])
|
||||
.withMessage('Invalid event state'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { channelId, eventId } = req.params
|
||||
const channel = await VideoChannelModel.scope([ 'WITH_ACCOUNT' ]).findByPk(channelId)
|
||||
|
||||
if (!channel) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Channel not found'
|
||||
})
|
||||
}
|
||||
|
||||
const event = await EventModel.findByIdAndChannelId(parseInt(eventId), parseInt(channelId))
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
const resLocals = res.locals as any
|
||||
resLocals.channel = channel
|
||||
resLocals.event = event
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const getEventValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid event id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const event = await EventModel.findByPk(req.params.id)
|
||||
|
||||
if (!event) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Event not found'
|
||||
})
|
||||
}
|
||||
|
||||
(res.locals as any).event = event
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
15
server/core/middlewares/validators/express.ts
Normal file
15
server/core/middlewares/validators/express.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as express from 'express'
|
||||
|
||||
const methodsValidator = (methods: string[]) => {
|
||||
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (methods.includes(req.method) !== true) {
|
||||
return res.sendStatus(405)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
methodsValidator
|
||||
}
|
||||
169
server/core/middlewares/validators/feeds.ts
Normal file
169
server/core/middlewares/validators/feeds.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { isValidRSSFeed } from '../../helpers/custom-validators/feeds.js'
|
||||
import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanSeeVideo,
|
||||
doesAccountHandleExist,
|
||||
doesAccountIdExist,
|
||||
doesChannelHandleExist,
|
||||
doesChannelIdExist,
|
||||
doesUserFeedTokenCorrespond,
|
||||
doesVideoExist
|
||||
} from './shared/index.js'
|
||||
|
||||
export const feedsFormatValidator = [
|
||||
param('format')
|
||||
.optional()
|
||||
.custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
|
||||
query('format')
|
||||
.optional()
|
||||
.custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function setFeedFormatContentType (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const format = req.query.format || req.params.format || 'rss'
|
||||
|
||||
let acceptableContentTypes: string[]
|
||||
if (format === 'atom' || format === 'atom1') {
|
||||
acceptableContentTypes = [ 'application/atom+xml', 'application/xml', 'text/xml' ]
|
||||
} else if (format === 'json' || format === 'json1') {
|
||||
acceptableContentTypes = [ 'application/json' ]
|
||||
} else if (format === 'rss' || format === 'rss2') {
|
||||
acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
|
||||
} else {
|
||||
acceptableContentTypes = [ 'application/xml', 'text/xml' ]
|
||||
}
|
||||
|
||||
return feedContentTypeResponse(req, res, next, acceptableContentTypes)
|
||||
}
|
||||
|
||||
export function setFeedPodcastContentType (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
|
||||
|
||||
return feedContentTypeResponse(req, res, next, acceptableContentTypes)
|
||||
}
|
||||
|
||||
export function feedContentTypeResponse (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
acceptableContentTypes: string[]
|
||||
) {
|
||||
if (req.accepts(acceptableContentTypes)) {
|
||||
res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
|
||||
} else {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_ACCEPTABLE_406,
|
||||
message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const feedsAccountOrChannelFiltersValidator = [
|
||||
query('accountId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('accountName')
|
||||
.optional(),
|
||||
|
||||
query('videoChannelId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('videoChannelName')
|
||||
.optional(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { accountId, videoChannelId, accountName, videoChannelName } = req.query
|
||||
const commonOptions = { req, res, checkCanManage: false, checkIsLocal: false, checkIsOwner: false }
|
||||
|
||||
if (accountId && !await doesAccountIdExist({ id: accountId, ...commonOptions })) return
|
||||
if (videoChannelId && !await doesChannelIdExist({ id: videoChannelId, ...commonOptions })) return
|
||||
|
||||
if (accountName && !await doesAccountHandleExist({ handle: accountName, ...commonOptions })) return
|
||||
if (videoChannelName && !await doesChannelHandleExist({ handle: videoChannelName, ...commonOptions })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const videoFeedsPodcastValidator = [
|
||||
query('videoChannelId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (
|
||||
!await doesChannelIdExist({
|
||||
id: req.query.videoChannelId,
|
||||
checkCanManage: false,
|
||||
checkIsLocal: false,
|
||||
checkIsOwner: false,
|
||||
req,
|
||||
res
|
||||
})
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const videoSubscriptionFeedsValidator = [
|
||||
query('accountId')
|
||||
.custom(isIdValid),
|
||||
|
||||
query('token')
|
||||
.custom(exists),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesAccountIdExist({ id: req.query.accountId, req, res, checkIsLocal: true, checkCanManage: false })) return
|
||||
if (!await doesUserFeedTokenCorrespond({ id: res.locals.account.userId, token: req.query.token, req, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoCommentsFeedsValidator = [
|
||||
query('videoId')
|
||||
.optional()
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
|
||||
return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
|
||||
}
|
||||
|
||||
if (req.query.videoId) {
|
||||
if (!await doesVideoExist(req.query.videoId, res)) return
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
156
server/core/middlewares/validators/follows.ts
Normal file
156
server/core/middlewares/validators/follows.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, ServerFollowCreate } from '@peertube/peertube-models'
|
||||
import { isProdInstance } from '@peertube/peertube-node-utils'
|
||||
import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows.js'
|
||||
import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors/index.js'
|
||||
import { getRemoteNameAndHost } from '@server/lib/activitypub/follow.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { MActorFollowActorsDefault } from '@server/types/models/index.js'
|
||||
import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor.js'
|
||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { WEBSERVER } from '../../initializers/constants.js'
|
||||
import { ActorFollowModel } from '../../models/actor/actor-follow.js'
|
||||
import { ActorModel } from '../../models/actor/actor.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const listFollowsValidator = [
|
||||
query('state')
|
||||
.optional()
|
||||
.custom(isFollowStateValid),
|
||||
query('actorType')
|
||||
.optional()
|
||||
.custom(isActorTypeValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const followValidator = [
|
||||
body('hosts')
|
||||
.toArray()
|
||||
.custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
|
||||
|
||||
body('handles')
|
||||
.toArray()
|
||||
.custom(isEachUniqueHandleValid).withMessage('Should have an array of handles'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// Force https if the administrator wants to follow remote actors
|
||||
if (isProdInstance() && WEBSERVER.SCHEME === 'http') {
|
||||
return res
|
||||
.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
|
||||
.json({
|
||||
error: 'Cannot follow on a non HTTPS web server.'
|
||||
})
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: ServerFollowCreate = req.body
|
||||
if (body.hosts.length === 0 && body.handles.length === 0) {
|
||||
return res
|
||||
.status(HttpStatusCode.BAD_REQUEST_400)
|
||||
.json({
|
||||
error: 'You must provide at least one handle or one host.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const removeFollowingValidator = [
|
||||
param('hostOrHandle')
|
||||
.custom(value => isHostValid(value) || isRemoteHandleValid(value)),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle)
|
||||
const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
|
||||
actorId: serverActor.id,
|
||||
targetName: name,
|
||||
targetHost: host
|
||||
})
|
||||
|
||||
if (!follow) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Follow ${req.params.hostOrHandle} not found.`
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.follow = follow
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getFollowerValidator = [
|
||||
param('handle')
|
||||
.custom(isValidActorHandle),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
let follow: MActorFollowActorsDefault
|
||||
try {
|
||||
const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.handle)
|
||||
const actor = await ActorModel.loadByUrl(actorUrl)
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
|
||||
} catch (err) {
|
||||
logger.warn('Cannot get actor from handle.', { handle: req.params.handle, err })
|
||||
}
|
||||
|
||||
if (!follow) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Follower ${req.params.handle} not found.`
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.follow = follow
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const acceptFollowerValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const follow = res.locals.follow
|
||||
if (follow.state !== 'pending' && follow.state !== 'rejected') {
|
||||
return res.fail({ message: 'Follow is not in pending/rejected state.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const rejectFollowerValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const follow = res.locals.follow
|
||||
if (follow.state !== 'pending' && follow.state !== 'accepted') {
|
||||
return res.fail({ message: 'Follow is not in pending/accepted state.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
followValidator,
|
||||
removeFollowingValidator,
|
||||
getFollowerValidator,
|
||||
acceptFollowerValidator,
|
||||
rejectFollowerValidator,
|
||||
listFollowsValidator
|
||||
}
|
||||
30
server/core/middlewares/validators/index.ts
Normal file
30
server/core/middlewares/validators/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export * from './abuse.js'
|
||||
export * from './account.js'
|
||||
export * from './activitypub/index.js'
|
||||
export * from './actor-image.js'
|
||||
export * from './blocklist.js'
|
||||
export * from './bulk.js'
|
||||
export * from './config.js'
|
||||
export * from './events.js'
|
||||
export * from './event-registrations.js'
|
||||
export * from './express.js'
|
||||
export * from './feeds.js'
|
||||
export * from './follows.js'
|
||||
export * from './jobs.js'
|
||||
export * from './logs.js'
|
||||
export * from './metrics.js'
|
||||
export * from './object-storage-proxy.js'
|
||||
export * from './oembed.js'
|
||||
export * from './pagination.js'
|
||||
export * from './plugins.js'
|
||||
export * from './redundancy.js'
|
||||
export * from './resumable-upload.js'
|
||||
export * from './search.js'
|
||||
export * from './server.js'
|
||||
export * from './sort.js'
|
||||
export * from './static.js'
|
||||
export * from './themes.js'
|
||||
export * from './webfinger.js'
|
||||
export * from './users/index.js'
|
||||
export * from './videos/index.js'
|
||||
export * from './runners/index.js'
|
||||
29
server/core/middlewares/validators/jobs.ts
Normal file
29
server/core/middlewares/validators/jobs.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs.js'
|
||||
import { loggerTagsFactory } from '../../helpers/logger.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const lTags = loggerTagsFactory('validators', 'jobs')
|
||||
|
||||
const listJobsValidator = [
|
||||
param('state')
|
||||
.optional()
|
||||
.custom(isValidJobState),
|
||||
|
||||
query('jobType')
|
||||
.optional()
|
||||
.custom(isValidJobType),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, lTags())) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
listJobsValidator
|
||||
}
|
||||
247
server/core/middlewares/validators/live-videos.ts
Normal file
247
server/core/middlewares/validators/live-videos.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isVideoFileMimeTypeValid, isVideoImageValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { LiveVideoModel, LiveVideoStatus } from '@server/models/video/live-video.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
function ensureAdmin (res: express.Response) {
|
||||
const user = res.locals.oauth?.token?.User
|
||||
return user?.role === 0
|
||||
}
|
||||
|
||||
async function checkScheduleConflict (req: express.Request, res: express.Response, next: express.NextFunction, excludeId?: number) {
|
||||
const startTime = new Date(req.body.startTime)
|
||||
const endTime = new Date(req.body.endTime)
|
||||
|
||||
const conflicts = await LiveVideoModel.listConflicts({
|
||||
targetCamera: req.body.targetCamera,
|
||||
startTime,
|
||||
endTime,
|
||||
excludeId
|
||||
})
|
||||
|
||||
if (conflicts.length !== 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Target camera is already assigned to another live video'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export const listLiveVideosValidator = [
|
||||
query('status')
|
||||
.optional()
|
||||
.isIn(Object.values(LiveVideoStatus))
|
||||
.withMessage('Invalid live video status'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const getLiveVideoValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid live video id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const liveVideo = await LiveVideoModel.loadById(parseInt(req.params.id))
|
||||
if (!liveVideo) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Live video not found'
|
||||
})
|
||||
}
|
||||
|
||||
;(res.locals as any).liveVideo = liveVideo
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const createLiveVideoValidator = [
|
||||
body('title')
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Live video title must be between 1 and 255 characters'),
|
||||
|
||||
body('startTime')
|
||||
.isISO8601()
|
||||
.withMessage('Start time must be a valid ISO 8601 date'),
|
||||
|
||||
body('endTime')
|
||||
.isISO8601()
|
||||
.withMessage('End time must be a valid ISO 8601 date'),
|
||||
|
||||
body('targetCamera')
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Target camera is required'),
|
||||
|
||||
body('thumbnailfile')
|
||||
.custom((_, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
|
||||
.withMessage('Should have a valid thumbnail file'),
|
||||
|
||||
body('videofile')
|
||||
.custom((_, { req }) => isVideoFileMimeTypeValid(req.files))
|
||||
.withMessage('Should have a valid video file'),
|
||||
|
||||
body('subtitlefile')
|
||||
.optional()
|
||||
.custom((_, { req }) => {
|
||||
if (!req.files || !req.files['subtitlefile']) return true
|
||||
const file = req.files['subtitlefile'][0]
|
||||
const validMimes = [ 'text/vtt', 'application/x-subrip', 'text/plain' ]
|
||||
return validMimes.includes(file.mimetype)
|
||||
})
|
||||
.withMessage('Should have a valid subtitle file (vtt or srt)'),
|
||||
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureAdmin(res)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only admins can manage live videos'
|
||||
})
|
||||
}
|
||||
|
||||
const startTime = new Date(req.body.startTime)
|
||||
const endTime = new Date(req.body.endTime)
|
||||
|
||||
if (endTime <= startTime) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
|
||||
message: 'End time must be after start time'
|
||||
})
|
||||
}
|
||||
|
||||
return checkScheduleConflict(req, res, next)
|
||||
}
|
||||
]
|
||||
|
||||
export const updateLiveVideoValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid live video id'),
|
||||
|
||||
body('title')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Live video title must be between 1 and 255 characters'),
|
||||
|
||||
body('startTime')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Start time must be a valid ISO 8601 date'),
|
||||
|
||||
body('endTime')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('End time must be a valid ISO 8601 date'),
|
||||
|
||||
body('targetCamera')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Target camera must be between 1 and 255 characters'),
|
||||
|
||||
body('status')
|
||||
.optional()
|
||||
.isIn(Object.values(LiveVideoStatus))
|
||||
.withMessage('Invalid live video status'),
|
||||
|
||||
body('thumbnailfile')
|
||||
.optional()
|
||||
.custom((_, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
|
||||
.withMessage('Should have a valid thumbnail file'),
|
||||
|
||||
body('videofile')
|
||||
.optional()
|
||||
.custom((_, { req }) => isVideoFileMimeTypeValid(req.files))
|
||||
.withMessage('Should have a valid video file'),
|
||||
body('subtitlefile')
|
||||
.optional()
|
||||
.custom((_, { req }) => {
|
||||
if (!req.files || !req.files['subtitlefile']) return true
|
||||
const file = req.files['subtitlefile'][0]
|
||||
const validMimes = [ 'text/vtt', 'application/x-subrip', 'text/plain' ]
|
||||
return validMimes.includes(file.mimetype)
|
||||
})
|
||||
.withMessage('Should have a valid subtitle file (vtt or srt)'),
|
||||
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureAdmin(res)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only admins can manage live videos'
|
||||
})
|
||||
}
|
||||
|
||||
const liveVideo = await LiveVideoModel.loadById(parseInt(req.params.id))
|
||||
if (!liveVideo) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Live video not found'
|
||||
})
|
||||
}
|
||||
|
||||
const startTime = req.body.startTime ? new Date(req.body.startTime) : liveVideo.startTime
|
||||
const endTime = req.body.endTime ? new Date(req.body.endTime) : liveVideo.endTime
|
||||
|
||||
if (endTime <= startTime) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
|
||||
message: 'End time must be after start time'
|
||||
})
|
||||
}
|
||||
|
||||
;(res.locals as any).liveVideo = liveVideo
|
||||
|
||||
if (req.body.startTime || req.body.endTime || req.body.targetCamera) {
|
||||
req.body.startTime = startTime.toISOString()
|
||||
req.body.endTime = endTime.toISOString()
|
||||
req.body.targetCamera = req.body.targetCamera ?? liveVideo.targetCamera
|
||||
|
||||
return checkScheduleConflict(req, res, next, liveVideo.id)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteLiveVideoValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid)
|
||||
.withMessage('Invalid live video id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureAdmin(res)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only admins can manage live videos'
|
||||
})
|
||||
}
|
||||
|
||||
const liveVideo = await LiveVideoModel.loadById(parseInt(req.params.id))
|
||||
if (!liveVideo) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Live video not found'
|
||||
})
|
||||
}
|
||||
|
||||
;(res.locals as any).liveVideo = liveVideo
|
||||
return next()
|
||||
}
|
||||
]
|
||||
93
server/core/middlewares/validators/logs.ts
Normal file
93
server/core/middlewares/validators/logs.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
isValidClientLogLevel,
|
||||
isValidClientLogMessage,
|
||||
isValidClientLogMeta,
|
||||
isValidClientLogStackTrace,
|
||||
isValidClientLogUserAgent,
|
||||
isValidLogLevel
|
||||
} from '../../helpers/custom-validators/logs.js'
|
||||
import { isDateValid } from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const createClientLogValidator = [
|
||||
body('message')
|
||||
.custom(isValidClientLogMessage),
|
||||
|
||||
body('url')
|
||||
.custom(isUrlValid),
|
||||
|
||||
body('level')
|
||||
.custom(isValidClientLogLevel),
|
||||
|
||||
body('stackTrace')
|
||||
.optional()
|
||||
.custom(isValidClientLogStackTrace),
|
||||
|
||||
body('meta')
|
||||
.optional()
|
||||
.custom(isValidClientLogMeta),
|
||||
|
||||
body('userAgent')
|
||||
.optional()
|
||||
.custom(isValidClientLogUserAgent),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (CONFIG.LOG.ACCEPT_CLIENT_LOG !== true) {
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getLogsValidator = [
|
||||
query('startDate')
|
||||
.custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
|
||||
query('level')
|
||||
.optional()
|
||||
.custom(isValidLogLevel),
|
||||
query('tagsOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid tags one of array'),
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getAuditLogsValidator = [
|
||||
query('startDate')
|
||||
.custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getLogsValidator,
|
||||
getAuditLogsValidator,
|
||||
createClientLogValidator
|
||||
}
|
||||
64
server/core/middlewares/validators/metrics.ts
Normal file
64
server/core/middlewares/validators/metrics.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { isValidPlayerMode } from '@server/helpers/custom-validators/metrics.js'
|
||||
import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { HttpStatusCode, PlaybackMetricCreate } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, doesVideoExist } from './shared/index.js'
|
||||
|
||||
const addPlaybackMetricValidator = [
|
||||
body('resolution')
|
||||
.isInt({ min: 0 }),
|
||||
body('fps')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('p2pPeers')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('p2pEnabled')
|
||||
.isBoolean(),
|
||||
|
||||
body('playerMode')
|
||||
.custom(isValidPlayerMode),
|
||||
|
||||
body('resolutionChanges')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('bufferStalled')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('errors')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('downloadedBytesP2P')
|
||||
.isInt({ min: 0 }),
|
||||
body('downloadedBytesHTTP')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('uploadedBytesP2P')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('videoId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
|
||||
const body: PlaybackMetricCreate = req.body
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(body.videoId, res, 'unsafe-only-immutable-attributes')) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
addPlaybackMetricValidator
|
||||
}
|
||||
20
server/core/middlewares/validators/object-storage-proxy.ts
Normal file
20
server/core/middlewares/validators/object-storage-proxy.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import express from 'express'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
const ensurePrivateObjectStorageProxyIsEnabled = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES !== true) {
|
||||
return res.fail({
|
||||
message: 'Private object storage proxy is not enabled',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
ensurePrivateObjectStorageProxyIsEnabled
|
||||
}
|
||||
157
server/core/middlewares/validators/oembed.ts
Normal file
157
server/core/middlewares/validators/oembed.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { join } from 'path'
|
||||
import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
|
||||
import { loadVideo } from '@server/lib/model-loaders/index.js'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { isIdOrUUIDValid, isUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc.js'
|
||||
import { WEBSERVER } from '../../initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const playlistPaths = [
|
||||
join('videos', 'watch', 'playlist'),
|
||||
join('w', 'p')
|
||||
]
|
||||
|
||||
const videoPaths = [
|
||||
join('videos', 'watch'),
|
||||
'w'
|
||||
]
|
||||
|
||||
function buildUrls (paths: string[]) {
|
||||
return paths.map(p => WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, p) + '/')
|
||||
}
|
||||
|
||||
const startPlaylistURLs = buildUrls(playlistPaths)
|
||||
const startVideoURLs = buildUrls(videoPaths)
|
||||
|
||||
const isURLOptions = {
|
||||
require_host: true,
|
||||
require_tld: true
|
||||
}
|
||||
|
||||
// We validate 'localhost', so we don't have the top level domain
|
||||
if (isTestOrDevInstance()) {
|
||||
isURLOptions.require_tld = false
|
||||
}
|
||||
|
||||
const oembedValidator = [
|
||||
query('url')
|
||||
.isURL(isURLOptions),
|
||||
query('maxwidth')
|
||||
.optional()
|
||||
.isInt(),
|
||||
query('maxheight')
|
||||
.optional()
|
||||
.isInt(),
|
||||
query('format')
|
||||
.optional()
|
||||
.isIn([ 'xml', 'json' ]),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.format !== undefined && req.query.format !== 'json') {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_IMPLEMENTED_501,
|
||||
message: 'Requested format is not implemented on server.',
|
||||
data: {
|
||||
format: req.query.format
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const url = req.query.url as string
|
||||
|
||||
let urlPath: string
|
||||
|
||||
try {
|
||||
urlPath = new URL(url).pathname
|
||||
} catch (err) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: err.message,
|
||||
data: {
|
||||
url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isPlaylist = startPlaylistURLs.some(u => url.startsWith(u))
|
||||
const isVideo = isPlaylist ? false : startVideoURLs.some(u => url.startsWith(u))
|
||||
|
||||
const startIsOk = isVideo || isPlaylist
|
||||
|
||||
const parts = urlPath.split('/')
|
||||
|
||||
if (startIsOk === false || parts.length === 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Invalid url.',
|
||||
data: {
|
||||
url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const elementId = toCompleteUUID(parts.pop())
|
||||
if (isIdOrUUIDValid(elementId) === false) {
|
||||
return res.fail({ message: 'Invalid video or playlist id.' })
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
const video = await loadVideo(elementId, 'all')
|
||||
|
||||
if (!video) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
video.privacy === VideoPrivacy.PUBLIC ||
|
||||
(video.privacy === VideoPrivacy.UNLISTED && isUUIDValid(elementId) === true)
|
||||
) {
|
||||
res.locals.videoAll = video
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Video is not publicly available'
|
||||
})
|
||||
}
|
||||
|
||||
// Is playlist
|
||||
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
|
||||
if (!videoPlaylist) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC ||
|
||||
(videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED && isUUIDValid(elementId))
|
||||
) {
|
||||
res.locals.videoPlaylistSummary = videoPlaylist
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Playlist is not public'
|
||||
})
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
oembedValidator
|
||||
}
|
||||
30
server/core/middlewares/validators/pagination.ts
Normal file
30
server/core/middlewares/validators/pagination.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { PAGINATION } from '@server/initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const paginationValidator = paginationValidatorBuilder()
|
||||
|
||||
function paginationValidatorBuilder (tags: string[] = []) {
|
||||
return [
|
||||
query('start')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
query('count')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
paginationValidator,
|
||||
paginationValidatorBuilder
|
||||
}
|
||||
117
server/core/middlewares/validators/player-settings.ts
Normal file
117
server/core/middlewares/validators/player-settings.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isPlayerChannelThemeSettingValid, isPlayerVideoThemeSettingValid } from '@server/helpers/custom-validators/player-settings.js'
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { areValidationErrors, isValidVideoIdParam } from './shared/utils.js'
|
||||
import { checkCanManageChannel } from './shared/video-channels.js'
|
||||
import { checkCanManageVideo, checkCanSeeVideo, doesVideoExist } from './shared/videos.js'
|
||||
|
||||
export const getVideoPlayerSettingsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
query('raw')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const raw = req.query.raw === true
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res, raw ? 'all' : 'only-video-and-blacklist')) return
|
||||
|
||||
const video = res.locals.onlyVideo || res.locals.videoAll
|
||||
if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
|
||||
|
||||
if (raw === true) {
|
||||
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 getChannelPlayerSettingsValidator = [
|
||||
query('raw')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.raw === true) {
|
||||
const user = res.locals.oauth?.token.User
|
||||
|
||||
if (
|
||||
!await checkCanManageChannel({
|
||||
channel: res.locals.videoChannel,
|
||||
user,
|
||||
req,
|
||||
res,
|
||||
checkCanManage: true,
|
||||
checkIsOwner: false,
|
||||
specialRight: UserRight.MANAGE_ANY_VIDEO_CHANNEL
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateVideoPlayerSettingsValidator = [
|
||||
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 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 updatePlayerSettingsValidatorFactory = (type: 'video' | 'channel') => [
|
||||
body('theme')
|
||||
.custom(v => {
|
||||
return type === 'video'
|
||||
? isPlayerVideoThemeSettingValid(v)
|
||||
: isPlayerChannelThemeSettingValid(v)
|
||||
}),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
204
server/core/middlewares/validators/plugins.ts
Normal file
204
server/core/middlewares/validators/plugins.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { HttpStatusCode, InstallOrUpdatePlugin, PluginType_Type } from '@peertube/peertube-models'
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
isSafePath,
|
||||
isStableOrUnstableVersionValid,
|
||||
toBooleanOrNull,
|
||||
toIntOrNull
|
||||
} from '../../helpers/custom-validators/misc.js'
|
||||
import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid } from '../../helpers/custom-validators/plugins.js'
|
||||
import { CONFIG } from '../../initializers/config.js'
|
||||
import { PluginManager } from '../../lib/plugins/plugin-manager.js'
|
||||
import { PluginModel } from '../../models/server/plugin.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
export const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) => {
|
||||
const validators: (ValidationChain | express.Handler)[] = [
|
||||
param('pluginName')
|
||||
.custom(isPluginNameValid)
|
||||
]
|
||||
|
||||
if (withVersion) {
|
||||
validators.push(
|
||||
param('pluginVersion')
|
||||
.custom(isStableOrUnstableVersionValid)
|
||||
)
|
||||
}
|
||||
|
||||
return validators.concat([
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
|
||||
const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
|
||||
|
||||
if (!plugin) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No plugin found named ' + npmName
|
||||
})
|
||||
}
|
||||
if (withVersion && plugin.version !== req.params.pluginVersion) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.registeredPlugin = plugin
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
export const getExternalAuthValidator = [
|
||||
param('authName')
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const plugin = res.locals.registeredPlugin
|
||||
if (!plugin.registerHelpers) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No registered helpers were found for this plugin'
|
||||
})
|
||||
}
|
||||
|
||||
const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
|
||||
if (!externalAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No external auths were found for this plugin'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.externalAuth = externalAuth
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const pluginStaticDirectoryValidator = [
|
||||
param('staticEndpoint')
|
||||
.custom(isSafePath),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listPluginsValidator = [
|
||||
query('pluginType')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isPluginTypeValid),
|
||||
query('uninstalled')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const installOrUpdatePluginValidator = [
|
||||
body('npmName')
|
||||
.optional()
|
||||
.custom(isNpmPluginNameValid),
|
||||
body('pluginVersion')
|
||||
.optional()
|
||||
.custom(isStableOrUnstableVersionValid),
|
||||
body('path')
|
||||
.optional()
|
||||
.custom(isSafePath),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: InstallOrUpdatePlugin = req.body
|
||||
if (!body.path && !body.npmName) {
|
||||
return res.fail({ message: 'Should have either a npmName or a path' })
|
||||
}
|
||||
if (body.pluginVersion && !body.npmName) {
|
||||
return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const uninstallPluginValidator = [
|
||||
body('npmName')
|
||||
.custom(isNpmPluginNameValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const existingPluginValidator = [
|
||||
param('npmName')
|
||||
.custom(isNpmPluginNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const plugin = await PluginModel.loadByNpmName(req.params.npmName)
|
||||
if (!plugin) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Plugin not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.plugin = plugin
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updatePluginSettingsValidator = [
|
||||
body('settings')
|
||||
.exists(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listAvailablePluginsValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.exists(),
|
||||
query('pluginType')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isPluginTypeValid),
|
||||
query('currentPeerTubeEngine')
|
||||
.optional()
|
||||
.custom(isStableOrUnstableVersionValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
|
||||
return res.fail({ message: 'Plugin index is not enabled' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
154
server/core/middlewares/validators/redundancy.ts
Normal file
154
server/core/middlewares/validators/redundancy.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies.js'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
toBooleanOrNull,
|
||||
toCompleteUUID,
|
||||
toIntOrNull
|
||||
} from '../../helpers/custom-validators/misc.js'
|
||||
import { isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy.js'
|
||||
import { ServerModel } from '../../models/server/server.js'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared/index.js'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
|
||||
const videoPlaylistRedundancyGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('streamingPlaylistType')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(exists),
|
||||
|
||||
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 (!canVideoBeFederated(video)) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above
|
||||
const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType)
|
||||
|
||||
if (!videoStreamingPlaylist) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist not found.'
|
||||
})
|
||||
}
|
||||
res.locals.videoStreamingPlaylist = videoStreamingPlaylist
|
||||
|
||||
const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
|
||||
if (!videoRedundancy) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video redundancy not found.'
|
||||
})
|
||||
}
|
||||
res.locals.videoRedundancy = videoRedundancy
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const updateServerRedundancyValidator = [
|
||||
param('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
body('redundancyAllowed')
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid redundancyAllowed boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const server = await ServerModel.loadByHost(req.params.host)
|
||||
|
||||
if (!server) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Server ${req.params.host} not found.`
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.server = server
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const listVideoRedundanciesValidator = [
|
||||
query('target')
|
||||
.custom(isVideoRedundancyTarget),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const addVideoRedundancyValidator = [
|
||||
body('videoId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
if (res.locals.onlyVideo.remote === false) {
|
||||
return res.fail({ message: 'Cannot create a redundancy on a local video' })
|
||||
}
|
||||
|
||||
if (res.locals.onlyVideo.isLive) {
|
||||
return res.fail({ message: 'Cannot create a redundancy of a live video' })
|
||||
}
|
||||
|
||||
const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
|
||||
if (alreadyExists) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'This video is already duplicated by your instance.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const removeVideoRedundancyValidator = [
|
||||
param('redundancyId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const redundancy = await VideoRedundancyModel.loadByIdWithVideo(forceNumber(req.params.redundancyId))
|
||||
if (!redundancy) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video redundancy not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoRedundancy = redundancy
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoPlaylistRedundancyGetValidator,
|
||||
updateServerRedundancyValidator,
|
||||
listVideoRedundanciesValidator,
|
||||
addVideoRedundancyValidator,
|
||||
removeVideoRedundancyValidator
|
||||
}
|
||||
36
server/core/middlewares/validators/resumable-upload.ts
Normal file
36
server/core/middlewares/validators/resumable-upload.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import express from 'express'
|
||||
import { body, header } from 'express-validator'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
|
||||
export const resumableInitValidator = [
|
||||
body('filename')
|
||||
.exists(),
|
||||
|
||||
header('x-upload-content-length')
|
||||
.isNumeric()
|
||||
.exists()
|
||||
.withMessage('Should specify the file length'),
|
||||
header('x-upload-content-type')
|
||||
.isString()
|
||||
.exists()
|
||||
.withMessage('Should specify the file mimetype'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking resumableInitValidator parameters and headers', {
|
||||
parameters: req.body,
|
||||
headers: req.headers
|
||||
})
|
||||
|
||||
if (areValidationErrors(req, res, { omitLog: true })) return cleanUpReqFiles(req)
|
||||
|
||||
res.locals.uploadVideoFileResumableMetadata = {
|
||||
mimetype: req.headers['x-upload-content-type'] as string,
|
||||
size: +req.headers['x-upload-content-length'],
|
||||
originalname: req.body.filename
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
3
server/core/middlewares/validators/runners/index.ts
Normal file
3
server/core/middlewares/validators/runners/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './jobs.js'
|
||||
export * from './registration-token.js'
|
||||
export * from './runners.js'
|
||||
60
server/core/middlewares/validators/runners/job-files.ts
Normal file
60
server/core/middlewares/validators/runners/job-files.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { basename } from 'path'
|
||||
import { isSafeFilename } from '@server/helpers/custom-validators/misc.js'
|
||||
import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobStudioTranscodingPayload } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
export const runnerJobGetVideoTranscodingFileValidator = [
|
||||
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 runnerJob = res.locals.runnerJob
|
||||
|
||||
if (runnerJob.privatePayload.videoUUID !== res.locals.videoAll.uuid) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Job is not associated to this video',
|
||||
tags: [ ...tags, res.locals.videoAll.uuid ]
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const runnerJobGetVideoStudioTaskFileValidator = [
|
||||
param('filename').custom(v => isSafeFilename(v)),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const filename = req.params.filename
|
||||
|
||||
const payload = res.locals.runnerJob.payload as RunnerJobStudioTranscodingPayload
|
||||
|
||||
const found = Array.isArray(payload?.tasks) && payload.tasks.some(t => {
|
||||
if (hasVideoStudioTaskFile(t)) {
|
||||
return basename(t.options.file) === filename
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!found) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'File is not associated to this edition task',
|
||||
tags: [ ...tags, res.locals.videoAll.uuid ]
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
200
server/core/middlewares/validators/runners/jobs.ts
Normal file
200
server/core/middlewares/validators/runners/jobs.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
RunnerJobSuccessBody,
|
||||
RunnerJobUpdateBody,
|
||||
ServerErrorCode
|
||||
} from '@peertube/peertube-models'
|
||||
import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isRunnerJobAbortReasonValid,
|
||||
isRunnerJobArrayOfStateValid,
|
||||
isRunnerJobErrorMessageValid,
|
||||
isRunnerJobProgressValid,
|
||||
isRunnerJobSuccessPayloadValid,
|
||||
isRunnerJobTokenValid,
|
||||
isRunnerJobUpdatePayloadValid
|
||||
} from '@server/helpers/custom-validators/runners/jobs.js'
|
||||
import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { runnerJobCanBeCancelled } from '@server/lib/runners/index.js'
|
||||
import { RunnerJobModel } from '@server/models/runner/runner-job.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
export const acceptRunnerJobValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (res.locals.runnerJob.state !== RunnerJobState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This runner job is not in pending state',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const abortRunnerJobValidator = [
|
||||
body('reason').custom(isRunnerJobAbortReasonValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateRunnerJobValidator = [
|
||||
body('progress').optional().custom(isRunnerJobProgressValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return cleanUpReqFiles(req)
|
||||
|
||||
const body = req.body as RunnerJobUpdateBody
|
||||
const job = res.locals.runnerJob
|
||||
|
||||
if (isRunnerJobUpdatePayloadValid(body.payload, job.type, req.files) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Payload is invalid',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const errorRunnerJobValidator = [
|
||||
body('message').custom(isRunnerJobErrorMessageValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const successRunnerJobValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const body = req.body as RunnerJobSuccessBody
|
||||
|
||||
if (isRunnerJobSuccessPayloadValid(body.payload, res.locals.runnerJob.type, req.files) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Payload is invalid',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const cancelRunnerJobValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const runnerJob = res.locals.runnerJob
|
||||
|
||||
if (runnerJobCanBeCancelled(runnerJob) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listRunnerJobsValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('stateOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isRunnerJobArrayOfStateValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const runnerJobGetValidator = [
|
||||
param('jobUUID').custom(isUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const runnerJob = await RunnerJobModel.loadWithRunner(req.params.jobUUID)
|
||||
|
||||
if (!runnerJob) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown runner job',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerJob = runnerJob
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function jobOfRunnerGetValidatorFactory (allowedStates: RunnerJobStateType[]) {
|
||||
return [
|
||||
param('jobUUID').custom(isUUIDValid),
|
||||
|
||||
body('runnerToken').custom(isRunnerTokenValid),
|
||||
body('jobToken').custom(isRunnerJobTokenValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return cleanUpReqFiles(req)
|
||||
|
||||
const runnerJob = await RunnerJobModel.loadByRunnerAndJobTokensWithRunner({
|
||||
uuid: req.params.jobUUID,
|
||||
runnerToken: req.body.runnerToken,
|
||||
jobToken: req.body.jobToken
|
||||
})
|
||||
|
||||
if (!runnerJob) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown runner job',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
if (!allowedStates.includes(runnerJob.state)) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
type: ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE,
|
||||
message: 'Job is not in "processing" state',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerJob = runnerJob
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { areValidationErrors } from '../shared/utils.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
const deleteRegistrationTokenValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const registrationToken = await RunnerRegistrationTokenModel.load(forceNumber(req.params.id))
|
||||
|
||||
if (!registrationToken) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration token not found',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerRegistrationToken = registrationToken
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
deleteRegistrationTokenValidator
|
||||
}
|
||||
108
server/core/middlewares/validators/runners/runners.ts
Normal file
108
server/core/middlewares/validators/runners/runners.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@peertube/peertube-models'
|
||||
import { isIdValid, isStableOrUnstableVersionValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isRunnerDescriptionValid,
|
||||
isRunnerNameValid,
|
||||
isRunnerRegistrationTokenValid,
|
||||
isRunnerTokenValid
|
||||
} from '@server/helpers/custom-validators/runners/runners.js'
|
||||
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
|
||||
import { RunnerModel } from '@server/models/runner/runner.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { areValidationErrors } from '../shared/utils.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
export const registerRunnerValidator = [
|
||||
body('registrationToken').custom(isRunnerRegistrationTokenValid),
|
||||
body('name').custom(isRunnerNameValid),
|
||||
body('description').optional().custom(isRunnerDescriptionValid),
|
||||
body('version').optional().custom(isStableOrUnstableVersionValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const body: RegisterRunnerBody = req.body
|
||||
|
||||
const runnerRegistrationToken = await RunnerRegistrationTokenModel.loadByRegistrationToken(body.registrationToken)
|
||||
|
||||
if (!runnerRegistrationToken) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration token is invalid',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
const existing = await RunnerModel.loadByName(body.name)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This runner name already exists on this instance',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerRegistrationToken = runnerRegistrationToken
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteRunnerValidator = [
|
||||
param('runnerId').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const runner = await RunnerModel.load(forceNumber(req.params.runnerId))
|
||||
|
||||
if (!runner) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Runner not found',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runner = runner
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const getRunnerFromTokenValidator = [
|
||||
body('runnerToken').custom(isRunnerTokenValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const runner = await RunnerModel.loadByToken(req.body.runnerToken)
|
||||
|
||||
if (!runner) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown runner token',
|
||||
type: ServerErrorCode.UNKNOWN_RUNNER_TOKEN,
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runner = runner
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const requestRunnerJobValidator = [
|
||||
body('version').optional().custom(isStableOrUnstableVersionValid),
|
||||
body('jobTypes').optional().isArray(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
108
server/core/middlewares/validators/search.ts
Normal file
108
server/core/middlewares/validators/search.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { isSearchTargetValid } from '@server/helpers/custom-validators/search.js'
|
||||
import { isHostValid } from '@server/helpers/custom-validators/servers.js'
|
||||
import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const videosSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
query('startDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
|
||||
|
||||
query('originallyPublishedStartDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a published start date that conforms to ISO 8601'),
|
||||
query('originallyPublishedEndDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'),
|
||||
|
||||
query('durationMin')
|
||||
.optional()
|
||||
.isInt(),
|
||||
query('durationMax')
|
||||
.optional()
|
||||
.isInt(),
|
||||
|
||||
query('uuids')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toCompleteUUIDs)
|
||||
.custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
|
||||
|
||||
query('searchTarget')
|
||||
.optional()
|
||||
.custom(isSearchTargetValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoChannelsListSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
query('host')
|
||||
.optional()
|
||||
.custom(isHostValid),
|
||||
|
||||
query('searchTarget')
|
||||
.optional()
|
||||
.custom(isSearchTargetValid),
|
||||
|
||||
query('handles')
|
||||
.optional()
|
||||
.toArray()
|
||||
.custom(isNotEmptyStringArray).withMessage('Should have valid array of handles'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistsListSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
query('host')
|
||||
.optional()
|
||||
.custom(isHostValid),
|
||||
|
||||
query('searchTarget')
|
||||
.optional()
|
||||
.custom(isSearchTargetValid),
|
||||
|
||||
query('uuids')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toCompleteUUIDs)
|
||||
.custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosSearchValidator,
|
||||
videoChannelsListSearchValidator,
|
||||
videoPlaylistsListSearchValidator
|
||||
}
|
||||
75
server/core/middlewares/validators/server.ts
Normal file
75
server/core/middlewares/validators/server.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers.js'
|
||||
import { isUserDisplayNameValid } from '../../helpers/custom-validators/users.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { CONFIG, isEmailEnabled } from '../../initializers/config.js'
|
||||
import { Redis } from '../../lib/redis.js'
|
||||
import { ServerModel } from '../../models/server/server.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const serverGetValidator = [
|
||||
body('host').custom(isHostValid).withMessage('Should have a valid host'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const server = await ServerModel.loadByHost(req.body.host)
|
||||
if (!server) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Server host not found.'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.server = server
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const contactAdministratorValidator = [
|
||||
body('fromName')
|
||||
.custom(isUserDisplayNameValid),
|
||||
body('fromEmail')
|
||||
.isEmail(),
|
||||
body('body')
|
||||
.custom(isValidContactBody),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.CONTACT_FORM.ENABLED === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Contact form is not enabled on this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
if (isEmailEnabled() === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'SMTP is not configured on this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
|
||||
logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'You already sent a contact form recently.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
serverGetValidator,
|
||||
contactAdministratorValidator
|
||||
}
|
||||
26
server/core/middlewares/validators/shared/abuses.ts
Normal file
26
server/core/middlewares/validators/shared/abuses.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Response } from 'express'
|
||||
import { AbuseModel } from '@server/models/abuse/abuse.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
|
||||
async function doesAbuseExist (abuseId: number | string, res: Response) {
|
||||
const abuse = await AbuseModel.loadByIdWithReporter(forceNumber(abuseId))
|
||||
|
||||
if (!abuse) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Abuse not found'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.abuse = abuse
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesAbuseExist
|
||||
}
|
||||
76
server/core/middlewares/validators/shared/accounts.ts
Normal file
76
server/core/middlewares/validators/shared/accounts.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { MAccountDefault } from '@server/types/models/index.js'
|
||||
import { Request, Response } from 'express'
|
||||
import { checkCanManageAccount } from './users.js'
|
||||
|
||||
export async function doesAccountIdExist (options: {
|
||||
id: string | number
|
||||
req: Request
|
||||
res: Response
|
||||
checkCanManage: boolean // Also check the user can manage the account
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}) {
|
||||
const { id, req, res, checkIsLocal, checkCanManage } = options
|
||||
|
||||
const account = await AccountModel.load(forceNumber(id))
|
||||
|
||||
return doesAccountExist({ account, req, res, checkIsLocal, checkCanManage })
|
||||
}
|
||||
|
||||
export async function doesAccountHandleExist (options: {
|
||||
handle: string
|
||||
req: Request
|
||||
res: Response
|
||||
checkCanManage: boolean // Also check the user can manage the account
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}) {
|
||||
const { handle, req, res, checkIsLocal, checkCanManage } = options
|
||||
|
||||
const account = await AccountModel.loadByHandle(handle)
|
||||
|
||||
return doesAccountExist({ account, req, res, checkIsLocal, checkCanManage })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function doesAccountExist (options: {
|
||||
account: MAccountDefault
|
||||
req: Request
|
||||
res: Response
|
||||
checkCanManage: boolean
|
||||
checkIsLocal: boolean
|
||||
}) {
|
||||
const { account, req, res, checkIsLocal, checkCanManage } = options
|
||||
|
||||
if (!account) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Account not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkCanManage) {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!checkCanManageAccount({ account, user, req, res, specialRight: UserRight.MANAGE_USERS })) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (checkIsLocal && account.Actor.isLocal() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This account is not owned by the platform')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.account = account
|
||||
return true
|
||||
}
|
||||
19
server/core/middlewares/validators/shared/images.ts
Normal file
19
server/core/middlewares/validators/shared/images.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { isActorImageFile } from '@server/helpers/custom-validators/actor-images.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { areValidationErrors } from './utils.js'
|
||||
|
||||
export const updateActorImageValidatorFactory = (fieldname: string) => [
|
||||
body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage(
|
||||
'This file is not supported or too large. Please, make sure it is of the following type : ' +
|
||||
CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
15
server/core/middlewares/validators/shared/index.ts
Normal file
15
server/core/middlewares/validators/shared/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from './abuses.js'
|
||||
export * from './accounts.js'
|
||||
export * from './images.js'
|
||||
export * from './users.js'
|
||||
export * from './utils.js'
|
||||
export * from './video-blacklists.js'
|
||||
export * from './video-captions.js'
|
||||
export * from './video-channels.js'
|
||||
export * from './video-channel-syncs.js'
|
||||
export * from './video-comments.js'
|
||||
export * from './video-imports.js'
|
||||
export * from './video-ownerships.js'
|
||||
export * from './video-playlists.js'
|
||||
export * from './video-passwords.js'
|
||||
export * from './videos.js'
|
||||
150
server/core/middlewares/validators/shared/users.ts
Normal file
150
server/core/middlewares/validators/shared/users.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MAccountId, MUserAccountId, MUserDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
|
||||
export function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
|
||||
}
|
||||
|
||||
export function checkUserEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(
|
||||
async () => {
|
||||
const users = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(users, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export function checkUserPendingEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(
|
||||
async () => {
|
||||
const users = await UserModel.loadByPendingEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(users, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export async function checkUsernameOrEmailDoNotAlreadyExist (options: {
|
||||
username: string
|
||||
email: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { username, email, req, res } = options
|
||||
|
||||
const existingUser = await UserModel.loadByUsernameOrEmailCaseInsensitive(username)
|
||||
const existingEmail = await UserModel.loadByUsernameOrEmailCaseInsensitive(email)
|
||||
|
||||
if (existingUser.length > 0 || existingEmail.length > 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('User with this username or email already exists.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const actor = await loadReservedActorName(username)
|
||||
if (actor) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Another actor (account/channel) with this name on this instance already exists or has already existed.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkEmailDoesNotAlreadyExist (options: {
|
||||
email: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { email, req, res } = options
|
||||
const user = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
if (user.length !== 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('User with this email already exists.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
|
||||
const user = await finder()
|
||||
|
||||
if (!user) {
|
||||
if (abortResponse === true) {
|
||||
res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkCanManageAccount (options: {
|
||||
user: MUserAccountId
|
||||
account: MAccountId
|
||||
specialRight: UserRightType
|
||||
req: express.Request
|
||||
res: express.Response | null
|
||||
}) {
|
||||
const { user, account, specialRight, res, req } = options
|
||||
|
||||
if (!user) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('Authentication is required')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (account.id === user.Account.id) return true
|
||||
if (specialRight && user.hasRight(specialRight) === true) return true
|
||||
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Only a user with sufficient right can manage this account resource.')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function doesUserFeedTokenCorrespond (options: {
|
||||
id: number
|
||||
token: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { id, token, req, res } = options
|
||||
const user = await UserModel.loadByIdWithChannels(forceNumber(id))
|
||||
|
||||
if (token !== user.feedToken) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('User and token mismatch')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
}
|
||||
69
server/core/middlewares/validators/shared/utils.ts
Normal file
69
server/core/middlewares/validators/shared/utils.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import express from 'express'
|
||||
import { param, validationResult } from 'express-validator'
|
||||
import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
|
||||
function areValidationErrors (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
options: {
|
||||
omitLog?: boolean
|
||||
omitBodyLog?: boolean
|
||||
tags?: (number | string)[]
|
||||
} = {}) {
|
||||
const { omitLog = false, omitBodyLog = false, tags = [] } = options
|
||||
|
||||
if (!omitLog) {
|
||||
logger.debug(
|
||||
'Checking %s - %s parameters',
|
||||
req.method, req.originalUrl,
|
||||
{
|
||||
body: omitBodyLog
|
||||
? 'omitted'
|
||||
: req.body,
|
||||
params: req.params,
|
||||
query: req.query,
|
||||
files: req.files,
|
||||
tags
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const errors = validationResult(req)
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
|
||||
|
||||
res.fail({
|
||||
message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '),
|
||||
instance: req.originalUrl,
|
||||
data: {
|
||||
'invalid-params': errors.mapped()
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isValidVideoIdParam (paramName: string) {
|
||||
return param(paramName)
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id (id, short UUID or UUID)')
|
||||
}
|
||||
|
||||
function isValidPlaylistIdParam (paramName: string) {
|
||||
return param(paramName)
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id (id, short UUID or UUID)')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
areValidationErrors,
|
||||
isValidVideoIdParam,
|
||||
isValidPlaylistIdParam
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Response } from 'express'
|
||||
import { VideoBlacklistModel } from '@server/models/video/video-blacklist.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoBlacklistExist (videoId: number, res: Response) {
|
||||
const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
|
||||
|
||||
if (videoBlacklist === null) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Blacklisted video not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoBlacklist = videoBlacklist
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoBlacklistExist
|
||||
}
|
||||
25
server/core/middlewares/validators/shared/video-captions.ts
Normal file
25
server/core/middlewares/validators/shared/video-captions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Response } from 'express'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { MVideoId } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
|
||||
const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
|
||||
|
||||
if (!videoCaption) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video caption not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCaption = videoCaption
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoCaptionExist
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import express from 'express'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoChannelSyncIdExist (id: number, res: express.Response) {
|
||||
const sync = await VideoChannelSyncModel.loadWithChannel(+id)
|
||||
|
||||
if (!sync) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video channel sync not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannelSync = sync
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoChannelSyncIdExist
|
||||
}
|
||||
124
server/core/middlewares/validators/shared/video-channels.ts
Normal file
124
server/core/middlewares/validators/shared/video-channels.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { HttpStatusCode, UserRight, UserRightType } from '@peertube/peertube-models'
|
||||
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { MChannelBannerAccountDefault, MChannelUserId, MUserAccountId } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { checkCanManageAccount } from './users.js'
|
||||
|
||||
type CommonOptions = {
|
||||
checkCanManage: boolean // Also check the user can manage the account
|
||||
checkIsOwner: boolean // Also check this is the owner of the channel
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
specialRight?: UserRightType
|
||||
}
|
||||
|
||||
export async function doesChannelIdExist (
|
||||
options: CommonOptions & {
|
||||
id: number
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}
|
||||
) {
|
||||
const { id, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight } = options
|
||||
|
||||
const channel = await VideoChannelModel.loadAndPopulateAccount(+id)
|
||||
|
||||
return processVideoChannelExist({ channel, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight })
|
||||
}
|
||||
|
||||
export async function doesChannelHandleExist (
|
||||
options: CommonOptions & {
|
||||
handle: string
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}
|
||||
) {
|
||||
const { handle, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight } = options
|
||||
|
||||
const channel = await VideoChannelModel.loadByHandleAndPopulateAccount(handle)
|
||||
|
||||
return processVideoChannelExist({ channel, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight })
|
||||
}
|
||||
|
||||
export async function checkCanManageChannel (
|
||||
options: CommonOptions & {
|
||||
user: MUserAccountId
|
||||
channel: MChannelUserId
|
||||
}
|
||||
) {
|
||||
const { channel, user, req, res, checkCanManage, checkIsOwner, specialRight = UserRight.MANAGE_ANY_VIDEO_CHANNEL } = options
|
||||
|
||||
if (!channel) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video channel not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkIsOwner || checkCanManage) {
|
||||
if (!user) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('Authentication is required')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const isOwner = checkCanManageAccount({
|
||||
account: channel.Account,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
specialRight
|
||||
})
|
||||
|
||||
if (!isOwner) {
|
||||
if (checkIsOwner) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This user has not owner rights on this channel')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkCanManage && !await VideoChannelCollaboratorModel.isCollaborator({ user, channel })) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This user cannot manage this channel')
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function processVideoChannelExist (
|
||||
options: CommonOptions & {
|
||||
channel: MChannelBannerAccountDefault
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}
|
||||
) {
|
||||
const { channel, req, res, checkCanManage, checkIsLocal, checkIsOwner, specialRight = UserRight.MANAGE_ANY_VIDEO_CHANNEL } = options
|
||||
|
||||
const user = res.locals.oauth?.token.User
|
||||
if (!await checkCanManageChannel({ channel, user, req, res, checkCanManage, checkIsOwner, specialRight })) return false
|
||||
|
||||
if (checkIsLocal && channel.Actor.isLocal() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('The channel must be local.')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannel = channel
|
||||
return true
|
||||
}
|
||||
80
server/core/middlewares/validators/shared/video-comments.ts
Normal file
80
server/core/middlewares/validators/shared/video-comments.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import express from 'express'
|
||||
import { VideoCommentModel } from '@server/models/video/video-comment.js'
|
||||
import { MVideoId } from '@server/types/models/index.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoComment = await VideoCommentModel.loadById(id)
|
||||
|
||||
if (!videoComment) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video comment thread not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.videoId !== video.id) {
|
||||
res.fail({
|
||||
type: ServerErrorCode.COMMENT_NOT_ASSOCIATED_TO_VIDEO,
|
||||
message: 'Video comment is not associated to this video.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.inReplyToCommentId !== null) {
|
||||
res.fail({ message: 'Video comment is not a thread.' })
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCommentThread = videoComment
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
|
||||
|
||||
if (!videoComment) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video comment thread not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.videoId !== video.id) {
|
||||
res.fail({
|
||||
type: ServerErrorCode.COMMENT_NOT_ASSOCIATED_TO_VIDEO,
|
||||
message: 'Video comment is not associated to this video.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCommentFull = videoComment
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesCommentIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
|
||||
|
||||
if (!videoComment) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video comment thread not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCommentFull = videoComment
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
doesVideoCommentThreadExist,
|
||||
doesVideoCommentExist,
|
||||
doesCommentIdExist
|
||||
}
|
||||
22
server/core/middlewares/validators/shared/video-imports.ts
Normal file
22
server/core/middlewares/validators/shared/video-imports.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import express from 'express'
|
||||
import { VideoImportModel } from '@server/models/video/video-import.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoImportExist (id: number, res: express.Response) {
|
||||
const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
|
||||
|
||||
if (!videoImport) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video import not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoImport = videoImport
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
doesVideoImportExist
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import express from 'express'
|
||||
import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
export async function doesChangeVideoOwnershipExist (idArg: number | string, req: express.Request, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
|
||||
|
||||
if (!videoChangeOwnership) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video ownership change not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChangeOwnership = videoChangeOwnership
|
||||
|
||||
return true
|
||||
}
|
||||
72
server/core/middlewares/validators/shared/video-passwords.ts
Normal file
72
server/core/middlewares/validators/shared/video-passwords.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { MUserAccountId, MVideoAccountLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { header } from 'express-validator'
|
||||
import { checkCanManageVideo } from './videos.js'
|
||||
|
||||
export function isValidVideoPasswordHeader () {
|
||||
return header('x-peertube-video-password')
|
||||
.optional()
|
||||
.isString()
|
||||
}
|
||||
|
||||
export function checkVideoIsPasswordProtected (req: express.Request, res: express.Response) {
|
||||
const video = getVideoWithAttributes(res)
|
||||
if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Video is not password protected')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function doesVideoPasswordExist (options: {
|
||||
id: number | string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { req, res } = options
|
||||
|
||||
const video = getVideoWithAttributes(res)
|
||||
const id = forceNumber(options.id)
|
||||
const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id })
|
||||
|
||||
if (!videoPassword) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video password not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoPassword = videoPassword
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkCanDeleteVideoPassword (options: {
|
||||
user: MUserAccountId
|
||||
video: MVideoAccountLight
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, video, req, res } = options
|
||||
|
||||
const passwordCount = await VideoPasswordModel.countByVideoId(video.id)
|
||||
|
||||
if (passwordCount <= 1) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot delete the last password of the protected video')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return checkCanManageVideo({ user, video, right: UserRight.UPDATE_ANY_VIDEO, req, res, checkIsLocal: true, checkIsOwner: false })
|
||||
}
|
||||
42
server/core/middlewares/validators/shared/video-playlists.ts
Normal file
42
server/core/middlewares/validators/shared/video-playlists.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import express from 'express'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { MVideoPlaylist } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
export type VideoPlaylistFetchType = 'summary' | 'all'
|
||||
|
||||
export async function doesVideoPlaylistExist (options: {
|
||||
id: number | string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
fetchType?: VideoPlaylistFetchType
|
||||
}) {
|
||||
const { id, req, res, fetchType = 'summary' } = options
|
||||
|
||||
if (fetchType === 'summary') {
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
|
||||
res.locals.videoPlaylistSummary = videoPlaylist
|
||||
|
||||
return handleVideoPlaylist(videoPlaylist, req, res)
|
||||
}
|
||||
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
|
||||
res.locals.videoPlaylistFull = videoPlaylist
|
||||
|
||||
return handleVideoPlaylist(videoPlaylist, req, res)
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function handleVideoPlaylist (playlist: MVideoPlaylist, req: express.Request, res: express.Response) {
|
||||
if (!playlist) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video playlist not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
364
server/core/middlewares/validators/shared/videos.ts
Normal file
364
server/core/middlewares/validators/shared/videos.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, UserRightType, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
import { VideoLoadType, loadVideo } from '@server/lib/model-loaders/index.js'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { VideoTokensManager } from '@server/lib/video-tokens-manager.js'
|
||||
import { authenticateOrFail } from '@server/middlewares/auth.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import {
|
||||
MUserAccountId,
|
||||
MUserAccountUrl,
|
||||
MUserId,
|
||||
MVideo,
|
||||
MVideoAccountLight,
|
||||
MVideoFormattableDetails,
|
||||
MVideoFullLight,
|
||||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnailBlacklist,
|
||||
MVideoUUID,
|
||||
MVideoWithRights
|
||||
} from '@server/types/models/index.js'
|
||||
import { Request, Response } from 'express'
|
||||
import { checkCanManageChannel } from './video-channels.js'
|
||||
|
||||
export async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
|
||||
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||
|
||||
const video = await loadVideo(id, fetchType, userId)
|
||||
|
||||
if (!video) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video not found'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
switch (fetchType) {
|
||||
case 'for-api':
|
||||
res.locals.videoAPI = video as MVideoFormattableDetails
|
||||
break
|
||||
|
||||
case 'all':
|
||||
res.locals.videoAll = video as MVideoFullLight
|
||||
break
|
||||
|
||||
case 'unsafe-only-immutable-attributes':
|
||||
res.locals.onlyImmutableVideo = video as MVideoImmutable
|
||||
break
|
||||
|
||||
case 'id':
|
||||
res.locals.videoId = video as MVideoId
|
||||
break
|
||||
|
||||
case 'only-video-and-blacklist':
|
||||
res.locals.onlyVideo = video as MVideoThumbnailBlacklist
|
||||
break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
|
||||
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
|
||||
res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanSeeVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
video: MVideo
|
||||
videoFileToken?: {
|
||||
user: MUserAccountUrl
|
||||
}
|
||||
}) {
|
||||
const { req, res, video, videoFileToken, paramId } = options
|
||||
|
||||
if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) {
|
||||
return checkCanSeeUserAuthVideo({ req, res, video, videoFileTokenUser: videoFileToken?.user })
|
||||
}
|
||||
|
||||
if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
|
||||
return checkCanSeePasswordProtectedVideo({ req, res, video, hasVideoFileToken: !!videoFileToken })
|
||||
}
|
||||
|
||||
if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) {
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error('Unknown video privacy when checking video right ' + video.url)
|
||||
}
|
||||
|
||||
async function checkCanSeeUserAuthVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideoId | MVideoWithRights
|
||||
videoFileTokenUser?: MUserAccountUrl
|
||||
}) {
|
||||
const { req, res, video } = options
|
||||
|
||||
const fail = () => {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot fetch information of private/internal/blocked video')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let user = options.videoFileTokenUser
|
||||
if (!user) {
|
||||
if (!await authenticateOrFail({ req, res })) return false
|
||||
|
||||
user = res.locals.oauth.token.User
|
||||
}
|
||||
|
||||
const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
|
||||
|
||||
const privacy = videoWithRights.privacy
|
||||
|
||||
if (privacy === VideoPrivacy.INTERNAL) {
|
||||
// We know we have a user
|
||||
return true
|
||||
}
|
||||
|
||||
if (videoWithRights.isBlacklisted()) {
|
||||
if (await canUserManageProtectedVideo({ user, req, video: videoWithRights, right: UserRight.MANAGE_VIDEO_BLACKLIST })) return true
|
||||
|
||||
return fail()
|
||||
}
|
||||
|
||||
if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
|
||||
if (await canUserManageProtectedVideo({ user, req, video: videoWithRights, right: UserRight.SEE_ALL_VIDEOS })) return true
|
||||
|
||||
return fail()
|
||||
}
|
||||
|
||||
// Should not happen
|
||||
return fail()
|
||||
}
|
||||
|
||||
async function checkCanSeePasswordProtectedVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideo
|
||||
hasVideoFileToken: boolean
|
||||
}) {
|
||||
const { req, res, video, hasVideoFileToken } = options
|
||||
|
||||
const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
|
||||
|
||||
const videoPassword = req.header('x-peertube-video-password')
|
||||
|
||||
if (!exists(videoPassword)) {
|
||||
if (hasVideoFileToken === true) return true
|
||||
|
||||
const errorMessage = req.t('Please provide a password to access this password protected video')
|
||||
const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD
|
||||
|
||||
if (!await authenticateOrFail({ req, res, errorMessage, errorStatus: HttpStatusCode.UNAUTHORIZED_401, errorType })) return false
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (await canUserManageProtectedVideo({ user, req, video: videoWithRights, right: UserRight.SEE_ALL_VIDEOS })) return true
|
||||
|
||||
res.fail({
|
||||
status: user
|
||||
? HttpStatusCode.FORBIDDEN_403
|
||||
: HttpStatusCode.UNAUTHORIZED_401,
|
||||
|
||||
type: errorType,
|
||||
message: errorMessage
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD,
|
||||
message: req.t('Incorrect video password. Access to the video is denied')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function canUserManageProtectedVideo (options: {
|
||||
req: Request
|
||||
user: MUserAccountId
|
||||
video: MVideoWithRights | MVideoAccountLight
|
||||
right: UserRightType
|
||||
}) {
|
||||
const { user, video, right, req } = options
|
||||
|
||||
return checkCanManageChannel({
|
||||
channel: video.VideoChannel,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
checkCanManage: true,
|
||||
checkIsOwner: false,
|
||||
specialRight: right
|
||||
})
|
||||
}
|
||||
|
||||
async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
||||
const channel = video.VideoChannel
|
||||
|
||||
if (channel?.id && channel?.Account?.userId && channel?.Account?.id) return video
|
||||
|
||||
return VideoModel.loadFull(video.id)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanAccessVideoStaticFiles (options: {
|
||||
video: MVideo
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
}): Promise<boolean> {
|
||||
const { video, req, res } = options
|
||||
|
||||
if (!checkVideoTokenIfNeeded(req, res, video)) return false
|
||||
|
||||
return checkCanSeeVideo({ ...options, videoFileToken: res.locals.videoFileToken })
|
||||
}
|
||||
|
||||
export async function checkCanAccessVideoSourceFile (options: {
|
||||
videoId: number
|
||||
req: Request
|
||||
res: Response
|
||||
}): Promise<boolean> {
|
||||
const { req, res, videoId } = options
|
||||
|
||||
const video = await VideoModel.loadFull(videoId)
|
||||
|
||||
let user = res.locals.oauth?.token.User
|
||||
if (!user) {
|
||||
if (!checkVideoTokenIfNeeded(req, res, video)) return false
|
||||
|
||||
user = res.locals.videoFileToken?.user
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
res.fail({ status: HttpStatusCode.UNAUTHORIZED_401, message: req.t('Authentication is required to access the video source file') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (await canUserManageProtectedVideo({ user, req, video, right: UserRight.SEE_ALL_VIDEOS }) === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
function checkVideoTokenIfNeeded (req: Request, res: Response, video: MVideoUUID) {
|
||||
const videoFileToken = req.query.videoFileToken
|
||||
|
||||
if (videoFileToken) {
|
||||
if (VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
||||
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
||||
|
||||
res.locals.videoFileToken = { user }
|
||||
} else {
|
||||
res.fail({
|
||||
message: req.t('Invalid video file token'),
|
||||
status: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanManageVideo (options: {
|
||||
user: MUserAccountId
|
||||
video: MVideoAccountLight
|
||||
right: UserRightType
|
||||
req: Request
|
||||
res: Response | null
|
||||
checkIsLocal: boolean
|
||||
checkIsOwner: boolean
|
||||
}) {
|
||||
const { user, video, right, req, res, checkIsLocal, checkIsOwner } = options
|
||||
|
||||
if (!user) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('Authentication is required.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkIsLocal && video.isLocal() === false) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage a video of another server.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
!await checkCanManageChannel({
|
||||
channel: video.VideoChannel,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
checkCanManage: true,
|
||||
checkIsOwner,
|
||||
specialRight: right
|
||||
})
|
||||
) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage a video of another user.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type NewType = MUserId
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkUserQuota (options: {
|
||||
user: NewType
|
||||
videoFileSize: number
|
||||
req: Request
|
||||
res: Response
|
||||
}) {
|
||||
const { user, videoFileSize, req, res } = options
|
||||
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
message: req.t('The user video quota is exceeded with this video'),
|
||||
type: ServerErrorCode.QUOTA_REACHED
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
73
server/core/middlewares/validators/sort.ts
Normal file
73
server/core/middlewares/validators/sort.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { SORTABLE_COLUMNS } from '../../initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
export const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
|
||||
export const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
|
||||
export const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
|
||||
export const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
|
||||
export const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS)
|
||||
export const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS)
|
||||
export const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH)
|
||||
export const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
|
||||
export const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH)
|
||||
export const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS)
|
||||
export const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
|
||||
export const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES)
|
||||
export const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS)
|
||||
export const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS)
|
||||
export const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS)
|
||||
export const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING)
|
||||
export const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
|
||||
export const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
|
||||
export const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
|
||||
export const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
|
||||
export const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
|
||||
export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
|
||||
export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
|
||||
export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
|
||||
export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS)
|
||||
export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS)
|
||||
|
||||
export const watchedWordsListsSortValidator = checkSortFactory(SORTABLE_COLUMNS.WATCHED_WORDS_LISTS)
|
||||
|
||||
export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
|
||||
export const videoChannelFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
|
||||
export const videoChannelActivitiesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_ACTIVITIES)
|
||||
|
||||
export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS)
|
||||
|
||||
export const tokenSessionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.TOKEN_SESSIONS)
|
||||
|
||||
export const runnersSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNERS)
|
||||
export const runnerRegistrationTokensSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_REGISTRATION_TOKENS)
|
||||
export const runnerJobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_JOBS)
|
||||
|
||||
export const liveSessionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.LIVE_SESSIONS)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkSortFactory (columns: string[], tags: string[] = []) {
|
||||
return checkSort(createSortableColumns(columns), tags)
|
||||
}
|
||||
|
||||
function checkSort (sortableColumns: string[], tags: string[] = []) {
|
||||
return [
|
||||
query('sort')
|
||||
.optional()
|
||||
.isIn(sortableColumns),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function createSortableColumns (sortableColumns: string[]) {
|
||||
const sortableColumnDesc = sortableColumns.map(sortableColumn => '-' + sortableColumn)
|
||||
|
||||
return sortableColumns.concat(sortableColumnDesc)
|
||||
}
|
||||
204
server/core/middlewares/validators/static.ts
Normal file
204
server/core/middlewares/validators/static.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
exists,
|
||||
isSafeFilename,
|
||||
isSafePeerTubeFilenameWithoutExtension,
|
||||
isUUIDValid,
|
||||
toBooleanOrNull
|
||||
} from '@server/helpers/custom-validators/misc.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { LRU_CACHE } from '@server/initializers/constants.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MStreamingPlaylist, MVideoFile, MVideoThumbnailBlacklist } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { basename } from 'path'
|
||||
import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
|
||||
|
||||
type LRUValue = {
|
||||
allowed: boolean
|
||||
video?: MVideoThumbnailBlacklist
|
||||
file?: MVideoFile
|
||||
playlist?: MStreamingPlaylist
|
||||
}
|
||||
|
||||
const staticFileTokenBypass = new LRUCache<string, LRUValue>({
|
||||
max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
|
||||
ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
|
||||
})
|
||||
|
||||
export const ensureCanAccessVideoPrivateWebVideoFiles = [
|
||||
query('videoFileToken').optional().custom(exists),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const token = extractTokenOrDie(req, res)
|
||||
if (!token) return
|
||||
|
||||
const cacheKey = token + '-' + req.originalUrl
|
||||
|
||||
if (staticFileTokenBypass.has(cacheKey)) {
|
||||
const { allowed, file, video } = staticFileTokenBypass.get(cacheKey)
|
||||
|
||||
if (allowed === true) {
|
||||
res.locals.onlyVideo = video
|
||||
res.locals.videoFile = file
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
const result = await isWebVideoAllowed(req, res)
|
||||
|
||||
staticFileTokenBypass.set(cacheKey, result)
|
||||
|
||||
if (result.allowed !== true) return
|
||||
|
||||
res.locals.onlyVideo = result.video
|
||||
res.locals.videoFile = result.file
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const privateM3U8PlaylistValidator = [
|
||||
param('videoUUID')
|
||||
.custom(isUUIDValid),
|
||||
|
||||
param('playlistNameWithoutExtension')
|
||||
.custom(v => isSafePeerTubeFilenameWithoutExtension(v)),
|
||||
|
||||
query('reinjectVideoFileToken')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const privateHLSFileValidator = [
|
||||
param('videoUUID')
|
||||
.custom(isUUIDValid),
|
||||
|
||||
param('filename')
|
||||
.custom(v => isSafeFilename(v)),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const ensureCanAccessPrivateVideoHLSFiles = [
|
||||
query('videoFileToken')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const videoUUID = req.params.videoUUID
|
||||
|
||||
const token = extractTokenOrDie(req, res)
|
||||
if (!token) return
|
||||
|
||||
const cacheKey = token + '-' + videoUUID
|
||||
|
||||
if (staticFileTokenBypass.has(cacheKey)) {
|
||||
const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey)
|
||||
|
||||
if (allowed === true) {
|
||||
res.locals.onlyVideo = video
|
||||
res.locals.videoFile = file
|
||||
res.locals.videoStreamingPlaylist = playlist
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
const result = await isHLSAllowed(req, res, videoUUID)
|
||||
|
||||
staticFileTokenBypass.set(cacheKey, result)
|
||||
|
||||
if (result.allowed !== true) return
|
||||
|
||||
res.locals.onlyVideo = result.video
|
||||
res.locals.videoFile = result.file
|
||||
res.locals.videoStreamingPlaylist = result.playlist
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function isWebVideoAllowed (req: express.Request, res: express.Response) {
|
||||
const filename = basename(req.path)
|
||||
|
||||
const file = await VideoFileModel.loadWithVideoByFilename(filename)
|
||||
if (!file) {
|
||||
logger.debug('Unknown static file %s to serve', req.originalUrl, { filename })
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return { allowed: false }
|
||||
}
|
||||
|
||||
const video = await VideoModel.loadWithBlacklist(file.getVideo().id)
|
||||
|
||||
return {
|
||||
file,
|
||||
video,
|
||||
allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
|
||||
}
|
||||
}
|
||||
|
||||
async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) {
|
||||
const filename = basename(req.path)
|
||||
|
||||
const video = await VideoModel.loadAndPopulateAccountAndFiles(videoUUID)
|
||||
|
||||
if (!video) {
|
||||
logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID })
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return { allowed: false }
|
||||
}
|
||||
|
||||
const file = await VideoFileModel.loadByFilename(filename)
|
||||
|
||||
return {
|
||||
file,
|
||||
video,
|
||||
playlist: video.getHLSPlaylist(),
|
||||
allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
|
||||
}
|
||||
}
|
||||
|
||||
function extractTokenOrDie (req: express.Request, res: express.Response) {
|
||||
const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken
|
||||
|
||||
if (!token) {
|
||||
return res.fail({
|
||||
message: 'Video password header, video file token query parameter and bearer token are all missing', //
|
||||
status: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
40
server/core/middlewares/validators/themes.ts
Normal file
40
server/core/middlewares/validators/themes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isSafePath, isStableOrUnstableVersionValid } from '../../helpers/custom-validators/misc.js'
|
||||
import { isPluginNameValid } from '../../helpers/custom-validators/plugins.js'
|
||||
import { PluginManager } from '../../lib/plugins/plugin-manager.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
export const serveThemeCSSValidator = [
|
||||
param('themeName')
|
||||
.custom(isPluginNameValid),
|
||||
param('themeVersion')
|
||||
.custom(isStableOrUnstableVersionValid),
|
||||
param('staticEndpoint')
|
||||
.custom(isSafePath),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
|
||||
|
||||
if (!theme || theme.version !== req.params.themeVersion) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion
|
||||
})
|
||||
}
|
||||
|
||||
if (theme.css.includes(req.params.staticEndpoint) === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No static endpoint was found for this theme'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.registeredPlugin = theme
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
46
server/core/middlewares/validators/token.ts
Normal file
46
server/core/middlewares/validators/token.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { OAuthTokenModel } from '@server/models/oauth/oauth-token.js'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { checkCanManageAccount, checkUserIdExist } from './shared/users.js'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
export const manageTokenSessionsValidator = [
|
||||
param('userId').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await checkUserIdExist(req.params.userId, res)) return
|
||||
|
||||
const authUser = res.locals.oauth.token.User
|
||||
const targetUser = res.locals.user
|
||||
|
||||
if (!checkCanManageAccount({ account: targetUser.Account, user: authUser, req, res, specialRight: UserRight.MANAGE_USERS })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const revokeTokenSessionValidator = [
|
||||
param('tokenSessionId').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const targetUser = res.locals.user
|
||||
const session = await OAuthTokenModel.loadSessionOf({ id: +req.params.tokenSessionId, userId: targetUser.id })
|
||||
|
||||
if (!session) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('The token session does not exist or does not belong to the user.')
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.tokenSession = session
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
81
server/core/middlewares/validators/two-factor.ts
Normal file
81
server/core/middlewares/validators/two-factor.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { exists, isIdValid } from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors, checkUserIdExist } from './shared/index.js'
|
||||
|
||||
const requestOrConfirmTwoFactorValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return
|
||||
|
||||
if (res.locals.user.otpSecret) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Two factor is already enabled.`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const confirmTwoFactorValidator = [
|
||||
body('requestToken').custom(exists),
|
||||
body('otpToken').custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const disableTwoFactorValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return
|
||||
|
||||
if (!res.locals.user.otpSecret) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Two factor is already disabled.`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
requestOrConfirmTwoFactorValidator,
|
||||
confirmTwoFactorValidator,
|
||||
disableTwoFactorValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanEnableOrDisableTwoFactor (userId: number | string, res: express.Response) {
|
||||
const authUser = res.locals.oauth.token.user
|
||||
|
||||
if (!await checkUserIdExist(userId, res)) return
|
||||
|
||||
if (res.locals.user.id !== authUser.id && authUser.hasRight(UserRight.MANAGE_USERS) !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: `User ${authUser.username} does not have right to change two factor setting of this user.`
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
7
server/core/middlewares/validators/users/index.ts
Normal file
7
server/core/middlewares/validators/users/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './user-email-verification.js'
|
||||
export * from './user-exports.js'
|
||||
export * from './user-history.js'
|
||||
export * from './user-notifications.js'
|
||||
export * from './user-registrations.js'
|
||||
export * from './user-subscriptions.js'
|
||||
export * from './users.js'
|
||||
1
server/core/middlewares/validators/users/shared/index.ts
Normal file
1
server/core/middlewares/validators/users/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './user-registrations.js'
|
||||
@@ -0,0 +1,64 @@
|
||||
import express from 'express'
|
||||
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
||||
import { MRegistration } from '@server/types/models/index.js'
|
||||
import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
|
||||
export function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
||||
}
|
||||
|
||||
export function checkRegistrationEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(
|
||||
async () => {
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(registrations, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||
username: string
|
||||
channelHandle: string
|
||||
email: string
|
||||
res: express.Response
|
||||
}) {
|
||||
const { res } = options
|
||||
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitiveOrHandle(
|
||||
pick(options, [ 'username', 'email', 'channelHandle' ])
|
||||
)
|
||||
|
||||
if (registrations.length !== 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Registration with this username, channel name or email already exists.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) {
|
||||
const registration = await finder()
|
||||
|
||||
if (!registration) {
|
||||
if (abortResponse === true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'User not found'
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.userRegistration = registration
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { checkRegistrationEmailExistPermissive, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
export const usersAskSendUserVerifyEmailValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const [ userEmail, userPendingEmail ] = await Promise.all([
|
||||
UserModel.loadByEmailCaseInsensitive(email).then(users => getByEmailPermissive(users, email)),
|
||||
UserModel.loadByPendingEmailCaseInsensitive(email).then(users => getByEmailPermissive(users, email))
|
||||
])
|
||||
|
||||
if (userEmail && userPendingEmail) {
|
||||
logger.error(`Found 2 users with email ${email} to send verification link.`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
if (!userEmail && !userPendingEmail) {
|
||||
logger.debug(`User with email ${email} does not exist (asking verify email).`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
res.locals.userEmail = userEmail
|
||||
res.locals.userPendingEmail = userPendingEmail
|
||||
|
||||
const user = userEmail || userPendingEmail
|
||||
|
||||
if (user.pluginAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot ask verification email of a user that uses a plugin authentication.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersAskSendRegistrationVerifyEmailValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const registrationExists = await checkRegistrationEmailExistPermissive(email, res, false)
|
||||
|
||||
if (!registrationExists) {
|
||||
logger.debug(`Registration with email ${email} does not exist (asking verify email).`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersVerifyEmailValidator = [
|
||||
param('id')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
body('verificationString')
|
||||
.not().isEmpty().withMessage('Should have a valid verification string'),
|
||||
body('isPendingEmail')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
const redisVerificationString = await Redis.Instance.getUserVerifyEmailLink(user.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const registrationVerifyEmailValidator = [
|
||||
param('registrationId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
|
||||
|
||||
body('verificationString')
|
||||
.not().isEmpty().withMessage('Should have a valid verification string'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
|
||||
|
||||
const registration = res.locals.userRegistration
|
||||
const redisVerificationString = await Redis.Instance.getRegistrationVerifyEmailLink(registration.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
155
server/core/middlewares/validators/users/user-exports.ts
Normal file
155
server/core/middlewares/validators/users/user-exports.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, ServerErrorCode, UserExportRequest, UserExportState, UserRight } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { UserExportModel } from '@server/models/user/user-export.js'
|
||||
import { MUserExport } from '@server/types/models/index.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { getOriginalVideoFileTotalFromUser } from '@server/lib/user.js'
|
||||
|
||||
export const userExportsListValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userExportRequestValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
body('withVideoFiles')
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have withVideoFiles boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
// Check not already created
|
||||
const exportsList = await UserExportModel.listByUser(res.locals.user)
|
||||
if (exportsList.filter(e => e.state !== UserExportState.ERRORED).length !== 0) {
|
||||
return res.fail({
|
||||
message: 'User has already processing or completed exports'
|
||||
})
|
||||
}
|
||||
|
||||
const body: UserExportRequest = req.body
|
||||
|
||||
if (body.withVideoFiles) {
|
||||
const quota = await getOriginalVideoFileTotalFromUser(res.locals.user)
|
||||
|
||||
if (quota > CONFIG.EXPORT.USERS.MAX_USER_VIDEO_QUOTA) {
|
||||
return res.fail({
|
||||
message: 'User video quota exceeds the maximum limit set by the admin to create a user archive containing videos',
|
||||
type: ServerErrorCode.MAX_USER_VIDEO_QUOTA_EXCEEDED_FOR_USER_EXPORT,
|
||||
status: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userExportDeleteValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
param('id')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
const userExport = await UserExportModel.load(req.params.id + '')
|
||||
if (!userExport) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (!checkUserExportRight(userExport, res)) return
|
||||
|
||||
if (!userExport.canBeSafelyRemoved()) {
|
||||
return res.fail({
|
||||
message: 'Cannot delete this user export because its state is not compatible with a deletion'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.userExport = userExport
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userExportDownloadValidator = [
|
||||
param('filename').exists(),
|
||||
|
||||
query('jwt').isJWT(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
|
||||
const userExport = await UserExportModel.loadByFilename(req.params.filename)
|
||||
if (!userExport) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (userExport.isJWTValid(req.query.jwt) !== true) return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
|
||||
res.locals.userExport = userExport
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserIdRight (userId: number | string, res: express.Response) {
|
||||
if (!await checkUserIdExist(userId, res)) return false
|
||||
|
||||
const oauthUser = res.locals.oauth.token.User
|
||||
|
||||
if (!oauthUser.hasRight(UserRight.MANAGE_USER_EXPORTS) && oauthUser.id !== res.locals.user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage exports of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkUserExportRight (userExport: MUserExport, res: express.Response) {
|
||||
if (userExport.userId !== res.locals.user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Export is not associated to this user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function ensureExportIsEnabled (res: express.Response) {
|
||||
if (CONFIG.EXPORT.USERS.ENABLED !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'User export is disabled on this instance'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
47
server/core/middlewares/validators/users/user-history.ts
Normal file
47
server/core/middlewares/validators/users/user-history.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { exists, isDateValid, isIdValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const userHistoryListValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userHistoryRemoveAllValidator = [
|
||||
body('beforeDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userHistoryRemoveElementValidator = [
|
||||
param('videoId')
|
||||
.custom(isIdValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
userHistoryListValidator,
|
||||
userHistoryRemoveElementValidator,
|
||||
userHistoryRemoveAllValidator
|
||||
}
|
||||
110
server/core/middlewares/validators/users/user-import.ts
Normal file
110
server/core/middlewares/validators/users/user-import.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { HttpStatusCode, ServerErrorCode, UserImportState, UserRight } from '@peertube/peertube-models'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { UserImportModel } from '@server/models/user/user-import.js'
|
||||
|
||||
export const userImportRequestResumableValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
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 checkUserIdRight(req.params.userId, res)) return cleanup()
|
||||
|
||||
if (CONFIG.IMPORT.USERS.ENABLED !== true) {
|
||||
res.fail({
|
||||
message: 'User import is not enabled by the administrator',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
res.locals.importUserFileResumable = { ...file, originalname: file.filename }
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userImportRequestResumableInitValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.IMPORT.USERS.ENABLED !== true) {
|
||||
return res.fail({
|
||||
message: 'User import is not enabled by the administrator',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
if (req.body.filename.endsWith('.zip') !== true) {
|
||||
return res.fail({
|
||||
message: 'User import file must be a zip',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
const fileMetadata = res.locals.uploadVideoFileResumableMetadata
|
||||
const user = res.locals.user
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: fileMetadata.size }) === false) {
|
||||
return res.fail({
|
||||
message: 'User video quota is exceeded with this import',
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
type: ServerErrorCode.QUOTA_REACHED
|
||||
})
|
||||
}
|
||||
|
||||
const userImport = await UserImportModel.loadLatestByUserId(user.id)
|
||||
if (userImport && userImport.state !== UserImportState.ERRORED && userImport.state !== UserImportState.COMPLETED) {
|
||||
return res.fail({
|
||||
message: 'An import is already being processed',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const getLatestImportStatusValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserIdRight (userId: number | string, res: express.Response) {
|
||||
if (!await checkUserIdExist(userId, res)) return false
|
||||
|
||||
const oauthUser = res.locals.oauth.token.User
|
||||
|
||||
if (!oauthUser.hasRight(UserRight.MANAGE_USER_IMPORTS) && oauthUser.id !== res.locals.user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage imports of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { isNumberArray } from '@server/helpers/custom-validators/search.js'
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { isNotEmptyIntArray, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isUserNotificationSettingValid } from '../../../helpers/custom-validators/user-notifications.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
export const listUserNotificationsValidator = [
|
||||
query('unread')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should have a valid unread boolean'),
|
||||
|
||||
query('typeOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid typeOneOf array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateNotificationSettingsValidator = [
|
||||
body('newVideoFromSubscription')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newCommentOnMyVideo')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('abuseAsModerator')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('videoAutoBlacklistAsModerator')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('blacklistOnMyVideo')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('myVideoImportFinished')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('myVideoPublished')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('commentMention')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newFollow')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newUserRegistration')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newInstanceFollower')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('autoInstanceFollowing')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const markAsReadUserNotificationsValidator = [
|
||||
body('ids')
|
||||
.optional()
|
||||
.custom(isNotEmptyIntArray).withMessage('Should have a valid array of notification ids'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
206
server/core/middlewares/validators/users/user-registrations.ts
Normal file
206
server/core/middlewares/validators/users/user-registrations.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@peertube/peertube-models'
|
||||
import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from '../../../helpers/custom-validators/users.js'
|
||||
import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../../lib/signup.js'
|
||||
import { areValidationErrors, checkUsernameOrEmailDoNotAlreadyExist } from '../shared/index.js'
|
||||
import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory()
|
||||
|
||||
const usersRequestRegistrationValidator = [
|
||||
...usersCommonRegistrationValidatorFactory([
|
||||
body('registrationReason')
|
||||
.custom(isRegistrationReasonValid)
|
||||
]),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const body: UserRegistrationRequest = req.body
|
||||
|
||||
if (CONFIG.SIGNUP.REQUIRES_APPROVAL !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Signup approval is not enabled on this instance')
|
||||
})
|
||||
}
|
||||
|
||||
const options = { username: body.username, email: body.email, channelHandle: body.channel?.name, res }
|
||||
if (!await checkRegistrationHandlesDoNotAlreadyExist(options)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ensureUserRegistrationAllowedFactory (signupMode: SignupMode) {
|
||||
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const allowedParams = {
|
||||
body: req.body,
|
||||
ip: req.ip,
|
||||
signupMode
|
||||
}
|
||||
|
||||
const allowedResult = await Hooks.wrapPromiseFun(
|
||||
isSignupAllowed,
|
||||
allowedParams,
|
||||
signupMode === 'direct-registration'
|
||||
? 'filter:api.user.signup.allowed.result'
|
||||
: 'filter:api.user.request-signup.allowed.result'
|
||||
)
|
||||
|
||||
if (allowedResult.allowed === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: allowedResult.errorMessage || req.t('User registration is not allowed')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
const ensureUserRegistrationAllowedForIP = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const allowed = isSignupAllowedForCurrentIP(req.ip)
|
||||
|
||||
if (allowed === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('You are not on a network authorized for registration.')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const acceptOrRejectRegistrationValidator = [
|
||||
param('registrationId')
|
||||
.custom(isIdValid),
|
||||
|
||||
body('moderationResponse')
|
||||
.custom(isRegistrationModerationResponseValid),
|
||||
|
||||
body('preventEmailDelivery')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have preventEmailDelivery boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
|
||||
|
||||
if (res.locals.userRegistration.state !== UserRegistrationState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('This registration is already accepted or rejected.')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const getRegistrationValidator = [
|
||||
param('registrationId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const listRegistrationsValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
acceptOrRejectRegistrationValidator,
|
||||
ensureUserRegistrationAllowedFactory,
|
||||
ensureUserRegistrationAllowedForIP,
|
||||
getRegistrationValidator,
|
||||
listRegistrationsValidator,
|
||||
usersDirectRegistrationValidator,
|
||||
usersRequestRegistrationValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function usersCommonRegistrationValidatorFactory (additionalValidationChain: ValidationChain[] = []) {
|
||||
return [
|
||||
body('username')
|
||||
.custom(isUserUsernameValid),
|
||||
body('password')
|
||||
.custom(isUserPasswordValid),
|
||||
body('email')
|
||||
.isEmail(),
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isUserDisplayNameValid),
|
||||
|
||||
body('channel.name')
|
||||
.optional()
|
||||
.custom(isVideoChannelUsernameValid),
|
||||
body('channel.displayName')
|
||||
.optional()
|
||||
.custom(isVideoChannelDisplayNameValid),
|
||||
|
||||
...additionalValidationChain,
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
|
||||
const body: UserRegister | UserRegistrationRequest = req.body
|
||||
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist({ username: body.username, email: body.email, req, res })) return
|
||||
|
||||
if (body.channel) {
|
||||
if (!body.channel.name || !body.channel.displayName) {
|
||||
return res.fail({
|
||||
message: req.t('Channel is optional but if you specify it, channel.name and channel.displayName are required.')
|
||||
})
|
||||
}
|
||||
|
||||
if (body.channel.name === body.username) {
|
||||
return res.fail({ message: req.t('Channel name cannot be the same as user username.') })
|
||||
}
|
||||
|
||||
const existing = await loadReservedActorName(body.channel.name)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t(`Channel with name {name} already exists.`, { name: body.channel.name })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
110
server/core/middlewares/validators/users/user-subscriptions.ts
Normal file
110
server/core/middlewares/validators/users/user-subscriptions.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { FollowState, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { areValidActorHandles, isValidActorHandle } from '../../../helpers/custom-validators/activitypub/actor.js'
|
||||
import { WEBSERVER } from '../../../initializers/constants.js'
|
||||
import { ActorFollowModel } from '../../../models/actor/actor-follow.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const userSubscriptionListValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userSubscriptionAddValidator = [
|
||||
body('uri')
|
||||
.custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const areSubscriptionsExistValidator = [
|
||||
query('uris')
|
||||
.customSanitizer(arrayify)
|
||||
.custom(areValidActorHandles).withMessage('Should have a valid array of URIs'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userSubscriptionGetValidator = [
|
||||
param('uri')
|
||||
.custom(isValidActorHandle),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesSubscriptionExist({ uri: req.params.uri, res, state: 'accepted' })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userSubscriptionDeleteValidator = [
|
||||
param('uri')
|
||||
.custom(isValidActorHandle),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesSubscriptionExist({ uri: req.params.uri, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
areSubscriptionsExistValidator,
|
||||
userSubscriptionListValidator,
|
||||
userSubscriptionAddValidator,
|
||||
userSubscriptionGetValidator,
|
||||
userSubscriptionDeleteValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesSubscriptionExist (options: {
|
||||
uri: string
|
||||
res: express.Response
|
||||
state?: FollowState
|
||||
}) {
|
||||
const { uri, res, state } = options
|
||||
|
||||
let [ name, host ] = uri.split('@')
|
||||
if (host === WEBSERVER.HOST) host = null
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
|
||||
actorId: user.Account.Actor.id,
|
||||
targetName: name,
|
||||
targetHost: host,
|
||||
state
|
||||
})
|
||||
|
||||
if (!subscription?.ActorFollowing.VideoChannel) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Subscription ${uri} not found.`
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.subscription = subscription
|
||||
|
||||
return true
|
||||
}
|
||||
522
server/core/middlewares/validators/users/users.ts
Normal file
522
server/core/middlewares/validators/users/users.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
import { arrayify, forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRole, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { isNSFWFlagsValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { MUser } from '@server/types/models/user/user.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { exists, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isThemeNameValid } from '../../../helpers/custom-validators/plugins.js'
|
||||
import {
|
||||
isUserAdminFlagsValid,
|
||||
isUserAutoPlayNextVideoValid,
|
||||
isUserAutoPlayVideoValid,
|
||||
isUserBlockedReasonValid,
|
||||
isUserDescriptionValid,
|
||||
isUserDisplayNameValid,
|
||||
isUserEmailPublicValid,
|
||||
isUserFeatureInfo,
|
||||
isUserLanguage,
|
||||
isUserNoModal,
|
||||
isUserNSFWPolicyValid,
|
||||
isUserP2PEnabledValid,
|
||||
isUserPasswordValid,
|
||||
isUserPasswordValidOrEmpty,
|
||||
isUserRoleValid,
|
||||
isUserUsernameValid,
|
||||
isUserVideoLanguages,
|
||||
isUserVideoQuotaDailyValid,
|
||||
isUserVideoQuotaValid,
|
||||
isUserVideosHistoryEnabledValid
|
||||
} from '../../../helpers/custom-validators/users.js'
|
||||
import { isVideoChannelUsernameValid } from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { isThemeRegistered } from '../../../lib/plugins/theme-utils.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkEmailDoesNotAlreadyExist,
|
||||
checkUserEmailExistPermissive,
|
||||
checkUserIdExist,
|
||||
checkUsernameOrEmailDoNotAlreadyExist,
|
||||
doesChannelIdExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const usersListValidator = [
|
||||
query('blocked')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should be a valid blocked boolean'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersAddValidator = [
|
||||
body('username')
|
||||
.custom(isUserUsernameValid)
|
||||
.withMessage('Should have a valid username (lowercase alphanumeric characters)'),
|
||||
body('password')
|
||||
.custom(isUserPasswordValidOrEmpty),
|
||||
body('email')
|
||||
.isEmail(),
|
||||
|
||||
body('channelName')
|
||||
.optional()
|
||||
.custom(isVideoChannelUsernameValid),
|
||||
|
||||
body('videoQuota')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaValid),
|
||||
|
||||
body('videoQuotaDaily')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaDailyValid),
|
||||
|
||||
body('role')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isUserRoleValid),
|
||||
|
||||
body('adminFlags')
|
||||
.optional()
|
||||
.custom(isUserAdminFlagsValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist({ username: req.body.username, email: req.body.email, req, res })) return
|
||||
|
||||
const authUser = res.locals.oauth.token.User
|
||||
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'You can only create users (and not administrators or moderators)'
|
||||
})
|
||||
}
|
||||
|
||||
if (req.body.channelName) {
|
||||
if (req.body.channelName === req.body.username) {
|
||||
return res.fail({ message: 'Channel name cannot be the same as user username.' })
|
||||
}
|
||||
|
||||
const existing = await loadReservedActorName(req.body.channelName)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: `Channel with name ${req.body.channelName} already exists.`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersRemoveValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
if (user.username === 'root') {
|
||||
return res.fail({ message: 'Cannot remove the root user' })
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersBlockToggleValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
body('reason')
|
||||
.optional()
|
||||
.custom(isUserBlockedReasonValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
if (user.username === 'root') {
|
||||
return res.fail({ message: 'Cannot block the root user' })
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteMeValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
if (user.username === 'root') {
|
||||
return res.fail({ message: 'You cannot delete your root account.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersUpdateValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
body('password')
|
||||
.optional()
|
||||
.custom(isUserPasswordValid),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail(),
|
||||
body('emailVerified')
|
||||
.optional()
|
||||
.isBoolean(),
|
||||
body('videoQuota')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaValid),
|
||||
body('videoQuotaDaily')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaDailyValid),
|
||||
body('pluginAuth')
|
||||
.optional()
|
||||
.exists(),
|
||||
body('role')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isUserRoleValid),
|
||||
body('adminFlags')
|
||||
.optional()
|
||||
.custom(isUserAdminFlagsValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
|
||||
return res.fail({ message: 'Cannot change root role.' })
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
|
||||
if (
|
||||
req.body.email &&
|
||||
req.body.email !== user.email &&
|
||||
!await checkEmailDoesNotAlreadyExist({ email: req.body.email, req, res })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersUpdateMeValidator = [
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isUserDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isUserDescriptionValid),
|
||||
body('currentPassword')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
body('password')
|
||||
.optional()
|
||||
.custom(isUserPasswordValid),
|
||||
body('emailPublic')
|
||||
.optional()
|
||||
.custom(isUserEmailPublicValid),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail(),
|
||||
|
||||
body('nsfwPolicy')
|
||||
.optional()
|
||||
.custom(isUserNSFWPolicyValid),
|
||||
body('nsfwFlagsDisplayed')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwFlagsHidden')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwFlagsWarned')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwFlagsBlurred')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
|
||||
body('autoPlayVideo')
|
||||
.optional()
|
||||
.custom(isUserAutoPlayVideoValid),
|
||||
body('p2pEnabled')
|
||||
.optional()
|
||||
.custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
|
||||
body('videoLanguages')
|
||||
.optional()
|
||||
.custom(isUserVideoLanguages),
|
||||
body('language')
|
||||
.optional()
|
||||
.custom(isUserLanguage),
|
||||
body('videosHistoryEnabled')
|
||||
.optional()
|
||||
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
|
||||
body('theme')
|
||||
.optional()
|
||||
.custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
|
||||
|
||||
body('noInstanceConfigWarningModal')
|
||||
.optional()
|
||||
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
|
||||
body('noWelcomeModal')
|
||||
.optional()
|
||||
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
|
||||
body('noAccountSetupWarningModal')
|
||||
.optional()
|
||||
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
|
||||
|
||||
body('autoPlayNextVideo')
|
||||
.optional()
|
||||
.custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const body = req.body as UserUpdateMe
|
||||
|
||||
if (
|
||||
((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsWarned || 0)) !== 0 ||
|
||||
((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsDisplayed || 0)) !== 0 ||
|
||||
((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsHidden || 0)) !== 0 ||
|
||||
((body.nsfwFlagsDisplayed || 0) & (body.nsfwFlagsHidden || 0)) !== 0 ||
|
||||
((body.nsfwFlagsDisplayed || 0) & (body.nsfwFlagsWarned || 0)) !== 0 ||
|
||||
((body.nsfwFlagsHidden || 0) & (body.nsfwFlagsWarned || 0)) !== 0
|
||||
) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t(
|
||||
'Cannot use same flags in nsfwFlagsDisplayed, nsfwFlagsHidden, nsfwFlagsBlurred and nsfwFlagsWarned at the same time'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (body.password || body.email) {
|
||||
if (user.pluginAuth !== null) {
|
||||
return res.fail({ message: req.t('You cannot update your email or password that is associated with an external auth system.') })
|
||||
}
|
||||
|
||||
if (!body.currentPassword) {
|
||||
return res.fail({ message: req.t('currentPassword parameter is missing') })
|
||||
}
|
||||
|
||||
if (await user.isPasswordMatch(body.currentPassword) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('currentPassword is invalid.'),
|
||||
type: ServerErrorCode.CURRENT_PASSWORD_IS_INVALID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
|
||||
if (
|
||||
body.email &&
|
||||
body.email !== user.email &&
|
||||
!await checkEmailDoesNotAlreadyExist({ email: body.email, req, res })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersGetValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
query('withStats')
|
||||
.optional()
|
||||
.isBoolean().withMessage('Should have a valid withStats boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersVideoRatingValidator = [
|
||||
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
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listMyVideosValidator = [
|
||||
query('channelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
query('channelNameOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid channelNameOneOf array'),
|
||||
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (
|
||||
req.query.channelId &&
|
||||
!await doesChannelIdExist({ id: req.query.channelId, checkCanManage: true, checkIsLocal: true, checkIsOwner: false, req, res })
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersAskResetPasswordValidator = [
|
||||
body('email')
|
||||
.isEmail(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.users.ask-reset-password.body')
|
||||
|
||||
const exists = await checkUserEmailExistPermissive(email, res, false)
|
||||
if (!exists) {
|
||||
logger.debug('User with email %s does not exist (asking reset password).', email)
|
||||
// Do not leak our emails
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
if (res.locals.user.pluginAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot recover password of a user that uses a plugin authentication.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersResetPasswordValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
body('verificationString')
|
||||
.not().isEmpty(),
|
||||
body('password')
|
||||
.custom(isUserPasswordValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Invalid verification string.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
|
||||
return [
|
||||
body('currentPassword').optional().custom(exists),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
|
||||
const targetUserId = forceNumber(targetUserIdGetter(req))
|
||||
|
||||
// Admin/moderator action on another user, skip the password check
|
||||
if (isAdminOrModerator && targetUserId !== user.id) {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (!req.body.currentPassword) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'currentPassword is missing'
|
||||
})
|
||||
}
|
||||
|
||||
if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'currentPassword is invalid.',
|
||||
type: ServerErrorCode.CURRENT_PASSWORD_IS_INVALID
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const userAutocompleteValidator = [
|
||||
param('search')
|
||||
.isString()
|
||||
.not().isEmpty()
|
||||
]
|
||||
|
||||
export const usersNewFeatureInfoReadValidator = [
|
||||
body('feature')
|
||||
.custom(isUserFeatureInfo),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkCanModerate (onUser: MUser, res: express.Response) {
|
||||
const authUser = res.locals.oauth.token.User
|
||||
|
||||
if (authUser.role === UserRole.ADMINISTRATOR) return true
|
||||
if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return true
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Users can only be managed by moderators or admins.'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
126
server/core/middlewares/validators/watched-words.ts
Normal file
126
server/core/middlewares/validators/watched-words.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { Awaitable } from '@peertube/peertube-typescript-utils'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { areWatchedWordsValid, isWatchedWordListNameValid } from '@server/helpers/custom-validators/watched-words.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import { WatchedWordsListModel } from '@server/models/watched-words/watched-words-list.js'
|
||||
import { MAccountId, MWatchedWordsList } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { ValidationChain, body, param } from 'express-validator'
|
||||
import { doesAccountHandleExist } from './shared/accounts.js'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
export const manageAccountWatchedWordsListValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountHandleExist({ handle: req.params.accountName, req, res, checkIsLocal: true, checkCanManage: true })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function getWatchedWordsListValidatorFactory (accountGetter: (res: express.Response) => Awaitable<MAccountId>) {
|
||||
return [
|
||||
param('listId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const watchedWordsList = await WatchedWordsListModel.load({ id: +req.params.listId, accountId: (await accountGetter(res)).id })
|
||||
if (!watchedWordsList) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown watched words list id for this account'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.watchedWordsList = watchedWordsList
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function buildUpdateOrAddValidators ({ optional }: { optional: boolean }) {
|
||||
const makeOptionalIfNeeded = (chain: ValidationChain) => {
|
||||
if (optional) return chain.optional()
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
return [
|
||||
makeOptionalIfNeeded(body('listName'))
|
||||
.trim()
|
||||
.custom(isWatchedWordListNameValid).withMessage(
|
||||
`Should have a list name between ` +
|
||||
`${CONSTRAINTS_FIELDS.WATCHED_WORDS.LIST_NAME.min} and ${CONSTRAINTS_FIELDS.WATCHED_WORDS.LIST_NAME.max} characters long`
|
||||
),
|
||||
|
||||
makeOptionalIfNeeded(body('words'))
|
||||
.custom(areWatchedWordsValid)
|
||||
.withMessage(
|
||||
`Should have an array of up to ${CONSTRAINTS_FIELDS.WATCHED_WORDS.WORDS.max} words between ` +
|
||||
`${CONSTRAINTS_FIELDS.WATCHED_WORDS.WORD.min} and ${CONSTRAINTS_FIELDS.WATCHED_WORDS.WORD.max} characters each`
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
export function addWatchedWordsListValidatorFactory (accountGetter: (res: express.Response) => Awaitable<MAccountId>) {
|
||||
return [
|
||||
...buildUpdateOrAddValidators({ optional: false }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const listName = req.body.listName
|
||||
if (!await checkListNameIsUnique({ accountId: (await accountGetter(res)).id, listName, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function updateWatchedWordsListValidatorFactory (accountGetter: (res: express.Response) => Awaitable<MAccountId>) {
|
||||
return [
|
||||
...buildUpdateOrAddValidators({ optional: true }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const currentList = res.locals.watchedWordsList
|
||||
const listName = req.body.listName
|
||||
if (listName && !await checkListNameIsUnique({ accountId: (await accountGetter(res)).id, listName, currentList, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkListNameIsUnique (options: {
|
||||
accountId: number
|
||||
listName: string
|
||||
res: express.Response
|
||||
currentList?: MWatchedWordsList
|
||||
}) {
|
||||
const { accountId, listName, currentList, res } = options
|
||||
|
||||
const existing = await WatchedWordsListModel.loadByListName({ accountId, listName })
|
||||
if (existing && (!currentList || currentList.id !== existing.id)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Watched words list with name ${listName} already exists`
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
37
server/core/middlewares/validators/webfinger.ts
Normal file
37
server/core/middlewares/validators/webfinger.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger.js'
|
||||
import { getHostWithPort } from '../../helpers/express-utils.js'
|
||||
import { ActorModel } from '../../models/actor/actor.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const webfingerValidator = [
|
||||
query('resource')
|
||||
.custom(isWebfingerLocalResourceValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
// Remove 'acct:' from the beginning of the string
|
||||
const handle = getHostWithPort(req.query.resource.substr(5))
|
||||
const [ name ] = handle.split('@')
|
||||
|
||||
const actor = await ActorModel.loadLocalUrlByName(name)
|
||||
if (!actor) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Actor not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.actorUrl = actor
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
webfingerValidator
|
||||
}
|
||||
Reference in New Issue
Block a user