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.

299 lines (249 loc) 7.87 kB
import { SiteData, getCurrentRoute, } from 'scrivito_sdk/app_support/current_page_data'; import { CrossSiteDestination, DestinationUnavailable, LocalDestination, generateDestinationUnavailable, recognizeDestinationUnavailable, } from 'scrivito_sdk/app_support/destination_types'; import { withDisabledPermalinkCache } from 'scrivito_sdk/app_support/permalink_cache'; import { setHomepageCallback } from 'scrivito_sdk/app_support/routing/homepage_callback'; import { generateRoutingPath, recognizeRoutingPath, } from 'scrivito_sdk/app_support/routing_path'; import { SiteMappingConfiguration, baseUrlForSite, initSiteMapping, recognizeSiteAndPath, } from 'scrivito_sdk/app_support/site_mapping'; import { QueryParameters, ScrivitoError, buildQueryString, currentOrigin, urlResource, } from 'scrivito_sdk/common'; import { BasicObj } from 'scrivito_sdk/models'; export type Hash = string | null; export type Route = ObjRoute | NotResponsibleRoute | ObjNotFoundRoute; interface CommonRouteProperties { query?: string; } interface NotResponsibleRoute extends CommonRouteProperties { objId?: undefined; sitePath: null; siteData?: SiteData; } interface DestinationUnavailableRecognized extends CommonRouteProperties { objId: string; sitePath: null; siteData?: undefined; } export interface ObjRoute extends CommonRouteProperties { objId: string; sitePath: string; siteData: SiteData; } interface ObjNotFoundRoute extends CommonRouteProperties { objId?: undefined; sitePath: string; siteData: SiteData; } interface GenerateUrlOptions { query?: QueryParameters | string; hash?: Hash; } export function isDestinationUnavailableRecognized( route: Route | DestinationUnavailableRecognized ): route is DestinationUnavailableRecognized { return !!route.objId && !route.siteData; } export function isNotResponsibleRoute( route: Route ): route is NotResponsibleRoute { return !route.sitePath; } export function isObjNotFoundRoute(route: Route): route is ObjNotFoundRoute { return !!route.sitePath && !route.objId; } interface GenerateObjUrlOptions extends GenerateUrlOptions { obj: BasicObj; objId?: undefined; } interface GenerateObjIdUrlOptions extends GenerateUrlOptions { objId: string; obj?: undefined; } export function generateUrl( options: GenerateObjUrlOptions | GenerateObjIdUrlOptions ): string { return destinationToUrl( options.obj ? generateDestination(options) : generateDestinationForId(options) ); } /** get the local url-path for the given obj * * returns undefined, if the obj's site does not belong to the current origin */ export function generateLocalPath(obj: BasicObj): string | undefined { const destination = generateDestination({ obj }); return destination.type === 'local' ? destination.resource : undefined; } function destinationToUrl(destination: RoutingDestination): string { switch (destination.type) { case 'local': return destination.resource; case 'crossSite': return destination.url; default: return destination.fallbackUrl; } } type RoutingDestination = | LocalDestination | CrossSiteDestination | DestinationUnavailable; export function generateDestination( options: GenerateObjUrlOptions ): RoutingDestination { const obj = options.obj; const currentRoute = getCurrentRoute(); const currentSiteData = currentRoute?.siteData; const currentSiteId = currentSiteData?.siteId; const siteId = obj.siteId() ?? currentSiteId; if (!siteId) return unavailableFor(options); if (siteId !== currentSiteId) { const url = canonicalUrlForSite(siteId, options); return url ? { type: 'crossSite', url } : unavailableFor(options); } const baseUrl = currentSiteData?.baseUrl; if (!baseUrl) return unavailableFor(options); const url = new URL( joinUri(baseUrl, generateRoutingPath(obj, siteId), options) ); return currentRoute?.sitePath || url.origin === currentOrigin() ? { type: 'local', resource: urlResource(url) } : { type: 'crossSite', url: url.href }; } /** generate an absolute URL for the given Obj, using the canonical origin * * the canonical origin is the origin that the baseUrlForSite callback defines. */ export function generateUrlWithCanonicalOrigin( options: GenerateObjUrlOptions ): string { const siteId = options.obj.siteId() ?? getCurrentRoute()?.siteData?.siteId; if (!siteId) return unavailableFor(options).fallbackUrl; return ( canonicalUrlForSite(siteId, options) ?? unavailableFor(options).fallbackUrl ); } function canonicalUrlForSite(siteId: string, options: GenerateObjUrlOptions) { const baseUrl = baseUrlForSite(siteId); return ( baseUrl && joinUri(baseUrl, generateRoutingPath(options.obj, siteId), options) ); } function unavailableFor(options: GenerateObjUrlOptions) { return generateDestinationUnavailable({ objId: options.obj.id(), query: options.query, hash: options.hash, }); } export function generateDestinationForId( options: GenerateObjIdUrlOptions ): RoutingDestination { const { objId, query, hash } = options; const currentRoute = getCurrentRoute(); if (!currentRoute?.siteData) { return generateDestinationUnavailable({ objId, query, hash }); } const url = new URL( joinUri(currentRoute.siteData.baseUrl, `/${objId}`, { query, hash }) ); return currentRoute.sitePath || url.origin === currentOrigin() ? { type: 'local', resource: urlResource(url) } : { type: 'crossSite', url: url.href }; } /** join url components into a URL * * baseUrl is expected to be normalized (= has no trailing slash) * path is expected to be normalized (= has leading slash) */ function joinUri( baseUrl: string, path: string, { query, hash }: GenerateUrlOptions ) { const urlString = path === '/' ? baseUrl : `${baseUrl}${path}`; if (!URL.canParse(urlString)) return urlString; const url = new URL(urlString); if (typeof query === 'string' && query !== '?') url.search = query; if (typeof query === 'object') url.search = buildQueryString(query); if (hash && hash !== '#') url.hash = hash; return url.href; } export function recognize( url: string ): Route | DestinationUnavailableRecognized { const destinationUnavailable = recognizeDestinationUnavailable(url); if (destinationUnavailable) { const { objId, query } = destinationUnavailable; return { objId, sitePath: null, query, }; } const recognized = recognizeSiteAndPath(url); const query = new URL(url, 'http://example.com').search.replace(/^\?/, ''); if (recognized.sitePath === null) { return { ...recognized, query, }; } return { ...recognized, objId: recognizeRoutingPath(recognized), query, }; } type RoutingConfiguration = { homepageCallback: () => BasicObj | null; } & SiteMappingConfiguration; export function initRouting({ homepageCallback, ...siteMappingConfiguration }: RoutingConfiguration): void { setHomepageCallback(homepageCallback); initSiteMapping(siteMappingConfiguration); } export function ensureRoutingDataAvailable(basicPage: BasicObj) { withDisabledPermalinkCache(() => { const url = generateUrlWithCanonicalOrigin({ obj: basicPage }); const route = recognize(url); if (route.objId !== basicPage.id()) { throw new ScrivitoError( `baseUrlForSite produced ${url} for ${basicPage.id()}, ` + 'but siteForUrl did not recognize that URL correctly.' ); } }); } export function isOriginLocal(url: string): boolean { return !URL.canParse(url) || new URL(url).origin === currentOrigin(); } export function isSiteLocal(url: string): boolean { const currentBaseUrl = getCurrentRoute()?.siteData?.baseUrl; return !!currentBaseUrl && url.indexOf(currentBaseUrl) === 0; }