Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/base/src/InitialConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import merge from "./thirdparty/merge.js";
import { getFeature } from "./FeaturesRegistry.js";
import { DEFAULT_THEME } from "./generated/AssetParameters.js";
import validateThemeRoot from "./validateThemeRoot.js";
import type OpenUI5Support from "./features/OpenUI5Support.js";
import type { FormatSettings } from "./config/FormatSettings.js";
import AnimationMode from "./types/AnimationMode.js";
Expand Down Expand Up @@ -156,7 +155,7 @@ const parseURLParameters = () => {
const normalizeThemeRootParamValue = (value: string) => {
const themeRoot = value.split("@")[1];

return validateThemeRoot(themeRoot);
return themeRoot;
};

const normalizeThemeParamValue = (param: string, value: string) => {
Expand Down
24 changes: 16 additions & 8 deletions packages/base/src/config/ThemeRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,11 @@ const setThemeRoot = (themeRoot: string): Promise<void> | undefined => {

currThemeRoot = themeRoot;

if (!validateThemeRoot(themeRoot)) {
console.warn(`The ${themeRoot} is not valid. Check the allowed origins as suggested in the "setThemeRoot" description.`); // eslint-disable-line
return;
}

return attachCustomThemeStylesToHead(getTheme());
};

const formatThemeLink = (theme: string) => {
return `${getThemeRoot()!}Base/baseLib/${theme}/css_variables.css`; // theme root is always set at this point.
const formatThemeLink = (theme: string, validatedThemeRoot: string) => {
return `${validatedThemeRoot}Base/baseLib/${theme}/css_variables.css`;
};

const attachCustomThemeStylesToHead = async (theme: string): Promise<void> => {
Expand All @@ -62,7 +57,20 @@ const attachCustomThemeStylesToHead = async (theme: string): Promise<void> => {
document.head.removeChild(link);
}

await createLinkInHead(formatThemeLink(theme), { "sap-ui-webcomponents-theme": theme });
const themeRoot = getThemeRoot();

if (!themeRoot) {
return;
}

const validatedThemeRoot = validateThemeRoot(themeRoot);

if (!validatedThemeRoot) {
console.warn(`The ${themeRoot} is not valid. Check the allowed origins as suggested in the "setThemeRoot" description.`); // eslint-disable-line
return;
}

await createLinkInHead(formatThemeLink(theme, validatedThemeRoot), { "sap-ui-webcomponents-theme": theme });
};

export {
Expand Down
49 changes: 35 additions & 14 deletions packages/base/src/validateThemeRoot.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,65 @@
const isSSR = typeof document === "undefined";

const getLocationHref = () => {
if (isSSR) {
return "";
}

return window.location.href;
};

const getMetaTagValue = (metaTagName: string) => {
const metaTag = document.querySelector(`META[name="${metaTagName}"]`),
metaTagContent = metaTag && metaTag.getAttribute("content");

return metaTagContent;
};

const validateThemeOrigin = (origin: string) => {
const allowedOrigins = getMetaTagValue("sap-allowedThemeOrigins");
const validateThemeOrigin = (origin: string, isSameOrigin: boolean = false) => {
const allowedOrigins = getMetaTagValue("sap-allowed-theme-origins") ?? getMetaTagValue("sap-allowedThemeOrigins"); // Prioritize the new meta tag name

return allowedOrigins && allowedOrigins.split(",").some(allowedOrigin => {
return allowedOrigin === "*" || origin === allowedOrigin.trim();
});
};
// If no allowed origins are specified, block.
if (!allowedOrigins) {
return false;
}

const buildCorrectUrl = (oldUrl: string, newOrigin: string) => {
const oldUrlPath = new URL(oldUrl).pathname;
// If it's same-origin (relative URL resolved to current page), allow it when there's any meta tag present
// The presence of the meta tag indicates the user wants to use theme roots
if (isSameOrigin) {
return true;
}

return new URL(oldUrlPath, newOrigin).toString();
return allowedOrigins.split(",").some(allowedOrigin => {
return allowedOrigin === "*" || origin === allowedOrigin.trim();
});
};

const validateThemeRoot = (themeRoot: string) => {
let resultUrl;
let isSameOrigin = false;

try {
if (themeRoot.startsWith(".") || themeRoot.startsWith("/")) {
// Handle relative url
// new URL("/newExmPath", "http://example.com/exmPath") => http://example.com/newExmPath
// new URL("./newExmPath", "http://example.com/exmPath") => http://example.com/exmPath/newExmPath
// new URL("../newExmPath", "http://example.com/exmPath") => http://example.com/newExmPath
resultUrl = new URL(themeRoot, window.location.href).toString();
resultUrl = new URL(themeRoot, getLocationHref()).toString();
isSameOrigin = true;
} else {
const themeRootURL = new URL(themeRoot);
const origin = themeRootURL.origin;
const currentOrigin = new URL(getLocationHref()).origin;

// Check if the absolute URL is same-origin
isSameOrigin = origin === currentOrigin;

if (origin && validateThemeOrigin(origin)) {
if (origin && validateThemeOrigin(origin, isSameOrigin)) {
// If origin is allowed, use it
resultUrl = themeRootURL.toString();
} else {
// If origin is not allow and the URL is not relative, we have to replace the origin
// with current location
resultUrl = buildCorrectUrl(themeRootURL.toString(), window.location.href);
// If origin is not allowed, return undefined to indicate validation failed
return undefined;
}
}

Expand All @@ -50,6 +70,7 @@ const validateThemeRoot = (themeRoot: string) => {
return `${resultUrl}UI5/`;
} catch (e) {
// Catch if URL is not correct
return undefined;
}
};

Expand Down
35 changes: 29 additions & 6 deletions packages/base/test/specs/ConfigurationURL.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,34 @@ describe("Some settings can be set via SAP UI URL params", () => {
const config = window['sap-ui-webcomponents-bundle'].configuration;
done(config.getThemeRoot());
});
assert.strictEqual(res, 'https://example.com/UI5/', "Theme root is https://example.com/UI5");
assert.strictEqual(res, 'https://example.com', "Theme root is https://example.com");

// The origin https://example.com is allowed via sap-allowedThemeOrigins meta tag,
// so the link should be added to the DOM.
let linkHref = await browser.executeAsync(done => {
const link = document.querySelector(`head > link[sap-ui-webcomponents-theme="sap_belize_hcb"]`);
done(link ? link.href : null);
});
assert.ok(linkHref, "A theme link is added to the DOM for an allowed origin");
assert.include(linkHref, "https://example.com", "Theme link href contains the allowed theme root");
assert.include(linkHref, "sap_belize_hcb/css_variables.css", "Theme link href points to the theme CSS variables file");

await browser.url("test/pages/Configuration.html?sap-ui-theme=sap_belize_hcb@https://another-example.com");

res = await browser.executeAsync(done => {
const config = window['sap-ui-webcomponents-bundle'].configuration;
done(config.getThemeRoot());
});
location = await browser.executeAsync(done => {
done(window.location);
});

assert.strictEqual(res, `${location.origin}/UI5/`, `Theme root is ${location.origin}/UI5/`);
assert.strictEqual(res, `https://another-example.com`, `Theme root is https://another-example.com`);

// The origin https://another-example.com is not in allowed origins and is cross-origin,
// so validation fails and no link should be added to the DOM.
linkHref = await browser.executeAsync(done => {
const link = document.querySelector(`head > link[sap-ui-webcomponents-theme="sap_belize_hcb"]`);
done(link ? link.href : null);
});
assert.isNull(linkHref, "No theme link is added to the DOM for a disallowed cross-origin theme root");

await browser.url("test/pages/Configuration.html?sap-ui-theme=sap_belize_hcb@./test");

Expand All @@ -56,7 +71,15 @@ describe("Some settings can be set via SAP UI URL params", () => {
done(config.getThemeRoot());
});

assert.ok(res.endsWith("/test/UI5/"), `Theme root is set correctly with relative url`);
assert.strictEqual(res, "./test", `Theme root is ./test`);

// A relative URL is treated as same-origin, so the link should be added to the DOM.
linkHref = await browser.executeAsync(done => {
const link = document.querySelector(`head > link[sap-ui-webcomponents-theme="sap_belize_hcb"]`);
done(link ? link.href : null);
});
assert.ok(linkHref, "A theme link is added to the DOM for a relative theme root");
assert.include(linkHref, "sap_belize_hcb/css_variables.css", "Theme link href points to the theme CSS variables file");
});

it("Tests that animationMode is applied", async () => {
Expand Down
Loading