diff --git a/openidm-servlet/src/main/java/org/forgerock/openidm/ui/internal/service/ResourceServlet.java b/openidm-servlet/src/main/java/org/forgerock/openidm/ui/internal/service/ResourceServlet.java index c9702b57ca..49ce07eb5d 100644 --- a/openidm-servlet/src/main/java/org/forgerock/openidm/ui/internal/service/ResourceServlet.java +++ b/openidm-servlet/src/main/java/org/forgerock/openidm/ui/internal/service/ResourceServlet.java @@ -15,7 +15,7 @@ * limitations under the License. * * Portions copyright 2013-2015 ForgeRock AS. - * Portions Copyrighted 2024-2025 3A Systems LLC. + * Portions Copyrighted 2024-2026 3A Systems LLC. */ package org.forgerock.openidm.ui.internal.service; @@ -25,6 +25,7 @@ import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.StandardCharsets; import java.util.Dictionary; import java.util.Hashtable; import java.util.Map; @@ -38,6 +39,7 @@ import org.forgerock.openidm.config.enhanced.EnhancedConfig; import org.forgerock.openidm.core.IdentityServer; import org.forgerock.openidm.core.PropertyUtil; +import org.forgerock.openidm.core.ServerConstants; import org.forgerock.openidm.servletregistration.ServletRegistration; import org.ops4j.pax.web.service.WebContainer; import org.osgi.service.component.ComponentContext; @@ -129,6 +131,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) } // Never cache index.html to ensure we always have the current product version for asset requests + // (Cache-Control is also set in handleIndexHtml for the index.html special case) if (target.equals("/index.html")) { res.setHeader("Cache-Control", "no-cache"); } @@ -157,12 +160,53 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) if (url == null) { res.sendError(HttpServletResponse.SC_NOT_FOUND); + } else if (target.equals("/index.html")) { + handleIndexHtml(res, url); } else { handle(req, res, url, target); } } } + /** + * Serves index.html with the openidm.context.path injected as a global JS variable. + * Replaces {@code } with a small inline script that sets + * {@code window.__openidm_context_path} before RequireJS boots. + */ + private void handleIndexHtml(HttpServletResponse res, URL url) throws IOException { + res.setContentType("text/html"); + res.setHeader("Cache-Control", "no-cache"); + + String contextPath = System.getProperty( + ServerConstants.OPENIDM_CONTEXT_PATH_PROPERTY, + ServerConstants.OPENIDM_CONTEXT_PATH_DEFAULT); + // Strip leading slash — the UI Constants.context value does not use it + String contextValue = contextPath.startsWith("/") ? contextPath.substring(1) : contextPath; + + byte[] raw; + try (InputStream is = url.openStream()) { + raw = is.readAllBytes(); + } + String html = new String(raw, StandardCharsets.UTF_8); + + // Inject a tiny script right before so it is available before RequireJS loads. + // Escape characters that could break out of the JS string or the script tag. + String safeContextValue = contextValue + .replace("&", "&") + .replace("<", "\\u003c") + .replace(">", "\\u003e") + .replace("\"", "\\\"") + .replace("'", "\\'"); + String injection = "\n"; + // replaceFirst to guard against malformed HTML with multiple tags + html = html.replaceFirst("", injection); + + byte[] out = html.getBytes(StandardCharsets.UTF_8); + res.setContentLength(out.length); + res.getOutputStream().write(out); + } + /** * Initializes the servlet and registers it with the WebContainer. * diff --git a/openidm-ui/openidm-ui-api/src/main/resources/index.html b/openidm-ui/openidm-ui-api/src/main/resources/index.html index bfa7a784c8..58e3f76f6d 100755 --- a/openidm-ui/openidm-ui-api/src/main/resources/index.html +++ b/openidm-ui/openidm-ui-api/src/main/resources/index.html @@ -51,7 +51,8 @@ url = decodeURIComponent(url[1]); } else { // default Swagger JSON URL - url = "/openidm/?_api"; + var ctx = (window.__openidm_context_path || "openidm"); + url = "/" + ctx + "/?_api"; } if (window.SwaggerTranslator) { diff --git a/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/util/Constants.js b/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/util/Constants.js index db45bdb12a..6f8c3585f9 100644 --- a/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/util/Constants.js +++ b/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/util/Constants.js @@ -12,12 +12,15 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ define([ "org/forgerock/commons/ui/common/util/Constants" ], function (commonConstants) { - commonConstants.context = "openidm"; + commonConstants.context = (typeof window !== "undefined" && window.__openidm_context_path) + ? window.__openidm_context_path + : "openidm"; commonConstants.HEADER_PARAM_PASSWORD = "X-OpenIDM-Password"; commonConstants.HEADER_PARAM_USERNAME = "X-OpenIDM-Username"; diff --git a/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/util/ConstantsTest.js b/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/util/ConstantsTest.js index 3e9e7c24f0..33963938d4 100644 --- a/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/util/ConstantsTest.js +++ b/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/util/ConstantsTest.js @@ -23,4 +23,28 @@ define([ "Custom context should produce the correct REST API path prefix"); Constants.context = originalContext; }); -}); \ No newline at end of file + + QUnit.test("window.__openidm_context_path overrides the default context", function (assert) { + var originalValue = window.__openidm_context_path; + window.__openidm_context_path = "myidm"; + var contextValue = (typeof window !== "undefined" && window.__openidm_context_path) + ? window.__openidm_context_path + : "openidm"; + assert.equal(contextValue, "myidm", + "window.__openidm_context_path should override the default 'openidm' context"); + window.__openidm_context_path = originalValue; + }); + + QUnit.test("window.__openidm_context_path falls back to 'openidm' when unset", function (assert) { + var originalValue = window.__openidm_context_path; + delete window.__openidm_context_path; + var contextValue = (typeof window !== "undefined" && window.__openidm_context_path) + ? window.__openidm_context_path + : "openidm"; + assert.equal(contextValue, "openidm", + "Should fall back to 'openidm' when window.__openidm_context_path is not set"); + if (originalValue !== undefined) { + window.__openidm_context_path = originalValue; + } + }); +});