Skip to content

Commit b3b8e60

Browse files
authored
Merge pull request #601 from maphew/feature/extend-kept-html-tags
Feature: user configurable list of allowed html tags in import
2 parents b28a377 + bc78455 commit b3b8e60

File tree

9 files changed

+149
-17
lines changed

9 files changed

+149
-17
lines changed

libraries/ckeditor/ckeditor.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/ckeditor/ckeditor.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/public/app/widgets/type_widgets/content_widget.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js";
2525
import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot_interval.js";
2626
import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js";
2727
import NetworkConnectionsOptions from "./options/other/network_connections.js";
28+
import HtmlImportTagsOptions from "./options/other/html_import_tags.js";
2829
import AdvancedSyncOptions from "./options/advanced/sync.js";
2930
import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js";
3031
import ConsistencyChecksOptions from "./options/advanced/consistency_checks.js";
@@ -94,7 +95,8 @@ const CONTENT_WIDGETS = {
9495
AttachmentErasureTimeoutOptions,
9596
RevisionsSnapshotIntervalOptions,
9697
RevisionSnapshotsLimitOptions,
97-
NetworkConnectionsOptions
98+
NetworkConnectionsOptions,
99+
HtmlImportTagsOptions
98100
],
99101
_optionsAdvanced: [
100102
DatabaseIntegrityCheckOptions,

src/public/app/widgets/type_widgets/editable_text.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
176176
});
177177

178178
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
179-
const editor = await editorClass.create(elementOrData, editorConfig);
179+
const editor = await editorClass.create(elementOrData, {
180+
...editorConfig,
181+
htmlSupport: {
182+
allow: JSON.parse(options.get("allowedHtmlTags")),
183+
styles: true,
184+
classes: true,
185+
attributes: true
186+
}
187+
});
180188

181189
await initSyntaxHighlighting(editor);
182190

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import OptionsWidget from "../options_widget.js";
2+
import { t } from "../../../../services/i18n.js";
3+
4+
// TODO: Deduplicate with src/services/html_sanitizer once there is a commons project between client and server.
5+
export const DEFAULT_ALLOWED_TAGS = [
6+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
7+
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
8+
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
9+
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
10+
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
11+
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
12+
'en-media', // for ENEX import
13+
// Additional tags (https://github.com/TriliumNext/Notes/issues/567)
14+
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
15+
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
16+
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
17+
];
18+
19+
const TPL = `
20+
<div class="options-section">
21+
<h4>${t("import.html_import_tags.title")}</h4>
22+
23+
<p>${t("import.html_import_tags.description")}</p>
24+
25+
<textarea class="allowed-html-tags form-control" style="height: 150px; font-family: monospace;"
26+
placeholder="${t("import.html_import_tags.placeholder")}"></textarea>
27+
28+
<div class="form-text">
29+
${t("import.html_import_tags.help")}
30+
</div>
31+
32+
<div>
33+
<button class="btn btn-sm btn-secondary reset-to-default">
34+
${t("import.html_import_tags.reset_button")}
35+
</button>
36+
</div>
37+
</div>`;
38+
39+
export default class HtmlImportTagsOptions extends OptionsWidget {
40+
doRender() {
41+
this.$widget = $(TPL);
42+
this.contentSized();
43+
44+
this.$allowedTags = this.$widget.find('.allowed-html-tags');
45+
this.$resetButton = this.$widget.find('.reset-to-default');
46+
47+
this.$allowedTags.on('change', () => this.saveTags());
48+
this.$resetButton.on('click', () => this.resetToDefault());
49+
50+
// Load initial tags
51+
this.refresh();
52+
}
53+
54+
async optionsLoaded(options) {
55+
try {
56+
if (options.allowedHtmlTags) {
57+
const tags = JSON.parse(options.allowedHtmlTags);
58+
this.$allowedTags.val(tags.join(' '));
59+
} else {
60+
// If no tags are set, show the defaults
61+
this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join(' '));
62+
}
63+
}
64+
catch (e) {
65+
console.error('Could not load HTML tags:', e);
66+
// On error, show the defaults
67+
this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join(' '));
68+
}
69+
}
70+
71+
async saveTags() {
72+
const tagsText = this.$allowedTags.val();
73+
const tags = tagsText.split(/[\n,\s]+/) // Split on newlines, commas, or spaces
74+
.map(tag => tag.trim())
75+
.filter(tag => tag.length > 0);
76+
77+
await this.updateOption('allowedHtmlTags', JSON.stringify(tags));
78+
}
79+
80+
async resetToDefault() {
81+
this.$allowedTags.val(DEFAULT_ALLOWED_TAGS.join('\n')); // Use actual newline
82+
await this.saveTags();
83+
}
84+
}

src/public/translations/en/translation.json

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

src/routes/api/options.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ const ALLOWED_OPTIONS = new Set([
6767
'locale',
6868
'firstDayOfWeek',
6969
'textNoteEditorType',
70-
'layoutOrientation'
70+
'layoutOrientation',
71+
'allowedHtmlTags' // Allow configuring HTML import tags
7172
]);
7273

7374
function getOptions() {

src/services/html_sanitizer.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import sanitizeHtml from "sanitize-html";
22
import sanitizeUrl from "@braintree/sanitize-url";
3+
import optionService from "./options.js";
4+
5+
// Default list of allowed HTML tags
6+
export const DEFAULT_ALLOWED_TAGS = [
7+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
8+
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
9+
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
10+
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
11+
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
12+
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
13+
'en-media', // for ENEX import
14+
// Additional tags (https://github.com/TriliumNext/Notes/issues/567)
15+
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
16+
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
17+
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
18+
] as const;
319

420
// intended mainly as protection against XSS via import
521
// secondarily, it (partly) protects against "CSS takeover"
@@ -23,17 +39,18 @@ function sanitize(dirtyHtml: string) {
2339
}
2440
}
2541

42+
// Get allowed tags from options, with fallback to default list if option not yet set
43+
let allowedTags;
44+
try {
45+
allowedTags = JSON.parse(optionService.getOption('allowedHtmlTags'));
46+
} catch (e) {
47+
// Fallback to default list if option doesn't exist or is invalid
48+
allowedTags = DEFAULT_ALLOWED_TAGS;
49+
}
50+
2651
// to minimize document changes, compress H
2752
return sanitizeHtml(dirtyHtml, {
28-
allowedTags: [
29-
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
30-
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
31-
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
32-
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
33-
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
34-
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
35-
'en-media' // for ENEX import
36-
],
53+
allowedTags,
3754
allowedAttributes: {
3855
'*': [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ]
3956
},

src/services/options_init.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,20 @@ const defaultOptions: DefaultOption[] = [
136136
// Text note configuration
137137
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
138138

139-
{ name: "layoutOrientation", value: "vertical", isSynced: false }
139+
// HTML import configuration
140+
{ name: "layoutOrientation", value: "vertical", isSynced: false },
141+
{ name: "allowedHtmlTags", value: JSON.stringify([
142+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
143+
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
144+
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
145+
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
146+
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
147+
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
148+
'en-media',
149+
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
150+
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
151+
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
152+
]), isSynced: true },
140153
];
141154

142155
/**

0 commit comments

Comments
 (0)