Merge pull request #601 from maphew/feature/extend-kept-html-tags

Feature: user configurable list of allowed html tags in import
This commit is contained in:
Elian Doran 2024-11-28 22:26:31 +02:00 committed by GitHub
commit b3b8e60192
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 149 additions and 17 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,7 @@ import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js";
import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot_interval.js";
import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js";
import NetworkConnectionsOptions from "./options/other/network_connections.js";
import HtmlImportTagsOptions from "./options/other/html_import_tags.js";
import AdvancedSyncOptions from "./options/advanced/sync.js";
import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js";
import ConsistencyChecksOptions from "./options/advanced/consistency_checks.js";
@ -94,7 +95,8 @@ const CONTENT_WIDGETS = {
AttachmentErasureTimeoutOptions,
RevisionsSnapshotIntervalOptions,
RevisionSnapshotsLimitOptions,
NetworkConnectionsOptions
NetworkConnectionsOptions,
HtmlImportTagsOptions
],
_optionsAdvanced: [
DatabaseIntegrityCheckOptions,

View File

@ -176,7 +176,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
});
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
const editor = await editorClass.create(elementOrData, editorConfig);
const editor = await editorClass.create(elementOrData, {
...editorConfig,
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")),
styles: true,
classes: true,
attributes: true
}
});
await initSyntaxHighlighting(editor);

View File

@ -0,0 +1,84 @@
import OptionsWidget from "../options_widget.js";
import { t } from "../../../../services/i18n.js";
// 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/Notes/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 = `
<div class="options-section">
<h4>${t("import.html_import_tags.title")}</h4>
<p>${t("import.html_import_tags.description")}</p>
<textarea class="allowed-html-tags form-control" style="height: 150px; font-family: monospace;"
placeholder="${t("import.html_import_tags.placeholder")}"></textarea>
<div class="form-text">
${t("import.html_import_tags.help")}
</div>
<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 {
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) {
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 = 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

@ -175,7 +175,14 @@
"codeImportedAsCode": "Import recognized code files (e.g. <code>.json</code>) as code notes if it's unclear from metadata",
"replaceUnderscoresWithSpaces": "Replace underscores with spaces in imported note names",
"import": "Import",
"failed": "Import failed: {{message}}."
"failed": "Import failed: {{message}}.",
"html_import_tags": {
"title": "HTML Import Tags",
"description": "Configure which HTML tags should be preserved when importing notes. Tags not in this list will be removed during import.",
"placeholder": "Enter HTML tags, one per line",
"help": "Enter HTML tags to preserve during import. Some tags (like 'script') are always removed for security.",
"reset_button": "Reset to Default List"
}
},
"include_note": {
"dialog_title": "Include note",

View File

@ -67,7 +67,8 @@ const ALLOWED_OPTIONS = new Set([
'locale',
'firstDayOfWeek',
'textNoteEditorType',
'layoutOrientation'
'layoutOrientation',
'allowedHtmlTags' // Allow configuring HTML import tags
]);
function getOptions() {

View File

@ -1,5 +1,21 @@
import sanitizeHtml from "sanitize-html";
import sanitizeUrl from "@braintree/sanitize-url";
import optionService from "./options.js";
// 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/Notes/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
// secondarily, it (partly) protects against "CSS takeover"
@ -23,17 +39,18 @@ function sanitize(dirtyHtml: string) {
}
}
// Get allowed tags from options, with fallback to default list if option not yet set
let allowedTags;
try {
allowedTags = JSON.parse(optionService.getOption('allowedHtmlTags'));
} catch (e) {
// Fallback to default list if option doesn't exist or is invalid
allowedTags = DEFAULT_ALLOWED_TAGS;
}
// to minimize document changes, compress H
return sanitizeHtml(dirtyHtml, {
allowedTags: [
'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
],
allowedTags,
allowedAttributes: {
'*': [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ]
},

View File

@ -136,7 +136,20 @@ const defaultOptions: DefaultOption[] = [
// Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
{ name: "layoutOrientation", value: "vertical", isSynced: false }
// HTML import configuration
{ name: "layoutOrientation", value: "vertical", isSynced: false },
{ name: "allowedHtmlTags", value: JSON.stringify([
'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',
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
]), isSynced: true },
];
/**