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
2 changes: 2 additions & 0 deletions packages/base/bundle.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import applyDirection from "./dist/locale/applyDirection.js";
import { getCurrentRuntimeIndex } from "./dist/Runtimes.js";
import { startMultipleDrag } from "./dist/DragAndDrop.js";
import LegacyDateFormats from "./dist/features/LegacyDateFormats.js";
import { boot } from "./dist/Boot.js";

window["sap-ui-webcomponents-bundle"] = {
configuration : {
Expand All @@ -54,6 +55,7 @@ window["sap-ui-webcomponents-bundle"] = {
parseProperties,
registerI18nLoader,
getI18nBundle,
boot,
renderFinished,
applyDirection,
EventProvider,
Expand Down
180 changes: 180 additions & 0 deletions packages/base/cypress/specs/ThemeRoot.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { internals } from "../../src/Location.js";
import TestGeneric from "../../test/test-elements/Generic.js";
import { resetConfiguration } from "../../src/InitialConfiguration.js";
import { getTheme } from "../../src/config/Theme.js";
import { getThemeRoot } from "../../src/config/ThemeRoot.js";
import applyTheme from "../../src/theming/applyTheme.js";

const THEME = "sap_horizon";

const addMetaTag = (content: string) => {
cy.window().then($win => {
const metaTag = $win.document.createElement("meta");
metaTag.name = "sap-allowed-theme-origins";
metaTag.content = content;
$win.document.head.append(metaTag);
});
};

const removeMetaTag = () => {
cy.window().then($win => {
$win.document.head.querySelector("[name='sap-allowed-theme-origins']")?.remove();
});
};

const removeThemeLink = () => {
cy.window().then($win => {
$win.document.head.querySelector("link[sap-ui-webcomponents-theme]")?.remove();
});
};

const applyAndCheckLink = (theme: string) => {
cy.wrap({ applyTheme, getTheme })
.invoke("getTheme")
.then(() => {
return cy.wrap({ applyTheme }).invoke("applyTheme", theme);
});
};

// ─── Without meta tag ────────────────────────────────────────────────────────

describe("ThemeRoot via URL param — without meta tag", () => {
afterEach(() => {
removeThemeLink();
});

[
{ label: "absolute URL (different origin)", themeRoot: "http://example2.com/themes/", blocked: true },
{ label: "absolute URL (different protocol)", themeRoot: "https://example.com/themes/", blocked: true },
{ label: "absolute URL (different port)", themeRoot: "http://example:9090.com/themes/", blocked: true },
{ label: "absolute URL (same host, no meta tag)", themeRoot: "http://example.com/themes/", blocked: true },
{ label: "protocol-relative (different origin)", themeRoot: "//example2.com/themes/", blocked: true },
{ label: "protocol-relative (different port)", themeRoot: "//example:9090.com/themes/", blocked: true },
{ label: "protocol-relative (same host, no meta tag)", themeRoot: "//example.com/themes/", blocked: true },
{ label: "root-relative", themeRoot: "/themes/", blocked: false },
{ label: "relative (current dir)", themeRoot: "./themes/", blocked: false },
{ label: "relative (parent dir)", themeRoot: "../themes/", blocked: false },
].forEach(({ label, themeRoot, blocked }) => {
describe(`${label}: ${themeRoot}`, () => {
before(() => {
cy.stub(internals, "search").callsFake(() => {
return `sap-ui-theme=${THEME}@${themeRoot}`;
});

cy.wrap({ resetConfiguration }).invoke("resetConfiguration", true);
cy.mount(<TestGeneric />);
});

it(blocked ? "should not create a link element" : "should create a link element (same-origin, no meta tag needed)", () => {
applyAndCheckLink(THEME);
if (blocked) {
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`).should("not.exist");
} else {
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`)
.should("exist")
.and("have.attr", "href")
.and("include", `UI5/Base/baseLib/${THEME}/css_variables.css`);
}
});
});
});
});

// ─── With meta tag ────────────────────────────────────────────────────────────

describe("ThemeRoot via URL param — with meta tag (allowed: http://example.com)", () => {
afterEach(() => {
removeThemeLink();
});

after(() => {
removeMetaTag();
});

before(() => {
addMetaTag("http://example.com");
});

const blocked = [
{ label: "absolute URL (different origin)", themeRoot: "http://example2.com/themes/" },
{ label: "absolute URL (different protocol)", themeRoot: "https://example.com/themes/" },
{ label: "absolute URL (different port)", themeRoot: "http://example:9090.com/themes/" },
{ label: "protocol-relative (different origin)", themeRoot: "//example2.com/themes/" },
{ label: "protocol-relative (different port)", themeRoot: "//example:9090.com/themes/" },
];

const allowed = [
{
label: "absolute URL (matches allowed origin)",
themeRoot: "http://example.com/themes/",
expectedHref: "http://example.com/themes/UI5/Base/baseLib/sap_horizon/css_variables.css",
},
{
label: "protocol-relative (resolves to allowed origin)",
themeRoot: "//example.com/themes/",
expectedHref: "http://example.com/themes/UI5/Base/baseLib/sap_horizon/css_variables.css",
},
];

const sameOriginAllowed = [
{ label: "root-relative", themeRoot: "/themes/" },
{ label: "relative (current dir)", themeRoot: "./themes/" },
{ label: "relative (parent dir)", themeRoot: "../themes/" },
];

blocked.forEach(({ label, themeRoot }) => {
describe(`blocked — ${label}: ${themeRoot}`, () => {
before(() => {
cy.stub(internals, "search").callsFake(() => {
return `sap-ui-theme=${THEME}@${themeRoot}`;
});
cy.wrap({ resetConfiguration }).invoke("resetConfiguration", true);
cy.mount(<TestGeneric />);
});

it("should not create a link element", () => {
applyAndCheckLink(THEME);
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`).should("not.exist");
});
});
});

allowed.forEach(({ label, themeRoot, expectedHref }) => {
describe(`allowed — ${label}: ${themeRoot}`, () => {
before(() => {
cy.stub(internals, "search").callsFake(() => {
return `sap-ui-theme=${THEME}@${themeRoot}`;
});
cy.wrap({ resetConfiguration }).invoke("resetConfiguration", true);
cy.mount(<TestGeneric />);
});

it("should create a link element with correct href", () => {
applyAndCheckLink(THEME);
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`)
.should("exist")
.and("have.attr", "href", expectedHref);
});
});
});

sameOriginAllowed.forEach(({ label, themeRoot }) => {
describe(`allowed (same-origin) — ${label}: ${themeRoot}`, () => {
before(() => {
cy.stub(internals, "search").callsFake(() => {
return `sap-ui-theme=${THEME}@${themeRoot}`;
});
cy.wrap({ resetConfiguration }).invoke("resetConfiguration", true);
cy.mount(<TestGeneric />);
});

it("should create a link element", () => {
applyAndCheckLink(THEME);
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`)
.should("exist")
.and("have.attr", "href")
.and("include", `UI5/Base/baseLib/${THEME}/css_variables.css`);
});
});
});
});
148 changes: 148 additions & 0 deletions packages/base/cypress/specs/ThemeRootAPI.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import TestGeneric from "../../test/test-elements/Generic.js";
import { setThemeRoot } from "../../src/config/ThemeRoot.js";
import { getTheme } from "../../src/config/Theme.js";
import applyTheme from "../../src/theming/applyTheme.js";

const THEME = "sap_horizon";

const addMetaTag = (content: string) => {
cy.window().then($win => {
const metaTag = $win.document.createElement("meta");
metaTag.name = "sap-allowed-theme-origins";
metaTag.content = content;
$win.document.head.append(metaTag);
});
};

const removeMetaTag = () => {
cy.window().then($win => {
$win.document.head.querySelector("[name='sap-allowed-theme-origins']")?.remove();
});
};

const removeThemeLink = () => {
cy.window().then($win => {
$win.document.head.querySelector("link[sap-ui-webcomponents-theme]")?.remove();
});
};

const applyAndCheckLink = (theme: string) => {
cy.wrap({ applyTheme, getTheme })
.invoke("getTheme")
.then(() => {
return cy.wrap({ applyTheme }).invoke("applyTheme", theme);
});
};

// ─── Without meta tag ────────────────────────────────────────────────────────

describe("ThemeRoot via setThemeRoot API — without meta tag", () => {
before(() => {
cy.mount(<TestGeneric />);
});

afterEach(() => {
removeThemeLink();
cy.wrap({ setThemeRoot }).invoke("setThemeRoot", undefined);
});

[
{ label: "absolute URL (different origin)", themeRoot: "http://example2.com/themes/", blocked: true },
{ label: "absolute URL (different protocol)", themeRoot: "https://example.com/themes/", blocked: true },
{ label: "absolute URL (different port)", themeRoot: "http://example:9090.com/themes/", blocked: true },
{ label: "absolute URL (same host, no meta tag)", themeRoot: "http://example.com/themes/", blocked: true },
{ label: "protocol-relative (different origin)", themeRoot: "//example2.com/themes/", blocked: true },
{ label: "protocol-relative (different port)", themeRoot: "//example:9090.com/themes/", blocked: true },
{ label: "protocol-relative (same host, no meta tag)", themeRoot: "//example.com/themes/", blocked: true },
{ label: "root-relative", themeRoot: "/themes/", blocked: false },
{ label: "relative (current dir)", themeRoot: "./themes/", blocked: false },
{ label: "relative (parent dir)", themeRoot: "../themes/", blocked: false },
].forEach(({ label, themeRoot, blocked }) => {
it(`${blocked ? "should not create" : "should create"} a link element — ${label}: ${themeRoot}`, () => {
cy.wrap({ setThemeRoot }).invoke("setThemeRoot", themeRoot);
applyAndCheckLink(THEME);
if (blocked) {
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`).should("not.exist");
} else {
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`)
.should("exist")
.and("have.attr", "href")
.and("include", `UI5/Base/baseLib/${THEME}/css_variables.css`);
}
});
});
});

// ─── With meta tag ────────────────────────────────────────────────────────────

describe("ThemeRoot via setThemeRoot API — with meta tag (allowed: http://example.com)", () => {
before(() => {
addMetaTag("http://example.com");
cy.mount(<TestGeneric />);
});

afterEach(() => {
removeThemeLink();
cy.wrap({ setThemeRoot }).invoke("setThemeRoot", undefined);
});

after(() => {
removeMetaTag();
});

const blocked = [
{ label: "absolute URL (different origin)", themeRoot: "http://example2.com/themes/" },
{ label: "absolute URL (different protocol)", themeRoot: "https://example.com/themes/" },
{ label: "absolute URL (different port)", themeRoot: "http://example:9090.com/themes/" },
{ label: "protocol-relative (different origin)", themeRoot: "//example2.com/themes/" },
{ label: "protocol-relative (different port)", themeRoot: "//example:9090.com/themes/" },
];

const allowed = [
{
label: "absolute URL (matches allowed origin)",
themeRoot: "http://example.com/themes/",
expectedHref: "http://example.com/themes/UI5/Base/baseLib/sap_horizon/css_variables.css",
},
{
label: "protocol-relative (resolves to allowed origin)",
themeRoot: "//example.com/themes/",
expectedHref: "http://example.com/themes/UI5/Base/baseLib/sap_horizon/css_variables.css",
},
];

const sameOriginAllowed = [
{ label: "root-relative", themeRoot: "/themes/" },
{ label: "relative (current dir)", themeRoot: "./themes/" },
{ label: "relative (parent dir)", themeRoot: "../themes/" },
];

blocked.forEach(({ label, themeRoot }) => {
it(`should not create a link element — blocked: ${label}: ${themeRoot}`, () => {
cy.wrap({ setThemeRoot }).invoke("setThemeRoot", themeRoot);
applyAndCheckLink(THEME);
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`).should("not.exist");
});
});

allowed.forEach(({ label, themeRoot, expectedHref }) => {
it(`should create a link element with correct href — allowed: ${label}: ${themeRoot}`, () => {
cy.wrap({ setThemeRoot }).invoke("setThemeRoot", themeRoot);
applyAndCheckLink(THEME);
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`)
.should("exist")
.and("have.attr", "href", expectedHref);
});
});

sameOriginAllowed.forEach(({ label, themeRoot }) => {
it(`should create a link element — same-origin: ${label}: ${themeRoot}`, () => {
cy.wrap({ setThemeRoot }).invoke("setThemeRoot", themeRoot);
applyAndCheckLink(THEME);
cy.get(`link[sap-ui-webcomponents-theme='${THEME}']`)
.should("exist")
.and("have.attr", "href")
.and("include", `UI5/Base/baseLib/${THEME}/css_variables.css`);
});
});
});
5 changes: 3 additions & 2 deletions packages/base/src/validateThemeRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ const validateThemeRoot = (themeRoot: string) => {
let isSameOrigin = false;

try {
if (themeRoot.startsWith(".") || themeRoot.startsWith("/")) {
if (themeRoot.startsWith(".") || (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, getLocationHref()).toString();
isSameOrigin = true;
} else {
const themeRootURL = new URL(themeRoot);
// Protocol-relative URLs (//host/path) need a base to resolve the protocol
const themeRootURL = themeRoot.startsWith("//") ? new URL(themeRoot, getLocationHref()) : new URL(themeRoot);
const origin = themeRootURL.origin;
const currentOrigin = new URL(getLocationHref()).origin;

Expand Down
Loading
Loading