feat(react/settings): port HTML import tags

This commit is contained in:
Elian Doran 2025-08-18 18:07:58 +03:00
parent c5a7f84250
commit 95af901808
No known key found for this signature in database
6 changed files with 146 additions and 279 deletions

View File

@ -6,6 +6,9 @@ import Button from "../../react/Button";
import FormText from "../../react/FormText"; import FormText from "../../react/FormText";
import OptionsSection from "./components/OptionsSection"; import OptionsSection from "./components/OptionsSection";
import TimeSelector from "./components/TimeSelector"; import TimeSelector from "./components/TimeSelector";
import { useMemo } from "preact/hooks";
import { useTriliumOptionJson } from "../../react/hooks";
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
export default function OtherSettings() { export default function OtherSettings() {
return ( return (
@ -13,6 +16,7 @@ export default function OtherSettings() {
<NoteErasureTimeout /> <NoteErasureTimeout />
<AttachmentErasureTimeout /> <AttachmentErasureTimeout />
<RevisionSnapshotInterval /> <RevisionSnapshotInterval />
<HtmlImportTags />
</> </>
) )
} }
@ -80,4 +84,43 @@ function RevisionSnapshotInterval() {
/> />
</OptionsSection> </OptionsSection>
) )
}
function HtmlImportTags() {
const [ allowedHtmlTags, setAllowedHtmlTags ] = useTriliumOptionJson<readonly string[]>("allowedHtmlTags");
const parsedValue = useMemo(() => {
return allowedHtmlTags.join(" ");
}, allowedHtmlTags);
return (
<OptionsSection title={t("import.html_import_tags.title")}>
<FormText>{t("import.html_import_tags.description")}</FormText>
<textarea
className="allowed-html-tags"
spellcheck={false}
placeholder={t("import.html_import_tags.placeholder")}
style={useMemo(() => ({
width: "100%",
height: "150px",
marginBottom: "12px",
fontFamily: "var(--monospace-font-family)"
}), [])}
value={parsedValue}
onChange={e => {
const tags = e.currentTarget.value
.split(/[\n,\s]+/) // Split on newlines, commas, or spaces
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
setAllowedHtmlTags(tags);
}}
/>
<Button
text={t("import.html_import_tags.reset_button")}
onClick={() => setAllowedHtmlTags(SANITIZER_DEFAULT_ALLOWED_TAGS)}
/>
</OptionsSection>
)
} }

View File

@ -1,176 +0,0 @@
import OptionsWidget from "../options_widget.js";
import { t } from "../../../../services/i18n.js";
import type { OptionMap } from "@triliumnext/commons";
// TODO: Deduplicate with src/services/html_sanitizer once there is a commons project between client and server.
export const DEFAULT_ALLOWED_TAGS = [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"li",
"b",
"i",
"strong",
"em",
"strike",
"s",
"del",
"abbr",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tfoot",
"tr",
"th",
"td",
"pre",
"section",
"img",
"figure",
"figcaption",
"span",
"label",
"input",
"details",
"summary",
"address",
"aside",
"footer",
"header",
"hgroup",
"main",
"nav",
"dl",
"dt",
"menu",
"bdi",
"bdo",
"dfn",
"kbd",
"mark",
"q",
"time",
"var",
"wbr",
"area",
"map",
"track",
"video",
"audio",
"picture",
"del",
"ins",
"en-media", // for ENEX import
// Additional tags (https://github.com/TriliumNext/Trilium/issues/567)
"acronym",
"article",
"big",
"button",
"cite",
"col",
"colgroup",
"data",
"dd",
"fieldset",
"form",
"legend",
"meter",
"noscript",
"option",
"progress",
"rp",
"samp",
"small",
"sub",
"sup",
"template",
"textarea",
"tt"
];
const TPL = /*html*/`
<div class="html-import-tags-settings options-section">
<style>
.html-import-tags-settings .allowed-html-tags {
height: 150px;
margin-bottom: 12px;
font-family: monospace;
}
</style>
<h4>${t("import.html_import_tags.title")}</h4>
<p class="form-text">${t("import.html_import_tags.description")}</p>
<textarea class="allowed-html-tags form-control" spellcheck="false"
placeholder="${t("import.html_import_tags.placeholder")}"></textarea>
<div>
<button class="btn btn-sm btn-secondary reset-to-default">
${t("import.html_import_tags.reset_button")}
</button>
</div>
</div>`;
export default class HtmlImportTagsOptions extends OptionsWidget {
private $allowedTags!: JQuery<HTMLElement>;
private $resetButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$allowedTags = this.$widget.find(".allowed-html-tags");
this.$resetButton = this.$widget.find(".reset-to-default");
this.$allowedTags.on("change", () => this.saveTags());
this.$resetButton.on("click", () => this.resetToDefault());
// Load initial tags
this.refresh();
}
async optionsLoaded(options: OptionMap) {
try {
if (options.allowedHtmlTags) {
const tags = JSON.parse(options.allowedHtmlTags);
this.$allowedTags.val(tags.join(" "));
} else {
// If no tags are set, show the defaults
this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join(" "));
}
} catch (e) {
console.error("Could not load HTML tags:", e);
// On error, show the defaults
this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join(" "));
}
}
async saveTags() {
const tagsText = String(this.$allowedTags.val()) || "";
const tags = tagsText
.split(/[\n,\s]+/) // Split on newlines, commas, or spaces
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
await this.updateOption("allowedHtmlTags", JSON.stringify(tags));
}
async resetToDefault() {
this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join("\n")); // Use actual newline
await this.saveTags();
}
}

View File

@ -1,6 +1,7 @@
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import { sanitizeUrl } from "@braintree/sanitize-url"; import { sanitizeUrl } from "@braintree/sanitize-url";
import optionService from "./options.js"; import optionService from "./options.js";
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
// Be consistent with `ALLOWED_PROTOCOLS` in `src\public\app\services\link.js` // Be consistent with `ALLOWED_PROTOCOLS` in `src\public\app\services\link.js`
// TODO: Deduplicate with client once we can. // TODO: Deduplicate with client once we can.
@ -12,105 +13,6 @@ export const ALLOWED_PROTOCOLS = [
'mid' 'mid'
]; ];
// Default list of allowed HTML tags
export const DEFAULT_ALLOWED_TAGS = [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"li",
"b",
"i",
"strong",
"em",
"strike",
"s",
"del",
"abbr",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tfoot",
"tr",
"th",
"td",
"pre",
"section",
"img",
"figure",
"figcaption",
"span",
"label",
"input",
"details",
"summary",
"address",
"aside",
"footer",
"header",
"hgroup",
"main",
"nav",
"dl",
"dt",
"menu",
"bdi",
"bdo",
"dfn",
"kbd",
"mark",
"q",
"time",
"var",
"wbr",
"area",
"map",
"track",
"video",
"audio",
"picture",
"del",
"ins",
"en-media", // for ENEX import
// Additional tags (https://github.com/TriliumNext/Trilium/issues/567)
"acronym",
"article",
"big",
"button",
"cite",
"col",
"colgroup",
"data",
"dd",
"fieldset",
"form",
"legend",
"meter",
"noscript",
"option",
"progress",
"rp",
"samp",
"small",
"sub",
"sup",
"template",
"textarea",
"tt"
] as const;
// intended mainly as protection against XSS via import // intended mainly as protection against XSS via import
// secondarily, it (partly) protects against "CSS takeover" // secondarily, it (partly) protects against "CSS takeover"
// sanitize also note titles, label values etc. - there are so many usages which make it difficult // sanitize also note titles, label values etc. - there are so many usages which make it difficult
@ -138,7 +40,7 @@ function sanitize(dirtyHtml: string) {
allowedTags = JSON.parse(optionService.getOption("allowedHtmlTags")); allowedTags = JSON.parse(optionService.getOption("allowedHtmlTags"));
} catch (e) { } catch (e) {
// Fallback to default list if option doesn't exist or is invalid // Fallback to default list if option doesn't exist or is invalid
allowedTags = DEFAULT_ALLOWED_TAGS; allowedTags = SANITIZER_DEFAULT_ALLOWED_TAGS;
} }
const colorRegex = [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/, /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/]; const colorRegex = [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/, /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/];

View File

@ -4,8 +4,7 @@ import { randomSecureToken, isWindows } from "./utils.js";
import log from "./log.js"; import log from "./log.js";
import dateUtils from "./date_utils.js"; import dateUtils from "./date_utils.js";
import keyboardActions from "./keyboard_actions.js"; import keyboardActions from "./keyboard_actions.js";
import type { KeyboardShortcutWithRequiredActionName, OptionMap, OptionNames } from "@triliumnext/commons"; import { SANITIZER_DEFAULT_ALLOWED_TAGS, type KeyboardShortcutWithRequiredActionName, type OptionMap, type OptionNames } from "@triliumnext/commons";
import { DEFAULT_ALLOWED_TAGS } from "./html_sanitizer.js";
function initDocumentOptions() { function initDocumentOptions() {
optionService.createOption("documentId", randomSecureToken(16), false); optionService.createOption("documentId", randomSecureToken(16), false);
@ -187,7 +186,7 @@ const defaultOptions: DefaultOption[] = [
{ name: "backgroundEffects", value: "true", isSynced: false }, { name: "backgroundEffects", value: "true", isSynced: false },
{ {
name: "allowedHtmlTags", name: "allowedHtmlTags",
value: JSON.stringify(DEFAULT_ALLOWED_TAGS), value: JSON.stringify(SANITIZER_DEFAULT_ALLOWED_TAGS),
isSynced: true isSynced: true
}, },

View File

@ -7,3 +7,4 @@ export * from "./lib/test-utils.js";
export * from "./lib/mime_type.js"; export * from "./lib/mime_type.js";
export * from "./lib/bulk_actions.js"; export * from "./lib/bulk_actions.js";
export * from "./lib/server_api.js"; export * from "./lib/server_api.js";
export * from "./lib/shared_constants.js";

View File

@ -0,0 +1,98 @@
// Default list of allowed HTML tags
export const SANITIZER_DEFAULT_ALLOWED_TAGS = [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"li",
"b",
"i",
"strong",
"em",
"strike",
"s",
"del",
"abbr",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tfoot",
"tr",
"th",
"td",
"pre",
"section",
"img",
"figure",
"figcaption",
"span",
"label",
"input",
"details",
"summary",
"address",
"aside",
"footer",
"header",
"hgroup",
"main",
"nav",
"dl",
"dt",
"menu",
"bdi",
"bdo",
"dfn",
"kbd",
"mark",
"q",
"time",
"var",
"wbr",
"area",
"map",
"track",
"video",
"audio",
"picture",
"del",
"ins",
"en-media", // for ENEX import
// Additional tags (https://github.com/TriliumNext/Trilium/issues/567)
"acronym",
"article",
"big",
"button",
"cite",
"col",
"colgroup",
"data",
"dd",
"fieldset",
"form",
"legend",
"meter",
"noscript",
"option",
"progress",
"rp",
"samp",
"small",
"sub",
"sup",
"template",
"textarea",
"tt"
] as const;