はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
+254
ファイルの表示
@@ -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(body.account.id, res)) 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.isOwned() === 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
}
+35
ファイルの表示
@@ -0,0 +1,35 @@
import express from 'express'
import { param } from 'express-validator'
import { isAccountNameValid } from '../../helpers/custom-validators/accounts.js'
import { areValidationErrors, doesAccountNameWithHostExist, doesLocalAccountNameExist } from './shared/index.js'
const localAccountValidator = [
param('name')
.custom(isAccountNameValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!await doesLocalAccountNameExist(req.params.name, res)) return
return next()
}
]
const accountNameWithHostGetValidator = [
param('accountName')
.exists(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
export {
localAccountValidator,
accountNameWithHostGetValidator
}
+29
ファイルの表示
@@ -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
ファイルの表示
@@ -0,0 +1,3 @@
export * from './activity.js'
export * from './signature.js'
export * from './pagination.js'
+25
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+22
ファイルの表示
@@ -0,0 +1,22 @@
import express from 'express'
import { body } from 'express-validator'
import { isActorImageFile } from '@server/helpers/custom-validators/actor-images.js'
import { cleanUpReqFiles } from '../../helpers/express-utils.js'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { areValidationErrors } from './shared/index.js'
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()
}
])
export const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
export const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
+45
ファイルの表示
@@ -0,0 +1,45 @@
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 { doesAccountNameWithHostExist } from './shared/accounts.js'
import { checkUserCanManageAccount } from './shared/users.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 doesAccountNameWithHostExist(req.params.accountName, res)) return
if (!checkUserCanManageAccount({ user: res.locals.oauth.token.User, account: res.locals.account, specialRight: null, res })) 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
ファイルの表示
@@ -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, doesAccountNameWithHostExist } 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 doesAccountNameWithHostExist(req.body.accountName, res)) 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 doesAccountNameWithHostExist(req.params.accountName, res)) 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 doesAccountNameWithHostExist(req.params.accountName, res)) 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
}
+37
ファイルの表示
@@ -0,0 +1,37 @@
import express from 'express'
import { body } from 'express-validator'
import { BulkRemoveCommentsOfBody, HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk.js'
import { areValidationErrors, doesAccountNameWithHostExist } from './shared/index.js'
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 doesAccountNameWithHostExist(req.body.accountName, res)) 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()
}
]
// ---------------------------------------------------------------------------
export {
bulkRemoveCommentsOfValidator
}
// ---------------------------------------------------------------------------
+198
ファイルの表示
@@ -0,0 +1,198 @@
import express from 'express'
import { body } from 'express-validator'
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
import { isIntOrNull } from '@server/helpers/custom-validators/misc.js'
import { CONFIG, isEmailEnabled } from '@server/initializers/config.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'
const customConfigUpdateValidator = [
body('instance.name').exists(),
body('instance.shortDescription').exists(),
body('instance.description').exists(),
body('instance.terms').exists(),
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.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('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.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(),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!checkInvalidConfigIfEmailDisabled(req.body, res)) return
if (!checkInvalidTranscodingConfig(req.body, res)) return
if (!checkInvalidSynchronizationConfig(req.body, res)) return
if (!checkInvalidLiveConfig(req.body, res)) return
if (!checkInvalidVideoStudioConfig(req.body, res)) return
return next()
}
]
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: 'Server configuration is static and cannot be edited'
})
}
return next()
}
// ---------------------------------------------------------------------------
export {
customConfigUpdateValidator,
ensureConfigIsEditable
}
function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
if (isEmailEnabled()) return true
if (customConfig.signup.requiresEmailVerification === true) {
res.fail({ message: 'SMTP is not configured but you require signup email verification.' })
return false
}
return true
}
function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) {
if (customConfig.transcoding.enabled === false) return true
if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) {
res.fail({ message: 'You need to enable at least web_videos transcoding or hls transcoding' })
return false
}
return true
}
function checkInvalidSynchronizationConfig (customConfig: CustomConfig, res: express.Response) {
if (customConfig.import.videoChannelSynchronization.enabled && !customConfig.import.videos.http.enabled) {
res.fail({ message: 'You need to enable HTTP video import in order to enable channel synchronization' })
return false
}
return true
}
function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Response) {
if (customConfig.live.enabled === false) return true
if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
return false
}
return true
}
function checkInvalidVideoStudioConfig (customConfig: CustomConfig, res: express.Response) {
if (customConfig.videoStudio.enabled === false) return true
if (customConfig.videoStudio.enabled === true && customConfig.transcoding.enabled === false) {
res.fail({ message: 'You cannot enable video studio if transcoding is not enabled' })
return false
}
return true
}
+15
ファイルの表示
@@ -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
}
+167
ファイルの表示
@@ -0,0 +1,167 @@
import express from 'express'
import { param, query } from 'express-validator'
import { HttpStatusCode } from '@peertube/peertube-models'
import { isValidRSSFeed } from '../../helpers/custom-validators/feeds.js'
import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc.js'
import {
areValidationErrors,
checkCanSeeVideo,
doesAccountIdExist,
doesAccountNameWithHostExist,
doesUserFeedTokenCorrespond,
doesVideoChannelIdExist,
doesVideoChannelNameWithHostExist,
doesVideoExist
} from './shared/index.js'
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()
}
]
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)
}
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)
}
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()
}
// ---------------------------------------------------------------------------
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
if (req.query.accountId && !await doesAccountIdExist(req.query.accountId, res)) return
if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
if (req.query.accountName && !await doesAccountNameWithHostExist(req.query.accountName, res)) return
if (req.query.videoChannelName && !await doesVideoChannelNameWithHostExist(req.query.videoChannelName, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
const videoFeedsPodcastValidator = [
query('videoChannelId')
.custom(isIdValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
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(req.query.accountId, res)) return
if (!await doesUserFeedTokenCorrespond(res.locals.account.userId, req.query.token, res)) return
return next()
}
]
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()
}
]
// ---------------------------------------------------------------------------
export {
feedsFormatValidator,
setFeedFormatContentType,
setFeedPodcastContentType,
feedsAccountOrChannelFiltersValidator,
videoFeedsPodcastValidator,
videoSubscriptionFeedsValidator,
videoCommentsFeedsValidator
}
+157
ファイルの表示
@@ -0,0 +1,157 @@
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('nameWithHost')
.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.nameWithHost)
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.nameWithHost, err })
}
if (!follow) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: `Follower ${req.params.nameWithHost} 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
}
+28
ファイルの表示
@@ -0,0 +1,28 @@
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 './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
ファイルの表示
@@ -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
}
+93
ファイルの表示
@@ -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 one of tags 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
}
+60
ファイルの表示
@@ -0,0 +1,60 @@
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('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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+216
ファイルの表示
@@ -0,0 +1,216 @@
import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator'
import { HttpStatusCode, InstallOrUpdatePlugin, PluginType_Type } from '@peertube/peertube-models'
import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc.js'
import {
isNpmPluginNameValid,
isPluginNameValid,
isPluginStableOrUnstableVersionValid,
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'
const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) => {
const validators: (ValidationChain | express.Handler)[] = [
param('pluginName')
.custom(isPluginNameValid)
]
if (withVersion) {
validators.push(
param('pluginVersion')
.custom(isPluginStableOrUnstableVersionValid)
)
}
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()
}
])
}
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()
}
]
const pluginStaticDirectoryValidator = [
param('staticEndpoint')
.custom(isSafePath),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
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()
}
]
const installOrUpdatePluginValidator = [
body('npmName')
.optional()
.custom(isNpmPluginNameValid),
body('pluginVersion')
.optional()
.custom(isPluginStableOrUnstableVersionValid),
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()
}
]
const uninstallPluginValidator = [
body('npmName')
.custom(isNpmPluginNameValid),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
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()
}
]
const updatePluginSettingsValidator = [
body('settings')
.exists(),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
const listAvailablePluginsValidator = [
query('search')
.optional()
.exists(),
query('pluginType')
.optional()
.customSanitizer(toIntOrNull)
.custom(isPluginTypeValid),
query('currentPeerTubeEngine')
.optional()
.custom(isPluginStableOrUnstableVersionValid),
(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()
}
]
// ---------------------------------------------------------------------------
export {
pluginStaticDirectoryValidator,
getPluginValidator,
updatePluginSettingsValidator,
uninstallPluginValidator,
listAvailablePluginsValidator,
existingPluginValidator,
installOrUpdatePluginValidator,
listPluginsValidator,
getExternalAuthValidator
}
+201
ファイルの表示
@@ -0,0 +1,201 @@
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 videoFileRedundancyGetValidator = [
isValidVideoIdParam('videoId'),
param('resolution')
.customSanitizer(toIntOrNull)
.custom(exists),
param('fps')
.optional()
.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
if (!canVideoBeFederated(res.locals.onlyVideo)) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const video = res.locals.videoAll
const paramResolution = req.params.resolution as unknown as number // We casted to int above
const paramFPS = req.params.fps as unknown as number // We casted to int above
const videoFile = video.VideoFiles.find(f => {
return f.resolution === paramResolution && (!req.params.fps || paramFPS)
})
if (!videoFile) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video file not found.'
})
}
res.locals.videoFile = videoFile
const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
if (!videoRedundancy) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video redundancy not found.'
})
}
res.locals.videoRedundancy = videoRedundancy
return next()
}
]
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 {
videoFileRedundancyGetValidator,
videoPlaylistRedundancyGetValidator,
updateServerRedundancyValidator,
listVideoRedundanciesValidator,
addVideoRedundancyValidator,
removeVideoRedundancyValidator
}
+36
ファイルの表示
@@ -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
ファイルの表示
@@ -0,0 +1,3 @@
export * from './jobs.js'
export * from './registration-token.js'
export * from './runners.js'
+60
ファイルの表示
@@ -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()
}
]
+217
ファイルの表示
@@ -0,0 +1,217 @@
import { arrayify } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
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 { LiveManager } from '@server/lib/live/index.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
})
}
if (res.locals.runnerJob.type === 'live-rtmp-hls-transcoding') {
const privatePayload = job.privatePayload as RunnerJobLiveRTMPHLSTranscodingPrivatePayload
if (!LiveManager.Instance.hasSession(privatePayload.sessionId)) {
cleanUpReqFiles(req)
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
type: ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE,
message: 'Session of this live ended',
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()
}
]
}
+37
ファイルの表示
@@ -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
}
+104
ファイルの表示
@@ -0,0 +1,104 @@
import express from 'express'
import { body, param } from 'express-validator'
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
import {
isRunnerDescriptionValid,
isRunnerNameValid,
isRunnerRegistrationTokenValid,
isRunnerTokenValid
} from '@server/helpers/custom-validators/runners/runners.js'
import { RunnerModel } from '@server/models/runner/runner.js'
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@peertube/peertube-models'
import { areValidationErrors } from '../shared/utils.js'
const tags = [ 'runner' ]
const registerRunnerValidator = [
body('registrationToken').custom(isRunnerRegistrationTokenValid),
body('name').custom(isRunnerNameValid),
body('description').optional().custom(isRunnerDescriptionValid),
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()
}
]
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()
}
]
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 {
registerRunnerValidator,
deleteRunnerValidator,
getRunnerFromTokenValidator
}
+112
ファイルの表示
@@ -0,0 +1,112 @@
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('host')
.optional()
.custom(isHostValid),
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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+66
ファイルの表示
@@ -0,0 +1,66 @@
import { Response } from 'express'
import { AccountModel } from '@server/models/account/account.js'
import { UserModel } from '@server/models/user/user.js'
import { MAccountDefault } from '@server/types/models/index.js'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
const promise = AccountModel.load(forceNumber(id))
return doesAccountExist(promise, res, sendNotFound)
}
function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) {
const promise = AccountModel.loadLocalByName(name)
return doesAccountExist(promise, res, sendNotFound)
}
function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
const promise = AccountModel.loadByNameWithHost(nameWithDomain)
return doesAccountExist(promise, res, sendNotFound)
}
async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sendNotFound: boolean) {
const account = await p
if (!account) {
if (sendNotFound === true) {
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Account not found'
})
}
return false
}
res.locals.account = account
return true
}
async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) {
const user = await UserModel.loadByIdWithChannels(forceNumber(id))
if (token !== user.feedToken) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'User and token mismatch'
})
return false
}
res.locals.user = user
return true
}
// ---------------------------------------------------------------------------
export {
doesAccountIdExist,
doesLocalAccountNameExist,
doesAccountNameWithHostExist,
doesAccountExist,
doesUserFeedTokenCorrespond
}
+14
ファイルの表示
@@ -0,0 +1,14 @@
export * from './abuses.js'
export * from './accounts.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'
+84
ファイルの表示
@@ -0,0 +1,84 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
import { ActorModel } from '@server/models/actor/actor.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 checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
}
export async function checkUserNameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
const user = await UserModel.loadByUsernameOrEmail(username, email)
if (user) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'User with this username or email already exists.'
})
return false
}
const actor = await ActorModel.loadLocalByName(username)
if (actor) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
})
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.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'User not found'
})
}
return false
}
res.locals.user = user
return true
}
export function checkUserCanManageAccount (options: {
user: MUserAccountId
account: MAccountId
specialRight: UserRightType
res: express.Response
}) {
const { user, account, specialRight, res } = options
if (account.id === user.Account.id) return true
if (specialRight && user.hasRight(specialRight) === true) return true
if (!specialRight) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Only the owner of this account can manage this account resource.'
})
return false
}
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Only a user with sufficient right can access this account resource.'
})
return false
}
+69
ファイルの表示
@@ -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
}
+24
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+24
ファイルの表示
@@ -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
}
+36
ファイルの表示
@@ -0,0 +1,36 @@
import express from 'express'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
import { HttpStatusCode } from '@peertube/peertube-models'
async function doesVideoChannelIdExist (id: number, res: express.Response) {
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
return processVideoChannelExist(videoChannel, res)
}
async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
return processVideoChannelExist(videoChannel, res)
}
// ---------------------------------------------------------------------------
export {
doesVideoChannelIdExist,
doesVideoChannelNameWithHostExist
}
function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
if (!videoChannel) {
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video channel not found'
})
return false
}
res.locals.videoChannel = videoChannel
return true
}
+80
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+25
ファイルの表示
@@ -0,0 +1,25 @@
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'
async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) {
const id = forceNumber(idArg)
const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
if (!videoChangeOwnership) {
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video change ownership not found'
})
return false
}
res.locals.videoChangeOwnership = videoChangeOwnership
return true
}
export {
doesChangeVideoOwnershipExist
}
+80
ファイルの表示
@@ -0,0 +1,80 @@
import express from 'express'
import { HttpStatusCode, UserRight, VideoPrivacy } from '@peertube/peertube-models'
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideoPasswordModel } from '@server/models/video/video-password.js'
import { header } from 'express-validator'
import { getVideoWithAttributes } from '@server/helpers/video.js'
function isValidVideoPasswordHeader () {
return header('x-peertube-video-password')
.optional()
.isString()
}
function checkVideoIsPasswordProtected (res: express.Response) {
const video = getVideoWithAttributes(res)
if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Video is not password protected'
})
return false
}
return true
}
async function doesVideoPasswordExist (idArg: number | string, res: express.Response) {
const video = getVideoWithAttributes(res)
const id = forceNumber(idArg)
const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id })
if (!videoPassword) {
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video password not found'
})
return false
}
res.locals.videoPassword = videoPassword
return true
}
async function isVideoPasswordDeletable (res: express.Response) {
const user = res.locals.oauth.token.User
const userAccount = user.Account
const video = res.locals.videoAll
// Check if the user who did the request is able to delete the video passwords
if (
user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator
video.VideoChannel.accountId !== userAccount.id // Not the video owner
) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot remove passwords of another user\'s video'
})
return false
}
const passwordCount = await VideoPasswordModel.countByVideoId(video.id)
if (passwordCount <= 1) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot delete the last password of the protected video'
})
return false
}
return true
}
export {
isValidVideoPasswordHeader,
checkVideoIsPasswordProtected as isVideoPasswordProtected,
doesVideoPasswordExist,
isVideoPasswordDeletable
}
+39
ファイルの表示
@@ -0,0 +1,39 @@
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'
async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
if (fetchType === 'summary') {
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
res.locals.videoPlaylistSummary = videoPlaylist
return handleVideoPlaylist(videoPlaylist, res)
}
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
res.locals.videoPlaylistFull = videoPlaylist
return handleVideoPlaylist(videoPlaylist, res)
}
// ---------------------------------------------------------------------------
export {
doesVideoPlaylistExist
}
// ---------------------------------------------------------------------------
function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
if (!videoPlaylist) {
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video playlist not found'
})
return false
}
return true
}
+323
ファイルの表示
@@ -0,0 +1,323 @@
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 { authenticatePromise } from '@server/middlewares/auth.js'
import { VideoChannelModel } from '@server/models/video/video-channel.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 {
MUser,
MUserAccountId,
MUserId,
MVideo,
MVideoAccountLight,
MVideoFormattableDetails,
MVideoFullLight,
MVideoId,
MVideoImmutable,
MVideoThumbnailBlacklist,
MVideoUUID,
MVideoWithRights
} from '@server/types/models/index.js'
import { Request, Response } from 'express'
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.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'VideoFile matching Video not found'
})
return false
}
return true
}
// ---------------------------------------------------------------------------
export async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
if (videoChannel === null) {
res.fail({ message: 'Unknown video "video channel" for this instance.' })
return false
}
// Don't check account id if the user can update any video
if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
res.locals.videoChannel = videoChannel
return true
}
if (videoChannel.Account.id !== user.Account.id) {
res.fail({
message: 'Unknown video "video channel" for this account.'
})
return false
}
res.locals.videoChannel = videoChannel
return true
}
// ---------------------------------------------------------------------------
export async function checkCanSeeVideo (options: {
req: Request
res: Response
paramId: string
video: MVideo
}) {
const { req, res, video, paramId } = options
if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) {
return checkCanSeeUserAuthVideo({ req, res, video })
}
if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
return checkCanSeePasswordProtectedVideo({ req, res, video })
}
if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) {
return true
}
throw new Error('Unknown video privacy when checking video right ' + video.url)
}
export async function checkCanSeeUserAuthVideo (options: {
req: Request
res: Response
video: MVideoId | MVideoWithRights
}) {
const { req, res, video } = options
const fail = () => {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot fetch information of private/internal/blocked video'
})
return false
}
await authenticatePromise({ req, res })
const user = res.locals.oauth?.token.User
if (!user) return fail()
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 (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true
return fail()
}
if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
return fail()
}
// Should not happen
return fail()
}
export async function checkCanSeePasswordProtectedVideo (options: {
req: Request
res: Response
video: MVideo
}) {
const { req, res, video } = options
const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
const videoPassword = req.header('x-peertube-video-password')
if (!exists(videoPassword)) {
const errorMessage = 'Please provide a password to access this password protected video'
const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD
if (req.header('authorization')) {
await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType })
const user = res.locals.oauth?.token.User
if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
}
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
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: 'Incorrect video password. Access to the video is denied.'
})
return false
}
export function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRightType) {
const isOwnedByUser = video.VideoChannel.Account.userId === user.id
return isOwnedByUser || user.hasRight(right)
}
export async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
return video.VideoChannel?.Account?.userId
? video
: VideoModel.loadFull(video.id)
}
// ---------------------------------------------------------------------------
export async function checkCanAccessVideoStaticFiles (options: {
video: MVideo
req: Request
res: Response
paramId: string
}) {
const { video, req, res } = options
if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) {
return checkCanSeeVideo(options)
}
assignVideoTokenIfNeeded(req, res, video)
if (res.locals.videoFileToken) return true
if (!video.hasPrivateStaticPath()) return true
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
return false
}
export async function checkCanAccessVideoSourceFile (options: {
videoId: number
req: Request
res: Response
}) {
const { req, res, videoId } = options
const video = await VideoModel.loadFull(videoId)
if (res.locals.oauth?.token.User) {
if (canUserAccessVideo(res.locals.oauth.token.User, video, UserRight.SEE_ALL_VIDEOS) === true) return true
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
return false
}
assignVideoTokenIfNeeded(req, res, video)
if (res.locals.videoFileToken) return true
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
return false
}
function assignVideoTokenIfNeeded (req: Request, res: Response, video: MVideoUUID) {
const videoFileToken = req.query.videoFileToken
if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
res.locals.videoFileToken = { user }
}
}
// ---------------------------------------------------------------------------
export function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRightType, res: Response, onlyOwned = true) {
if (onlyOwned && video.isOwned() === false) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage a video of another server.'
})
return false
}
const account = video.VideoChannel.Account
if (user.hasRight(right) === false && account.userId !== user.id) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage a video of another user.'
})
return false
}
return true
}
// ---------------------------------------------------------------------------
export async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
res.fail({
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
message: 'The user video quota is exceeded with this video.',
type: ServerErrorCode.QUOTA_REACHED
})
return false
}
return true
}
+68
ファイルの表示
@@ -0,0 +1,68 @@
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 videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS)
export const runnersSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNERS)
export const runnerRegistrationTokensSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_REGISTRATION_TOKENS)
export const runnerJobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_JOBS)
// ---------------------------------------------------------------------------
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)
}
+183
ファイルの表示
@@ -0,0 +1,183 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { exists, 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 { query } from 'express-validator'
import { LRUCache } from 'lru-cache'
import { basename, dirname } 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
})
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()
}
]
const ensureCanAccessPrivateVideoHLSFiles = [
query('videoFileToken')
.optional()
.custom(exists),
query('reinjectVideoFileToken')
.optional()
.customSanitizer(toBooleanOrNull)
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
query('playlistName')
.optional()
.customSanitizer(isSafePeerTubeFilenameWithoutExtension),
isValidVideoPasswordHeader(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
const videoUUID = basename(dirname(req.originalUrl))
if (!isUUIDValid(videoUUID)) {
logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
}
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()
}
]
export {
ensureCanAccessPrivateVideoHLSFiles, ensureCanAccessVideoPrivateWebVideoFiles
}
// ---------------------------------------------------------------------------
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.FORBIDDEN_403
})
}
return token
}
+46
ファイルの表示
@@ -0,0 +1,46 @@
import express from 'express'
import { param } from 'express-validator'
import { HttpStatusCode } from '@peertube/peertube-models'
import { isSafePath } from '../../helpers/custom-validators/misc.js'
import { isPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins.js'
import { PluginManager } from '../../lib/plugins/plugin-manager.js'
import { areValidationErrors } from './shared/index.js'
const serveThemeCSSValidator = [
param('themeName')
.custom(isPluginNameValid),
param('themeVersion')
.custom(isPluginStableOrUnstableVersionValid),
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()
}
]
// ---------------------------------------------------------------------------
export {
serveThemeCSSValidator
}
+81
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -0,0 +1 @@
export * from './user-registrations.js'
+60
ファイルの表示
@@ -0,0 +1,60 @@
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'
function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
const id = forceNumber(idArg)
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
}
function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) {
return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse)
}
async function checkRegistrationHandlesDoNotAlreadyExist (options: {
username: string
channelHandle: string
email: string
res: express.Response
}) {
const { res } = options
const registration = await UserRegistrationModel.loadByEmailOrHandle(pick(options, [ 'username', 'email', 'channelHandle' ]))
if (registration) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Registration with this username, channel name or email already exists.'
})
return false
}
return true
}
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
}
export {
checkRegistrationIdExist,
checkRegistrationEmailExist,
checkRegistrationHandlesDoNotAlreadyExist,
checkRegistrationExist
}
+94
ファイルの表示
@@ -0,0 +1,94 @@
import express from 'express'
import { body, param } from 'express-validator'
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '../../../helpers/logger.js'
import { Redis } from '../../../lib/redis.js'
import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from '../shared/index.js'
import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations.js'
const usersAskSendVerifyEmailValidator = [
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 [ userExists, registrationExists ] = await Promise.all([
checkUserEmailExist(req.body.email, res, false),
checkRegistrationEmailExist(req.body.email, res, false)
])
if (!userExists && !registrationExists) {
logger.debug('User or registration with email %s does not exist (asking verify email).', req.body.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 ask verification email of a user that uses a plugin authentication.'
})
}
return next()
}
]
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()
}
]
// ---------------------------------------------------------------------------
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()
}
]
// ---------------------------------------------------------------------------
export {
usersAskSendVerifyEmailValidator,
usersVerifyEmailValidator,
registrationVerifyEmailValidator
}
+155
ファイルの表示
@@ -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
ファイルの表示
@@ -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
ファイルの表示
@@ -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
}
+71
ファイルの表示
@@ -0,0 +1,71 @@
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'
const listUserNotificationsValidator = [
query('unread')
.optional()
.customSanitizer(toBooleanOrNull)
.isBoolean().withMessage('Should have a valid unread boolean'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
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()
}
]
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()
}
]
// ---------------------------------------------------------------------------
export {
listUserNotificationsValidator,
updateNotificationSettingsValidator,
markAsReadUserNotificationsValidator
}
+208
ファイルの表示
@@ -0,0 +1,208 @@
import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator'
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 { Hooks } from '@server/lib/plugins/hooks.js'
import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@peertube/peertube-models'
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 { ActorModel } from '../../../models/actor/actor.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: '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 || '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: '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: '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 {
usersDirectRegistrationValidator,
usersRequestRegistrationValidator,
ensureUserRegistrationAllowedFactory,
ensureUserRegistrationAllowedForIP,
getRegistrationValidator,
listRegistrationsValidator,
acceptOrRejectRegistrationValidator
}
// ---------------------------------------------------------------------------
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(body.username, body.email, res)) return
if (body.channel) {
if (!body.channel.name || !body.channel.displayName) {
return res.fail({ message: '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: 'Channel name cannot be the same as user username.' })
}
const existing = await ActorModel.loadLocalByName(body.channel.name)
if (existing) {
return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: `Channel with name ${body.channel.name} already exists.`
})
}
}
return next()
}
]
}
+110
ファイルの表示
@@ -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
}
+477
ファイルの表示
@@ -0,0 +1,477 @@
import express from 'express'
import { body, param, query } from 'express-validator'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserRight, UserRole } from '@peertube/peertube-models'
import { exists, isBooleanValid, 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,
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 { ActorModel } from '../../../models/actor/actor.js'
import {
areValidationErrors,
checkUserEmailExist,
checkUserIdExist,
checkUserNameOrEmailDoNotAlreadyExist,
checkUserCanManageAccount,
doesVideoChannelIdExist,
doesVideoExist,
isValidVideoIdParam
} from '../shared/index.js'
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()
}
]
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(req.body.username, req.body.email, 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 ActorModel.loadLocalByName(req.body.channelName)
if (existing) {
return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: `Channel with name ${req.body.channelName} already exists.`
})
}
}
return next()
}
]
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' })
}
return next()
}
]
const usersBlockingValidator = [
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' })
}
return next()
}
]
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()
}
]
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.' })
}
return next()
}
]
const usersUpdateMeValidator = [
body('displayName')
.optional()
.custom(isUserDisplayNameValid),
body('description')
.optional()
.custom(isUserDescriptionValid),
body('currentPassword')
.optional()
.custom(isUserPasswordValid),
body('password')
.optional()
.custom(isUserPasswordValid),
body('emailPublic')
.optional()
.custom(isUserEmailPublicValid),
body('email')
.optional()
.isEmail(),
body('nsfwPolicy')
.optional()
.custom(isUserNSFWPolicyValid),
body('autoPlayVideo')
.optional()
.custom(isUserAutoPlayVideoValid),
body('p2pEnabled')
.optional()
.custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
body('videoLanguages')
.optional()
.custom(isUserVideoLanguages),
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
if (req.body.password || req.body.email) {
if (user.pluginAuth !== null) {
return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
}
if (!req.body.currentPassword) {
return res.fail({ message: 'currentPassword parameter is missing.' })
}
if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
return res.fail({
status: HttpStatusCode.UNAUTHORIZED_401,
message: 'currentPassword is invalid.'
})
}
}
if (areValidationErrors(req, res, { omitBodyLog: true })) return
return next()
}
]
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()
}
]
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()
}
]
const usersVideosValidator = [
query('isLive')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
query('channelId')
.optional()
.customSanitizer(toIntOrNull)
.custom(isIdValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
return next()
}
]
const usersAskResetPasswordValidator = [
body('email')
.isEmail(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
const exists = await checkUserEmailExist(req.body.email, res, false)
if (!exists) {
logger.debug('User with email %s does not exist (asking reset password).', req.body.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()
}
]
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()
}
]
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.'
})
}
return next()
}
]
}
const userAutocompleteValidator = [
param('search')
.isString()
.not().isEmpty()
]
const ensureAuthUserOwnsAccountValidator = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.User
if (!checkUserCanManageAccount({ user, account: res.locals.account, specialRight: null, res })) return
return next()
}
]
const ensureCanManageChannelOrAccount = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.user
const account = res.locals.videoChannel?.Account ?? res.locals.account
if (!checkUserCanManageAccount({ account, user, res, specialRight: UserRight.MANAGE_ANY_VIDEO_CHANNEL })) return
return next()
}
]
const ensureCanModerateUser = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
const authUser = res.locals.oauth.token.User
const onUser = res.locals.user
if (authUser.role === UserRole.ADMINISTRATOR) return next()
if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Users can only be managed by moderators or admins.'
})
}
]
// ---------------------------------------------------------------------------
export {
usersListValidator,
usersAddValidator,
deleteMeValidator,
usersBlockingValidator,
usersRemoveValidator,
usersUpdateValidator,
usersUpdateMeValidator,
usersVideoRatingValidator,
usersCheckCurrentPasswordFactory,
usersGetValidator,
usersVideosValidator,
usersAskResetPasswordValidator,
usersResetPasswordValidator,
userAutocompleteValidator,
ensureAuthUserOwnsAccountValidator,
ensureCanModerateUser,
ensureCanManageChannelOrAccount
}
+20
ファイルの表示
@@ -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'
+2
ファイルの表示
@@ -0,0 +1,2 @@
export * from './upload.js'
export * from './video-validators.js'
+42
ファイルの表示
@@ -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
}
+139
ファイルの表示
@@ -0,0 +1,139 @@
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: {
res: express.Response
user: MUserAccountId
videoFileSize: number
files: express.UploadFilesForCheck
}): Promise<boolean> {
const { res, user, videoFileSize, files } = options
if (!isVideoFileMimeTypeValid(files)) {
res.fail({
status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
message: `This file is not supported. Please, make sure it is of the following type: ${CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')}`
})
return false
}
if (!isVideoFileSizeValid(videoFileSize.toString())) {
res.fail({
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
message: '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, 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 || acceptedResult.accepted !== true) {
logger.info('Refused local video file.', { acceptedResult, acceptParameters })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult.errorMessage || 'Refused local video file'
})
return false
}
return true
}
export function checkVideoFileCanBeEdited (video: MVideo, res: express.Response) {
if (video.isLive) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: '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: 'Cannot edit video that is already waiting for transcoding/edition'
})
return false
}
const validStates = new Set<VideoStateType>([
VideoState.PUBLISHED,
VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED,
VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED,
VideoState.TRANSCODING_FAILED
])
if (!validStates.has(video.state)) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Video state is not compatible with edition'
})
return false
}
return true
}
export function checkVideoCanBeTranscribedOrTranscripted (video: MVideo, res: express.Response) {
if (video.remote) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot run this task on a remote video'
})
return false
}
if (video.isLive) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot run this task on a live'
})
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
])
if (incompatibleStates.has(video.state)) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: `Cannot run this task on a video with "${VIDEO_STATES[video.state]}" state`
})
return false
}
return true
}
+87
ファイルの表示
@@ -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
}
+146
ファイルの表示
@@ -0,0 +1,146 @@
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,
checkUserCanManageVideo,
doesVideoCaptionExist,
doesVideoExist,
isValidVideoIdParam,
isValidVideoPasswordHeader
} from '../shared/index.js'
import { checkVideoCanBeTranscribedOrTranscripted } 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 (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) 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 (!checkVideoCanBeTranscribedOrTranscripted(video, res)) return
// Check if the user who did the request is able to update the video
const user = res.locals.oauth.token.User
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) 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 (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) 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()
}
]
+56
ファイルの表示
@@ -0,0 +1,56 @@
import * as express from 'express'
import { body, param } from 'express-validator'
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
import { CONFIG } from '@server/initializers/config.js'
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models'
import { areValidationErrors, doesVideoChannelIdExist } 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 doesVideoChannelIdExist(body.videoChannelId, 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 doesVideoChannelIdExist(res.locals.videoChannelSync.videoChannelId, res)) return
return next()
}
]
+193
ファイルの表示
@@ -0,0 +1,193 @@
import express from 'express'
import { body, param, query } from 'express-validator'
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 { MChannelAccountDefault } from '@server/types/models/index.js'
import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js'
import {
isVideoChannelDescriptionValid,
isVideoChannelDisplayNameValid,
isVideoChannelSupportValid,
isVideoChannelUsernameValid
} from '../../../helpers/custom-validators/video-channels.js'
import { ActorModel } from '../../../models/actor/actor.js'
import { VideoChannelModel } from '../../../models/video/video-channel.js'
import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } 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 ActorModel.loadLocalByName(req.body.name)
if (actor) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
})
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: `You cannot create more than ${CONFIG.VIDEO_CHANNELS.MAX_PER_USER} channels` })
return false
}
return next()
}
]
export const videoChannelsUpdateValidator = [
param('nameWithHost')
.exists(),
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, res)) return
return next()
}
]
export const videoChannelsNameWithHostValidator = [
param('nameWithHost')
.exists(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
return next()
}
]
export const ensureIsLocalChannel = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (res.locals.videoChannel.Actor.isOwned() === false) {
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'This channel is not owned.'
})
}
return next()
}
]
export const ensureChannelOwnerCanUpload = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const channel = res.locals.videoChannel
const user = { id: channel.Account.userId }
if (!await checkUserQuota(user, 1, res)) return
next()
}
]
export const videoChannelStatsValidator = [
query('withStats')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid stats flag boolean'),
(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: '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: 'This channel sync is not owned by this channel'
})
}
return next()
}
]
// ---------------------------------------------------------------------------
async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
if (count <= 1) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Cannot remove the last channel of this user'
})
return false
}
return true
}
+34
ファイルの表示
@@ -0,0 +1,34 @@
import express from 'express'
import { body } from 'express-validator'
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import {
areValidationErrors, checkUserCanManageVideo, 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 (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
return next()
}
]
+358
ファイルの表示
@@ -0,0 +1,358 @@
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 { 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,
checkCanSeeVideo,
checkUserCanManageAccount,
checkUserCanManageVideo,
doesVideoChannelIdExist,
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 doesVideoChannelIdExist(req.query.videoChannelId, res)) return
return next()
}
]
export const listCommentsOnUserVideosValidator = [
...getCommonVideoCommentsValidators(),
query('isHeldForReview')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid)
.withMessage('Should have a valid isHeldForReview 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, 'all')) return
if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
const user = res.locals.oauth.token.User
const video = res.locals.videoAll
if (video && !checkUserCanManageVideo(user, video, UserRight.SEE_ALL_COMMENTS, res)) return
const channel = res.locals.videoChannel
if (channel && !checkUserCanManageAccount({ account: channel.Account, user, res, specialRight: UserRight.SEE_ALL_COMMENTS })) 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 (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, 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 (!checkUserCanApproveVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, 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 checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
if (videoComment.isDeleted()) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'This comment is already deleted'
})
return false
}
const userAccount = user.Account
if (
user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT) === false && // Not a moderator
videoComment.accountId !== userAccount.id && // Not the comment owner
videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot remove video comment of another user'
})
return false
}
return true
}
function checkUserCanApproveVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
if (videoComment.isDeleted()) {
res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'This comment is deleted'
})
return false
}
if (videoComment.heldForReview !== true) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'This comment is not held for review'
})
return false
}
const userAccount = user.Account
if (
user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT) === false && // Not a moderator
videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot approve video comment of another user'
})
return false
}
return true
}
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 || 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')
]
}
+163
ファイルの表示
@@ -0,0 +1,163 @@
import express from 'express'
import { param } from 'express-validator'
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
import { MVideo } from '@server/types/models/index.js'
import { HttpStatusCode } from '@peertube/peertube-models'
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
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()
}
]
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()
}
]
// ---------------------------------------------------------------------------
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()
}
]
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
if (!video.getHLSPlaylist()) {
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'This video does not have HLS files'
})
}
const hlsFiles = video.getHLSPlaylist().VideoFiles
if (!hlsFiles.find(f => f.id === +req.params.videoFileId)) {
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'
})
}
return next()
}
]
export {
videoFilesDeleteWebVideoValidator,
videoFilesDeleteWebVideoFileValidator,
videoFilesDeleteHLSValidator,
videoFilesDeleteHLSFileValidator
}
// ---------------------------------------------------------------------------
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
}
+204
ファイルの表示
@@ -0,0 +1,204 @@
import express from 'express'
import { body, param, query } from 'express-validator'
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, MVideoImport } from '@server/types/models/index.js'
import { isIdValid, 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, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared/index.js'
import { getCommonVideoEditAttributes } from './videos.js'
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 user = res.locals.oauth.token.User
const torrentFile = req.files?.['torrentfile'] ? req.files['torrentfile'][0] : undefined
if (areValidationErrors(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: '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: 'Torrent/magnet URI import is not enabled on this instance.'
})
}
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) 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: '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: 'Cannot use non unicast IP as targetUrl.'
})
}
}
if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
return next()
}
])
const getMyVideoImportsValidator = [
query('videoChannelSyncId')
.optional()
.custom(isIdValid),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
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 (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
if (res.locals.videoImport.state === VideoImportState.PENDING) {
return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Cannot delete a pending video import. Cancel it or wait for the end of the import first.'
})
}
return next()
}
]
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 (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
if (res.locals.videoImport.state !== VideoImportState.PENDING) {
return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Cannot cancel a non pending video import.'
})
}
return next()
}
]
// ---------------------------------------------------------------------------
export {
videoImportAddValidator,
videoImportCancelValidator,
videoImportDeleteValidator,
getMyVideoImportsValidator
}
// ---------------------------------------------------------------------------
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 || acceptedResult.accepted !== true) {
logger.info('Refused to import video.', { acceptedResult, acceptParameters })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult.errorMessage || 'Refused to import video'
})
return false
}
return true
}
function checkUserCanManageImport (user: MUserAccountId, videoImport: MVideoImport, res: express.Response) {
if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === false && videoImport.userId !== user.id) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage video import of another user'
})
return false
}
return true
}
+333
ファイルの表示
@@ -0,0 +1,333 @@
import express from 'express'
import { body } from 'express-validator'
import { 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 { VideoModel } from '@server/models/video/video.js'
import { VideoLiveModel } from '@server/models/video/video-live.js'
import { VideoLiveSessionModel } from '@server/models/video/video-live-session.js'
import {
HttpStatusCode,
LiveVideoCreate,
LiveVideoLatencyMode,
LiveVideoUpdate,
ServerErrorCode,
UserRight,
VideoState
} from '@peertube/peertube-models'
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,
checkUserCanManageVideo,
doesVideoChannelOfAccountExist,
doesVideoExist,
isValidVideoIdParam
} from '../shared/index.js'
import { getCommonVideoEditAttributes } from './videos.js'
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.loadByVideoId(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()
}
]
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.'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(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'
})
}
const user = res.locals.oauth.token.User
if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) 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 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()
}
])
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),
(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 (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
return next()
}
]
const videoLiveListSessionsValidator = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
// Check the user can manage the live
const user = res.locals.oauth.token.User
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
return next()
}
]
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()
}
]
// ---------------------------------------------------------------------------
export {
videoLiveAddValidator,
videoLiveUpdateValidator,
videoLiveListSessionsValidator,
videoLiveFindReplaySessionValidator,
videoLiveGetValidator
}
// ---------------------------------------------------------------------------
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 || 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
}
+107
ファイルの表示
@@ -0,0 +1,107 @@
import express from 'express'
import { param } from 'express-validator'
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership.js'
import { AccountModel } from '@server/models/account/account.js'
import { MVideoWithAllFiles } from '@server/types/models/index.js'
import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models'
import {
areValidationErrors,
checkUserCanManageVideo,
checkUserQuota,
doesChangeVideoOwnershipExist,
doesVideoChannelOfAccountExist,
doesVideoExist,
isValidVideoIdParam
} from '../shared/index.js'
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 (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
const nextOwner = await AccountModel.loadLocalByName(req.body.username)
if (!nextOwner) {
res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
return
}
res.locals.nextOwner = nextOwner
return next()
}
]
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, res)) return
// Check if the user who did the request is able to change the ownership of the video
if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
const videoChangeOwnership = res.locals.videoChangeOwnership
if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Ownership already accepted or refused'
})
return
}
return next()
}
]
const videosAcceptChangeOwnershipValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const body = req.body as VideoChangeOwnershipAccept
if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
const videoChangeOwnership = res.locals.videoChangeOwnership
const video = videoChangeOwnership.Video
if (!await checkCanAccept(video, res)) return
return next()
}
]
export {
videosChangeOwnershipValidator,
videosTerminateChangeOwnershipValidator,
videosAcceptChangeOwnershipValidator
}
// ---------------------------------------------------------------------------
async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response): Promise<boolean> {
if (video.isLive) {
if (video.state !== VideoState.WAITING_FOR_LIVE) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: '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, video.getMaxQualityFile().size, res)) return false
return true
}
+77
ファイルの表示
@@ -0,0 +1,77 @@
import express from 'express'
import {
areValidationErrors,
doesVideoExist,
isVideoPasswordProtected,
isValidVideoIdParam,
doesVideoPasswordExist,
isVideoPasswordDeletable,
checkUserCanManageVideo
} from '../shared/index.js'
import { body, param } from 'express-validator'
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos.js'
import { UserRight } from '@peertube/peertube-models'
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 (!isVideoPasswordProtected(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 (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return
return next()
}
]
const updateVideoPasswordListValidator = [
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 doesVideoExist(req.params.videoId, res)) return
if (!isValidPasswordProtectedPrivacy(req, res)) return
// Check if the user who did the request is able to update video passwords
const user = res.locals.oauth.token.User
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
return next()
}
]
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 (!isVideoPasswordProtected(res)) return
if (!await doesVideoPasswordExist(req.params.passwordId, res)) return
if (!await isVideoPasswordDeletable(res)) return
return next()
}
]
// ---------------------------------------------------------------------------
export {
listVideoPasswordValidator,
updateVideoPasswordListValidator,
removeVideoPasswordValidator
}
+425
ファイルの表示
@@ -0,0 +1,425 @@
import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator'
import { forceNumber } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
UserRight,
UserRightType,
VideoPlaylistCreate,
VideoPlaylistPrivacy,
VideoPlaylistType,
VideoPlaylistUpdate
} from '@peertube/peertube-models'
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
import { MUserAccountId } from '@server/types/models/index.js'
import {
isArrayOf,
isIdOrUUIDValid,
isIdValid,
isUUIDValid,
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 { MVideoPlaylist } from '../../../types/models/video/video-playlist.js'
import { authenticatePromise } from '../../auth.js'
import {
areValidationErrors,
doesVideoChannelIdExist,
doesVideoExist,
doesVideoPlaylistExist,
isValidPlaylistIdParam,
VideoPlaylistFetchType
} from '../shared/index.js'
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 doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
if (
!body.videoChannelId &&
(body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
) {
cleanUpReqFiles(req)
return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
}
return next()
}
])
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(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
const videoPlaylist = getPlaylist(res)
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, 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: 'Cannot set "public" a playlist that is not assigned to a channel.' })
}
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
cleanUpReqFiles(req)
return res.fail({ message: 'Cannot update a watch later playlist.' })
}
if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
return next()
}
])
const videoPlaylistsDeleteValidator = [
isValidPlaylistIdParam('playlistId'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
const videoPlaylist = getPlaylist(res)
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
return res.fail({ message: 'Cannot delete a watch later playlist.' })
}
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
return
}
return next()
}
]
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(req.params.playlistId, 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: 'Playlist not found'
})
}
if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
await authenticatePromise({ req, res })
const user = res.locals.oauth ? res.locals.oauth.token.User : null
if (
!user ||
(videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
) {
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot get this private video playlist.'
})
}
return next()
}
return next()
}
]
}
const videoPlaylistsSearchValidator = [
query('search')
.optional()
.not().isEmpty(),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
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(req.params.playlistId, res, 'all')) return
if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return
const videoPlaylist = getPlaylist(res)
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
return
}
return next()
}
]
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(req.params.playlistId, res, 'all')) return
const videoPlaylist = getPlaylist(res)
const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
if (!videoPlaylistElement) {
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video playlist element not found'
})
return
}
res.locals.videoPlaylistElement = videoPlaylistElement
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
return next()
}
]
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: 'Video playlist element not found'
})
return
}
if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot get this private video playlist.'
})
}
res.locals.videoPlaylistElementAP = videoPlaylistElement
return next()
}
]
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(req.params.playlistId, res, 'all')) return
const videoPlaylist = getPlaylist(res)
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, 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: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
return
}
if (reorderLength && reorderLength + startPosition > nextPosition) {
res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
return
}
return next()
}
]
const commonVideoPlaylistFiltersValidator = [
query('playlistType')
.optional()
.custom(isVideoPlaylistTypeValid),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
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()
}
]
// ---------------------------------------------------------------------------
export {
videoPlaylistsAddValidator,
videoPlaylistsUpdateValidator,
videoPlaylistsDeleteValidator,
videoPlaylistsGetValidator,
videoPlaylistsSearchValidator,
videoPlaylistsAddVideoValidator,
videoPlaylistsUpdateOrRemoveVideoValidator,
videoPlaylistsReorderVideosValidator,
videoPlaylistElementAPGetValidator,
commonVideoPlaylistFiltersValidator,
doVideosInPlaylistExistValidator
}
// ---------------------------------------------------------------------------
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)[]
}
function checkUserCanManageVideoPlaylist (
user: MUserAccountId,
videoPlaylist: MVideoPlaylist,
right: UserRightType,
res: express.Response
) {
if (videoPlaylist.isOwned() === false) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage video playlist of another server.'
})
return false
}
// Check if the user can manage the video playlist
// The user can delete it if s/he is an admin
// Or if s/he is the video playlist's owner
if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage video playlist of another user'
})
return false
}
return true
}
function getPlaylist (res: express.Response) {
return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
}
+72
ファイルの表示
@@ -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('name')
.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.name, +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
ファイルの表示
@@ -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()
}
]
+131
ファイルの表示
@@ -0,0 +1,131 @@
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,
checkUserCanManageVideo,
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 (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
res.locals.videoSource = await VideoSourceModel.loadLatest(video.id)
if (!res.locals.videoSource) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: '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({ 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: '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: '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 (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return false
if (!checkVideoFileCanBeEdited(video, res)) return false
return true
}
+108
ファイルの表示
@@ -0,0 +1,108 @@
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, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
const videoOverallStatsValidator = [
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()
}
]
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()
}
]
const videoTimeserieStatsValidator = [
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: 'Star date and end date interval is too big'
})
}
return next()
}
]
// ---------------------------------------------------------------------------
export {
videoOverallStatsValidator,
videoTimeserieStatsValidator,
videoRetentionStatsValidator
}
// ---------------------------------------------------------------------------
async function commonStatsCheck (req: express.Request, res: express.Response) {
if (!await doesVideoExist(req.params.videoId, res, 'all')) return false
if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) 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
}
+105
ファイルの表示
@@ -0,0 +1,105 @@
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, checkUserCanManageVideo, 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)
const body: VideoStudioCreateEdition = req.body
const files = req.files as Express.Multer.File[]
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 (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: file does not contain a video stream`
})
return cleanUpReqFiles(req)
}
}
}
if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
const video = res.locals.videoAll
if (!checkVideoFileCanBeEdited(video, res)) return cleanUpReqFiles(req)
const user = res.locals.oauth.token.User
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) 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, additionalBytes, 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
ファイルの表示
@@ -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
}
+57
ファイルの表示
@@ -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 { checkVideoCanBeTranscribedOrTranscripted } 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 (!checkVideoCanBeTranscribedOrTranscripted(video, 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
}
+60
ファイルの表示
@@ -0,0 +1,60 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { isVideoTimeValid } 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(),
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()
}
]
+559
ファイルの表示
@@ -0,0 +1,559 @@
import { arrayify } from '@peertube/peertube-core-utils'
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@peertube/peertube-models'
import { Redis } from '@server/lib/redis.js'
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
import { getServerActor } from '@server/models/application/application.js'
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
import { ValidationChain, body, param, query } from 'express-validator'
import {
exists,
isBooleanValid,
isDateValid,
isFileValid,
isIdValid,
toBooleanOrNull,
toIntOrNull,
toValueOrNull
} from '../../../helpers/custom-validators/misc.js'
import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search.js'
import {
areVideoTagsValid,
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,
checkCanSeeVideo,
checkUserCanManageVideo,
doesVideoChannelOfAccountExist,
doesVideoExist,
doesVideoFileOfVideoExist,
isValidVideoIdParam,
isValidVideoPasswordHeader
} from '../shared/index.js'
import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
body('videofile')
.custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
.withMessage('Should have a file'),
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.'),
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: 'videosAddvideosAddLegacyValidatorResumableValidator' }) ||
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' })
) {
return cleanUpReqFiles(req)
}
return next()
}
])
/**
* Gets called after the last PUT request
*/
const videosAddResumableValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.User
const file = buildUploadXFile(req.body as express.CustomUploadXFile<express.UploadNewVideoXFileMetadata>)
const cleanup = () => safeUploadXCleanup(file)
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: 'The upload is already being processed'
})
}
await Redis.Instance.setUploadSession(uploadId)
if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) 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
*
*/
const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
body('filename')
.custom(isVideoSourceFilenameValid),
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.'),
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()
}
])
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 (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
const video = getVideoWithAttributes(res)
if (exists(req.body.privacy) && video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) {
return res.fail({ message: '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 (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
return next()
}
])
async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
const video = getVideoWithAttributes(res)
// Anybody can watch local videos
if (video.isOwned() === true) return next()
// Logged user
if (res.locals.oauth) {
// Users can search or watch remote videos
if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
}
// Anybody can search or watch remote videos
if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
// Check our instance follows an actor that shared this video
const serverActor = await getServerActor()
if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot get this video regarding follow constraints',
type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
data: {
originUrl: video.url
}
})
}
const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes') => {
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()
}
]
}
const videosGetValidator = videosCustomGetValidator('all')
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()
}
])
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()
}
]
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 (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
return next()
}
]
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()
}
]
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('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`
),
// TODO: remove, deprecated in PeerTube 6.2
body('commentsEnabled')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have valid commentsEnabled boolean'),
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)[]
}
const commonVideosFiltersValidator = [
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('isLive')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid isLive 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('hasWebtorrentFiles') // TODO: remove in v7
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles 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'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
const user = res.locals.oauth?.token.User
if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
if (req.query.include || req.query.privacyOneOf || req.query.autoTagOneOf) {
return res.fail({
status: HttpStatusCode.UNAUTHORIZED_401,
message: 'You are not allowed to see all videos, specify a custom include or auto tags filter.'
})
}
}
if (!user && exists(req.query.excludeAlreadyWatched)) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot use excludeAlreadyWatched parameter when auth token is not provided'
})
return false
}
if (req.query.filter) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: '"filter" query parameter is not supported anymore by PeerTube. Please use "isLocal" and "include" instead'
})
return false
}
return next()
}
]
// ---------------------------------------------------------------------------
export {
checkVideoFollowConstraints,
commonVideosFiltersValidator,
getCommonVideoEditAttributes,
videoFileMetadataGetValidator,
videosAddLegacyValidator,
videosAddResumableInitValidator,
videosAddResumableValidator,
videosCustomGetValidator,
videosDownloadValidator,
videosGetValidator,
videosOverviewValidator,
videosRemoveValidator,
videosUpdateValidator
}
// ---------------------------------------------------------------------------
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: 'Schedule update at 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, user } = options
if (areErrorsInScheduleUpdate(req, res)) return false
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
if (!await commonVideoFileChecks(options)) return false
return true
}
+128
ファイルの表示
@@ -0,0 +1,128 @@
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 { doesAccountNameWithHostExist } from './shared/accounts.js'
import { checkUserCanManageAccount } from './shared/users.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 doesAccountNameWithHostExist(req.params.accountName, res)) return
if (!checkUserCanManageAccount({ user: res.locals.oauth.token.User, account: res.locals.account, specialRight: null, res })) 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
ファイルの表示
@@ -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 nameWithHost = getHostWithPort(req.query.resource.substr(5))
const [ name ] = nameWithHost.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
}