From 30193aa18945e96c4c8c99df6d604272c97afd9b Mon Sep 17 00:00:00 2001 From: coltondemetriou Date: Mon, 29 Dec 2025 15:23:39 -0500 Subject: [PATCH 1/3] support *.optimizelocation.com for useMessage --- .../src/internal/hooks/useMessage.ts | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/visual-editor/src/internal/hooks/useMessage.ts b/packages/visual-editor/src/internal/hooks/useMessage.ts index 6cbb7d1b0..991c2f917 100644 --- a/packages/visual-editor/src/internal/hooks/useMessage.ts +++ b/packages/visual-editor/src/internal/hooks/useMessage.ts @@ -39,6 +39,43 @@ export type EventHandler = ( payload: Payload ) => unknown; +/** + * Checks if an origin matches any of the target origins, or matches the optimizelocation.com pattern. + * @param origin - The origin to check (e.g., "https://subdomain.optimizelocation.com") + * @param targetOrigins - Array of allowed origins (exact matches only, no wildcards) + * @returns true if the origin matches any target origin or matches *.optimizelocation.com pattern + */ +const isOriginAllowed = (origin: string, targetOrigins: string[]): boolean => { + // Check for exact match in targetOrigins + if (targetOrigins.includes(origin)) { + return true; + } + + // Check if origin matches *.optimizelocation.com pattern + try { + const url = new URL(origin); + if ( + url.hostname.endsWith(".optimizelocation.com") || + url.hostname === "optimizelocation.com" + ) { + // Check if origin matches the pattern ${protocol}//*.optimizelocation.com + const expectedPattern = `${url.protocol}//*.optimizelocation.com`; + // Convert wildcard pattern to regex: escape special chars except *, then replace * with .* + const pattern = expectedPattern + .replace(/[.+?^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, ".*"); + const regex = new RegExp(`^${pattern}$`); + if (regex.test(origin)) { + return true; + } + } + } catch { + // Invalid origin URL, no match + } + + return false; +}; + export const TARGET_ORIGINS = [ "http://localhost", "https://dev.yext.com", @@ -189,7 +226,7 @@ const useListenAndRespondMessage = ( if (data?.source?.startsWith("react-devtools")) { return; } - if (!targetOrigins.includes(origin)) { + if (!isOriginAllowed(origin, targetOrigins)) { return; } From af91f9ddb20583979a278199bf2636c6526cefe7 Mon Sep 17 00:00:00 2001 From: coltondemetriou Date: Tue, 30 Dec 2025 14:12:54 -0500 Subject: [PATCH 2/3] add unit tests --- .../src/internal/hooks/useMessage.test.ts | 130 ++++++++++++++++++ .../src/internal/hooks/useMessage.ts | 5 +- 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 packages/visual-editor/src/internal/hooks/useMessage.test.ts diff --git a/packages/visual-editor/src/internal/hooks/useMessage.test.ts b/packages/visual-editor/src/internal/hooks/useMessage.test.ts new file mode 100644 index 000000000..4135338ce --- /dev/null +++ b/packages/visual-editor/src/internal/hooks/useMessage.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from "vitest"; +import { isOriginAllowed, TARGET_ORIGINS } from "./useMessage.ts"; + +describe("isOriginAllowed", () => { + describe("exact matches with target origins", () => { + it("should return false for origins not in targetOrigins", () => { + expect(isOriginAllowed("https://example.com", TARGET_ORIGINS)).toBe( + false + ); + expect(isOriginAllowed("https://unknown.yext.com", TARGET_ORIGINS)).toBe( + false + ); + }); + + it("should work with TARGET_ORIGINS constant", () => { + expect(isOriginAllowed("http://localhost", TARGET_ORIGINS)).toBe(true); + expect(isOriginAllowed("https://dev.yext.com", TARGET_ORIGINS)).toBe( + true + ); + expect(isOriginAllowed("https://qa.yext.com", TARGET_ORIGINS)).toBe(true); + expect(isOriginAllowed("https://sandbox.yext.com", TARGET_ORIGINS)).toBe( + true + ); + expect(isOriginAllowed("https://www.yext.com", TARGET_ORIGINS)).toBe( + true + ); + expect( + isOriginAllowed("https://app-qa.eu.yext.com", TARGET_ORIGINS) + ).toBe(true); + expect(isOriginAllowed("https://app.eu.yext.com", TARGET_ORIGINS)).toBe( + true + ); + }); + }); + + describe("optimizelocation.com pattern matching", () => { + it("should return true for http://xyz.optimizelocation.com", () => { + const targetOrigins: string[] = []; + expect( + isOriginAllowed("http://xyz.optimizelocation.com", targetOrigins) + ).toBe(true); + }); + + it("should return true for https://xyz.optimizelocation.com", () => { + const targetOrigins: string[] = []; + expect( + isOriginAllowed("https://xyz.optimizelocation.com", targetOrigins) + ).toBe(true); + }); + + it("should return true for various subdomains with http", () => { + const targetOrigins: string[] = []; + expect( + isOriginAllowed("http://subdomain.optimizelocation.com", targetOrigins) + ).toBe(true); + expect( + isOriginAllowed("http://test.optimizelocation.com", targetOrigins) + ).toBe(true); + expect( + isOriginAllowed("http://abc123.optimizelocation.com", targetOrigins) + ).toBe(true); + }); + + it("should return true for various subdomains with https", () => { + const targetOrigins: string[] = []; + expect( + isOriginAllowed("https://subdomain.optimizelocation.com", targetOrigins) + ).toBe(true); + expect( + isOriginAllowed("https://test.optimizelocation.com", targetOrigins) + ).toBe(true); + expect( + isOriginAllowed("https://abc123.optimizelocation.com", targetOrigins) + ).toBe(true); + }); + + it("should return true for optimizelocation.com (no subdomain)", () => { + const targetOrigins: string[] = []; + // Note: The pattern matching requires a subdomain, but the hostname check allows the base domain + // The regex pattern `*.optimizelocation.com` doesn't match the base domain without a subdomain + // So this test expects false based on current implementation + expect( + isOriginAllowed("http://optimizelocation.com", targetOrigins) + ).toBe(false); + expect( + isOriginAllowed("https://optimizelocation.com", targetOrigins) + ).toBe(false); + }); + + it("should return false for domains that are not optimizelocation.com", () => { + const targetOrigins: string[] = []; + expect(isOriginAllowed("http://xyz.example.com", targetOrigins)).toBe( + false + ); + expect( + isOriginAllowed("https://subdomain.otherdomain.com", targetOrigins) + ).toBe(false); + }); + + it("should return false for optimizelocation.com with path", () => { + const targetOrigins: string[] = []; + // Note: URL constructor will parse this, but the origin check should still work + // The origin is just the protocol + hostname + port, paths don't affect it + const url = new URL("http://xyz.optimizelocation.com/path"); + expect(isOriginAllowed(url.origin, targetOrigins)).toBe(true); + }); + }); + + describe("combined scenarios", () => { + it("should allow both exact matches and pattern matches", () => { + expect(isOriginAllowed("https://dev.yext.com", TARGET_ORIGINS)).toBe( + true + ); + expect( + isOriginAllowed("http://xyz.optimizelocation.com", TARGET_ORIGINS) + ).toBe(true); + expect( + isOriginAllowed("https://abc.optimizelocation.com", TARGET_ORIGINS) + ).toBe(true); + }); + }); + + describe("edge cases", () => { + it("should return false for invalid URLs", () => { + expect(isOriginAllowed("not-a-url", TARGET_ORIGINS)).toBe(false); + expect(isOriginAllowed("", TARGET_ORIGINS)).toBe(false); + expect(isOriginAllowed("://invalid", TARGET_ORIGINS)).toBe(false); + }); + }); +}); diff --git a/packages/visual-editor/src/internal/hooks/useMessage.ts b/packages/visual-editor/src/internal/hooks/useMessage.ts index 991c2f917..d4e44acf1 100644 --- a/packages/visual-editor/src/internal/hooks/useMessage.ts +++ b/packages/visual-editor/src/internal/hooks/useMessage.ts @@ -45,7 +45,10 @@ export type EventHandler = ( * @param targetOrigins - Array of allowed origins (exact matches only, no wildcards) * @returns true if the origin matches any target origin or matches *.optimizelocation.com pattern */ -const isOriginAllowed = (origin: string, targetOrigins: string[]): boolean => { +export const isOriginAllowed = ( + origin: string, + targetOrigins: string[] +): boolean => { // Check for exact match in targetOrigins if (targetOrigins.includes(origin)) { return true; From 103dd2bbb306758e3c3686ef84fa24f1b8051776 Mon Sep 17 00:00:00 2001 From: coltondemetriou Date: Tue, 30 Dec 2025 14:20:38 -0500 Subject: [PATCH 3/3] simplify --- .../src/internal/hooks/useMessage.test.ts | 120 +++++------------- .../src/internal/hooks/useMessage.ts | 25 +--- 2 files changed, 41 insertions(+), 104 deletions(-) diff --git a/packages/visual-editor/src/internal/hooks/useMessage.test.ts b/packages/visual-editor/src/internal/hooks/useMessage.test.ts index 4135338ce..2fd120386 100644 --- a/packages/visual-editor/src/internal/hooks/useMessage.test.ts +++ b/packages/visual-editor/src/internal/hooks/useMessage.test.ts @@ -1,130 +1,80 @@ import { describe, it, expect } from "vitest"; -import { isOriginAllowed, TARGET_ORIGINS } from "./useMessage.ts"; +import { isOriginAllowed } from "./useMessage.ts"; describe("isOriginAllowed", () => { describe("exact matches with target origins", () => { - it("should return false for origins not in targetOrigins", () => { - expect(isOriginAllowed("https://example.com", TARGET_ORIGINS)).toBe( - false - ); - expect(isOriginAllowed("https://unknown.yext.com", TARGET_ORIGINS)).toBe( - false - ); + it("should return false for origins not in TARGET_ORIGINS", () => { + expect(isOriginAllowed("https://example.com")).toBe(false); + expect(isOriginAllowed("https://unknown.yext.com")).toBe(false); }); it("should work with TARGET_ORIGINS constant", () => { - expect(isOriginAllowed("http://localhost", TARGET_ORIGINS)).toBe(true); - expect(isOriginAllowed("https://dev.yext.com", TARGET_ORIGINS)).toBe( - true - ); - expect(isOriginAllowed("https://qa.yext.com", TARGET_ORIGINS)).toBe(true); - expect(isOriginAllowed("https://sandbox.yext.com", TARGET_ORIGINS)).toBe( - true - ); - expect(isOriginAllowed("https://www.yext.com", TARGET_ORIGINS)).toBe( - true - ); - expect( - isOriginAllowed("https://app-qa.eu.yext.com", TARGET_ORIGINS) - ).toBe(true); - expect(isOriginAllowed("https://app.eu.yext.com", TARGET_ORIGINS)).toBe( - true - ); + expect(isOriginAllowed("http://localhost")).toBe(true); + expect(isOriginAllowed("https://dev.yext.com")).toBe(true); + expect(isOriginAllowed("https://qa.yext.com")).toBe(true); + expect(isOriginAllowed("https://sandbox.yext.com")).toBe(true); + expect(isOriginAllowed("https://www.yext.com")).toBe(true); + expect(isOriginAllowed("https://app-qa.eu.yext.com")).toBe(true); + expect(isOriginAllowed("https://app.eu.yext.com")).toBe(true); }); }); describe("optimizelocation.com pattern matching", () => { it("should return true for http://xyz.optimizelocation.com", () => { - const targetOrigins: string[] = []; - expect( - isOriginAllowed("http://xyz.optimizelocation.com", targetOrigins) - ).toBe(true); + expect(isOriginAllowed("http://xyz.optimizelocation.com")).toBe(true); }); it("should return true for https://xyz.optimizelocation.com", () => { - const targetOrigins: string[] = []; - expect( - isOriginAllowed("https://xyz.optimizelocation.com", targetOrigins) - ).toBe(true); + expect(isOriginAllowed("https://xyz.optimizelocation.com")).toBe(true); }); it("should return true for various subdomains with http", () => { - const targetOrigins: string[] = []; - expect( - isOriginAllowed("http://subdomain.optimizelocation.com", targetOrigins) - ).toBe(true); - expect( - isOriginAllowed("http://test.optimizelocation.com", targetOrigins) - ).toBe(true); - expect( - isOriginAllowed("http://abc123.optimizelocation.com", targetOrigins) - ).toBe(true); + expect(isOriginAllowed("http://subdomain.optimizelocation.com")).toBe( + true + ); + expect(isOriginAllowed("http://test.optimizelocation.com")).toBe(true); + expect(isOriginAllowed("http://abc123.optimizelocation.com")).toBe(true); }); it("should return true for various subdomains with https", () => { - const targetOrigins: string[] = []; - expect( - isOriginAllowed("https://subdomain.optimizelocation.com", targetOrigins) - ).toBe(true); - expect( - isOriginAllowed("https://test.optimizelocation.com", targetOrigins) - ).toBe(true); - expect( - isOriginAllowed("https://abc123.optimizelocation.com", targetOrigins) - ).toBe(true); + expect(isOriginAllowed("https://subdomain.optimizelocation.com")).toBe( + true + ); + expect(isOriginAllowed("https://test.optimizelocation.com")).toBe(true); + expect(isOriginAllowed("https://abc123.optimizelocation.com")).toBe(true); }); it("should return true for optimizelocation.com (no subdomain)", () => { - const targetOrigins: string[] = []; - // Note: The pattern matching requires a subdomain, but the hostname check allows the base domain - // The regex pattern `*.optimizelocation.com` doesn't match the base domain without a subdomain - // So this test expects false based on current implementation - expect( - isOriginAllowed("http://optimizelocation.com", targetOrigins) - ).toBe(false); - expect( - isOriginAllowed("https://optimizelocation.com", targetOrigins) - ).toBe(false); + expect(isOriginAllowed("http://optimizelocation.com")).toBe(true); + expect(isOriginAllowed("https://optimizelocation.com")).toBe(true); }); it("should return false for domains that are not optimizelocation.com", () => { - const targetOrigins: string[] = []; - expect(isOriginAllowed("http://xyz.example.com", targetOrigins)).toBe( - false - ); - expect( - isOriginAllowed("https://subdomain.otherdomain.com", targetOrigins) - ).toBe(false); + expect(isOriginAllowed("http://xyz.example.com")).toBe(false); + expect(isOriginAllowed("https://subdomain.otherdomain.com")).toBe(false); }); - it("should return false for optimizelocation.com with path", () => { - const targetOrigins: string[] = []; + it("should return true for optimizelocation.com with path", () => { // Note: URL constructor will parse this, but the origin check should still work // The origin is just the protocol + hostname + port, paths don't affect it const url = new URL("http://xyz.optimizelocation.com/path"); - expect(isOriginAllowed(url.origin, targetOrigins)).toBe(true); + expect(isOriginAllowed(url.origin)).toBe(true); }); }); describe("combined scenarios", () => { it("should allow both exact matches and pattern matches", () => { - expect(isOriginAllowed("https://dev.yext.com", TARGET_ORIGINS)).toBe( - true - ); - expect( - isOriginAllowed("http://xyz.optimizelocation.com", TARGET_ORIGINS) - ).toBe(true); - expect( - isOriginAllowed("https://abc.optimizelocation.com", TARGET_ORIGINS) - ).toBe(true); + expect(isOriginAllowed("https://dev.yext.com")).toBe(true); + expect(isOriginAllowed("http://xyz.optimizelocation.com")).toBe(true); + expect(isOriginAllowed("https://abc.optimizelocation.com")).toBe(true); }); }); describe("edge cases", () => { it("should return false for invalid URLs", () => { - expect(isOriginAllowed("not-a-url", TARGET_ORIGINS)).toBe(false); - expect(isOriginAllowed("", TARGET_ORIGINS)).toBe(false); - expect(isOriginAllowed("://invalid", TARGET_ORIGINS)).toBe(false); + expect(isOriginAllowed("not-a-url")).toBe(false); + expect(isOriginAllowed("")).toBe(false); + expect(isOriginAllowed("://invalid")).toBe(false); }); }); }); diff --git a/packages/visual-editor/src/internal/hooks/useMessage.ts b/packages/visual-editor/src/internal/hooks/useMessage.ts index d4e44acf1..c1fc75712 100644 --- a/packages/visual-editor/src/internal/hooks/useMessage.ts +++ b/packages/visual-editor/src/internal/hooks/useMessage.ts @@ -42,15 +42,11 @@ export type EventHandler = ( /** * Checks if an origin matches any of the target origins, or matches the optimizelocation.com pattern. * @param origin - The origin to check (e.g., "https://subdomain.optimizelocation.com") - * @param targetOrigins - Array of allowed origins (exact matches only, no wildcards) * @returns true if the origin matches any target origin or matches *.optimizelocation.com pattern */ -export const isOriginAllowed = ( - origin: string, - targetOrigins: string[] -): boolean => { - // Check for exact match in targetOrigins - if (targetOrigins.includes(origin)) { +export const isOriginAllowed = (origin: string): boolean => { + // Check for exact match in TARGET_ORIGINS + if (TARGET_ORIGINS.includes(origin)) { return true; } @@ -61,16 +57,7 @@ export const isOriginAllowed = ( url.hostname.endsWith(".optimizelocation.com") || url.hostname === "optimizelocation.com" ) { - // Check if origin matches the pattern ${protocol}//*.optimizelocation.com - const expectedPattern = `${url.protocol}//*.optimizelocation.com`; - // Convert wildcard pattern to regex: escape special chars except *, then replace * with .* - const pattern = expectedPattern - .replace(/[.+?^${}()|[\]\\]/g, "\\$&") - .replace(/\*/g, ".*"); - const regex = new RegExp(`^${pattern}$`); - if (regex.test(origin)) { - return true; - } + return true; } } catch { // Invalid origin URL, no match @@ -229,7 +216,7 @@ const useListenAndRespondMessage = ( if (data?.source?.startsWith("react-devtools")) { return; } - if (!isOriginAllowed(origin, targetOrigins)) { + if (!isOriginAllowed(origin)) { return; } @@ -241,7 +228,7 @@ const useListenAndRespondMessage = ( callback(data, origin, source); } }, - [messageName, targetOrigins, setSource, setOrigin, callback] + [messageName, setSource, setOrigin, callback] ); useEffect(() => {