From 3de712aca4300d1d7591d06bc66606e074ea4973 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 11 Mar 2026 18:32:53 +0200 Subject: [PATCH] fix(server/search): invalid canvas crashing search (closes #9004) --- ...note_content_fulltext_preprocessor.spec.ts | 22 +++++++++ .../note_content_fulltext_preprocessor.ts | 48 +++++++++++-------- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.spec.ts b/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.spec.ts index e01e7f2aa8..acce9d5f6c 100644 --- a/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.spec.ts +++ b/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.spec.ts @@ -15,4 +15,26 @@ describe("Mind map preprocessing", () => { expect(preprocessContent("", type, mime)).toEqual(""); expect(preprocessContent(`{ "node": " }`, type, mime)).toEqual(""); }); + + it("reads data", () => { + expect(preprocessContent(`{ "nodedata": { "topic": "Root", "children": [ { "topic": "Child 1" }, { "topic": "Child 2", "children": [ { "topic": "Grandchild" } ] } ] } }`, type, mime)).toEqual("root, child 1, child 2, grandchild"); + }); +}); + +describe("Canvas preprocessing", () => { + const type: NoteType = "canvas"; + const mime = "application/json"; + + it("supports empty JSON", () => { + expect(preprocessContent("{}", type, mime)).toEqual(""); + }); + + it("supports blank text / invalid JSON", () => { + expect(preprocessContent("", type, mime)).toEqual(""); + }); + + it("reads elements", () => { + expect(preprocessContent(`{ "elements": [ { "type": "text", "text": "Hello" } ] }`, type, mime)).toEqual("hello"); + expect(preprocessContent(`{ "elements": [ { "type": "text" }, { "type": "text", "text": "World" }, { "type": "rectangle", "text": "Ignored" } ] }`, type, mime)).toEqual("world"); + }); }); \ No newline at end of file diff --git a/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.ts b/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.ts index ff0893b083..795c0346de 100644 --- a/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.ts +++ b/apps/server/src/services/search/expressions/note_content_fulltext_preprocessor.ts @@ -15,25 +15,7 @@ export default function preprocessContent(content: string | Buffer, type: string } else if (type === "mindMap" && mime === "application/json") { content = processMindmapContent(content); } else if (type === "canvas" && mime === "application/json") { - interface Element { - type: string; - text?: string; // Optional since not all objects have a `text` property - id: string; - [key: string]: any; // Other properties that may exist - } - - const canvasContent = JSON.parse(content); - const elements = canvasContent.elements; - - if (Array.isArray(elements)) { - const texts = elements - .filter((element: Element) => element.type === "text" && element.text) // Filter for 'text' type elements with a 'text' property - .map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering - - content = normalize(texts.join(" ")); - } else { - content = ""; - } + content = processCanvasContent(content); } return content.trim(); @@ -98,6 +80,34 @@ function processMindmapContent(content: string) { return normalize(topicsString.toString()); } +function processCanvasContent(content: string) { + interface Element { + type: string; + text?: string; // Optional since not all objects have a `text` property + id: string; + [key: string]: any; // Other properties that may exist + } + + let canvasContent; + try { + canvasContent = JSON.parse(content); + } catch (e) { + return ""; + } + const elements = canvasContent.elements; + + if (Array.isArray(elements)) { + const texts = elements + .filter((element: Element) => element.type === "text" && element.text) // Filter for 'text' type elements with a 'text' property + .map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering + + content = normalize(texts.join(" ")); + } else { + content = ""; + } + return content; +} + function stripTags(content: string) { // we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 // we want to insert space in place of block tags (because they imply text separation)