UNPKG

scrivito

Version:

Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.

224 lines (178 loc) 6.01 kB
import { withForbiddenSiteContext } from 'scrivito_sdk/app_support/current_page'; import { SiteData } from 'scrivito_sdk/app_support/current_page_data'; import { ArgumentError, ScrivitoError, currentOrigin, docUrl, onReset, prettyPrint, throwNextTick, } from 'scrivito_sdk/common'; export type SiteMappingConfiguration = ClassicConfig | MultisiteConfig; interface ClassicConfig { origin?: string; routingBasePath?: string; baseUrlForSite?: undefined; siteForUrl?: undefined; } interface MultisiteConfig { baseUrlForSite: BaseUrlForSiteCallback; siteForUrl: SiteForUrlCallback; } export type BaseUrlForSiteCallback = (siteId: string) => string | undefined; // since this callback is from userland, we must assume unknown as the return type let baseUrlForSiteCallback: ((siteId: string) => unknown) | undefined; export type SiteForUrlCallback = (url: string) => SiteForUrlResult; export type SiteForUrlResult = { siteId: string; baseUrl: string } | undefined; // since this callback is from userland, we must assume unknown as the return type let siteForUrlCallback: ((url: string) => unknown) | undefined; export function initSiteMapping(config: SiteMappingConfiguration = {}): void { if (config.baseUrlForSite) { baseUrlForSiteCallback = config.baseUrlForSite; siteForUrlCallback = config.siteForUrl; return; } // "desugar" the ClassicConfig const basePath = config.routingBasePath ?? ''; let baseUrl: string | undefined; baseUrlForSiteCallback = (siteId) => { if (siteId === null || siteId === 'default') { if (!baseUrl) { const origin = config.origin ?? currentOrigin() ?? throwNoOrigin(); baseUrl = `${origin}/${basePath.replace(/^\/+/, '')}`; } return baseUrl; } }; siteForUrlCallback = (url) => { const { origin } = new URL(url); if (origin !== config.origin && origin !== currentOrigin()) return; if (!basePath) { return { siteId: 'default', baseUrl: origin }; } return { siteId: 'default', baseUrl: new URL(basePath, origin).href, }; }; } export function baseUrlForSite(siteId: string): string | null { const result = executeBaseUrlForSiteCallback(siteId); if (result === undefined) return null; if (result === '') return null; if (typeof result === 'string') return removeTrailingSlashes(result); reportUnexpectedReturnValue('baseUrlForSite', result, 'String | Void'); return null; } export interface SiteDataAndPath { siteData: SiteData; sitePath: string; } export function recognizeSiteAndPath( uriToRecognize: string ): SiteDataAndPath | { siteData?: SiteData; sitePath: null } { if (!siteForUrlCallback) throwNotInitialized(); const url = normalizeUri(uriToRecognize); const result = siteForUrl(url); if (!result) return { sitePath: null }; return { siteData: { siteId: result.siteId, baseUrl: result.baseUrl }, sitePath: determineSitePath(result.baseUrl, url), }; } function determineSitePath(baseUrl: string, url: string) { if (!startsWith(baseUrl, url)) return null; const restOfUrl = url.substring(baseUrl.length); const path = removeNonPathComponents(restOfUrl); if (path === '') return '/'; if (path.charAt(0) !== '/') return null; return path; } function startsWith(prefix: string, input: string) { return input.substring(0, prefix.length) === prefix; } function removeNonPathComponents(resource: string) { const { pathname } = new URL(resource, 'http://example.com'); return resource.startsWith('/') ? pathname : pathname.substring(1); } function normalizeUri(url: string) { const baseUrl = URL.canParse(url) ? undefined : currentOrigin() ?? throwNoOrigin(); const location = new URL(url, baseUrl); location.pathname = location.pathname.replace(/\/+/g, '/'); return location.href; } function siteForUrl(url: string): SiteForUrlResult { const result = withForbiddenSiteContext( 'Access to current site inside siteForUrl. Forgot to use onAllSites?', () => siteForUrlCallback?.call(null, removeQueryAndHash(url)) ); if (!isSiteForUrlResult(result)) { reportUnexpectedReturnValue( 'siteForUrl', result, '{siteId: String, baseUrl: String} | Void' ); return undefined; } return ( result && { siteId: result.siteId, baseUrl: removeTrailingSlashes(result.baseUrl), } ); } function removeQueryAndHash(url: string) { const urlObj = new URL(url); urlObj.search = ''; urlObj.hash = ''; return urlObj.href; } function removeTrailingSlashes(input: string) { return input.replace(/([^/]|^)\/+$/, '$1'); } function reportUnexpectedReturnValue( callbackName: string, actual: unknown, expectedType: string ) { const errorMessage = `Unexpected return value from ${callbackName}: got ${prettyPrint( actual )}, expected type ${expectedType}. ${SEE_CONFIGURE}`; throwNextTick(new ArgumentError(errorMessage)); } function executeBaseUrlForSiteCallback(siteId: string) { if (!baseUrlForSiteCallback) throwNotInitialized(); return baseUrlForSiteCallback.call(null, siteId); } // For test purpose only. export function reset() { baseUrlForSiteCallback = undefined; siteForUrlCallback = undefined; } function throwNotInitialized(): never { throw new ScrivitoError( 'Cannot use routing before Scrivito.configure was called.' ); } function throwNoOrigin(): never { throw new ScrivitoError( `Cannot compute an absolute URL without a configured origin or base URL. ${SEE_CONFIGURE}` ); } function isSiteForUrlResult( maybeSiteForUrlResult: unknown ): maybeSiteForUrlResult is SiteForUrlResult { const siteForUrlResult = maybeSiteForUrlResult as SiteForUrlResult; if (siteForUrlResult === undefined) return true; return ( typeof siteForUrlResult?.siteId === 'string' && typeof siteForUrlResult?.baseUrl === 'string' ); } const SEE_CONFIGURE = `Visit ${docUrl( 'js-sdk/configure' )} for more information.`; onReset(reset);