UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

648 lines (542 loc) 21.4 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*global HTMLScriptElement, HTMLLinkElement, XMLHttpRequest */ /* * Provides the AppCacheBuster mechanism to load application files using a timestamp */ sap.ui.define([ 'sap/ui/base/ManagedObject', 'sap/ui/util/_URL', 'sap/base/config', 'sap/base/Log', 'sap/base/i18n/Localization', 'sap/base/util/extend', 'sap/base/util/mixedFetch', 'sap/base/strings/escapeRegExp', 'sap/ui/core/_IconRegistry' ], function(ManagedObject, _URL, BaseConfig, Log, Localization, extend, mixedFetch, escapeRegExp, _IconRegistry) { "use strict"; /* * The AppCacheBuster is only aware of resources which are relative to the * current application or have been registered via resource mapping (e.g. * sap.ui.loader.config({paths...})}. */ // intercept function to avoid usage of cachebuster // URL normalizer // 1.) Can be enabled // 2.) Must match to index // 3.) hook to suppress // ==> ManagedObject -> validateProperty // API // setURLFilter,onConvertURL => return true, false // convertURL // what about being not on the root with the HTML page // appcachebuster is always relative to the HTML page // we need a detection for the root location // --> to avoid registerComponent("./") // --> configuration? // nested components? // indexOf check in convertURL will not work here! // determine the language and loading mode from the configuration const sLanguage = Localization.getLanguage(); const sAppCacheBusterMode = BaseConfig.get({ name: "sapUiXxAppCacheBusterMode", type: BaseConfig.Type.String, defaultValue: "sync", external: true, freeze: true }); const bSync = sAppCacheBusterMode === "sync"; const bBatch = sAppCacheBusterMode === "batch"; // AppCacheBuster session (will be created initially for compat reasons with mIndex) // - oSession.index: file index (maps file to timestamp) / avoid duplicate loading of known base paths // - oSession.active: flag, whether the session is active or not var oSession = { index: {}, active: false }; // store the original function / property description to intercept var fnValidateProperty, descScriptSrc, descLinkHref, fnXhrOpenOrig, fnEnhancedXhrOpen; // determine the application base url var sLocation = document.baseURI.replace(/\?.*|#.*/g, ""); // determine the base urls (normalize and then calculate the resources and test-resources urls) var oUri = new _URL(sap.ui.require.toUrl("") + "/../"); var sOrgBaseUrl = oUri.sourceUrl; if (!oUri.isAbsolute()) { oUri = new _URL(oUri.originUrl, sLocation); } var sBaseUrl = oUri.toString(); var sResBaseUrl = new _URL("resources", sBaseUrl).toString(); //var sTestResBaseUrl = new _URL("test-resources", sBaseUrl).toString(); // create resources check regex var oFilter = new RegExp("^" + escapeRegExp(sResBaseUrl)); // helper function to append the trailing slashes if missing var fnEnsureTrailingSlash = function(sUrl) { // append the missing trailing slash if (sUrl.length > 0 && sUrl.slice(-1) !== "/") { sUrl += "/"; } return sUrl; }; // internal registration function (with SyncPoint usage) var fnRegister = function(sBaseUrl, oSyncPoint) { // determine the index var mIndex = oSession.index; // the request object var oInit; var sUrl; var sAbsoluteBaseUrl; var fnSuccessCallback, fnErrorCallback; // in case of an incoming array we register each base url on its own // except in case of the batch mode => there we pass all URLs in a POST request. if (Array.isArray(sBaseUrl) && !bBatch) { sBaseUrl.forEach(function(sBaseUrlEntry) { fnRegister(sBaseUrlEntry, oSyncPoint); }); } else if (Array.isArray(sBaseUrl) && bBatch) { // BATCH MODE: send all base urls via POST request to the server // -> server returns a JSON object for containing the index for // different base urls. // // returns e.g.: // { // "<absolute_url>": { ...<index>... }, // ... // } var sRootUrl = fnEnsureTrailingSlash(sBaseUrl[0]); var sContent = []; // log Log.debug("sap.ui.core.AppCacheBuster.register(\"" + sRootUrl + "\"); // BATCH MODE!"); // determine the base URL var sAbsoluteRootUrl = AppCacheBuster.normalizeURL(sRootUrl); // "./" removes the html doc from path // log Log.debug(" --> normalized to: \"" + sAbsoluteRootUrl + "\""); // create the list of absolute base urls sBaseUrl.forEach(function(sUrlEntry) { sUrl = fnEnsureTrailingSlash(sUrlEntry); var sAbsoluteUrl = AppCacheBuster.normalizeURL(sUrl); if (!mIndex[sAbsoluteBaseUrl]) { sContent.push(sAbsoluteUrl); } }); // if we need to fetch some base urls we trigger the request otherwise // we gracefully ignore the function call if (sContent.length > 0) { // create the URL for the index file sUrl = sAbsoluteRootUrl + "sap-ui-cachebuster-info.json?sap-ui-language=" + sLanguage; // configure request; check how to execute the request (sync|async) oInit = { body: sContent.join("\n"), headers: { "Accept": mixedFetch.ContentTypes.JSON, "Content-Type": "text/plain" }, mode: "POST" }; fnSuccessCallback = function(data) { // notify that the content has been loaded AppCacheBuster.onIndexLoaded(sUrl, data); // add the index file to the index map extend(mIndex, data); }; fnErrorCallback = function(sUrl) { Log.error("Failed to batch load AppCacheBuster index file from: \"" + sUrl + "\"."); }; } } else { // ensure the trailing slash sBaseUrl = fnEnsureTrailingSlash(sBaseUrl); // log Log.debug("sap.ui.core.AppCacheBuster.register(\"" + sBaseUrl + "\");"); // determine the base URL sAbsoluteBaseUrl = AppCacheBuster.normalizeURL(sBaseUrl); // "./" removes the html doc from path // log Log.debug(" --> normalized to: \"" + sAbsoluteBaseUrl + "\""); // if the index file has not been loaded yet => load! if (!mIndex[sAbsoluteBaseUrl]) { // create the URL for the index file sUrl = sAbsoluteBaseUrl + "sap-ui-cachebuster-info.json?sap-ui-language=" + sLanguage; // configure request; check how to execute the request (sync|async) oInit = { headers: { Accept: mixedFetch.ContentTypes.JSON }, mode: "POST" }; fnSuccessCallback = function(data) { // notify that the content has been loaded AppCacheBuster.onIndexLoaded(sUrl, data); // add the index file to the index map mIndex[sAbsoluteBaseUrl] = data; }; fnErrorCallback = function(sUrl) { Log.error("Failed to load AppCacheBuster index file from: \"" + sUrl + "\"."); }; } } // only request in case of having a correct request object! if (oInit) { // hook to onIndexLoad to allow to inject the index file manually var mIndexInfo = AppCacheBuster.onIndexLoad(sUrl); // if anything else than undefined or null is returned we will use this // content as data for the cache buster index if (mIndexInfo != null) { Log.info("AppCacheBuster index file injected for: \"" + sUrl + "\"."); fnSuccessCallback(mIndexInfo); } else { var bAsync = !bSync && !!oSyncPoint; // use the syncpoint only during boot => otherwise the syncpoint // is not given because during runtime the registration needs to // be done synchronously. if (bAsync) { var iSyncPoint = oSyncPoint.startTask("load " + sUrl); var fnSuccess = fnSuccessCallback, fnError = fnErrorCallback; fnSuccessCallback = function(data) { fnSuccess.apply(this, arguments); oSyncPoint.finishTask(iSyncPoint); }; fnErrorCallback = function() { fnError.apply(this, arguments); oSyncPoint.finishTask(iSyncPoint, false); }; } // load it Log.info("Loading AppCacheBuster index file from: \"" + sUrl + "\"."); mixedFetch(sUrl, oInit, !bAsync) .then(function(oResponse) { if (oResponse.ok) { return oResponse.json(); } else { throw new Error("Status code: " + oResponse.status); } }) .then(fnSuccessCallback) .catch(function() { fnErrorCallback(sUrl); }); } } }; /** * The AppCacheBuster is used to hook into URL relevant functions in jQuery * and SAPUI5 and rewrite the URLs with a timestamp segment. The timestamp * information is fetched from the server and used later on for the URL * rewriting. * * @namespace * @public * @alias sap.ui.core.AppCacheBuster */ var AppCacheBuster = { /** * Boots the AppCacheBuster by initializing and registering the * base URLs configured in the UI5 bootstrap. * * @param {Object} [oSyncPoint] the sync point which is used to chain the execution of the AppCacheBuster * @param {Object} [oConfig] the AppCacheBuster configuration * * @private */ boot: function(oSyncPoint, oConfig) { if (oConfig && oConfig.length > 0) { // flag to activate the cachebuster var bActive = true; // fallback for old boolean configuration (only 1 string entry) // restriction: the values true, false and x are reserved as fallback values // and cannot be used as base url locations var sValue = String(oConfig[0]).toLowerCase(); if (oConfig.length === 1) { if (sValue === "true" || sValue === "x") { // register the current base URL (if it is a relative URL) // hint: if UI5 is referenced relative on a server it might be possible // with the mechanism to register another base URL. var oUri = new _URL(sOrgBaseUrl); oConfig = !oUri.isAbsolute() ? [oUri.sourceUrl] : []; } else if (sValue === "false") { bActive = false; } } // activate the cachebuster if (bActive) { // initialize the AppCacheBuster AppCacheBuster.init(); // register the components fnRegister(oConfig, oSyncPoint); } } }, /** * Initializes the AppCacheBuster. Hooks into the relevant functions * in the Core to intercept the code which are dealing with URLs and * converts those URLs into cachebuster URLs. * * The intercepted functions are: * <ul> * <li><code>XMLHttpRequest.prototype.open</code></li> * <li><code>HTMLScriptElement.prototype.src</code></li> * <li><code>HTMLLinkElement.prototype.href</code></li> * <li><code>sap.ui.base.ManagedObject.prototype.validateProperty</code></li> * <li><code>IconPool._convertUrl</code></li> * </ul> * * @private */ init: function() { // activate the session (do not create the session for compatibility reasons with mIndex previously) oSession.active = true; // store the original function / property description to intercept fnValidateProperty = ManagedObject.prototype.validateProperty; descScriptSrc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src"); descLinkHref = Object.getOwnPropertyDescriptor(HTMLLinkElement.prototype, "href"); // function shortcuts (better performance when used frequently!) var fnConvertUrl = AppCacheBuster.convertURL; var fnNormalizeUrl = AppCacheBuster.normalizeURL; // resources URL's will be handled via standard // UI5 cachebuster mechanism (so we simply ignore them) var fnIsACBUrl = function(sUrl) { if (this.active === true && sUrl && typeof (sUrl) === "string") { sUrl = fnNormalizeUrl(sUrl); return !sUrl.match(oFilter); } return false; }.bind(oSession); // enhance xhr with appCacheBuster functionality fnXhrOpenOrig = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(sMethod, sUrl) { if (sUrl && fnIsACBUrl(sUrl)) { arguments[1] = fnConvertUrl(sUrl); } fnXhrOpenOrig.apply(this, arguments); }; fnEnhancedXhrOpen = XMLHttpRequest.prototype.open; // enhance the validateProperty function to intercept URI types // test via: new sap.m.Image({src: "acctest/img/Employee.png"}).getSrc() // new sap.m.Image({src: "./acctest/../acctest/img/Employee.png"}).getSrc() ManagedObject.prototype.validateProperty = function(sPropertyName, oValue) { var oMetadata = this.getMetadata(), oProperty = oMetadata.getProperty(sPropertyName), oArgs; if (oProperty && oProperty.type === "sap.ui.core.URI") { oArgs = Array.prototype.slice.apply(arguments); try { if (fnIsACBUrl(oArgs[1] /* oValue */)) { oArgs[1] = fnConvertUrl(oArgs[1] /* oValue */); } } catch (e) { // URI normalization or conversion failed, fall back to normal processing } } // either forward the modified or the original arguments return fnValidateProperty.apply(this, oArgs || arguments); }; _IconRegistry._convertUrl = function(sUrl) { return fnConvertUrl(sUrl); }; // create an interceptor description which validates the value // of the setter whether to rewrite the URL or not var fnCreateInterceptorDescriptor = function(descriptor) { var newDescriptor = { get: descriptor.get, set: function(val) { if (fnIsACBUrl(val)) { val = fnConvertUrl(val); } descriptor.set.call(this, val); }, enumerable: descriptor.enumerable, configurable: descriptor.configurable }; newDescriptor.set._sapUiCoreACB = true; return newDescriptor; }; // try to setup the property descriptor interceptors (not supported on all browsers, e.g. iOS9) var bError = false; try { Object.defineProperty(HTMLScriptElement.prototype, "src", fnCreateInterceptorDescriptor(descScriptSrc)); } catch (ex) { Log.error("Your browser doesn't support redefining the src property of the script tag. Disabling AppCacheBuster as it is not supported on your browser!\nError: " + ex); bError = true; } try { Object.defineProperty(HTMLLinkElement.prototype, "href", fnCreateInterceptorDescriptor(descLinkHref)); } catch (ex) { Log.error("Your browser doesn't support redefining the href property of the link tag. Disabling AppCacheBuster as it is not supported on your browser!\nError: " + ex); bError = true; } // in case of setup issues we stop the AppCacheBuster support if (bError) { this.exit(); } }, /** * Terminates the AppCacheBuster and removes the hooks from the URL * specific functions. This will also clear the index which is used * to prefix matching URLs. * * @private */ exit: function() { // remove the function interceptions ManagedObject.prototype.validateProperty = fnValidateProperty; // remove the function from IconPool delete _IconRegistry._convertUrl; // only remove xhr interception if xhr#open was not modified meanwhile if (XMLHttpRequest.prototype.open === fnEnhancedXhrOpen) { XMLHttpRequest.prototype.open = fnXhrOpenOrig; } // remove the property descriptor interceptions (but only if not overridden again) var descriptor; if ((descriptor = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src")) && descriptor.set && descriptor.set._sapUiCoreACB === true) { Object.defineProperty(HTMLScriptElement.prototype, "src", descScriptSrc); } if ((descriptor = Object.getOwnPropertyDescriptor(HTMLLinkElement.prototype, "href")) && descriptor.set && descriptor.set._sapUiCoreACB === true) { Object.defineProperty(HTMLLinkElement.prototype, "href", descLinkHref); } // clear the session (disables URL rewrite for session) oSession.index = {}; oSession.active = false; // create a new session for the next initialization oSession = { index: {}, active: false }; }, /** * Registers an application. Loads the cachebuster index file from this * locations. All registered files will be considered by the cachebuster * and the URLs will be prefixed with the timestamp of the index file. * * @param {string} sBaseUrl base URL of an application providing a cachebuster index file * * @public */ register: function(sBaseUrl) { fnRegister(sBaseUrl); }, /** * Converts the given URL if it matches a URL in the cachebuster index. * If not then the same URL will be returned. To prevent URLs from being * modified by the application cachebuster you can implement the function * <code>sap.ui.core.AppCacheBuster.handleURL</code>. * * @param {string} sUrl any URL * @return {string} modified URL when matching the index or unmodified when not * * @public */ convertURL: function(sUrl) { Log.debug("sap.ui.core.AppCacheBuster.convertURL(\"" + sUrl + "\");"); var mIndex = oSession.index; // modify the incoming url if found in the appCacheBuster file // AND: ignore URLs starting with a hash from being normalized and converted if (mIndex && sUrl && !/^#/.test(sUrl)) { // normalize the URL // local resources are registered with "./" => we remove the leading "./"! // (code location for this: sap/ui/Global.js:sap.ui.localResources) var sNormalizedUrl = AppCacheBuster.normalizeURL(sUrl); Log.debug(" --> normalized to: \"" + sNormalizedUrl + "\""); // should the URL be handled? if (sNormalizedUrl && AppCacheBuster.handleURL(sNormalizedUrl)) { // scan for a matching base URL (by default we use the default index) // we lookup the base url in the index list and if found we split the // url into the base and path where the timestamp is added in between for (var sBaseUrl in mIndex) { var mBaseUrlIndex = mIndex[sBaseUrl], sUrlToAppend, sUrlPath; if (sBaseUrl && sNormalizedUrl.length >= sBaseUrl.length && sNormalizedUrl.slice(0, sBaseUrl.length) === sBaseUrl ) { sUrlToAppend = sNormalizedUrl.slice(sBaseUrl.length); sUrlPath = sUrlToAppend.match(/([^?#]*)/)[1]; if (mBaseUrlIndex[sUrlPath]) { // return the normalized URL only if found in the index sUrl = sBaseUrl + "~" + mBaseUrlIndex[sUrlPath] + "~/" + sUrlToAppend; Log.debug(" ==> rewritten to \"" + sUrl + "\";"); break; } } } } } return sUrl; }, /** * Normalizes the given URL and make it absolute. * * @param {string} sUrl any URL * @return {string} normalized URL * * @public */ normalizeURL: function(sUrl) { // local resources are registered with "./" => we remove the leading "./"! // (code location for this: sap/ui/Global.js:sap.ui.localResources) // we by default normalize all relative URLs for a common base try { // Use native URL constructor with sLocation as base // This automatically handles relative URLs and normalization // (protocol, hostname, port, path normalization) var url = new URL(sUrl || "./", sLocation); return url.toString(); } catch (e) { // Fallback for invalid URLs - return original or handle gracefully return sUrl || "./"; } }, /** * Callback function which can be overwritten to programmatically decide * whether to rewrite the given URL or not. * * @param {string} sUrl any URL * @return {boolean} <code>true</code> to rewrite or <code>false</code> to ignore * * @public */ handleURL: function(sUrl) { // API function to be overridden by apps // to exclude URLs from being manipulated return true; }, /** * Hook to intercept the load of the cache buster info. Returns either the * JSON object with the cache buster info or null/undefined if the URL should * be handled by AppCacheBuster's default implementation. * * The cache buster info object is a map which contains the relative * paths for the resources as key and a timestamp/etag as string as * value for the entry. The value is used to be added as part of the * URL to create a new URL if the resource has been changed. * @param {string} sUrl URL from where to load the cachebuster info * @returns {object{null|undefined} cache buster info object or null/undefined * @private */ onIndexLoad: function(sUrl) { return null; }, /** * Hook to intercept the result of the cache buster info request. It will pass * the loaded cache buster info object to this function to do something with that * information. * @param {string} sUrl URL from where to load the cachebuster info * @param {object} mIndexInfo cache buster info object * @private */ onIndexLoaded: function(sUrl, mIndexInfo) { } }; // check for pre-defined callback handlers and register the callbacks const mHooks = BaseConfig.get({ name: "sapUiXxAppCacheBusterHooks", type: BaseConfig.Type.Object, defaultValue: undefined, freeze: true }); if (mHooks) { ["handleURL", "onIndexLoad", "onIndexLoaded"].forEach(function(sFunction) { if (typeof mHooks[sFunction] === "function") { AppCacheBuster[sFunction] = mHooks[sFunction]; } }); } return AppCacheBuster; }, /* bExport= */ true);