はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
+153
ファイルの表示
@@ -0,0 +1,153 @@
import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@peertube/peertube-models'
import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor.js'
import { getAPId } from '@server/lib/activitypub/activity.js'
import { wrapWithSpanAndContext } from '@server/lib/opentelemetry/tracing.js'
import { NextFunction, Request, Response } from 'express'
import { logger } from '../helpers/logger.js'
import { isHTTPSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto.js'
import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants.js'
import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors/index.js'
export async function checkSignature (req: Request, res: Response, next: NextFunction) {
try {
const httpSignatureChecked = await checkHttpSignature(req, res)
if (httpSignatureChecked !== true) return
const actor = res.locals.signature.actor
// Forwarded activity
const bodyActor = req.body.actor
const bodyActorId = getAPId(bodyActor)
if (bodyActorId && bodyActorId !== actor.url) {
const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
if (jsonLDSignatureChecked !== true) return
}
return next()
} catch (err) {
const activity: ActivityDelete = req.body
if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
logger.debug('Handling signature error on actor delete activity', { err })
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
logger.warn('Error in ActivityPub signature checker.', { err })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'ActivityPub signature could not be checked'
})
}
}
export function executeIfActivityPub (req: Request, res: Response, next: NextFunction) {
const accepted = req.accepts(ACCEPT_HEADERS)
if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.includes(accepted) === false) {
// Bypass this route
return next('route')
}
logger.debug('ActivityPub request for %s.', req.url)
return next()
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
async function checkHttpSignature (req: Request, res: Response) {
return wrapWithSpanAndContext('peertube.activitypub.checkHTTPSignature', async () => {
// FIXME: compatibility with http-signature < v1.3
const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string
if (sig && sig.startsWith('Signature ') === true) req.headers[HTTP_SIGNATURE.HEADER_NAME] = sig.replace(/^Signature /, '')
let parsed: any
try {
parsed = parseHTTPSignature(req, HTTP_SIGNATURE.CLOCK_SKEW_SECONDS)
} catch (err) {
logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: err.message
})
return false
}
const keyId = parsed.keyId
if (!keyId) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Invalid key ID',
data: {
keyId
}
})
return false
}
logger.debug('Checking HTTP signature of actor %s...', keyId)
let [ actorUrl ] = keyId.split('#')
if (actorUrl.startsWith('acct:')) {
actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
}
const actor = await getOrCreateAPActor(actorUrl)
const verified = isHTTPSignatureVerified(parsed, actor)
if (verified !== true) {
logger.warn('Signature from %s is invalid', actorUrl, { parsed })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Invalid signature',
data: {
actorUrl
}
})
return false
}
res.locals.signature = { actor }
return true
})
}
async function checkJsonLDSignature (req: Request, res: Response) {
// Lazy load the module as it's quite big with json.ld dependency
const { compactJSONLDAndCheckSignature } = await import('../helpers/peertube-jsonld.js')
return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => {
const signatureObject: ActivityPubSignature = req.body.signature
if (!signatureObject?.creator) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Object and creator signature do not match'
})
return false
}
const [ creator ] = signatureObject.creator.split('#')
logger.debug('Checking JsonLD signature of actor %s...', creator)
const actor = await getOrCreateAPActor(creator)
const verified = await compactJSONLDAndCheckSignature(actor, req)
if (verified !== true) {
logger.warn('Signature not verified.', req.body)
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Signature could not be verified'
})
return false
}
res.locals.signature = { actor }
return true
})
}
+48
ファイルの表示
@@ -0,0 +1,48 @@
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
import { NextFunction, Request, RequestHandler, Response } from 'express'
import { ValidationChain } from 'express-validator'
import { retryTransactionWrapper } from '../helpers/database-utils.js'
// Syntactic sugar to avoid try/catch in express controllers/middlewares
export type RequestPromiseHandler = ValidationChain | ExpressPromiseHandler
function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]) {
return async (req: Request, res: Response, next: NextFunction) => {
if (Array.isArray(fun) !== true) {
return Promise.resolve((fun as RequestHandler)(req, res, next))
.catch(err => next(err))
}
try {
for (const f of fun) {
await new Promise<void>((resolve, reject) => {
return asyncMiddleware(f)(req, res, err => {
if (err) return reject(err)
return resolve()
})
})
}
next()
} catch (err) {
next(err)
}
}
}
function asyncRetryTransactionMiddleware (fun: (req: Request, res: Response, next: NextFunction) => Promise<any>) {
return (req: Request, res: Response, next: NextFunction) => {
return Promise.resolve(
retryTransactionWrapper(fun, req, res, next)
).catch(err => next(err))
}
}
// ---------------------------------------------------------------------------
export {
asyncMiddleware,
asyncRetryTransactionMiddleware
}
+112
ファイルの表示
@@ -0,0 +1,112 @@
import express from 'express'
import { Socket } from 'socket.io'
import { HttpStatusCode, HttpStatusCodeType, ServerErrorCodeType } from '@peertube/peertube-models'
import { getAccessToken } from '@server/lib/auth/oauth-model.js'
import { RunnerModel } from '@server/models/runner/runner.js'
import { logger } from '../helpers/logger.js'
import { handleOAuthAuthenticate } from '../lib/auth/oauth.js'
function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
handleOAuthAuthenticate(req, res)
.then((token: any) => {
res.locals.oauth = { token }
res.locals.authenticated = true
return next()
})
.catch(err => {
logger.info('Cannot authenticate.', { err })
return res.fail({
status: err.status,
message: 'Token is invalid',
type: err.name
})
})
}
function authenticateSocket (socket: Socket, next: (err?: any) => void) {
const accessToken = socket.handshake.query['accessToken']
logger.debug('Checking access token in runner.')
if (!accessToken) return next(new Error('No access token provided'))
if (typeof accessToken !== 'string') return next(new Error('Access token is invalid'))
getAccessToken(accessToken)
.then(tokenDB => {
const now = new Date()
if (!tokenDB || tokenDB.accessTokenExpiresAt < now || tokenDB.refreshTokenExpiresAt < now) {
return next(new Error('Invalid access token.'))
}
socket.handshake.auth.user = tokenDB.User
return next()
})
.catch(err => logger.error('Cannot get access token.', { err }))
}
function authenticatePromise (options: {
req: express.Request
res: express.Response
errorMessage?: string
errorStatus?: HttpStatusCodeType
errorType?: ServerErrorCodeType
}) {
const { req, res, errorMessage = 'Not authenticated', errorStatus = HttpStatusCode.UNAUTHORIZED_401, errorType } = options
return new Promise<void>(resolve => {
// Already authenticated? (or tried to)
if (res.locals.oauth?.token.User) return resolve()
if (res.locals.authenticated === false) {
return res.fail({
status: errorStatus,
type: errorType,
message: errorMessage
})
}
authenticate(req, res, () => resolve())
})
}
function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.header('authorization')) return authenticate(req, res, next)
res.locals.authenticated = false
return next()
}
// ---------------------------------------------------------------------------
function authenticateRunnerSocket (socket: Socket, next: (err?: any) => void) {
const runnerToken = socket.handshake.auth['runnerToken']
logger.debug('Checking runner token in socket.')
if (!runnerToken) return next(new Error('No runner token provided'))
if (typeof runnerToken !== 'string') return next(new Error('Runner token is invalid'))
RunnerModel.loadByToken(runnerToken)
.then(runner => {
if (!runner) return next(new Error('Invalid runner token.'))
socket.handshake.auth.runner = runner
return next()
})
.catch(err => logger.error('Cannot get runner token.', { err }))
}
// ---------------------------------------------------------------------------
export {
authenticate,
authenticateSocket,
authenticatePromise,
optionalAuthenticate,
authenticateRunnerSocket
}
+58
ファイルの表示
@@ -0,0 +1,58 @@
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { ApiCache, APICacheOptions } from './shared/index.js'
const defaultOptions: APICacheOptions = {
excludeStatus: [
HttpStatusCode.FORBIDDEN_403,
HttpStatusCode.NOT_FOUND_404
]
}
export function cacheRoute (duration: string) {
const instance = new ApiCache(defaultOptions)
return instance.buildMiddleware(duration)
}
export function cacheRouteFactory (options: APICacheOptions = {}) {
const instance = new ApiCache({ ...defaultOptions, ...options })
return { instance, middleware: instance.buildMiddleware.bind(instance) }
}
// ---------------------------------------------------------------------------
export function buildPodcastGroupsCache (options: {
channelId: number
}) {
return 'podcast-feed-' + options.channelId
}
export function buildAPVideoChaptersGroupsCache (options: {
videoId: number | string
}) {
return 'ap-video-chapters-' + options.videoId
}
// ---------------------------------------------------------------------------
export const videoFeedsPodcastSetCacheKey = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.query.videoChannelId) {
res.locals.apicacheGroups = [ buildPodcastGroupsCache({ channelId: req.query.videoChannelId }) ]
}
return next()
}
]
export const apVideoChaptersSetCacheKey = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.params.id) {
res.locals.apicacheGroups = [ buildAPVideoChaptersGroupsCache({ videoId: req.params.id }) ]
}
return next()
}
]
+1
ファイルの表示
@@ -0,0 +1 @@
export * from './cache.js'
+315
ファイルの表示
@@ -0,0 +1,315 @@
// Thanks: https://github.com/kwhitley/apicache
// We duplicated the library because it is unmaintened and prevent us to upgrade to recent NodeJS versions
import express from 'express'
import { OutgoingHttpHeaders } from 'http'
import { HttpStatusCodeType } from '@peertube/peertube-models'
import { isTestInstance } from '@peertube/peertube-node-utils'
import { parseDurationToMs } from '@server/helpers/core-utils.js'
import { logger } from '@server/helpers/logger.js'
import { Redis } from '@server/lib/redis.js'
import { asyncMiddleware } from '@server/middlewares/index.js'
export interface APICacheOptions {
headerBlacklist?: string[]
excludeStatus?: HttpStatusCodeType[]
}
interface CacheObject {
status: number
headers: OutgoingHttpHeaders
data: any
encoding: BufferEncoding
timestamp: number
}
export class ApiCache {
private readonly options: APICacheOptions
private readonly timers: { [ id: string ]: NodeJS.Timeout } = {}
private readonly index = {
groups: [] as string[],
all: [] as string[]
}
// Cache keys per group
private groups: { [groupIndex: string]: string[] } = {}
private readonly seed: number
constructor (options: APICacheOptions) {
this.seed = new Date().getTime()
this.options = {
headerBlacklist: [],
excludeStatus: [],
...options
}
}
buildMiddleware (strDuration: string) {
const duration = parseDurationToMs(strDuration)
return asyncMiddleware(
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const key = this.getCacheKey(req)
const redis = Redis.Instance.getClient()
if (!Redis.Instance.isConnected()) return this.makeResponseCacheable(res, next, key, duration)
try {
const obj = await redis.hgetall(key)
if (obj?.response) {
return this.sendCachedResponse(req, res, JSON.parse(obj.response), duration)
}
return this.makeResponseCacheable(res, next, key, duration)
} catch (err) {
return this.makeResponseCacheable(res, next, key, duration)
}
}
)
}
clearGroupSafe (group: string) {
const run = async () => {
const cacheKeys = this.groups[group]
if (!cacheKeys) return
for (const key of cacheKeys) {
try {
await this.clear(key)
} catch (err) {
logger.error('Cannot clear ' + key, { err })
}
}
delete this.groups[group]
}
void run()
}
private getCacheKey (req: express.Request) {
return Redis.Instance.getPrefix() + 'api-cache-' + this.seed + '-' + req.originalUrl
}
private shouldCacheResponse (response: express.Response) {
if (!response) return false
if (this.options.excludeStatus.includes(response.statusCode as HttpStatusCodeType)) return false
return true
}
private addIndexEntries (key: string, res: express.Response) {
this.index.all.unshift(key)
const groups = res.locals.apicacheGroups || []
for (const group of groups) {
if (!this.groups[group]) this.groups[group] = []
this.groups[group].push(key)
}
}
private filterBlacklistedHeaders (headers: OutgoingHttpHeaders) {
return Object.keys(headers)
.filter(key => !this.options.headerBlacklist.includes(key))
.reduce((acc, header) => {
acc[header] = headers[header]
return acc
}, {})
}
private createCacheObject (status: number, headers: OutgoingHttpHeaders, data: any, encoding: BufferEncoding) {
return {
status,
headers: this.filterBlacklistedHeaders(headers),
data,
encoding,
// Seconds since epoch, used to properly decrement max-age headers in cached responses.
timestamp: new Date().getTime() / 1000
} as CacheObject
}
private async cacheResponse (key: string, value: object, duration: number) {
const redis = Redis.Instance.getClient()
if (Redis.Instance.isConnected()) {
await Promise.all([
redis.hset(key, 'response', JSON.stringify(value)),
redis.hset(key, 'duration', duration + ''),
redis.expire(key, duration / 1000)
])
}
// add automatic cache clearing from duration, includes max limit on setTimeout
this.timers[key] = setTimeout(() => {
this.clear(key)
.catch(err => logger.error('Cannot clear Redis key %s.', key, { err }))
}, Math.min(duration, 2147483647))
}
private accumulateContent (res: express.Response, content: any) {
if (!content) return
if (typeof content === 'string') {
res.locals.apicache.content = (res.locals.apicache.content || '') + content
return
}
if (Buffer.isBuffer(content)) {
let oldContent = res.locals.apicache.content
if (typeof oldContent === 'string') {
oldContent = Buffer.from(oldContent)
}
if (!oldContent) {
oldContent = Buffer.alloc(0)
}
res.locals.apicache.content = Buffer.concat(
[ oldContent, content ],
oldContent.length + content.length
)
return
}
res.locals.apicache.content = content
}
private makeResponseCacheable (res: express.Response, next: express.NextFunction, key: string, duration: number) {
const self = this
res.locals.apicache = {
write: res.write.bind(res),
writeHead: res.writeHead.bind(res),
end: res.end.bind(res),
cacheable: true,
content: undefined,
headers: undefined
}
// Patch express
res.writeHead = function () {
if (self.shouldCacheResponse(res)) {
res.setHeader('cache-control', 'max-age=' + (duration / 1000).toFixed(0))
} else {
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
}
res.locals.apicache.headers = Object.assign({}, res.getHeaders())
return res.locals.apicache.writeHead.apply(this, arguments as any)
}
res.write = function (chunk: any) {
self.accumulateContent(res, chunk)
return res.locals.apicache.write.apply(this, arguments as any)
}
res.end = function (content: any, encoding: BufferEncoding) {
if (self.shouldCacheResponse(res)) {
self.accumulateContent(res, content)
if (res.locals.apicache.cacheable && res.locals.apicache.content) {
self.addIndexEntries(key, res)
const headers = res.locals.apicache.headers || res.getHeaders()
const cacheObject = self.createCacheObject(
res.statusCode,
headers,
res.locals.apicache.content,
encoding
)
self.cacheResponse(key, cacheObject, duration)
.catch(err => logger.error('Cannot cache response', { err }))
}
}
res.locals.apicache.end.apply(this, arguments as any)
} as any
next()
}
private sendCachedResponse (request: express.Request, response: express.Response, cacheObject: CacheObject, duration: number) {
const headers = response.getHeaders()
if (isTestInstance()) {
Object.assign(headers, {
'x-api-cache-cached': 'true'
})
}
Object.assign(headers, this.filterBlacklistedHeaders(cacheObject.headers || {}), {
// Set properly decremented max-age header
// This ensures that max-age is in sync with the cache expiration
'cache-control':
'max-age=' +
Math.max(
0,
(duration / 1000 - (new Date().getTime() / 1000 - cacheObject.timestamp))
).toFixed(0)
})
// unstringify buffers
let data = cacheObject.data
if (data && data.type === 'Buffer') {
data = typeof data.data === 'number'
? Buffer.alloc(data.data)
: Buffer.from(data.data)
}
// Test Etag against If-None-Match for 304
const cachedEtag = cacheObject.headers.etag
const requestEtag = request.headers['if-none-match']
if (requestEtag && cachedEtag === requestEtag) {
response.writeHead(304, headers)
return response.end()
}
response.writeHead(cacheObject.status || 200, headers)
return response.end(data, cacheObject.encoding)
}
private async clear (target: string) {
const redis = Redis.Instance.getClient()
if (target) {
clearTimeout(this.timers[target])
delete this.timers[target]
try {
await redis.del(target)
} catch (err) {
logger.error('Cannot delete %s in redis cache.', target, { err })
}
this.index.all = this.index.all.filter(key => key !== target)
} else {
for (const key of this.index.all) {
clearTimeout(this.timers[key])
delete this.timers[key]
try {
await redis.del(key)
} catch (err) {
logger.error('Cannot delete %s in redis cache.', key, { err })
}
}
this.index.all = []
}
return this.index
}
}
+1
ファイルの表示
@@ -0,0 +1 @@
export * from './api-cache.js'
+47
ファイルの表示
@@ -0,0 +1,47 @@
import { contentSecurityPolicy } from 'helmet'
import { CONFIG } from '../initializers/config.js'
const baseDirectives = Object.assign({},
{
defaultSrc: [ '\'none\'' ], // by default, not specifying default-src = '*'
connectSrc: [ '*', 'data:' ],
mediaSrc: [ '\'self\'', 'https:', 'blob:' ],
fontSrc: [ '\'self\'', 'data:' ],
imgSrc: [ '\'self\'', 'data:', 'blob:' ],
scriptSrc: [ '\'self\' \'unsafe-inline\' \'unsafe-eval\'', 'blob:' ],
scriptSrcAttr: [ '\'unsafe-inline\'' ],
styleSrc: [ '\'self\' \'unsafe-inline\'' ],
objectSrc: [ '\'none\'' ], // only define to allow plugins, else let defaultSrc 'none' block it
formAction: [ '\'self\'' ],
frameAncestors: [ '\'none\'' ],
baseUri: [ '\'self\'' ],
manifestSrc: [ '\'self\'' ],
frameSrc: [ '\'self\'' ], // instead of deprecated child-src / self because of test-embed
workerSrc: [ '\'self\'', 'blob:' ] // instead of deprecated child-src
},
CONFIG.CSP.REPORT_URI
? { reportUri: CONFIG.CSP.REPORT_URI }
: {},
CONFIG.WEBSERVER.SCHEME === 'https'
? { upgradeInsecureRequests: [] }
: {}
)
const baseCSP = contentSecurityPolicy({
directives: baseDirectives,
reportOnly: CONFIG.CSP.REPORT_ONLY
})
const embedCSP = contentSecurityPolicy({
directives: Object.assign({}, baseDirectives, { frameAncestors: [ '*' ] }),
reportOnly: CONFIG.CSP.REPORT_ONLY
})
// ---------------------------------------------------------------------------
export {
baseCSP,
embedCSP
}
+15
ファイルの表示
@@ -0,0 +1,15 @@
import * as express from 'express'
const advertiseDoNotTrack = (_, res: express.Response, next: express.NextFunction) => {
if (!res.headersSent) {
res.setHeader('Tk', 'N')
}
return next()
}
// ---------------------------------------------------------------------------
export {
advertiseDoNotTrack
}
+16
ファイルの表示
@@ -0,0 +1,16 @@
import express from 'express'
function openapiOperationDoc (options: {
url?: string
operationId?: string
}) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.locals.docUrl = options.url || 'https://docs.joinpeertube.org/api-rest-reference.html#operation/' + options.operationId
if (next) return next()
}
}
export {
openapiOperationDoc
}
+63
ファイルの表示
@@ -0,0 +1,63 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import express from 'express'
import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
res.fail = options => {
const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance, tags, logLevel = 'debug' } = options
const extension = new ProblemDocumentExtension({
...data,
docs: res.locals.docUrl,
code: type,
// For <= 3.2 compatibility
error: message
})
const json = new ProblemDocument({
status,
title,
instance,
detail: message,
type: type
? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
: undefined
}, extension)
logger.log(logLevel, 'Bad HTTP request.', { json, tags })
res.status(status)
// Cannot display a proper error to the client since headers are already sent
if (res.headersSent) return
res.setHeader('Content-Type', 'application/problem+json')
res.json(json)
}
if (next) next()
}
function handleStaticError (err: any, req: express.Request, res: express.Response, next: express.NextFunction) {
const message = err.message || ''
if (message.includes('ENOENT')) {
return res.fail({
status: err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: err.message,
type: err.name
})
}
return next(err)
}
export {
apiFailMiddleware,
handleStaticError
}
+18
ファイルの表示
@@ -0,0 +1,18 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import express from 'express'
export function setReqTimeout (timeoutMs: number) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
req.setTimeout(timeoutMs, () => {
logger.error('Express request timeout in ' + req.originalUrl)
return res.fail({
status: HttpStatusCode.REQUEST_TIMEOUT_408,
message: 'Request has timed out.'
})
})
next()
}
}
+15
ファイルの表示
@@ -0,0 +1,15 @@
export * from './validators/index.js'
export * from './cache/index.js'
export * from './activitypub.js'
export * from './async.js'
export * from './auth.js'
export * from './pagination.js'
export * from './rate-limiter.js'
export * from './servers.js'
export * from './sort.js'
export * from './user-right.js'
export * from './dnt.js'
export * from './error.js'
export * from './express.js'
export * from './doc.js'
export * from './csp.js'
+19
ファイルの表示
@@ -0,0 +1,19 @@
import express from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { PAGINATION } from '../initializers/constants.js'
function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.query.start) req.query.start = 0
else req.query.start = forceNumber(req.query.start)
if (!req.query.count) req.query.count = PAGINATION.GLOBAL.COUNT.DEFAULT
else req.query.count = forceNumber(req.query.count)
return next()
}
// ---------------------------------------------------------------------------
export {
setDefaultPagination
}
+62
ファイルの表示
@@ -0,0 +1,62 @@
import express from 'express'
import RateLimit, { Options as RateLimitHandlerOptions } from 'express-rate-limit'
import { UserRole, UserRoleType } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import { RunnerModel } from '@server/models/runner/runner.js'
import { optionalAuthenticate } from './auth.js'
import { logger } from '@server/helpers/logger.js'
const whitelistRoles = new Set<UserRoleType>([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ])
export function buildRateLimiter (options: {
windowMs: number
max: number
skipFailedRequests?: boolean
}) {
return RateLimit({
windowMs: options.windowMs,
max: options.max,
skipFailedRequests: options.skipFailedRequests,
handler: (req, res, next, options) => {
// Bypass rate limit for registered runners
if (req.body?.runnerToken) {
return RunnerModel.loadByToken(req.body.runnerToken)
.then(runner => {
if (runner) return next()
return sendRateLimited(req, res, options)
})
}
// Bypass rate limit for admins/moderators
return optionalAuthenticate(req, res, () => {
if (res.locals.authenticated === true && whitelistRoles.has(res.locals.oauth.token.User.role)) {
return next()
}
return sendRateLimited(req, res, options)
})
}
})
}
export const apiRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
max: CONFIG.RATES_LIMIT.API.MAX
})
export const activityPubRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.ACTIVITY_PUB.WINDOW_MS,
max: CONFIG.RATES_LIMIT.ACTIVITY_PUB.MAX
})
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function sendRateLimited (req: express.Request, res: express.Response, options: RateLimitHandlerOptions) {
logger.debug('Rate limit exceeded for route ' + req.originalUrl)
return res.status(options.statusCode).send(options.message)
}
+29
ファイルの表示
@@ -0,0 +1,29 @@
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { getHostWithPort } from '../helpers/express-utils.js'
function setBodyHostsPort (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.body.hosts) return next()
for (let i = 0; i < req.body.hosts.length; i++) {
const hostWithPort = getHostWithPort(req.body.hosts[i])
// Problem with the url parsing?
if (hostWithPort === null) {
return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Could not parse hosts'
})
}
req.body.hosts[i] = hostWithPort
}
return next()
}
// ---------------------------------------------------------------------------
export {
setBodyHostsPort
}
+29
ファイルの表示
@@ -0,0 +1,29 @@
import express from 'express'
const setDefaultSort = setDefaultSortFactory('-createdAt')
const setDefaultVideosSort = setDefaultSortFactory('-publishedAt')
const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name')
const setDefaultSearchSort = setDefaultSortFactory('-match')
const setBlacklistSort = setDefaultSortFactory('-createdAt')
// ---------------------------------------------------------------------------
export {
setDefaultSort,
setDefaultSearchSort,
setDefaultVideosSort,
setDefaultVideoRedundanciesSort,
setBlacklistSort
}
// ---------------------------------------------------------------------------
function setDefaultSortFactory (sort: string) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (!req.query.sort) req.query.sort = sort
return next()
}
}
+26
ファイルの表示
@@ -0,0 +1,26 @@
import express from 'express'
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
import { logger } from '../helpers/logger.js'
function ensureUserHasRight (userRight: UserRightType) {
return function (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.user
if (user.hasRight(userRight) === false) {
const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.`
logger.info(message)
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message
})
}
return next()
}
}
// ---------------------------------------------------------------------------
export {
ensureUserHasRight
}
+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[])
}

変更されたファイルが多すぎるため,一部のファイルは表示されません さらに表示