mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react/settings): port HTML import tags
This commit is contained in:
parent
c5a7f84250
commit
95af901808
@ -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 />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -81,3 +85,42 @@ 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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*\)$/];
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
98
packages/commons/src/lib/shared_constants.ts
Normal file
98
packages/commons/src/lib/shared_constants.ts
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user