Rocky_Mountain_Vending/lib/manuals-asset-paths.ts

127 lines
3 KiB
TypeScript

export type ManualAssetKind = "manual" | "thumbnail"
function safeDecodeSegment(value: string) {
try {
return decodeURIComponent(value)
} catch {
return value
}
}
function decodePath(pathname: string) {
return pathname
.split("/")
.map((segment) => safeDecodeSegment(segment))
.join("/")
}
function stripLeadingSlash(value: string) {
return value.replace(/^\/+/, "")
}
function stripAssetPrefix(value: string, kind: ManualAssetKind) {
if (kind === "manual") {
return value
.replace(/^api\/manuals\//i, "")
.replace(/^manuals\//i, "")
.replace(/^\/api\/manuals\//i, "")
.replace(/^\/manuals\//i, "")
}
return value
.replace(/^api\/thumbnails\//i, "")
.replace(/^thumbnails\//i, "")
.replace(/^\/api\/thumbnails\//i, "")
.replace(/^\/thumbnails\//i, "")
}
function looksLikePrivateHost(hostname: string) {
const host = hostname.trim().toLowerCase()
if (!host) {
return true
}
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
return true
}
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host)) {
return true
}
const match172 = host.match(/^172\.(\d{1,3})\./)
if (match172) {
const octet = Number.parseInt(match172[1] || "", 10)
if (Number.isFinite(octet) && octet >= 16 && octet <= 31) {
return true
}
}
return false
}
function normalizeRelativePath(value: string, kind: ManualAssetKind) {
const decoded = decodePath(stripLeadingSlash(value.trim()))
const withoutPrefix = stripAssetPrefix(decoded, kind).trim()
if (!withoutPrefix) {
return null
}
if (withoutPrefix.includes("..")) {
return null
}
return withoutPrefix
}
function extractRelativePathFromAbsolute(url: URL, kind: ManualAssetKind) {
const decodedPath = decodePath(url.pathname)
return normalizeRelativePath(decodedPath, kind)
}
export function isPrivateOrLocalHost(hostname: string) {
return looksLikePrivateHost(hostname)
}
export function normalizeManualAssetValue(
value: string | null | undefined,
kind: ManualAssetKind
): string | null {
const raw = String(value || "").trim()
if (!raw) {
return null
}
if (!/^https?:\/\//i.test(raw)) {
return normalizeRelativePath(raw, kind)
}
try {
const parsed = new URL(raw)
const extractedRelativePath = extractRelativePathFromAbsolute(parsed, kind)
if (looksLikePrivateHost(parsed.hostname)) {
return extractedRelativePath
}
// Normalize site-proxy URLs to relative paths to keep persisted values tenant-safe.
if (extractedRelativePath) {
const lowerPath = parsed.pathname.toLowerCase()
if (
lowerPath.includes("/api/manuals/") ||
lowerPath.includes("/api/thumbnails/") ||
lowerPath.includes("/manuals/") ||
lowerPath.includes("/thumbnails/")
) {
return extractedRelativePath
}
}
return raw
} catch {
return null
}
}
export function isAbsoluteHttpUrl(value: string) {
return /^https?:\/\//i.test(value.trim())
}