Compare commits

..

26 Commits

Author SHA1 Message Date
Elian Doran
9a4fef80b9 chore(deps): fix pnpm lock 2026-04-05 12:15:07 +03:00
Elian Doran
79dc4b39f1 chore(client): address requested changes 2026-04-05 12:11:05 +03:00
Elian Doran
9bc18b774e test(server): add unit tests for sanitizeSvg 2026-04-05 12:11:05 +03:00
Elian Doran
465c36407c Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:52 +03:00
Elian Doran
b99486259e Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:44 +03:00
Elian Doran
ecf5475966 Update apps/desktop/package.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:29 +03:00
Elian Doran
90822cc8a3 chore: address requested changes 2026-04-05 11:59:45 +03:00
Elian Doran
5c46209ddc feat(server): improve request handling for SVGs 2026-04-05 11:28:28 +03:00
Elian Doran
176de87b6b feat(desktop): add Electron fuses 2026-04-05 11:01:22 +03:00
Elian Doran
7f199c527b feat(share): improve request handling for SVGs 2026-04-05 10:52:36 +03:00
Elian Doran
2432e230c5 chore(etapi): enforce MIME for image upload 2026-04-05 10:44:47 +03:00
Elian Doran
fc1be0d23d fix(ckeditor5-mermaid): use textContent for diagram source rendering 2026-04-05 10:17:16 +03:00
Elian Doran
626aca5181 fix(client): toasts could render HTML content 2026-04-04 22:21:25 +03:00
Elian Doran
8204322b46 fix(openid): use more secure RNG 2026-04-04 22:02:33 +03:00
Elian Doran
ed3b86cd49 fix(import): no longer preserve named note IDs 2026-04-04 21:27:37 +03:00
Elian Doran
b371675494 chore(commons): mark docName as a dangerous attribute 2026-04-04 21:25:05 +03:00
Elian Doran
ff06c8e7bd fix(client): validate docName attribute in doc renderer 2026-04-04 21:21:50 +03:00
Elian Doran
8ff41d8fa9 fix(server): align attachment upload validation with note upload 2026-04-04 20:46:03 +03:00
Elian Doran
8ac9daa5d3 chore(release): prepare for v0.102.1 2026-03-08 10:43:59 +02:00
Elian Doran
0b506c6327 chore(pdfjs): bump pdfjs viewer version 2026-03-08 10:41:21 +02:00
Elian Doran
d2b62540ec fix(ci): migrate all the jank docker ci to use crane instead (#8869) 2026-03-08 10:37:49 +02:00
Elian Doran
64418c7fec docs(release): prepare for v0.102.1 2026-03-08 10:36:06 +02:00
Elian Doran
8c1a58e64f fix(pdf): cache buster not working in all circumstances 2026-03-08 10:29:57 +02:00
Adorian Doran
b27fd31c1f style/pdf viewer: fix some layout issues in toolbar 2026-03-08 10:25:05 +02:00
Elian Doran
f18a531924 fix(mindmap): crashing on auto-switch to dark theme (closes #8879) 2026-03-08 10:22:21 +02:00
Elian Doran
3cabb4b661 fix(pdf): not accessible on Nginx Proxy Manager with block common exploits (closes #8877) 2026-03-08 09:30:27 +02:00
31 changed files with 540 additions and 238 deletions

View File

@@ -166,9 +166,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
@@ -189,13 +187,6 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
@@ -204,9 +195,7 @@ jobs:
file: apps/server/${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: |
type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
type=image,name=${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
outputs: type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
@@ -239,18 +228,8 @@ jobs:
- name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
- name: Set up crane
uses: imjasonh/setup-crane@v0.4
- name: Login to GHCR
uses: docker/login-action@v3
@@ -266,48 +245,69 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create manifest list and push
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha
flavor: |
latest=false
- name: Verify digests exist on GHCR
working-directory: /tmp/digests
run: |
# Extract the branch or tag name from the ref
REF_NAME=$(echo "${GITHUB_REF}" | sed 's/refs\/heads\///' | sed 's/refs\/tags\///')
echo "Verifying all digests are available on GHCR..."
for DIGEST_FILE in *; do
DIGEST="sha256:${DIGEST_FILE}"
echo -n " ${DIGEST}: "
crane manifest "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" > /dev/null
echo "OK"
done
# Create and push the manifest list with both the branch/tag name and the commit SHA
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Create and push multi-arch manifest
working-directory: /tmp/digests
run: |
GHCR_IMAGE="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}"
DOCKERHUB_IMAGE="${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}"
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Build -m flags for crane index append from digest files
MANIFEST_ARGS=""
for d in *; do
MANIFEST_ARGS="${MANIFEST_ARGS} -m ${GHCR_IMAGE}@sha256:${d}"
done
# If the ref is a tag, also tag the image as stable as this is part of a 'release'
# and only go in the `if` if there is NOT a `-` in the tag's name, due to tagging of `-alpha`, `-beta`, etc...
# Create multi-arch manifest for each tag from metadata, plus copy to DockerHub
while IFS= read -r TAG; do
echo "Creating manifest: ${TAG}"
crane index append ${MANIFEST_ARGS} -t "${TAG}"
SUFFIX="${TAG#*:}"
echo "Copying to DockerHub: ${DOCKERHUB_IMAGE}:${SUFFIX}"
crane copy "${TAG}" "${DOCKERHUB_IMAGE}:${SUFFIX}"
done <<< "${{ steps.meta.outputs.tags }}"
# For stable releases (tags without hyphens), also create stable + latest
REF_NAME="${GITHUB_REF#refs/tags/}"
if [[ "${GITHUB_REF}" == refs/tags/* && ! "${REF_NAME}" =~ - ]]; then
# First create stable tags
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Small delay to ensure stable tag is fully propagated
sleep 5
# Now update latest tags
docker buildx imagetools create \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
docker buildx imagetools create \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
echo "Creating stable tags..."
crane index append ${MANIFEST_ARGS} -t "${GHCR_IMAGE}:stable"
crane copy "${GHCR_IMAGE}:stable" "${DOCKERHUB_IMAGE}:stable"
echo "Creating latest tags..."
crane copy "${GHCR_IMAGE}:stable" "${GHCR_IMAGE}:latest"
crane copy "${GHCR_IMAGE}:latest" "${DOCKERHUB_IMAGE}:latest"
fi
- name: Inspect image
- name: Inspect manifests
run: |
docker buildx imagetools inspect ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
REF_NAME="${GITHUB_REF#refs/heads/}"
REF_NAME="${REF_NAME#refs/tags/}"
echo "=== GHCR ==="
crane manifest "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME}"
echo ""
echo "=== DockerHub ==="
crane manifest "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME}"

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.102.0",
"version": "0.102.1",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { isValidDocName } from "./doc_renderer.js";
describe("isValidDocName", () => {
it("accepts valid docNames", () => {
expect(isValidDocName("launchbar_intro")).toBe(true);
expect(isValidDocName("User Guide/Quick Start")).toBe(true);
expect(isValidDocName("User Guide/User Guide/Quick Start")).toBe(true);
expect(isValidDocName("Quick Start Guide")).toBe(true);
expect(isValidDocName("quick_start_guide")).toBe(true);
expect(isValidDocName("quick-start-guide")).toBe(true);
});
it("rejects path traversal attacks", () => {
expect(isValidDocName("..")).toBe(false);
expect(isValidDocName("../etc/passwd")).toBe(false);
expect(isValidDocName("foo/../bar")).toBe(false);
expect(isValidDocName("../../../../api/notes/_malicious/open")).toBe(false);
expect(isValidDocName("..\\etc\\passwd")).toBe(false);
expect(isValidDocName("foo\\bar")).toBe(false);
});
it("rejects URL manipulation attacks", () => {
expect(isValidDocName("../../../../api/notes/_malicious/open?x=")).toBe(false);
expect(isValidDocName("foo#bar")).toBe(false);
expect(isValidDocName("%2e%2e")).toBe(false);
expect(isValidDocName("%2e%2e%2f%2e%2e%2fapi")).toBe(false);
});
});

View File

@@ -3,22 +3,39 @@ import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_help
import { getCurrentLanguage } from "./i18n.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
/**
* Validates a docName to prevent path traversal attacks.
* Allows forward slashes for subdirectories (e.g., "User Guide/Quick Start")
* but blocks traversal sequences and URL manipulation characters.
*/
export function isValidDocName(docName: string): boolean {
// Allow alphanumeric characters, spaces, underscores, hyphens, and forward slashes.
const validDocNameRegex = /^[a-zA-Z0-9_/\- ]+$/;
return validDocNameRegex.test(docName);
}
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
let docName = note.getLabelValue("docName");
const docName = note.getLabelValue("docName");
const $content = $("<div>");
if (docName) {
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
if (url) {
$content.load(url, async (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content)
if (fallbackUrl) {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
resolve($content);
});
} else {
resolve($content);
});
}
return;
}
@@ -28,8 +45,6 @@ export default function renderDoc(note: FNote) {
} else {
resolve($content);
}
return $content;
});
}
@@ -39,7 +54,7 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
$content.find("img").each((i, el) => {
const $img = $(el);
$img.attr("src", dir + "/" + $img.attr("src"));
$img.attr("src", `${dir}/${$img.attr("src")}`);
});
formatCodeBlocks($content);
@@ -48,10 +63,17 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
await applyReferenceLinks($content[0]);
}
function getUrl(docNameValue: string, language: string) {
function getUrl(docNameValue: string | null, language: string) {
if (!docNameValue) return;
if (!isValidDocName(docNameValue)) {
console.error(`Invalid docName: ${docNameValue}`);
return null;
}
// Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20");
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
const basePath = window.glob.isDev ? `${window.glob.assetPath }/..` : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
}

View File

@@ -5,7 +5,6 @@ import { useEffect } from "preact/hooks";
import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
import Icon from "./react/Icon";
import { RawHtmlBlock } from "./react/RawHtml";
import Button from "./react/Button";
export default function ToastContainer() {
@@ -54,7 +53,7 @@ function Toast({ id, title, timeout, progress, message, icon, buttons }: ToastOp
<div class="toast-icon">{toastIcon}</div>
)}
<RawHtmlBlock className="toast-body" html={message} />
<div className="toast-body">{message}</div>
{!title && <div class="toast-header">{closeButton}</div>}

View File

@@ -6,7 +6,7 @@ import "./MindMap.css";
import nodeMenu from "@mind-elixir/node-menu";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { snapdom } from "@zumer/snapdom";
import { default as VanillaMindElixir,MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME, DARK_THEME } from "mind-elixir";
import { DARK_THEME, default as VanillaMindElixir, MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME } from "mind-elixir";
import { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
@@ -154,6 +154,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
const apiRef = useRef<MindElixirInstance>(null);
const [ locale ] = useTriliumOption("locale");
const colorScheme = useColorScheme();
const defaultColorScheme = useRef(colorScheme);
function reinitialize() {
if (!containerRef.current) return;
@@ -162,7 +163,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
el: containerRef.current,
locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
editable,
theme: LIGHT_THEME
theme: defaultColorScheme.current === "dark" ? DARK_THEME : LIGHT_THEME
});
if (editable) {
@@ -188,7 +189,11 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
if (!apiRef.current) return;
const newTheme = colorScheme === "dark" ? DARK_THEME : LIGHT_THEME;
if (apiRef.current.theme === newTheme) return; // Avoid unnecessary theme changes, which can be expensive to render.
apiRef.current.changeTheme(newTheme);
try {
apiRef.current.changeTheme(newTheme);
} catch (e) {
console.warn("Failed to change mind map theme:", e);
}
}, [ colorScheme ]);
useEffect(() => {

View File

@@ -184,7 +184,7 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
<PdfViewer
iframeRef={iframeRef}
tabIndex={300}
pdfUrl={`../../api/notes/${note.noteId}/open`}
pdfUrl={new URL(`${window.glob.baseApiUrl}notes/${note.noteId}/open`, window.location.href).pathname}
onLoad={() => {
const win = iframeRef.current?.contentWindow;
if (win) {

View File

@@ -1,7 +1,8 @@
import type { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import Inter from "./../../../fonts/Inter/Inter-VariableFont_opsz,wght.ttf";
import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import Inter from "./../../../fonts/Inter/Inter-VariableFont_opsz,wght.ttf";
interface FontDefinition {
name: string;
@@ -10,11 +11,11 @@ interface FontDefinition {
const FONTS: FontDefinition[] = [
{name: "Inter", url: Inter},
]
];
interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabIndex"> {
iframeRef?: RefObject<HTMLIFrameElement>;
/** Note: URLs are relative to /pdfjs/web. */
/** Note: URLs are relative to /pdfjs/web, ideally use absolute paths (but without domain name) to avoid issues with some proxies. */
pdfUrl: string;
onLoad?(): void;
/**
@@ -37,7 +38,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
ref={iframeRef}
class="pdf-preview"
style={{width: "100%", height: "100%"}}
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
src={`pdfjs/web/viewer.html?v=${glob.triliumVersion}&file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
onLoad={() => {
injectStyles();
onLoad?.();
@@ -63,7 +64,7 @@ function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
const fontStyles = doc.createElement("style");
fontStyles.textContent = FONTS.map(injectFont).join("\n");
doc.head.appendChild(fontStyles);
}, [ iframeRef ]);
// React to changes.
@@ -107,4 +108,4 @@ function injectFont(font: FontDefinition) {
src: url('${font.url}');
}
`;
}
}

View File

@@ -1,4 +1,5 @@
import type { ForgeConfig } from "@electron-forge/shared-types";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { LOCALES } from "@triliumnext/commons";
import { existsSync } from "fs";
import fs from "fs-extra";
@@ -166,6 +167,17 @@ const config: ForgeConfig = {
{
name: "@electron-forge/plugin-auto-unpack-natives",
config: {}
},
{
name: "@electron-forge/plugin-fuses",
config: {
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true
}
}
],
hooks: {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.102.0",
"version": "0.102.1",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "src/main.ts",
@@ -27,15 +27,10 @@
"electron-debug": "4.1.0",
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",
"jquery.fancytree": "2.38.5",
"jquery-hotkeys": "0.2.2"
"jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.5"
},
"devDependencies": {
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.1",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",
@@ -44,6 +39,13 @@
"@electron-forge/maker-squirrel": "7.11.1",
"@electron-forge/maker-zip": "7.11.1",
"@electron-forge/plugin-auto-unpack-natives": "7.11.1",
"@electron-forge/plugin-fuses": "7.11.1",
"@electron/fuses": "1.0.0",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"@types/electron-squirrel-startup": "1.0.2",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.1",
"prebuild-install": "7.1.3"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/edit-docs",
"version": "0.102.0",
"version": "0.102.1",
"private": true,
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/server",
"version": "0.102.0",
"version": "0.102.1",
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"main": "./src/main.ts",

View File

@@ -66,6 +66,11 @@ function register(router: Router) {
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
const params = _params as NoteParams;
// Validate MIME type for image notes
if (params.type === "image" && params.mime && !params.mime.toLowerCase().startsWith("image/")) {
throw new eu.EtapiError(400, "INVALID_MIME_FOR_IMAGE", `MIME type '${params.mime}' is not allowed for image notes. MIME must start with 'image/'.`);
}
try {
const resp = noteService.createNewNote(params);
@@ -93,6 +98,14 @@ function register(router: Router) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
}
// Validate MIME type for image notes (check both current and new type/mime)
const effectiveType = req.body.type ?? note.type;
const effectiveMime = req.body.mime ?? note.mime;
const normalizedEffectiveMime = typeof effectiveMime === "string" ? effectiveMime.toLowerCase() : effectiveMime;
if (effectiveType === "image" && normalizedEffectiveMime && !normalizedEffectiveMime.startsWith("image/")) {
throw new eu.EtapiError(400, "INVALID_MIME_FOR_IMAGE", `MIME type '${effectiveMime}' is not allowed for image notes. MIME must start with 'image/'.`);
}
noteService.saveRevisionIfNeeded(note);
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
note.save();

View File

@@ -232,6 +232,10 @@ function uploadModifiedFileToAttachment(req: Request) {
const { attachmentId } = req.params;
const { filePath } = req.body;
if (!createdTemporaryFiles.has(filePath)) {
throw new ValidationError(`File '${filePath}' is not a temporary file.`);
}
const attachment = becca.getAttachmentOrThrow(attachmentId);
log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);

View File

@@ -1,12 +1,14 @@
"use strict";
import imageService from "../../services/image.js";
import becca from "../../becca/becca.js";
import fs from "fs";
import type { Request, Response } from "express";
import fs from "fs";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
import { sanitizeSvg } from "../../services/utils.js";
function returnImageFromNote(req: Request, res: Response) {
const image = becca.getNote(req.params.noteId);
@@ -37,28 +39,33 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
} else {
res.set("Content-Type", image.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(image.getContent());
if (image.mime === "image/svg+xml") {
sendSanitizedSvg(res, image.getContent());
} else {
res.send(image.getContent());
}
}
}
export function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
let svg: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
let svgContent: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {
svg = attachment.getContent();
svgContent = attachment.getContent();
} else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg;
if (contentSvg) {
svg = contentSvg;
svgContent = contentSvg;
}
}
res.set("Content-Type", "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(svg);
sendSanitizedSvg(res, svgContent);
}
function returnAttachedImage(req: Request, res: Response) {
@@ -75,7 +82,12 @@ function returnAttachedImage(req: Request, res: Response) {
res.set("Content-Type", attachment.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(attachment.getContent());
if (attachment.mime === "image/svg+xml") {
sendSanitizedSvg(res, attachment.getContent());
} else {
res.send(attachment.getContent());
}
}
function updateImage(req: Request) {
@@ -116,3 +128,9 @@ export default {
returnAttachedImage,
updateImage
};
function sendSanitizedSvg(res: Response, content: string | Buffer) {
const svgString = typeof content === "string" ? content : content.toString("utf-8");
res.set("Content-Security-Policy", "script-src 'none'");
res.send(sanitizeSvg(svgString));
}

View File

@@ -51,8 +51,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return "empty_note_id";
}
if (origNoteId === "root" || origNoteId.startsWith("_") || opts?.preserveIds) {
// these "named" noteIds don't differ between Trilium instances
if (origNoteId === "root" || opts?.preserveIds) {
return origNoteId;
}

View File

@@ -1,14 +1,14 @@
import type { NextFunction, Request, Response } from "express";
import openIDEncryption from "./encryption/open_id_encryption.js";
import sqlInit from "./sql_init.js";
import options from "./options.js";
import type { Session } from "express-openid-connect";
import sql from "./sql.js";
import config from "./config.js";
import config from "./config.js";
import openIDEncryption from "./encryption/open_id_encryption.js";
import options from "./options.js";
import sql from "./sql.js";
import sqlInit from "./sql_init.js";
function checkOpenIDConfig() {
const missingVars: string[] = []
const missingVars: string[] = [];
if (config.MultiFactorAuthentication.oauthBaseUrl === "") {
missingVars.push("oauthBaseUrl");
}
@@ -27,7 +27,7 @@ function isOpenIDEnabled() {
function isUserSaved() {
const data = sql.getValue<string>("SELECT isSetup FROM user_data;");
return data === "true" ? true : false;
return data === "true";
}
function getUsername() {
@@ -59,34 +59,31 @@ function getOAuthStatus() {
};
}
function isTokenValid(req: Request, res: Response, next: NextFunction) {
async function isTokenValid(req: Request, res: Response, next: NextFunction) {
const userStatus = openIDEncryption.isSubjectIdentifierSaved();
if (req.oidc !== undefined) {
const result = req.oidc
.fetchUserInfo()
.then((result) => {
return {
success: true,
message: "Token is valid",
user: userStatus,
};
})
.catch((result) => {
return {
success: false,
message: "Token is not valid",
user: userStatus,
};
});
return result;
} else {
return {
success: false,
message: "Token not set up",
user: userStatus,
};
try {
await req.oidc.fetchUserInfo();
return {
success: true,
message: "Token is valid",
user: userStatus,
};
} catch {
return {
success: false,
message: "Token is not valid",
user: userStatus,
};
}
}
return {
success: false,
message: "Token not set up",
user: userStatus,
};
}
function getSSOIssuerName() {
@@ -121,11 +118,10 @@ function generateOAuthConfig() {
scope: "openid profile email",
access_type: "offline",
prompt: "consent",
state: "random_state_" + Math.random().toString(36).substring(2)
},
routes: authRoutes,
idpLogout: true,
logoutParams: logoutParams,
logoutParams,
afterCallback: async (req: Request, res: Response, session: Session) => {
if (!sqlInit.isDbInitialized()) return session;

View File

@@ -705,3 +705,110 @@ describe("#slugify", () => {
expect(result).toBe(expectedSlug);
});
});
describe("#sanitizeSvg", () => {
it("should remove script elements", () => {
const maliciousSvg = '<svg><script>alert("XSS")</script><rect width="100" height="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100" height="100"/></svg>');
});
it("should remove script elements with attributes", () => {
const maliciousSvg = '<svg><script type="text/javascript">alert("XSS")</script></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should remove multiline script elements", () => {
const maliciousSvg = `<svg><script>
var x = 1;
alert(x);
</script></svg>`;
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should remove onclick event handlers with double quotes", () => {
const maliciousSvg = '<svg><rect onclick="doEvil()" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onclick event handlers with single quotes", () => {
const maliciousSvg = "<svg><rect onclick='doEvil()' width=\"100\"/></svg>";
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onload event handlers", () => {
const maliciousSvg = '<svg onload="doEvil()"><rect width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onerror event handlers", () => {
const maliciousSvg = '<svg><image onerror="alert(1)" href="invalid.jpg"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><image href="invalid.jpg"/></svg>');
});
it("should remove onmouseover event handlers", () => {
const maliciousSvg = '<svg><rect onmouseover="alert(1)" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove event handlers without quotes", () => {
const maliciousSvg = '<svg><rect onclick=alert(1) width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should replace javascript: URLs in href with #", () => {
const maliciousSvg = '<svg><a href="javascript:alert(1)"><text>Click me</text></a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><a href="#"><text>Click me</text></a></svg>');
});
it("should replace javascript: URLs in xlink:href with #", () => {
const maliciousSvg = '<svg><a xlink:href="javascript:alert(1)"><text>Click me</text></a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><a xlink:href="#"><text>Click me</text></a></svg>');
});
it("should preserve valid SVG content", () => {
const validSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="10" width="80" height="80" fill="blue"/><circle cx="50" cy="50" r="30" fill="red"/></svg>';
const result = utils.sanitizeSvg(validSvg);
expect(result).toBe(validSvg);
});
it("should preserve valid href URLs", () => {
const validSvg = '<svg><a href="https://example.com"><text>Link</text></a></svg>';
const result = utils.sanitizeSvg(validSvg);
expect(result).toBe(validSvg);
});
it("should handle multiple malicious elements", () => {
const maliciousSvg = '<svg onload="evil()"><script>evil()</script><rect onclick="bad()" width="100"/><a href="javascript:attack()">link</a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/><a href="#">link</a></svg>');
});
it("should handle empty SVG", () => {
const emptySvg = '<svg></svg>';
const result = utils.sanitizeSvg(emptySvg);
expect(result).toBe('<svg></svg>');
});
it("should be case insensitive for script tags", () => {
const maliciousSvg = '<svg><SCRIPT>alert(1)</SCRIPT><Script>alert(2)</Script></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should be case insensitive for event handlers", () => {
const maliciousSvg = '<svg><rect ONCLICK="alert(1)" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
});

View File

@@ -119,6 +119,22 @@ export function sanitizeSqlIdentifier(str: string) {
return str.replace(/[^A-Za-z0-9_]/g, "");
}
/**
* Sanitize SVG to remove potentially dangerous elements and attributes.
* This prevents XSS via script injection in SVG content.
*/
export function sanitizeSvg(svg: string): string {
return svg
// Remove script elements
.replace(/<script[\s\S]*?<\/script>/gi, '')
// Remove on* event handlers (onclick, onload, onerror, etc.)
.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
// Remove javascript: URLs
.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"')
.replace(/xlink:href\s*=\s*["']javascript:[^"']*["']/gi, 'xlink:href="#"');
}
export const escapeHtml = escape;
export const unescapeHtml = unescape;
@@ -556,6 +572,7 @@ export default {
replaceAll,
safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier,
sanitizeSvg,
stripTags,
slugify,
timeLimit,

View File

@@ -9,7 +9,7 @@ import SearchContext from "../services/search/search_context.js";
import type SNote from "./shaca/entities/snote.js";
import type SAttachment from "./shaca/entities/sattachment.js";
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
import utils from "../services/utils.js";
import utils, { sanitizeSvg } from "../services/utils.js";
function addNoIndexHeader(note: SNote, res: Response) {
if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
@@ -102,9 +102,10 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri
}
}
const svg = svgString;
const svg = sanitizeSvg(svgString);
res.set("Content-Type", "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.set("Content-Security-Policy", "script-src 'none'");
res.send(svg);
}

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.101.3",
"appVersion": "0.102.0",
"files": [
{
"isClone": false,
@@ -61,6 +61,32 @@
"attachments": [],
"dirFileName": "Release Notes",
"children": [
{
"isClone": false,
"noteId": "4FTGCuCiG7s7",
"notePath": [
"hD3V4hiu2VW4",
"4FTGCuCiG7s7"
],
"title": "v0.102.1",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "template",
"value": "wyurrlcDl416",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "v0.102.1.md",
"attachments": []
},
{
"isClone": false,
"noteId": "d582eD4RY4OM",
@@ -69,7 +95,7 @@
"d582eD4RY4OM"
],
"title": "v0.102.0",
"notePosition": 10,
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -95,7 +121,7 @@
"IlBzLeN3MJhw"
],
"title": "v0.101.3",
"notePosition": 20,
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -121,7 +147,7 @@
"vcBthaXcwAm6"
],
"title": "v0.101.2",
"notePosition": 30,
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -147,7 +173,7 @@
"AgUcrU9nFXuW"
],
"title": "v0.101.1",
"notePosition": 40,
"notePosition": 50,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -173,7 +199,7 @@
"uYwlZ594eyJu"
],
"title": "v0.101.0",
"notePosition": 50,
"notePosition": 60,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -199,7 +225,7 @@
"iPGKEk7pwJXK"
],
"title": "v0.100.0",
"notePosition": 60,
"notePosition": 70,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -225,7 +251,7 @@
"7HKMTjmopLcM"
],
"title": "v0.99.5",
"notePosition": 70,
"notePosition": 80,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -251,7 +277,7 @@
"RMBaNYPsRpIr"
],
"title": "v0.99.4",
"notePosition": 80,
"notePosition": 90,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -277,7 +303,7 @@
"yuroLztFfpu5"
],
"title": "v0.99.3",
"notePosition": 90,
"notePosition": 100,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -303,7 +329,7 @@
"z207sehwMJ6C"
],
"title": "v0.99.2",
"notePosition": 100,
"notePosition": 110,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -329,7 +355,7 @@
"WGQsXq2jNyTi"
],
"title": "v0.99.1",
"notePosition": 110,
"notePosition": 120,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -355,7 +381,7 @@
"cyw2Yue9vXf3"
],
"title": "v0.99.0",
"notePosition": 120,
"notePosition": 130,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -381,7 +407,7 @@
"QOJwjruOUr4k"
],
"title": "v0.98.1",
"notePosition": 130,
"notePosition": 140,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -407,7 +433,7 @@
"PLUoryywi0BC"
],
"title": "v0.98.0",
"notePosition": 140,
"notePosition": 150,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -433,7 +459,7 @@
"lvOuiWsLDv8F"
],
"title": "v0.97.2",
"notePosition": 150,
"notePosition": 160,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -459,7 +485,7 @@
"OtFZ6Nd9vM3n"
],
"title": "v0.97.1",
"notePosition": 160,
"notePosition": 170,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -485,7 +511,7 @@
"SJZ5PwfzHSQ1"
],
"title": "v0.97.0",
"notePosition": 170,
"notePosition": 180,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -511,7 +537,7 @@
"mYXFde3LuNR7"
],
"title": "v0.96.0",
"notePosition": 180,
"notePosition": 190,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -537,7 +563,7 @@
"jthwbL0FdaeU"
],
"title": "v0.95.0",
"notePosition": 190,
"notePosition": 200,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -563,7 +589,7 @@
"7HGYsJbLuhnv"
],
"title": "v0.94.1",
"notePosition": 200,
"notePosition": 210,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -589,7 +615,7 @@
"Neq53ujRGBqv"
],
"title": "v0.94.0",
"notePosition": 210,
"notePosition": 220,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -615,7 +641,7 @@
"VN3xnce1vLkX"
],
"title": "v0.93.0",
"notePosition": 220,
"notePosition": 230,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -633,7 +659,7 @@
"WRaBfQqPr6qo"
],
"title": "v0.92.7",
"notePosition": 230,
"notePosition": 240,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -659,7 +685,7 @@
"a2rwfKNmUFU1"
],
"title": "v0.92.6",
"notePosition": 240,
"notePosition": 250,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -677,7 +703,7 @@
"fEJ8qErr0BKL"
],
"title": "v0.92.5-beta",
"notePosition": 250,
"notePosition": 260,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -695,7 +721,7 @@
"kkkZQQGSXjwy"
],
"title": "v0.92.4",
"notePosition": 260,
"notePosition": 270,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -713,7 +739,7 @@
"vAroNixiezaH"
],
"title": "v0.92.3-beta",
"notePosition": 270,
"notePosition": 280,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -731,7 +757,7 @@
"mHEq1wxAKNZd"
],
"title": "v0.92.2-beta",
"notePosition": 280,
"notePosition": 290,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -749,7 +775,7 @@
"IykjoAmBpc61"
],
"title": "v0.92.1-beta",
"notePosition": 290,
"notePosition": 300,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -767,7 +793,7 @@
"dq2AJ9vSBX4Y"
],
"title": "v0.92.0-beta",
"notePosition": 300,
"notePosition": 310,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -785,7 +811,7 @@
"3a8aMe4jz4yM"
],
"title": "v0.91.6",
"notePosition": 310,
"notePosition": 320,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -803,7 +829,7 @@
"8djQjkiDGESe"
],
"title": "v0.91.5",
"notePosition": 320,
"notePosition": 330,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -821,7 +847,7 @@
"OylxVoVJqNmr"
],
"title": "v0.91.4-beta",
"notePosition": 330,
"notePosition": 340,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -839,7 +865,7 @@
"tANGQDvnyhrj"
],
"title": "v0.91.3-beta",
"notePosition": 340,
"notePosition": 350,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -857,7 +883,7 @@
"hMoBfwSoj1SC"
],
"title": "v0.91.2-beta",
"notePosition": 350,
"notePosition": 360,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -875,7 +901,7 @@
"a2XMSKROCl9z"
],
"title": "v0.91.1-beta",
"notePosition": 360,
"notePosition": 370,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -893,7 +919,7 @@
"yqXFvWbLkuMD"
],
"title": "v0.90.12",
"notePosition": 370,
"notePosition": 380,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -911,7 +937,7 @@
"veS7pg311yJP"
],
"title": "v0.90.11-beta",
"notePosition": 380,
"notePosition": 390,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -929,7 +955,7 @@
"sq5W9TQxRqMq"
],
"title": "v0.90.10-beta",
"notePosition": 390,
"notePosition": 400,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -947,7 +973,7 @@
"yFEGVCUM9tPx"
],
"title": "v0.90.9-beta",
"notePosition": 400,
"notePosition": 410,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -965,7 +991,7 @@
"o4wAGqOQuJtV"
],
"title": "v0.90.8",
"notePosition": 410,
"notePosition": 420,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -998,7 +1024,7 @@
"i4A5g9iOg9I0"
],
"title": "v0.90.7-beta",
"notePosition": 420,
"notePosition": 430,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1016,7 +1042,7 @@
"ThNf2GaKgXUs"
],
"title": "v0.90.6-beta",
"notePosition": 430,
"notePosition": 440,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1034,7 +1060,7 @@
"G4PAi554kQUr"
],
"title": "v0.90.5-beta",
"notePosition": 440,
"notePosition": 450,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1061,7 +1087,7 @@
"zATRobGRCmBn"
],
"title": "v0.90.4",
"notePosition": 450,
"notePosition": 460,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1079,7 +1105,7 @@
"sCDLf8IKn3Iz"
],
"title": "v0.90.3",
"notePosition": 460,
"notePosition": 470,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1097,7 +1123,7 @@
"VqqyBu4AuTjC"
],
"title": "v0.90.2-beta",
"notePosition": 470,
"notePosition": 480,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1115,7 +1141,7 @@
"RX3Nl7wInLsA"
],
"title": "v0.90.1-beta",
"notePosition": 480,
"notePosition": 490,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1133,7 +1159,7 @@
"GyueACukPWjk"
],
"title": "v0.90.0-beta",
"notePosition": 490,
"notePosition": 500,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1151,7 +1177,7 @@
"kzjHexDTTeVB"
],
"title": "v0.48",
"notePosition": 500,
"notePosition": 510,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1218,7 +1244,7 @@
"wyurrlcDl416"
],
"title": "Release Template",
"notePosition": 510,
"notePosition": 520,
"prefix": null,
"isExpanded": false,
"type": "text",

View File

@@ -0,0 +1,22 @@
# v0.102.1
> [!NOTE]
> If you enjoyed this release, consider showing a token of appreciation by:
>
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right).
> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran).
> * If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links).
> [!IMPORTANT]
> This is a hotfix of v0.102.0, addressing some blocking issues. For more information about the previous major version, see [v0.102.0 changelog](https://github.com/TriliumNext/Trilium/releases/tag/v0.102.0).
## 🐞 Bugfixes
* [Mind Map feature breaks rendering in v0.102.0](https://github.com/TriliumNext/Trilium/issues/8879)
* Fixes for the PDF viewer:
* [PDF view is '403 Forbidden' on Nginx Proxy Manager](https://github.com/TriliumNext/Trilium/issues/8877)
* [PDF: address some layout issues](https://github.com/TriliumNext/Trilium/commit/8712e7dd160564f9a923a88bf5871e63c79d40f0) by @adoriandoran
* Cache not properly invalidated across versions.
## 🛠️ Technical updates
* [Rework Docker infrastructure to use crane](https://github.com/TriliumNext/Trilium/pull/8869) by @perfectra1n

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/source",
"version": "0.102.0",
"version": "0.102.1",
"description": "Build your personal knowledge base with Trilium Notes",
"directories": {
"doc": "docs"

View File

@@ -183,7 +183,7 @@ export default class MermaidEditing extends Plugin {
const mermaidSource = data.item.getAttribute( 'source' ) as string;
const domElement = this.toDomElement( domDocument );
domElement.innerHTML = mermaidSource;
domElement.textContent = mermaidSource;
window.setTimeout( () => {
// @todo: by the looks of it the domElement needs to be hooked to tree in order to allow for rendering.
@@ -219,7 +219,7 @@ export default class MermaidEditing extends Plugin {
const domPreviewWrapper = domConverter.viewToDom(child);
if ( domPreviewWrapper ) {
domPreviewWrapper.innerHTML = newSource;
domPreviewWrapper.textContent = newSource;
domPreviewWrapper.removeAttribute( 'data-processed' );
this._renderMermaid( domPreviewWrapper );

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/commons",
"version": "0.102.0",
"version": "0.102.1",
"description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.",
"private": true,
"type": "module",

View File

@@ -81,6 +81,7 @@ export default [
{ type: "label", name: "webViewSrc", isDangerous: true },
{ type: "label", name: "hideHighlightWidget" },
{ type: "label", name: "iconPack", isDangerous: true },
{ type: "label", name: "docName", isDangerous: true },
{ type: "label", name: "printLandscape" },
{ type: "label", name: "printPageSize" },

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/pdfjs-viewer",
"version": "1.0.0",
"version": "0.102.1",
"private": true,
"scripts": {
"build": "tsx scripts/build.ts",
@@ -12,4 +12,4 @@
"devDependencies": {
"pdfjs-dist": "5.4.624"
}
}
}

View File

@@ -71,13 +71,17 @@ function patchCacheBuster(htmlFilePath: string) {
const version = packageJson.version;
console.log(`Versioned URLs: ${version}.`)
let html = readFileSync(htmlFilePath, "utf-8");
html = html.replace(
`<link rel="stylesheet" href="custom.css" />`,
`<link rel="stylesheet" href="custom.css?v=${version}" />`);
html = html.replace(
`<script src="custom.mjs" type="module"></script>`,
`<script src="custom.mjs?v=${version}" type="module"></script>`
);
for (const file of [ "viewer.css", "custom.css" ]) {
html = html.replace(
`<link rel="stylesheet" href="${file}" />`,
`<link rel="stylesheet" href="${file}?v=${version}" />`);
}
for (const file of [ "viewer.mjs", "custom.mjs" ]) {
html = html.replace(
`<script src="${file}" type="module"></script>`,
`<script src="${file}?v=${version}" type="module"></script>`
);
}
writeFileSync(htmlFilePath, html);
}

View File

@@ -91,7 +91,7 @@ input[type="number"] {
--input-horizontal-padding: 8px;
border-radius: 4px !important;
font-size: .85rem !important;
font-size: .85rem;
&:hover {
--field-bg-color: var(--tn-input-hover-background);
@@ -135,12 +135,14 @@ input[type="color"] {
/* #region Toolbar */
#toolbarContainer select.scaleSelect,
#toolbarContainer input.pageNumber {
#toolbarContainer select#scaleSelect,
#toolbarContainer input#pageNumber {
height: calc(var(--toolbar-height) - 8px);
padding-block: 0;
font-size: 13px;
}
#toolbarContainer {
padding-inline: 12px;
}
@@ -230,14 +232,19 @@ input[type="color"] {
}
#toolbarContainer #toolbarViewer #pageNumber {
font-size: 12px;
font-weight: 600;
}
#numPages {
font-size: 13px;
line-height: unset;
}
#scaleSelectContainer {
--dropdown-btn-bg-color: transparent;
--button-hover-color: transparent;
border-radius: 6px;
margin-top: 1px;
&:hover,
&:focus-within{

68
pnpm-lock.yaml generated
View File

@@ -445,6 +445,12 @@ importers:
'@electron-forge/plugin-auto-unpack-natives':
specifier: 7.11.1
version: 7.11.1
'@electron-forge/plugin-fuses':
specifier: 7.11.1
version: 7.11.1(@electron/fuses@1.0.0)
'@electron/fuses':
specifier: 1.0.0
version: 1.0.0
'@triliumnext/commons':
specifier: workspace:*
version: link:../../packages/commons
@@ -2345,6 +2351,12 @@ packages:
resolution: {integrity: sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==}
engines: {node: '>= 16.4.0'}
'@electron-forge/plugin-fuses@7.11.1':
resolution: {integrity: sha512-Td517mHf+RjQAayFDM2kKb7NaGdRXrZfPbc7KOHlGbXthp5YTkFu2cCZGWokiqt1y1wsFaAodULhqBIg7vbbbw==}
engines: {node: '>= 16.4.0'}
peerDependencies:
'@electron/fuses': ^1.0.0
'@electron-forge/publisher-base@7.11.1':
resolution: {integrity: sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==}
engines: {node: '>= 16.4.0'}
@@ -2382,6 +2394,9 @@ packages:
engines: {node: '>=10.12.0'}
hasBin: true
'@electron/fuses@1.0.0':
resolution: {integrity: sha512-VjWIlZHEB7a93tXl+6tX2YzN+s1/mS0RM8WX4GZlMOqAzlmRfTMP6pp0MM0LtkzWZB+KQOv+zJt5Dlgdik+DUQ==}
'@electron/get@2.0.3':
resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==}
engines: {node: '>=12'}
@@ -16058,6 +16073,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.4.0
'@ckeditor/ckeditor5-upload': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-ai@47.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)':
dependencies:
@@ -16198,12 +16215,16 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-cloud-services@47.4.0':
dependencies:
'@ckeditor/ckeditor5-core': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.4.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -16396,6 +16417,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-classic@47.4.0':
dependencies:
@@ -16405,6 +16428,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-decoupled@47.4.0':
dependencies:
@@ -16414,6 +16439,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-inline@47.4.0':
dependencies:
@@ -16447,8 +16474,6 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-emoji@47.4.0':
dependencies:
@@ -16505,8 +16530,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-export-word@47.4.0':
dependencies:
@@ -16531,6 +16554,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-font@47.4.0':
dependencies:
@@ -16666,8 +16691,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-indent@47.4.0':
dependencies:
@@ -16791,8 +16814,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-merge-fields@47.4.0':
dependencies:
@@ -16805,8 +16826,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-minimap@47.4.0':
dependencies:
@@ -16815,8 +16834,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-operations-compressor@47.4.0':
dependencies:
@@ -16871,8 +16888,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-pagination@47.4.0':
dependencies:
@@ -16992,8 +17007,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-source-editing-enhanced@47.4.0':
dependencies:
@@ -17041,8 +17054,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-table@47.4.0':
dependencies:
@@ -17055,8 +17066,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-template@47.4.0':
dependencies:
@@ -17131,8 +17140,6 @@ snapshots:
'@ckeditor/ckeditor5-icons': 47.4.0
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-upload@47.4.0':
dependencies:
@@ -17169,8 +17176,6 @@ snapshots:
'@ckeditor/ckeditor5-engine': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-widget@47.4.0':
dependencies:
@@ -17190,8 +17195,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@codemirror/autocomplete@6.18.6':
dependencies:
@@ -17615,6 +17618,15 @@ snapshots:
- bluebird
- supports-color
'@electron-forge/plugin-fuses@7.11.1(@electron/fuses@1.0.0)':
dependencies:
'@electron-forge/plugin-base': 7.11.1
'@electron-forge/shared-types': 7.11.1
'@electron/fuses': 1.0.0
transitivePeerDependencies:
- bluebird
- supports-color
'@electron-forge/publisher-base@7.11.1':
dependencies:
'@electron-forge/shared-types': 7.11.1
@@ -17697,6 +17709,10 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.2
'@electron/fuses@1.0.0':
dependencies:
fs-extra: 9.1.0
'@electron/get@2.0.3':
dependencies:
debug: 4.4.3(supports-color@8.1.1)

View File

@@ -30,7 +30,7 @@ function main() {
patchPackageJson(join(__dirname, "..", "apps", appName, "package.json"), version);
}
for (const packageName of ["commons"]) {
for (const packageName of ["commons", "pdfjs-viewer"]) {
patchPackageJson(join(__dirname, "..", "packages", packageName, "package.json"), version);
}
}