From c6197e520d61d4eb0e1ca88eb9093aeb66e1084f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 15:41:34 +0200 Subject: [PATCH] chore(core): integrate some more utils --- apps/server/package.json | 7 ++-- apps/server/src/services/utils.ts | 34 +++---------------- packages/trilium-core/package.json | 9 +++-- .../src/becca/entities/battachment.ts | 4 +-- .../trilium-core/src/becca/entities/bnote.ts | 4 +-- .../src/services/entity_changes.ts | 6 ++-- .../trilium-core/src/services/sanitizer.ts | 5 +++ .../trilium-core/src/services/utils/index.ts | 34 +++++++++++++++++++ 8 files changed, 60 insertions(+), 43 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index d52068829..f16b11ec7 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -55,8 +55,7 @@ "@types/express-session": "1.18.2", "@types/fs-extra": "11.0.4", "@types/html": "1.0.4", - "@types/ini": "4.1.1", - "@types/mime-types": "3.0.1", + "@types/ini": "4.1.1", "@types/multer": "2.0.0", "@types/safe-compare": "1.1.2", "@types/sax": "1.2.7", @@ -108,14 +107,12 @@ "jimp": "1.6.0", "lorem-ipsum": "2.0.8", "marked": "17.0.1", - "mime-types": "3.0.2", "multer": "2.0.2", "normalize-strings": "1.1.1", "ollama": "0.6.3", "openai": "6.15.0", "rand-token": "1.0.1", - "safe-compare": "1.1.4", - "sanitize-filename": "1.6.3", + "safe-compare": "1.1.4", "sax": "1.4.3", "serve-favicon": "2.5.1", "stream-throttle": "0.1.3", diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index e0e40094d..ee1e18302 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -5,11 +5,8 @@ import chardet from "chardet"; import crypto from "crypto"; import escape from "escape-html"; import { t } from "i18next"; -import mimeTypes from "mime-types"; import { release as osRelease } from "os"; import path from "path"; -import { generator } from "rand-token"; -import sanitize from "sanitize-filename"; import stripBom from "strip-bom"; import unescape from "unescape"; @@ -140,12 +137,12 @@ export async function crash(message: string) { } } +/** @deprecated */ export function getContentDisposition(filename: string) { - const sanitizedFilename = sanitize(filename).trim() || "file"; - const uriEncodedFilename = encodeURIComponent(sanitizedFilename); - return `file; filename="${uriEncodedFilename}"; filename*=UTF-8''${uriEncodedFilename}`; + return coreUtils.getContentDisposition(filename); } +/** @deprecated */ export function isStringNote(type: string | undefined, mime: string) { return coreUtils.isStringNote(type, mime); } @@ -160,30 +157,9 @@ export function replaceAll(string: string, replaceWhat: string, replaceWith: str return coreUtils.replaceAll(string, replaceWhat, replaceWith); } +/** @deprecated */ export function formatDownloadTitle(fileName: string, type: string | null, mime: string) { - const fileNameBase = !fileName ? "untitled" : sanitize(fileName); - - const getExtension = () => { - if (type === "text") return ".html"; - if (type === "relationMap" || type === "canvas" || type === "search") return ".json"; - if (!mime) return ""; - - const mimeLc = mime.toLowerCase(); - - // better to just return the current name without a fake extension - // it's possible that the title still preserves the correct extension anyways - if (mimeLc === "application/octet-stream") return ""; - - // if fileName has an extension matching the mime already - reuse it - const mimeTypeFromFileName = mimeTypes.lookup(fileName); - if (mimeTypeFromFileName === mimeLc) return ""; - - // as last resort try to get extension from mimeType - const extensions = mimeTypes.extension(mime); - return extensions ? `.${extensions}` : ""; - }; - - return `${fileNameBase}${getExtension()}`; + return coreUtils.formatDownloadTitle(fileName, type, mime); } export function removeTextFileExtension(filePath: string) { diff --git a/packages/trilium-core/package.json b/packages/trilium-core/package.json index c01697bfd..147aaa0ef 100644 --- a/packages/trilium-core/package.json +++ b/packages/trilium-core/package.json @@ -8,8 +8,13 @@ }, "dependencies": { "@triliumnext/commons": "workspace:*", - "sanitize-html": "2.17.0", + "sanitize-html": "2.17.0", + "@braintree/sanitize-url": "7.1.1", + "sanitize-filename": "1.6.3", + "mime-types": "3.0.2" + }, + "devDependencies": { "@types/sanitize-html": "2.16.0", - "@braintree/sanitize-url": "7.1.1" + "@types/mime-types": "3.0.1" } } diff --git a/packages/trilium-core/src/becca/entities/battachment.ts b/packages/trilium-core/src/becca/entities/battachment.ts index bdc28d15d..027413a65 100644 --- a/packages/trilium-core/src/becca/entities/battachment.ts +++ b/packages/trilium-core/src/becca/entities/battachment.ts @@ -10,7 +10,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js"; import type BBranch from "./bbranch.js"; import type BNote from "./bnote.js"; import { getSql } from "../../services/sql/index.js"; -import { isStringNote, replaceAll } from "../../services/utils"; +import { formatDownloadTitle, isStringNote, replaceAll } from "../../services/utils"; const attachmentRoleToNoteTypeMapping = { image: "image", @@ -201,7 +201,7 @@ class BAttachment extends AbstractBeccaEntity { getFileName() { const type = this.role === "image" ? "image" : "file"; - return utils.formatDownloadTitle(this.title, type, this.mime); + return formatDownloadTitle(this.title, type, this.mime); } override beforeSaving() { diff --git a/packages/trilium-core/src/becca/entities/bnote.ts b/packages/trilium-core/src/becca/entities/bnote.ts index f573b0e0e..ad97d93fb 100644 --- a/packages/trilium-core/src/becca/entities/bnote.ts +++ b/packages/trilium-core/src/becca/entities/bnote.ts @@ -19,7 +19,7 @@ import BAttribute from "./battribute.js"; import type BBranch from "./bbranch.js"; import BRevision from "./brevision.js"; import { getSql } from "../../services/sql/index.js"; -import { isStringNote, normalize, randomString, replaceAll } from "../../services/utils"; +import { formatDownloadTitle, isStringNote, normalize, randomString, replaceAll } from "../../services/utils"; const LABEL = "label"; const RELATION = "relation"; @@ -1654,7 +1654,7 @@ class BNote extends AbstractBeccaEntity { } getFileName() { - return utils.formatDownloadTitle(this.title, this.type, this.mime); + return formatDownloadTitle(this.title, this.type, this.mime); } override beforeSaving() { diff --git a/packages/trilium-core/src/services/entity_changes.ts b/packages/trilium-core/src/services/entity_changes.ts index d77bd4a58..c2f6ebb1f 100644 --- a/packages/trilium-core/src/services/entity_changes.ts +++ b/packages/trilium-core/src/services/entity_changes.ts @@ -2,10 +2,10 @@ import type { BlobRow, EntityChange } from "@triliumnext/commons"; import becca from "../becca/becca.js"; import dateUtils from "./utils/date.js"; -import { getLog } from "./log.js"; +import { getLog } from "./log.js"; import { randomString } from "./utils/index.js"; import { getSql } from "./sql/index.js"; -import { getComponentId } from "./context.js"; +import * as cls from "./context.js"; import events from "./events.js"; import blobService from "./blob.js"; import getInstanceId from "./instance_id.js"; @@ -33,7 +33,7 @@ function putEntityChange(origEntityChange: EntityChange) { ec.changeId = randomString(12); } - ec.componentId = ec.componentId || getComponentId() || "NA"; // NA = not available + ec.componentId = ec.componentId || cls.getComponentId() || "NA"; // NA = not available ec.instanceId = ec.instanceId || getInstanceId(); ec.isSynced = ec.isSynced ? 1 : 0; ec.isErased = ec.isErased ? 1 : 0; diff --git a/packages/trilium-core/src/services/sanitizer.ts b/packages/trilium-core/src/services/sanitizer.ts index 0301214f2..3d2446d37 100644 --- a/packages/trilium-core/src/services/sanitizer.ts +++ b/packages/trilium-core/src/services/sanitizer.ts @@ -3,6 +3,7 @@ import { ALLOWED_PROTOCOLS, SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/ import optionService from "./options.js"; import sanitize from "sanitize-html"; +import sanitizeFileNameInternal from "sanitize-filename"; // intended mainly as protection against XSS via import // secondarily, it (partly) protects against "CSS takeover" @@ -88,3 +89,7 @@ export function sanitizeHtmlCustom(dirtyHtml: string, config: sanitize.IOptions) export function sanitizeUrl(url: string) { return sanitizeUrlInternal(url).trim(); } + +export function sanitizeFileName(fileName: string) { + return sanitizeFileNameInternal(fileName); +} diff --git a/packages/trilium-core/src/services/utils/index.ts b/packages/trilium-core/src/services/utils/index.ts index fc39276f1..6cd7c7f06 100644 --- a/packages/trilium-core/src/services/utils/index.ts +++ b/packages/trilium-core/src/services/utils/index.ts @@ -1,5 +1,7 @@ import { getCrypto } from "../encryption/crypto"; +import { sanitizeFileName } from "../sanitizer"; import { encodeBase64 } from "./binary"; +import mimeTypes from "mime-types"; // render and book are string note in the sense that they are expected to contain empty string const STRING_NOTE_TYPES = new Set(["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas", "webView"]); @@ -65,3 +67,35 @@ export function sanitizeAttributeName(origName: string) { return fixedName; } + +export function getContentDisposition(filename: string) { + const sanitizedFilename = sanitizeFileName(filename).trim() || "file"; + const uriEncodedFilename = encodeURIComponent(sanitizedFilename); + return `file; filename="${uriEncodedFilename}"; filename*=UTF-8''${uriEncodedFilename}`; +} + +export function formatDownloadTitle(fileName: string, type: string | null, mime: string) { + const fileNameBase = !fileName ? "untitled" : sanitizeFileName(fileName); + + const getExtension = () => { + if (type === "text") return ".html"; + if (type === "relationMap" || type === "canvas" || type === "search") return ".json"; + if (!mime) return ""; + + const mimeLc = mime.toLowerCase(); + + // better to just return the current name without a fake extension + // it's possible that the title still preserves the correct extension anyways + if (mimeLc === "application/octet-stream") return ""; + + // if fileName has an extension matching the mime already - reuse it + const mimeTypeFromFileName = mimeTypes.lookup(fileName); + if (mimeTypeFromFileName === mimeLc) return ""; + + // as last resort try to get extension from mimeType + const extensions = mimeTypes.extension(mime); + return extensions ? `.${extensions}` : ""; + }; + + return `${fileNameBase}${getExtension()}`; +}