From b8081d970cf9813d18599816bc2019d5fa65bbaa Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Tue, 20 Aug 2024 09:38:56 +0200 Subject: [PATCH] Fixes #1049 centralizes page URL encoding and decoding --- common/spaces/http_space_primitives.ts | 9 +++++---- plug-api/lib/page_ref.test.ts | 27 ++++++++++++++++++++++++-- plug-api/lib/page_ref.ts | 18 +++++++++++++++++ plugs/editor/upload.ts | 3 ++- plugs/share/share.ts | 7 +++++-- server/http_server.ts | 3 ++- web/client.ts | 5 +++-- web/cm_plugins/wiki_link.ts | 3 ++- web/navigator.ts | 12 ++++++++---- web/service_worker.ts | 7 +++++-- 10 files changed, 75 insertions(+), 19 deletions(-) diff --git a/common/spaces/http_space_primitives.ts b/common/spaces/http_space_primitives.ts index 89d8d6be..efaa7098 100644 --- a/common/spaces/http_space_primitives.ts +++ b/common/spaces/http_space_primitives.ts @@ -1,6 +1,7 @@ import type { SpacePrimitives } from "./space_primitives.ts"; import type { FileMeta } from "../../plug-api/types.ts"; import { flushCachesAndUnregisterServiceWorker } from "../sw_util.ts"; +import { encodePageURI } from "@silverbulletmd/silverbullet/lib/page_ref"; const defaultFetchTimeout = 30000; // 30 seconds @@ -110,7 +111,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { name: string, ): Promise<{ data: Uint8Array; meta: FileMeta }> { const res = await this.authenticatedFetch( - `${this.url}/${encodeURIComponent(name)}`, + `${this.url}/${encodePageURI(name)}`, { method: "GET", headers: { @@ -144,7 +145,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { } const res = await this.authenticatedFetch( - `${this.url}/${encodeURIComponent(name)}`, + `${this.url}/${encodePageURI(name)}`, { method: "PUT", headers, @@ -157,7 +158,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { async deleteFile(name: string): Promise { const req = await this.authenticatedFetch( - `${this.url}/${encodeURIComponent(name)}`, + `${this.url}/${encodePageURI(name)}`, { method: "DELETE", }, @@ -169,7 +170,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { async getFileMeta(name: string): Promise { const res = await this.authenticatedFetch( - `${this.url}/${encodeURIComponent(name)}`, + `${this.url}/${encodePageURI(name)}`, // This used to use HEAD, but it seems that Safari on iOS is blocking cookies/credentials to be sent along with HEAD requests // so we'll use GET instead with a magic header which the server may or may not use to omit the body. { diff --git a/plug-api/lib/page_ref.test.ts b/plug-api/lib/page_ref.test.ts index 7871a015..d21b1b4c 100644 --- a/plug-api/lib/page_ref.test.ts +++ b/plug-api/lib/page_ref.test.ts @@ -1,5 +1,16 @@ -import { encodePageRef, parsePageRef, validatePageName } from "./page_ref.ts"; -import { assertEquals, AssertionError, assertThrows } from "@std/assert"; +import { + decodePageURI, + encodePageRef, + encodePageURI, + parsePageRef, + validatePageName, +} from "./page_ref.ts"; +import { + assert, + assertEquals, + AssertionError, + assertThrows, +} from "@std/assert"; Deno.test("Page utility functions", () => { // Base cases @@ -79,3 +90,15 @@ Deno.test("Page utility functions", () => { ); } }); + +Deno.test("Page URI encoding", () => { + assertEquals(encodePageURI("foo"), "foo"); + assertEquals(encodePageURI("folder/foo"), "folder/foo"); + assertEquals(encodePageURI("hello there"), "hello%20there"); + assertEquals(encodePageURI("hello?there"), "hello%3Fthere"); + // Now ensure all these cases are reversible + assertEquals(decodePageURI("foo"), "foo"); + assertEquals(decodePageURI("folder/foo"), "folder/foo"); + assertEquals(decodePageURI("hello%20there"), "hello there"); + assertEquals(decodePageURI("hello%3Fthere"), "hello?there"); +}); diff --git a/plug-api/lib/page_ref.ts b/plug-api/lib/page_ref.ts index f89814ed..2dbd37a1 100644 --- a/plug-api/lib/page_ref.ts +++ b/plug-api/lib/page_ref.ts @@ -130,3 +130,21 @@ export function positionOfLine( ); return targetPos - targetLine.length + columnOffset; } + +/** + * Encodes a page name for use in a URI. Basically does encodeURIComponent, but puts slashes back in place. + * @param page page name to encode + * @returns + */ +export function encodePageURI(page: string): string { + return encodeURIComponent(page).replace(/%2F/g, "/"); +} + +/** + * Decodes a page name from a URI. + * @param page page name to decode + * @returns + */ +export function decodePageURI(page: string): string { + return decodeURIComponent(page); +} diff --git a/plugs/editor/upload.ts b/plugs/editor/upload.ts index 608cafae..2ee15f68 100644 --- a/plugs/editor/upload.ts +++ b/plugs/editor/upload.ts @@ -5,6 +5,7 @@ import { maximumAttachmentSize, } from "../../web/constants.ts"; import { resolvePath } from "@silverbulletmd/silverbullet/lib/resolve"; +import { encodePageURI } from "@silverbulletmd/silverbullet/lib/page_ref"; export async function saveFile(file: UploadFile) { const maxSize = await system.getSpaceConfig( @@ -46,7 +47,7 @@ export async function saveFile(file: UploadFile) { if (linkStyle === "wikilink") { attachmentMarkdown = `[[${attachmentPath}]]`; } else { - attachmentMarkdown = `[${finalFileName}](${encodeURI(finalFileName)})`; + attachmentMarkdown = `[${finalFileName}](${encodePageURI(finalFileName)})`; } if (file.contentType.startsWith("image/")) { attachmentMarkdown = "!" + attachmentMarkdown; diff --git a/plugs/share/share.ts b/plugs/share/share.ts index 3f957e43..d1530b75 100644 --- a/plugs/share/share.ts +++ b/plugs/share/share.ts @@ -7,7 +7,10 @@ import { import { findNodeOfType, renderToText } from "../../plug-api/lib/tree.ts"; import { replaceNodesMatching } from "../../plug-api/lib/tree.ts"; import type { ParseTree } from "../../plug-api/lib/tree.ts"; -import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref"; +import { + encodePageURI, + parsePageRef, +} from "@silverbulletmd/silverbullet/lib/page_ref"; type ShareOption = { id: string; @@ -85,7 +88,7 @@ export function cleanMarkdown(tree: ParseTree): ParseTree { return { text: `[${linkText}](${ typeof location !== "undefined" ? location.origin : "" - }/${encodeURI(pageRef.page)})`, + }/${encodePageURI(pageRef.page)})`, }; } case "NamedAnchor": diff --git a/server/http_server.ts b/server/http_server.ts index b27d19ab..0c6c351c 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -15,6 +15,7 @@ import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts"; import { parse } from "$common/markdown_parser/parse_tree.ts"; import { renderMarkdownToHtml } from "../plugs/markdown/markdown_render.ts"; import { + decodePageURI, looksLikePathWithExtension, parsePageRef, } from "@silverbulletmd/silverbullet/lib/page_ref"; @@ -149,7 +150,7 @@ export class HttpServer { // Fallback, serve the UI index.html this.app.use("*", (c) => { const url = new URL(c.req.url); - const pageName = decodeURIComponent(url.pathname.slice(1)); + const pageName = decodePageURI(url.pathname.slice(1)); return this.renderHtmlPage(this.spaceServer, pageName, c); }); diff --git a/web/client.ts b/web/client.ts index 2a0e7c5c..355b2ef2 100644 --- a/web/client.ts +++ b/web/client.ts @@ -41,6 +41,7 @@ import { FallbackSpacePrimitives } from "$common/spaces/fallback_space_primitive import { FilteredSpacePrimitives } from "$common/spaces/filtered_space_primitives.ts"; import { encodePageRef, + encodePageURI, validatePageName, } from "@silverbulletmd/silverbullet/lib/page_ref"; import { ClientSystem } from "./client_system.ts"; @@ -1015,10 +1016,10 @@ export class Client implements ConfigContainer { if (newWindow) { console.log( "Navigating to new page in new window", - `${location.origin}/${encodeURIComponent(encodePageRef(pageRef))}`, + `${location.origin}/${encodePageURI(encodePageRef(pageRef))}`, ); const win = globalThis.open( - `${location.origin}/${encodeURIComponent(encodePageRef(pageRef))}`, + `${location.origin}/${encodePageURI(encodePageRef(pageRef))}`, "_blank", ); if (win) { diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index a4a6f7d4..c36e31de 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -6,6 +6,7 @@ import { decoratorStateField, isCursorInRange, LinkWidget } from "./util.ts"; import { resolvePath } from "@silverbulletmd/silverbullet/lib/resolve"; import { encodePageRef, + encodePageURI, parsePageRef, } from "@silverbulletmd/silverbullet/lib/page_ref"; @@ -101,7 +102,7 @@ export function cleanWikiLinkPlugin(client: Client) { title: fileExists ? `Navigate to ${encodePageRef(pageRef)}` : `Create ${pageRef.page}`, - href: `/${encodeURIComponent(encodePageRef(pageRef))}`, + href: `/${encodePageURI(encodePageRef(pageRef))}`, cssClass, from, callback: (e) => { diff --git a/web/navigator.ts b/web/navigator.ts index a0f16696..ca8dadd2 100644 --- a/web/navigator.ts +++ b/web/navigator.ts @@ -1,4 +1,8 @@ -import { type PageRef, parsePageRef } from "../plug-api/lib/page_ref.ts"; +import { + encodePageURI, + type PageRef, + parsePageRef, +} from "../plug-api/lib/page_ref.ts"; import type { Client } from "./client.ts"; import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve"; import { renderTheTemplate } from "$common/syscalls/template.ts"; @@ -57,18 +61,18 @@ export class PathPageNavigator { window.history.replaceState( cleanState, "", - `/${encodeURIComponent(currentState.page)}`, + `/${encodePageURI(currentState.page)}`, ); window.history.pushState( pageRef, "", - `/${encodeURIComponent(pageRef.page)}`, + `/${encodePageURI(pageRef.page)}`, ); } else { window.history.replaceState( pageRef, "", - `/${encodeURIComponent(pageRef.page)}`, + `/${encodePageURI(pageRef.page)}`, ); } globalThis.dispatchEvent( diff --git a/web/service_worker.ts b/web/service_worker.ts index a1c4ae11..39012e9f 100644 --- a/web/service_worker.ts +++ b/web/service_worker.ts @@ -2,7 +2,10 @@ import type { FileContent } from "$common/spaces/datastore_space_primitives.ts"; import { simpleHash } from "$lib/crypto.ts"; import { DataStore } from "$lib/data/datastore.ts"; import { IndexedDBKvPrimitives } from "$lib/data/indexeddb_kv_primitives.ts"; -import { looksLikePathWithExtension } from "@silverbulletmd/silverbullet/lib/page_ref"; +import { + decodePageURI, + looksLikePathWithExtension, +} from "@silverbulletmd/silverbullet/lib/page_ref"; const CACHE_NAME = "{{CACHE_NAME}}_{{CONFIG_HASH}}"; @@ -124,7 +127,7 @@ async function handleLocalFileRequest( request: Request, pathname: string, ): Promise { - const path = decodeURIComponent(pathname.slice(1)); + const path = decodePageURI(pathname.slice(1)); const data = await ds?.get([...filesContentPrefix, path]); if (data) { // console.log("Serving from space", path);