mirror of
https://github.com/TriliumNext/Trilium.git
synced 2026-10-11 22:49:56 +00:00
fix(share): encode and escape the share id when building links
A shareAlias is free-form text, so a share id can carry spaces, quotes and path separators. buildShareLink() percent-encodes it into a single path segment the /share/:shareId route decodes back, the share info bar escapes the link it renders, and the share index escapes the href it builds for each child. Also implements the SBranch TODO in buildShareNote() so a mocked note's children reach getChildNotes(), which the index test needs. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
44f7bfaee8
commit
3975ef7146
@@ -35,4 +35,20 @@ describe("buildShareLink", () => {
|
||||
setGlob({ httpBaseUrl: "http://127.0.0.1:37742" });
|
||||
expect(buildShareLink("", undefined)).toBe("http://127.0.0.1:37742/share/");
|
||||
});
|
||||
|
||||
it("percent-encodes a shareId carrying characters that are not path-safe", () => {
|
||||
// A shareAlias is free-form text, so it can hold spaces, quotes, separators and anything
|
||||
// else a title can. Each branch must produce one path segment the /share/:shareId route
|
||||
// decodes back to the alias.
|
||||
const shareId = `my alias"/?x`;
|
||||
const encoded = "my%20alias%22%2F%3Fx";
|
||||
const syncServerHost = "https://notes.example.com";
|
||||
|
||||
setGlob({ httpBaseUrl: "http://127.0.0.1:37742" });
|
||||
expect(buildShareLink(shareId, syncServerHost)).toBe(`${syncServerHost}/share/${encoded}`);
|
||||
expect(buildShareLink(shareId, undefined)).toBe(`http://127.0.0.1:37742/share/${encoded}`);
|
||||
|
||||
setGlob({});
|
||||
expect(buildShareLink(shareId, null)).toBe(`http://localhost:3000/share/${encoded}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
* origin (which is correct for the server build and for a browser hitting the desktop).
|
||||
*/
|
||||
export function buildShareLink(shareId: string, syncServerHost: string | null | undefined): string {
|
||||
// A shareId is either a noteId or a free-form shareAlias, so it can hold spaces, quotes and
|
||||
// path separators. Encoding keeps it a single segment the /share/:shareId route decodes back.
|
||||
const encodedShareId = encodeURIComponent(shareId);
|
||||
|
||||
if (syncServerHost) {
|
||||
return new URL(`/share/${shareId}`, syncServerHost).href;
|
||||
return new URL(`/share/${encodedShareId}`, syncServerHost).href;
|
||||
}
|
||||
|
||||
if (window.glob.httpBaseUrl) {
|
||||
return new URL(`/share/${shareId}`, window.glob.httpBaseUrl).href;
|
||||
return new URL(`/share/${encodedShareId}`, window.glob.httpBaseUrl).href;
|
||||
}
|
||||
|
||||
return `${location.protocol}//${location.host}${location.pathname}share/${shareId}`;
|
||||
return `${location.protocol}//${location.host}${location.pathname}share/${encodedShareId}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildShareLinkHtml } from "./shared_info";
|
||||
|
||||
describe("buildShareLinkHtml", () => {
|
||||
it("keeps the whole link in the anchor's href and text, whatever characters it carries", () => {
|
||||
const link = `http://host/share/my alias"x`;
|
||||
const anchor = parseAnchor(buildShareLinkHtml(link));
|
||||
|
||||
expect(anchor.getAttribute("href")).toBe(link);
|
||||
expect(anchor.textContent).toBe(link);
|
||||
expect(anchor.getAttributeNames().sort()).toEqual([ "class", "href" ]);
|
||||
});
|
||||
});
|
||||
|
||||
function parseAnchor(html: string) {
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = html;
|
||||
|
||||
const anchor = container.querySelector("a");
|
||||
if (!anchor) {
|
||||
throw new Error(`No anchor in ${html}`);
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import FNote from "../entities/fnote";
|
||||
import attributes from "../services/attributes";
|
||||
import { t } from "../services/i18n";
|
||||
import { buildShareLink } from "../services/share_link";
|
||||
import { isElectron } from "../services/utils";
|
||||
import { escapeHtml, isElectron } from "../services/utils";
|
||||
import HelpButton from "./react/HelpButton";
|
||||
import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks";
|
||||
import InfoBar from "./react/InfoBar";
|
||||
@@ -43,7 +43,7 @@ export function useShareInfo(note: FNote | null | undefined) {
|
||||
|
||||
const link = buildShareLink(getShareId(note), syncServerHost);
|
||||
|
||||
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
|
||||
setLink(buildShareLinkHtml(link));
|
||||
setLinkHref(link);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ export function useShareInfo(note: FNote | null | undefined) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildShareLinkHtml(link: string) {
|
||||
const escapedLink = escapeHtml(link);
|
||||
|
||||
return `<a href="${escapedLink}" class="external tn-link">${escapedLink}</a>`;
|
||||
}
|
||||
|
||||
function getShareId(note: FNote) {
|
||||
if (note.hasOwnedLabel("shareRoot")) {
|
||||
return "";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { buildShareNote, buildShareNotes } from "../test/shaca_mocking.js";
|
||||
import { getContent, renderCode, type Result, shouldSyntaxHighlight } from "./content_renderer.js";
|
||||
import shareRoot from "./share_root.js";
|
||||
|
||||
describe("content_renderer", () => {
|
||||
beforeAll(() => {
|
||||
@@ -533,4 +534,26 @@ describe("content_renderer", () => {
|
||||
expect(shouldSyntaxHighlight("x".repeat(50_001))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Share index", () => {
|
||||
it("points each index entry at the child's shareId, whatever characters it carries", () => {
|
||||
buildShareNote({
|
||||
id: shareRoot.SHARE_ROOT_NOTE_ID,
|
||||
children: [
|
||||
{ "id": "child1", "title": "Child", "#shareAlias": `my alias"x` }
|
||||
]
|
||||
});
|
||||
const note = buildShareNote({
|
||||
"id": "indexNote",
|
||||
"content": "<p>Index</p>",
|
||||
"#shareIndex": ""
|
||||
});
|
||||
|
||||
const result = getContent(note);
|
||||
const anchor = parse(String(result.content)).querySelector("#index a");
|
||||
|
||||
expect(anchor?.getAttribute("href")).toBe(`./my alias"x`);
|
||||
expect(Object.keys(anchor?.attributes ?? {}).sort()).toEqual([ "class", "href" ]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -340,7 +340,7 @@ function renderIndex(result: Result) {
|
||||
for (const childNote of rootNote.getChildNotes()) {
|
||||
const isExternalLink = childNote.hasLabel("shareExternalLink");
|
||||
const rawHref = childNote.getLabelValue("shareExternalLink") ?? "";
|
||||
const href = isExternalLink ? escapeHtml(sanitize.sanitizeUrl(rawHref)) : `./${childNote.shareId}`;
|
||||
const href = escapeHtml(isExternalLink ? sanitize.sanitizeUrl(rawHref) : `./${childNote.shareId}`);
|
||||
const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : "";
|
||||
result.content += `<li><a class="${childNote.type}" href="${href}" ${target}>${childNote.escapedTitle}</a></li>`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import utils from "../services/utils.js";
|
||||
import SAttachment from "../share/shaca/entities/sattachment.js";
|
||||
import SAttribute from "../share/shaca/entities/sattribute.js";
|
||||
import SBranch from "../share/shaca/entities/sbranch.js";
|
||||
import SNote from "../share/shaca/entities/snote.js";
|
||||
import shaca from "../share/shaca/shaca.js";
|
||||
|
||||
@@ -87,12 +88,20 @@ export function buildShareNote(noteDef: NoteDefinition) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle children.
|
||||
// Handle children. The SBranch constructor is what links the two notes, so getChildNotes()
|
||||
// and getVisibleChildNotes() only see a child once its branch exists.
|
||||
if (noteDef.children) {
|
||||
for (const childDef of noteDef.children) {
|
||||
const childNote = buildShareNote(childDef);
|
||||
|
||||
// TODO: Create corresponding SBranch.
|
||||
new SBranch([
|
||||
utils.randomString(12),
|
||||
childNote.noteId,
|
||||
note.noteId,
|
||||
"", // prefix
|
||||
"", // isExpanded
|
||||
false // utcDateModified
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user