Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: user configurable list of allowed html tags in import #601

Merged
merged 10 commits into from
Nov 28, 2024
4 changes: 2 additions & 2 deletions libraries/ckeditor/ckeditor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion libraries/ckeditor/ckeditor.js.map

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/public/app/widgets/type_widgets/content_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -94,7 +95,8 @@ const CONTENT_WIDGETS = {
AttachmentErasureTimeoutOptions,
RevisionsSnapshotIntervalOptions,
RevisionSnapshotsLimitOptions,
NetworkConnectionsOptions
NetworkConnectionsOptions,
HtmlImportTagsOptions
],
_optionsAdvanced: [
DatabaseIntegrityCheckOptions,
Expand Down
10 changes: 9 additions & 1 deletion src/public/app/widgets/type_widgets/editable_text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
9 changes: 8 additions & 1 deletion src/public/translations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/routes/api/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ const ALLOWED_OPTIONS = new Set([
'locale',
'firstDayOfWeek',
'textNoteEditorType',
'layoutOrientation'
'layoutOrientation',
'allowedHtmlTags' // Allow configuring HTML import tags
]);

function getOptions() {
Expand Down
35 changes: 26 additions & 9 deletions src/services/html_sanitizer.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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-*' ]
},
Expand Down
15 changes: 14 additions & 1 deletion src/services/options_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
];

/**
Expand Down