From 168c24f786b82c4fcdcfc2a36be73cb8d8df77d9 Mon Sep 17 00:00:00 2001 From: DavidBabinec Date: Fri, 14 Aug 2026 09:50:04 +0200 Subject: [PATCH] Fix WordPress URLs in Figma sandbox --- packages/figma/main/code.ts | 27 +++-------- packages/figma/main/wordpressConnection.ts | 42 +++++++++++++++++ .../figma/tests/wordpressConnection.test.ts | 47 +++++++++++++++++++ 3 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 packages/figma/main/wordpressConnection.ts create mode 100644 packages/figma/tests/wordpressConnection.test.ts diff --git a/packages/figma/main/code.ts b/packages/figma/main/code.ts index 31c07fa..8853262 100644 --- a/packages/figma/main/code.ts +++ b/packages/figma/main/code.ts @@ -1,27 +1,15 @@ import { SimpleVariable } from "../src/types"; +import { parseHttpUrl, parseWordPressConnectionKey } from "./wordpressConnection"; const PRESET_API_KEY_STORAGE_KEY = "cf_plugin_project_api_key"; const PRESET_LOCAL_STORAGE_KEY = "cf_plugin_project_local"; -function parseWordPressConnectionKey(connectionKey: string) { - if (connectionKey.length <= 24) return null; - - try { - const siteUrl = new URL(decodeURIComponent(connectionKey.slice(24))); - if (!["https:", "http:"].includes(siteUrl.protocol)) return null; - - return { connectionKey, siteUrl: siteUrl.origin }; - } catch { - return null; - } -} - async function fetchPreset(connectionKey: string) { const connection = parseWordPressConnectionKey(connectionKey); if (!connection) throw new Error("Invalid WordPress connection key"); - const endpoint = new URL("/wp-json/core-framework/v2/preset", connection.siteUrl); - const response = await fetch(endpoint.toString(), { + const endpoint = `${connection.siteUrl}/wp-json/core-framework/v2/preset`; + const response = await fetch(endpoint, { method: "GET", headers: { "Content-Type": "application/json", @@ -76,20 +64,19 @@ async function handleWordPressRequest(msg: { }) { try { const connection = await getWordPressConnection(); - const target = new URL(msg.url); - const connectedSite = connection ? new URL(connection.siteUrl) : null; + const target = parseHttpUrl(msg.url); if ( !connection || - !connectedSite || - target.origin !== connectedSite.origin || + !target || + target.origin !== connection.siteUrl || !ALLOWED_WORDPRESS_PATHS.has(target.pathname) || !["GET", "POST", "PUT"].includes(msg.method) ) { throw new Error("Blocked WordPress request"); } - const response = await fetch(target.toString(), { + const response = await fetch(target.href, { method: msg.method, headers: { "Content-Type": "application/json", diff --git a/packages/figma/main/wordpressConnection.ts b/packages/figma/main/wordpressConnection.ts new file mode 100644 index 0000000..e648d6c --- /dev/null +++ b/packages/figma/main/wordpressConnection.ts @@ -0,0 +1,42 @@ +const CONNECTION_KEY_SECRET_LENGTH = 24; + +export interface ParsedHttpUrl { + href: string; + origin: string; + pathname: string; +} + +export interface WordPressConnection { + connectionKey: string; + siteUrl: string; +} + +export function parseHttpUrl(value: string): ParsedHttpUrl | null { + const match = value.match(/^(https?):\/\/([^/?#]+)(\/[^?#]*)?(\?[^#]*)?(?:#.*)?$/i); + if (!match) return null; + + const [, scheme, authority, path = "/", query = ""] = match; + if (!authority || authority.includes("@") || /[\\\s]/.test(authority)) return null; + + const origin = `${scheme.toLowerCase()}://${authority.toLowerCase()}`; + + return { + href: `${origin}${path}${query}`, + origin, + pathname: path, + }; +} + +export function parseWordPressConnectionKey(rawConnectionKey: string): WordPressConnection | null { + const connectionKey = rawConnectionKey.trim(); + if (connectionKey.length <= CONNECTION_KEY_SECRET_LENGTH) return null; + + try { + const siteUrl = parseHttpUrl(decodeURIComponent(connectionKey.slice(CONNECTION_KEY_SECRET_LENGTH))); + if (!siteUrl) return null; + + return { connectionKey, siteUrl: siteUrl.origin }; + } catch { + return null; + } +} diff --git a/packages/figma/tests/wordpressConnection.test.ts b/packages/figma/tests/wordpressConnection.test.ts new file mode 100644 index 0000000..994ac11 --- /dev/null +++ b/packages/figma/tests/wordpressConnection.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "bun:test"; +import { parseHttpUrl, parseWordPressConnectionKey } from "../main/wordpressConnection"; + +describe("parseWordPressConnectionKey", () => { + test("parses the 79-character WordPress connection key format used by 2.0", () => { + const secret = "a".repeat(24); + const connectionKey = `${secret}${encodeURIComponent( + "https://darkgrey-chicken-782355.hostingersite.com", + )}`; + + expect(connectionKey).toHaveLength(79); + expect(parseWordPressConnectionKey(connectionKey)).toEqual({ + connectionKey, + siteUrl: "https://darkgrey-chicken-782355.hostingersite.com", + }); + }); + + test("does not depend on the browser URL constructor", () => { + const originalUrl = globalThis.URL; + Reflect.deleteProperty(globalThis, "URL"); + + try { + const connectionKey = `${"b".repeat(24)}${encodeURIComponent("https://example.com")}`; + expect(parseWordPressConnectionKey(connectionKey)?.siteUrl).toBe("https://example.com"); + } finally { + globalThis.URL = originalUrl; + } + }); + + test("rejects missing, non-HTTP, and credential-bearing site URLs", () => { + expect(parseWordPressConnectionKey("a".repeat(24))).toBeNull(); + expect(parseWordPressConnectionKey(`${"a".repeat(24)}ftp%3A%2F%2Fexample.com`)).toBeNull(); + expect( + parseWordPressConnectionKey(`${"a".repeat(24)}https%3A%2F%2Fuser%40example.com`), + ).toBeNull(); + }); +}); + +describe("parseHttpUrl", () => { + test("returns the origin, path, and query without browser APIs", () => { + expect(parseHttpUrl("https://Example.com/wp-json/core-framework/v2/preset?context=figma#ignored")).toEqual({ + href: "https://example.com/wp-json/core-framework/v2/preset?context=figma", + origin: "https://example.com", + pathname: "/wp-json/core-framework/v2/preset", + }); + }); +});