diff --git a/src/util/util/Url.ts b/src/util/util/Url.ts new file mode 100644 index 0000000..808bdea --- /dev/null +++ b/src/util/util/Url.ts @@ -0,0 +1,41 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2025 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +export function normalizeUrl(input: string): string { + try { + const u = new URL(input); + // Remove fragment + u.hash = ""; + // Normalize pathname - remove trailing slash except for root "/" + if (u.pathname !== "/" && u.pathname.endsWith("/")) { + u.pathname = u.pathname.slice(0, -1); + } + // Normalize query params: sort by key + if (u.search) { + const params = Array.from(u.searchParams.entries()); + params.sort(([a], [b]) => a.localeCompare(b)); + u.search = params.length ? "?" + params.map(([k, v]) => `${k}=${v}`).join("&") : ""; + } else { + // Ensure no empty search string + u.search = ""; + } + return u.toString(); + } catch (e) { + return input; + } +} diff --git a/src/util/util/extensions/Url.test.ts b/src/util/util/extensions/Url.test.ts deleted file mode 100644 index 37afd0a..0000000 --- a/src/util/util/extensions/Url.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import moduleAlias from "module-alias"; -moduleAlias(); -import './Url'; -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe("URL extensions", () => { - - it("normalize", async () => { - const tests: [string, string][] = [ - ["http://example.com", "http://example.com/"], - ["http://example.com/", "http://example.com/"], - ["http://example.com/path/", "http://example.com/path"], - ["http://example.com/path//", "http://example.com/path/"], - ["http://example.com/path?b=2&a=1", "http://example.com/path?a=1&b=2"], - ["http://example.com/path?b=2&a=1&", "http://example.com/path?a=1&b=2"], - ["http://example.com/path?", "http://example.com/path"], - ["http://example.com/path#fragment", "http://example.com/path"], - ["http://example.com/path/?b=2&a=1#fragment", "http://example.com/path?a=1&b=2"], - ["ftp://example.com/resource/", "ftp://example.com/resource"], - ["https://example.com/resource?z=3&y=2&x=1", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource?z=3&y=2&x=1#", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource?z=3&y=2&x=1#section", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource/?z=3&y=2&x=1#section", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource//?z=3&y=2&x=1#section", "https://example.com/resource/?x=1&y=2&z=3"], - ["https://example.com/", "https://example.com/"], - ["https://example.com", "https://example.com/"], - ]; - for (const [input, expected] of tests) { - assert.doesNotThrow(() => new URL(input), `URL("${input}") should not throw`); - const url = new URL(input); - const normalized = url.normalize(); - assert.strictEqual(normalized, expected, `normalize("${input}") = "${normalized}", expected "${expected}"`); - } - }); - -}); \ No newline at end of file diff --git a/src/util/util/extensions/Url.ts b/src/util/util/extensions/Url.ts deleted file mode 100644 index c0f07b3..0000000 --- a/src/util/util/extensions/Url.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2025 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -declare module "url" { - interface URL { - normalize(): string; - } -} - -/** - * Normalize a URL by: - * - Removing trailing slashes (except root path) - * - Sorting query params alphabetically - * - Removing empty query strings - * - Removing fragments - */ -export function normalizeUrl(input: string): string { - try { - const u = new URL(input); - // Remove fragment - u.hash = ""; - // Normalize pathname - remove trailing slash except for root "/" - if (u.pathname !== "/" && u.pathname.endsWith("/")) { - u.pathname = u.pathname.slice(0, -1); - } - // Normalize query params: sort by key - if (u.search) { - const params = Array.from(u.searchParams.entries()); - params.sort(([a], [b]) => a.localeCompare(b)); - u.search = params.length ? "?" + params.map(([k, v]) => `${k}=${v}`).join("&") : ""; - } else { - // Ensure no empty search string - u.search = ""; - } - return u.toString(); - } catch (e) { - return input; - } -} - -// register extensions -if (!URL.prototype.normalize) - URL.prototype.normalize = function () { - return normalizeUrl(this.toString()); - }; diff --git a/src/util/util/extensions/index.ts b/src/util/util/extensions/index.ts index afd6c0b..b7dcf4d 100644 --- a/src/util/util/extensions/index.ts +++ b/src/util/util/extensions/index.ts @@ -1,3 +1,2 @@ export * from "./Array"; -export * from "./Url"; export * from "./String"; diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 11c4899..920c330 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -48,4 +48,5 @@ export * from "./NameValidation"; export * from "../../schemas/HelperTypes"; export * from "./extensions"; -export * from "./Random"; \ No newline at end of file +export * from "./Random"; +export * from "./Url";