From a510b04013c7e1b457fef3db1f4a6f6503c5d829 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 27 Mar 2025 18:43:44 +0100 Subject: [PATCH 1/9] working but UNSAFE, requires sanatizer such as dompurify --- .../or-dashboard-builder/src/index.ts | 3 + .../src/settings/html-settings.ts | 100 ++++++++++++++ .../src/widgets/html-widget.ts | 124 ++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 ui/component/or-dashboard-builder/src/settings/html-settings.ts create mode 100644 ui/component/or-dashboard-builder/src/widgets/html-widget.ts diff --git a/ui/component/or-dashboard-builder/src/index.ts b/ui/component/or-dashboard-builder/src/index.ts index d7a7c14fb0..13ea7db1c7 100644 --- a/ui/component/or-dashboard-builder/src/index.ts +++ b/ui/component/or-dashboard-builder/src/index.ts @@ -31,6 +31,8 @@ import {MapWidget} from "./widgets/map-widget"; import {AttributeInputWidget} from "./widgets/attribute-input-widget"; import {TableWidget} from "./widgets/table-widget"; import {GatewayWidget} from "./widgets/gateway-widget"; +import {HtmlWidget} from "./widgets/html-widget"; + // language=CSS const styling = css` @@ -218,6 +220,7 @@ export function registerWidgetTypes() { widgetTypes.set("attributeinput", AttributeInputWidget.getManifest()); widgetTypes.set("table", TableWidget.getManifest()); widgetTypes.set("gateway", GatewayWidget.getManifest()); + widgetTypes.set("html", HtmlWidget.getManifest()); } @customElement("or-dashboard-builder") diff --git a/ui/component/or-dashboard-builder/src/settings/html-settings.ts b/ui/component/or-dashboard-builder/src/settings/html-settings.ts new file mode 100644 index 0000000000..ffdbe8f284 --- /dev/null +++ b/ui/component/or-dashboard-builder/src/settings/html-settings.ts @@ -0,0 +1,100 @@ +import {css, html, TemplateResult } from "lit"; +import { customElement } from "lit/decorators.js"; +import {HtmlWidgetConfig} from "../widgets/html-widget"; +import { InputType, OrMwcInput } from "@openremote/or-mwc-components/or-mwc-input"; +import {WidgetSettings} from "../util/widget-settings"; +import {i18next} from "@openremote/or-translate"; +import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; +import {createRef, Ref, ref } from "lit/directives/ref.js"; + +const styling = css` + .switch-container { + display: flex; + align-items: center; + justify-content: space-between; + } + + or-mwc-dialog { + margin-bottom: 20px; + margin-right: 16px; + width:100%; + height:100%; + } +`; + +@customElement("html-settings") +export class HtmlSettings extends WidgetSettings { + + protected readonly widgetConfig!: HtmlWidgetConfig; + + + static get styles() { + return [...super.styles, styling]; + } + + protected render(): TemplateResult { + return html` +
+ + +
+ +
+ Online HTML markup editor +
+ + + `; + } + + + + protected openHtmlInputDialog(content?: string) { + const reference: Ref = createRef(); + const dialog = showDialog(new OrMwcDialog() + .setHeading(i18next.t("Insert18there")) + .setContent(()=> html ` +
+ +
+ `) + .setStyles(html` + + `) + .setActions([{ + actionName: "cancel", + content: "cancel" + }, { + actionName: "ok", + content: "ok", + action: () => { + if (reference.value?.value) { + this.widgetConfig.html = reference.value.value + this.notifyConfigUpdate(); + } + } + }]) + ) + console.log("generated config:", this.widgetConfig.html) + + } + +} diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts new file mode 100644 index 0000000000..a8dcd0f159 --- /dev/null +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -0,0 +1,124 @@ +import {css, html, PropertyValues, TemplateResult } from "lit"; +import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; +import {OrWidget} from "../util/or-widget"; +import {WidgetConfig} from "../util/widget-config"; +import {WidgetManifest} from "../util/or-widget"; +import {WidgetSettings} from "../util/widget-settings"; +import { customElement, query, queryAll, state, property } from "lit/decorators.js"; +import {HtmlSettings} from "../settings/html-settings"; +import { when } from "lit/directives/when.js"; +import {throttle} from "lodash"; +import {Util} from "@openremote/core"; + +export interface HtmlWidgetConfig extends WidgetConfig { + html: string; + css: string, +} + +function getDefaultWidgetConfig() { + return { + html: '', + css: '', + } as HtmlWidgetConfig; +} + +const styling = css` + #widget-wrapper { + height: 100%; + justify-content: center; + align-items: center; + overflow: hidden; + } + + #error-txt { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + } + + .attr-input { + width: 100%; + box-sizing: border-box; + } +` +//This widget requires some good security checks, free input fields may allow code injection! + +@customElement("html-widget") +export class HtmlWidget extends OrWidget { + + protected widgetConfig!: HtmlWidgetConfig; + + @state() + protected _loading = false; + + @query("#widget-wrapper") + protected widgetWrapperElem?: HTMLElement; + + @queryAll(".attr-input") + protected attributeInputElems?: NodeList; + + protected resizeObserver?: ResizeObserver; + + static getManifest(): WidgetManifest { + return { + displayName: "HTML", + displayIcon: "language-html5", + getContentHtml(config: WidgetConfig): OrWidget { + return new HtmlWidget(config); + }, + getDefaultConfig(): WidgetConfig { + return getDefaultWidgetConfig(); + }, + getSettingsHtml(config: WidgetConfig): WidgetSettings { + return new HtmlSettings(config); + } + + } + } + + // TODO: Improve this to be more efficient + refreshContent(force: boolean): void { + this.widgetConfig = JSON.parse(JSON.stringify(this.widgetConfig)) as HtmlWidgetConfig; + } + + static get styles() { + return [...super.styles, styling]; + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.resizeObserver?.disconnect(); + delete this.resizeObserver; + } + + protected willUpdate(changedProps: PropertyValues) { + + // If widgetConfig, and the attributeRefs of them have changed... + if(changedProps.has("widgetConfig") && this.widgetConfig) { + } + + // Workaround for an issue with scalability of or-attribute-input when using 'display: flex'. + // The percentage slider doesn't scale properly, causing the dragging knob to glitch. + // Why? Because the Material Design element listens to a window resize, not a container resize. + // So we manually trigger this event when the attribute-input-widget changes in size. + if(!this.resizeObserver && this.widgetWrapperElem) { + this.resizeObserver = new ResizeObserver(throttle(() => { + window.dispatchEvent(new Event('resize')); + }, 200)); + this.resizeObserver.observe(this.widgetWrapperElem); + } + + return super.willUpdate(changedProps); + } + + + protected render(): TemplateResult { + return html` +
+ ${unsafeHTML(this.widgetConfig.html)} +
+ `; + } +} From 8451ad8dedde243f674b11fd6985657b9b885d2e Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Mon, 31 Mar 2025 14:35:21 +0200 Subject: [PATCH 2/9] non working dump with ace --- docker-compose.yml | 2 + profile/dev-ui.yml | 2 +- .../or-components/src/or-ace-editor.ts | 1 + .../or-dashboard-builder/package.json | 1 + .../src/settings/html-settings.ts | 66 ++++++++----------- .../src/widgets/html-widget.ts | 38 ++++++++++- yarn.lock | 15 ++++- 7 files changed, 83 insertions(+), 42 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 779b2e8707..7691d52230 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,8 @@ services: volumes: - postgresql-data:/var/lib/postgresql/data - manager-data:/storage + ports: + - "5432:5432" keycloak: restart: always diff --git a/profile/dev-ui.yml b/profile/dev-ui.yml index 8e99947654..3274d9bbbc 100644 --- a/profile/dev-ui.yml +++ b/profile/dev-ui.yml @@ -42,7 +42,7 @@ services: extends: file: deploy.yml service: manager - image: openremote/manager:${MANAGER_VERSION:-develop} + image: openremote/manager:${MANAGER_VERSION:-latest} build: context: ../manager/build/install/manager/ dockerfile: Dockerfile diff --git a/ui/component/or-components/src/or-ace-editor.ts b/ui/component/or-components/src/or-ace-editor.ts index ddab993c97..773a6e5865 100644 --- a/ui/component/or-components/src/or-ace-editor.ts +++ b/ui/component/or-components/src/or-ace-editor.ts @@ -4,6 +4,7 @@ import ace, {Ace} from "ace-builds"; import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/mode-groovy"; +import "ace-builds/src-noconflict/mode-html"; import "ace-builds/webpack-resolver"; export class OrAceEditorChangedEvent extends CustomEvent<{ value: string, valid: boolean }> { diff --git a/ui/component/or-dashboard-builder/package.json b/ui/component/or-dashboard-builder/package.json index ecd4bb7c12..d3484cabee 100644 --- a/ui/component/or-dashboard-builder/package.json +++ b/ui/component/or-dashboard-builder/package.json @@ -20,6 +20,7 @@ "@openremote/model": "workspace:*", "@openremote/or-chart": "workspace:*", "@openremote/rest": "workspace:*", + "dompurify": "^3.2.4", "gridstack": "^7.2.0", "lit": "^2.0.2" }, diff --git a/ui/component/or-dashboard-builder/src/settings/html-settings.ts b/ui/component/or-dashboard-builder/src/settings/html-settings.ts index ffdbe8f284..61ce930449 100644 --- a/ui/component/or-dashboard-builder/src/settings/html-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/html-settings.ts @@ -6,6 +6,10 @@ import {WidgetSettings} from "../util/widget-settings"; import {i18next} from "@openremote/or-translate"; import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; import {createRef, Ref, ref } from "lit/directives/ref.js"; +import {OrAceEditor} from "@openremote/or-components/or-ace-editor"; +import "ace-builds/src-noconflict/mode-html"; +import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar"; +import DOMPurify from 'dompurify' const styling = css` .switch-container { @@ -27,7 +31,6 @@ export class HtmlSettings extends WidgetSettings { protected readonly widgetConfig!: HtmlWidgetConfig; - static get styles() { return [...super.styles, styling]; } @@ -38,7 +41,7 @@ export class HtmlSettings extends WidgetSettings {
- +
Online HTML markup editor @@ -51,50 +54,39 @@ export class HtmlSettings extends WidgetSettings { protected openHtmlInputDialog(content?: string) { - const reference: Ref = createRef(); - const dialog = showDialog(new OrMwcDialog() - .setHeading(i18next.t("Insert18there")) + const editorRef: Ref = createRef(); + showDialog(new OrMwcDialog() + .setHeading(i18next.t("HTML Editor")) .setContent(()=> html `
- +
`) - .setStyles(html` - - `) - .setActions([{ - actionName: "cancel", - content: "cancel" - }, { - actionName: "ok", - content: "ok", - action: () => { - if (reference.value?.value) { - this.widgetConfig.html = reference.value.value - this.notifyConfigUpdate(); - } - } }]) ) console.log("generated config:", this.widgetConfig.html) } + + } diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts index a8dcd0f159..5b258fef41 100644 --- a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -8,17 +8,29 @@ import { customElement, query, queryAll, state, property } from "lit/decorators. import {HtmlSettings} from "../settings/html-settings"; import { when } from "lit/directives/when.js"; import {throttle} from "lodash"; -import {Util} from "@openremote/core"; +import DOMPurify from 'dompurify' export interface HtmlWidgetConfig extends WidgetConfig { - html: string; + html: string, css: string, + sanitizerConfig: Object } function getDefaultWidgetConfig() { return { html: '', css: '', + sanitizerConfig: { + ALLOWED_TAGS: ['p', 'div', 'span', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], + ALLOWED_ATTR: ['href', 'target', 'rel', 'class'], + ALLOWED_STYLES: [ + 'color', 'font-size', 'text-align', 'margin', 'padding', + 'font-weight', 'font-style', 'text-decoration' + ], + ADD_ATTR: ['target'], // Allow target="_blank" for links + RETURN_DOM_FRAGMENT: false, + RETURN_DOM: false + } } as HtmlWidgetConfig; } @@ -61,6 +73,20 @@ export class HtmlWidget extends OrWidget { protected resizeObserver?: ResizeObserver; + // DOMPurify configuration + private sanitizerConfig = { + ALLOWED_TAGS: ['p', 'div', 'span', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], + ALLOWED_ATTR: ['href', 'target', 'rel', 'class'], + ALLOWED_STYLES: [ + 'color', 'font-size', 'text-align', 'margin', 'padding', + 'font-weight', 'font-style', 'text-decoration' + ], + ADD_ATTR: ['target'], // Allow target="_blank" for links + RETURN_DOM_FRAGMENT: false, + RETURN_DOM: false + }; + + static getManifest(): WidgetManifest { return { displayName: "HTML", @@ -115,10 +141,16 @@ export class HtmlWidget extends OrWidget { protected render(): TemplateResult { + const sanitizedContent = this.getSanitizedContent(); return html`
- ${unsafeHTML(this.widgetConfig.html)} +
${unsafeHTML(sanitizedContent)}
`; } + + // Sanitize the HTML content + private getSanitizedContent(): string { + return DOMPurify.sanitize(this.widgetConfig.html, this.widgetConfig.sanitizerConfig); + } } diff --git a/yarn.lock b/yarn.lock index 511b515e60..bc980dbbbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2608,6 +2608,7 @@ __metadata: "@openremote/or-chart": "workspace:*" "@openremote/rest": "workspace:*" "@openremote/util": "workspace:*" + dompurify: "npm:^3.2.4" gridstack: "npm:^7.2.0" lit: "npm:^2.0.2" languageName: unknown @@ -3277,7 +3278,7 @@ __metadata: languageName: node linkType: hard -"@types/trusted-types@npm:^2.0.2": +"@types/trusted-types@npm:^2.0.2, @types/trusted-types@npm:^2.0.7": version: 2.0.7 resolution: "@types/trusted-types@npm:2.0.7" checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c @@ -4832,6 +4833,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.2.4": + version: 3.2.4 + resolution: "dompurify@npm:3.2.4" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10c0/6be56810fb7ad2776155c8fc2967af5056783c030094362c7d0cf1ad13f2129cf922d8eefab528a34bdebfb98e2f44b306a983ab93aefb9d6f24c18a3d027a05 + languageName: node + linkType: hard + "domutils@npm:^2.5.2, domutils@npm:^2.8.0": version: 2.8.0 resolution: "domutils@npm:2.8.0" From 7a7a9611e54ba444d55083ec8b39c42592fc9b1a Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Tue, 1 Apr 2025 17:29:11 +0200 Subject: [PATCH 3/9] working version, temp ace-editor workaround, purify check needs tweak --- .../or-components/src/or-ace-editor.ts | 6 +- .../src/settings/html-settings.ts | 83 ++++++++++++------- .../src/widgets/html-widget.ts | 22 +++-- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/ui/component/or-components/src/or-ace-editor.ts b/ui/component/or-components/src/or-ace-editor.ts index 773a6e5865..dc56c72c03 100644 --- a/ui/component/or-components/src/or-ace-editor.ts +++ b/ui/component/or-components/src/or-ace-editor.ts @@ -78,7 +78,7 @@ export class OrAceEditor extends LitElement { @property({attribute: false}) public value?: any; - @property({type: String, attribute: false}) + @property({type: String, attribute: true}) public mode: string = "ace/mode/json"; @query("#ace-editor") @@ -125,7 +125,7 @@ export class OrAceEditor extends LitElement { protected initEditor() { if (this._aceElem) { this._aceEditor = ace.edit(this._aceElem, { - mode: this.mode, + mode: 'ace/mode/html', value: this._lastValue, useSoftTabs: true, tabSize: 2, @@ -178,10 +178,12 @@ export class OrAceEditor extends LitElement { public validate(): boolean { if (!this._aceEditor) { + console.log("editor bestaat niet"); return false; } const annotations = this._aceEditor.getSession().getAnnotations(); + console.log("annotations:", annotations); return !annotations || annotations.length === 0; } } diff --git a/ui/component/or-dashboard-builder/src/settings/html-settings.ts b/ui/component/or-dashboard-builder/src/settings/html-settings.ts index 61ce930449..c0da17c81a 100644 --- a/ui/component/or-dashboard-builder/src/settings/html-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/html-settings.ts @@ -6,8 +6,10 @@ import {WidgetSettings} from "../util/widget-settings"; import {i18next} from "@openremote/or-translate"; import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; import {createRef, Ref, ref } from "lit/directives/ref.js"; +import "@openremote/or-components/or-ace-editor"; import {OrAceEditor} from "@openremote/or-components/or-ace-editor"; import "ace-builds/src-noconflict/mode-html"; +import "ace-builds/webpack-resolver"; import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar"; import DOMPurify from 'dompurify' @@ -18,12 +20,12 @@ const styling = css` justify-content: space-between; } - or-mwc-dialog { - margin-bottom: 20px; - margin-right: 16px; - width:100%; - height:100%; - } + //or-mwc-dialog { + // margin-bottom: 20px; + // margin-right: 16px; + // width:100%; + // height:100%; + //} `; @customElement("html-settings") @@ -43,9 +45,6 @@ export class HtmlSettings extends WidgetSettings {
- Online HTML markup editor -
`; @@ -55,33 +54,55 @@ export class HtmlSettings extends WidgetSettings { protected openHtmlInputDialog(content?: string) { const editorRef: Ref = createRef(); + showDialog(new OrMwcDialog() .setHeading(i18next.t("HTML Editor")) .setContent(()=> html ` -
- -
- `) - .setActions([ - {actionName: "cancel", content: "cancel"}, - {actionName: "save", content: "save", action: () => { - if (editorRef.value) { - const editor = editorRef.value; - console.log('Editor:', editor) - if (!editor!.validate()) { - console.warn("HMTL was not valid"); - showSnackbar(undefined, i18next.t('errorOccurred')); - return; - } else { - this.widgetConfig.html = DOMPurify.sanitize(editor!.value!.value, this.widgetConfig.sanitizerConfig) - this.notifyConfigUpdate(); - } - if (editor!.value?.value != this.widgetConfig.html) { - console.warn("Potentially Harmful HTML code present, will be purified"); - } +
+ +
+ `) + .setActions([ + {actionName: "cancel", content: "cancel"}, + {actionName: "save", content: "save", action: () => { + if (editorRef.value) { + const editor = editorRef.value; + console.log('Editor:', editor) + if (!editor!.validate()) { + console.warn("HMTL was not valid"); + showSnackbar(undefined, i18next.t('errorOccurred')); + return; + } else { + this.widgetConfig.html = DOMPurify.sanitize(editor!.getValue() ?? "", this.widgetConfig.sanitizerConfig) + console.log("value.value:",editor!.getValue()) + console.log("widgetconfigbeforeupdate:", this.widgetConfig.html) + this.notifyConfigUpdate(); + console.log("widgetconfigafterupdate:", this.widgetConfig.html) + } + if (this.widgetConfig.html != editor!.getValue() ) { + console.warn("Potentially Harmful HTML was present, is purified"); } } - }]) + } + }]) + .setStyles(html` + + `) ) console.log("generated config:", this.widgetConfig.html) diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts index 5b258fef41..39454cb7c3 100644 --- a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -12,26 +12,30 @@ import DOMPurify from 'dompurify' export interface HtmlWidgetConfig extends WidgetConfig { html: string, - css: string, sanitizerConfig: Object } -function getDefaultWidgetConfig() { +function getDefaultConfig(): HtmlWidgetConfig { return { - html: '', - css: '', + html: '' + + '

      ' + + '  OpenRemote ' + + ' HTML widget

Paste your HTML, use any WYSIWYG editor for easy generation.

' + + ' ' + + '

screenshot

' + + '

Write markup with HTML', sanitizerConfig: { - ALLOWED_TAGS: ['p', 'div', 'span', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], - ALLOWED_ATTR: ['href', 'target', 'rel', 'class'], + ALLOWED_TAGS: ['p', 'div', 'span', 'img', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3', '!DOCTYPE html'], + ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'style', 'src'], ALLOWED_STYLES: [ 'color', 'font-size', 'text-align', 'margin', 'padding', - 'font-weight', 'font-style', 'text-decoration' + 'font-weight', 'font-style', 'text-decoration', 'background' ], ADD_ATTR: ['target'], // Allow target="_blank" for links RETURN_DOM_FRAGMENT: false, RETURN_DOM: false } - } as HtmlWidgetConfig; + } } const styling = css` @@ -95,7 +99,7 @@ export class HtmlWidget extends OrWidget { return new HtmlWidget(config); }, getDefaultConfig(): WidgetConfig { - return getDefaultWidgetConfig(); + return getDefaultConfig(); }, getSettingsHtml(config: WidgetConfig): WidgetSettings { return new HtmlSettings(config); From e115094557ada8e3c1c8c404781e3e5698055b86 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Wed, 2 Apr 2025 15:16:35 +0200 Subject: [PATCH 4/9] purify fixed, ace-editor html mode requires fixing --- .../src/settings/html-settings.ts | 14 +++++-------- .../src/widgets/html-widget.ts | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ui/component/or-dashboard-builder/src/settings/html-settings.ts b/ui/component/or-dashboard-builder/src/settings/html-settings.ts index c0da17c81a..ccdd62d890 100644 --- a/ui/component/or-dashboard-builder/src/settings/html-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/html-settings.ts @@ -69,18 +69,14 @@ export class HtmlSettings extends WidgetSettings { const editor = editorRef.value; console.log('Editor:', editor) if (!editor!.validate()) { - console.warn("HMTL was not valid"); + console.warn("HMTL is not valid"); showSnackbar(undefined, i18next.t('errorOccurred')); - return; - } else { + } else if (editor!.getValue()!.length > 0) { this.widgetConfig.html = DOMPurify.sanitize(editor!.getValue() ?? "", this.widgetConfig.sanitizerConfig) - console.log("value.value:",editor!.getValue()) - console.log("widgetconfigbeforeupdate:", this.widgetConfig.html) + if (DOMPurify.removed.length >= 1) { + console.warn("Purified Content:", DOMPurify.removed); + } this.notifyConfigUpdate(); - console.log("widgetconfigafterupdate:", this.widgetConfig.html) - } - if (this.widgetConfig.html != editor!.getValue() ) { - console.warn("Potentially Harmful HTML was present, is purified"); } } } diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts index 39454cb7c3..0d05b8d847 100644 --- a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -17,23 +17,25 @@ export interface HtmlWidgetConfig extends WidgetConfig { function getDefaultConfig(): HtmlWidgetConfig { return { - html: '' + - '

      ' + - '  OpenRemote ' + - ' HTML widget

Paste your HTML, use any WYSIWYG editor for easy generation.

' + - ' ' + - '

screenshot

' + - '

Write markup with HTML', + html: ' ' + + '

      ' + + '  OpenRemote ' + + ' HTML widget

Paste your HTML, use any WYSIWYG editor for easy generation.

' + + '

' + + '

Write markup with HTML

', sanitizerConfig: { - ALLOWED_TAGS: ['p', 'div', 'span', 'img', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3', '!DOCTYPE html'], + ALLOWED_TAGS: ['p', 'div', 'span', 'img', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'style', 'src'], ALLOWED_STYLES: [ 'color', 'font-size', 'text-align', 'margin', 'padding', 'font-weight', 'font-style', 'text-decoration', 'background' ], + ADD_TAGS: ['!doctype'], ADD_ATTR: ['target'], // Allow target="_blank" for links RETURN_DOM_FRAGMENT: false, - RETURN_DOM: false + RETURN_DOM: false, + WHOLE_DOCUMENT: true, + RETURN_TRUSTED_TYPE: true } } } From bb15310d91c0905376a2e6aec01f988748e7817a Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Wed, 2 Apr 2025 17:11:22 +0200 Subject: [PATCH 5/9] ace-editor restored and usage fixed, widget files merged --- .../or-components/src/or-ace-editor.ts | 6 +- .../src/settings/html-settings.ts | 23 +--- .../src/widgets/html-widget.ts | 121 ++++++++++++++---- 3 files changed, 107 insertions(+), 43 deletions(-) diff --git a/ui/component/or-components/src/or-ace-editor.ts b/ui/component/or-components/src/or-ace-editor.ts index dc56c72c03..773a6e5865 100644 --- a/ui/component/or-components/src/or-ace-editor.ts +++ b/ui/component/or-components/src/or-ace-editor.ts @@ -78,7 +78,7 @@ export class OrAceEditor extends LitElement { @property({attribute: false}) public value?: any; - @property({type: String, attribute: true}) + @property({type: String, attribute: false}) public mode: string = "ace/mode/json"; @query("#ace-editor") @@ -125,7 +125,7 @@ export class OrAceEditor extends LitElement { protected initEditor() { if (this._aceElem) { this._aceEditor = ace.edit(this._aceElem, { - mode: 'ace/mode/html', + mode: this.mode, value: this._lastValue, useSoftTabs: true, tabSize: 2, @@ -178,12 +178,10 @@ export class OrAceEditor extends LitElement { public validate(): boolean { if (!this._aceEditor) { - console.log("editor bestaat niet"); return false; } const annotations = this._aceEditor.getSession().getAnnotations(); - console.log("annotations:", annotations); return !annotations || annotations.length === 0; } } diff --git a/ui/component/or-dashboard-builder/src/settings/html-settings.ts b/ui/component/or-dashboard-builder/src/settings/html-settings.ts index ccdd62d890..19e4a696e8 100644 --- a/ui/component/or-dashboard-builder/src/settings/html-settings.ts +++ b/ui/component/or-dashboard-builder/src/settings/html-settings.ts @@ -19,13 +19,6 @@ const styling = css` align-items: center; justify-content: space-between; } - - //or-mwc-dialog { - // margin-bottom: 20px; - // margin-right: 16px; - // width:100%; - // height:100%; - //} `; @customElement("html-settings") @@ -59,20 +52,18 @@ export class HtmlSettings extends WidgetSettings { .setHeading(i18next.t("HTML Editor")) .setContent(()=> html `
- +
`) .setActions([ {actionName: "cancel", content: "cancel"}, {actionName: "save", content: "save", action: () => { if (editorRef.value) { - const editor = editorRef.value; - console.log('Editor:', editor) - if (!editor!.validate()) { + if (!editorRef.value.validate()) { console.warn("HMTL is not valid"); showSnackbar(undefined, i18next.t('errorOccurred')); - } else if (editor!.getValue()!.length > 0) { - this.widgetConfig.html = DOMPurify.sanitize(editor!.getValue() ?? "", this.widgetConfig.sanitizerConfig) + } else if (editorRef.value.getValue()!.length > 0) { + this.widgetConfig.html = DOMPurify.sanitize(editorRef.value.getValue() ?? "", this.widgetConfig.sanitizerConfig) if (DOMPurify.removed.length >= 1) { console.warn("Purified Content:", DOMPurify.removed); } @@ -100,10 +91,10 @@ export class HtmlSettings extends WidgetSettings { `) ) - console.log("generated config:", this.widgetConfig.html) - } - + protected _getMode() { + return "ace/mode/html"; + } } diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts index 0d05b8d847..03e11cbb36 100644 --- a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -4,10 +4,16 @@ import {OrWidget} from "../util/or-widget"; import {WidgetConfig} from "../util/widget-config"; import {WidgetManifest} from "../util/or-widget"; import {WidgetSettings} from "../util/widget-settings"; -import { customElement, query, queryAll, state, property } from "lit/decorators.js"; -import {HtmlSettings} from "../settings/html-settings"; -import { when } from "lit/directives/when.js"; +import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; +import { customElement, query, queryAll, state} from "lit/decorators.js"; +import {createRef, Ref, ref } from "lit/directives/ref.js"; +import { InputType } from "@openremote/or-mwc-components/or-mwc-input"; import {throttle} from "lodash"; +import {i18next} from "@openremote/or-translate"; +import {OrAceEditor} from "@openremote/or-components/or-ace-editor"; +import "ace-builds/src-noconflict/mode-html"; +import "ace-builds/webpack-resolver"; +import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar"; import DOMPurify from 'dompurify' export interface HtmlWidgetConfig extends WidgetConfig { @@ -17,11 +23,11 @@ export interface HtmlWidgetConfig extends WidgetConfig { function getDefaultConfig(): HtmlWidgetConfig { return { - html: ' ' + - '

      ' + - '  OpenRemote ' + - ' HTML widget

Paste your HTML, use any WYSIWYG editor for easy generation.

' + - '

' + + html: '\n' + + '

     \n' + + '  OpenRemote \n' + + ' HTML widget

Paste your HTML, use any WYSIWYG editor for easy generation.

\n' + + '

\n' + '

Write markup with HTML

', sanitizerConfig: { ALLOWED_TAGS: ['p', 'div', 'span', 'img', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], @@ -60,8 +66,13 @@ const styling = css` width: 100%; box-sizing: border-box; } + + .switch-container { + display: flex; + align-items: center; + justify-content: space-between; + } ` -//This widget requires some good security checks, free input fields may allow code injection! @customElement("html-widget") export class HtmlWidget extends OrWidget { @@ -79,20 +90,6 @@ export class HtmlWidget extends OrWidget { protected resizeObserver?: ResizeObserver; - // DOMPurify configuration - private sanitizerConfig = { - ALLOWED_TAGS: ['p', 'div', 'span', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], - ALLOWED_ATTR: ['href', 'target', 'rel', 'class'], - ALLOWED_STYLES: [ - 'color', 'font-size', 'text-align', 'margin', 'padding', - 'font-weight', 'font-style', 'text-decoration' - ], - ADD_ATTR: ['target'], // Allow target="_blank" for links - RETURN_DOM_FRAGMENT: false, - RETURN_DOM: false - }; - - static getManifest(): WidgetManifest { return { displayName: "HTML", @@ -160,3 +157,81 @@ export class HtmlWidget extends OrWidget { return DOMPurify.sanitize(this.widgetConfig.html, this.widgetConfig.sanitizerConfig); } } + +@customElement("html-settings") +export class HtmlSettings extends WidgetSettings { + + protected readonly widgetConfig!: HtmlWidgetConfig; + + static get styles() { + return [...super.styles, styling]; + } + + protected render(): TemplateResult { + return html` +
+ + +
+ +
+
+ `; + } + + + + protected openHtmlInputDialog(content?: string) { + const editorRef: Ref = createRef(); + + showDialog(new OrMwcDialog() + .setHeading(i18next.t("HTML Editor")) + .setContent(()=> html ` +
+ +
+ `) + .setActions([ + {actionName: "cancel", content: "cancel"}, + {actionName: "save", content: "save", action: () => { + if (editorRef.value) { + if (!editorRef.value.validate()) { + console.warn("HMTL is not valid"); + showSnackbar(undefined, i18next.t('errorOccurred')); + } else if (editorRef.value.getValue()!.length > 0) { + this.widgetConfig.html = DOMPurify.sanitize(editorRef.value.getValue() ?? "", this.widgetConfig.sanitizerConfig) + if (DOMPurify.removed.length >= 1) { + console.warn("Purified Content:", DOMPurify.removed); + } + this.notifyConfigUpdate(); + } + } + } + }]) + .setStyles(html` + + `) + ) + } + + protected _getMode() { + return "ace/mode/html"; + } + +} From d96a5988bff14091cf9f203d767051d88e8265e2 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Wed, 2 Apr 2025 17:17:33 +0200 Subject: [PATCH 6/9] small tunings --- profile/dev-ui.yml | 2 +- .../or-dashboard-builder/src/index.ts | 1 - .../src/settings/html-settings.ts | 100 ------------------ 3 files changed, 1 insertion(+), 102 deletions(-) delete mode 100644 ui/component/or-dashboard-builder/src/settings/html-settings.ts diff --git a/profile/dev-ui.yml b/profile/dev-ui.yml index 3274d9bbbc..8e99947654 100644 --- a/profile/dev-ui.yml +++ b/profile/dev-ui.yml @@ -42,7 +42,7 @@ services: extends: file: deploy.yml service: manager - image: openremote/manager:${MANAGER_VERSION:-latest} + image: openremote/manager:${MANAGER_VERSION:-develop} build: context: ../manager/build/install/manager/ dockerfile: Dockerfile diff --git a/ui/component/or-dashboard-builder/src/index.ts b/ui/component/or-dashboard-builder/src/index.ts index 13ea7db1c7..794de937ee 100644 --- a/ui/component/or-dashboard-builder/src/index.ts +++ b/ui/component/or-dashboard-builder/src/index.ts @@ -33,7 +33,6 @@ import {TableWidget} from "./widgets/table-widget"; import {GatewayWidget} from "./widgets/gateway-widget"; import {HtmlWidget} from "./widgets/html-widget"; - // language=CSS const styling = css` diff --git a/ui/component/or-dashboard-builder/src/settings/html-settings.ts b/ui/component/or-dashboard-builder/src/settings/html-settings.ts deleted file mode 100644 index 19e4a696e8..0000000000 --- a/ui/component/or-dashboard-builder/src/settings/html-settings.ts +++ /dev/null @@ -1,100 +0,0 @@ -import {css, html, TemplateResult } from "lit"; -import { customElement } from "lit/decorators.js"; -import {HtmlWidgetConfig} from "../widgets/html-widget"; -import { InputType, OrMwcInput } from "@openremote/or-mwc-components/or-mwc-input"; -import {WidgetSettings} from "../util/widget-settings"; -import {i18next} from "@openremote/or-translate"; -import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; -import {createRef, Ref, ref } from "lit/directives/ref.js"; -import "@openremote/or-components/or-ace-editor"; -import {OrAceEditor} from "@openremote/or-components/or-ace-editor"; -import "ace-builds/src-noconflict/mode-html"; -import "ace-builds/webpack-resolver"; -import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar"; -import DOMPurify from 'dompurify' - -const styling = css` - .switch-container { - display: flex; - align-items: center; - justify-content: space-between; - } -`; - -@customElement("html-settings") -export class HtmlSettings extends WidgetSettings { - - protected readonly widgetConfig!: HtmlWidgetConfig; - - static get styles() { - return [...super.styles, styling]; - } - - protected render(): TemplateResult { - return html` -
- - -
- -
-
- `; - } - - - - protected openHtmlInputDialog(content?: string) { - const editorRef: Ref = createRef(); - - showDialog(new OrMwcDialog() - .setHeading(i18next.t("HTML Editor")) - .setContent(()=> html ` -
- -
- `) - .setActions([ - {actionName: "cancel", content: "cancel"}, - {actionName: "save", content: "save", action: () => { - if (editorRef.value) { - if (!editorRef.value.validate()) { - console.warn("HMTL is not valid"); - showSnackbar(undefined, i18next.t('errorOccurred')); - } else if (editorRef.value.getValue()!.length > 0) { - this.widgetConfig.html = DOMPurify.sanitize(editorRef.value.getValue() ?? "", this.widgetConfig.sanitizerConfig) - if (DOMPurify.removed.length >= 1) { - console.warn("Purified Content:", DOMPurify.removed); - } - this.notifyConfigUpdate(); - } - } - } - }]) - .setStyles(html` - - `) - ) - } - - protected _getMode() { - return "ace/mode/html"; - } - -} From 4ca3b11ec0e45715cbc3a79215b7469ffa896f48 Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Wed, 2 Apr 2025 17:20:41 +0200 Subject: [PATCH 7/9] small tunings --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7691d52230..779b2e8707 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,8 +39,6 @@ services: volumes: - postgresql-data:/var/lib/postgresql/data - manager-data:/storage - ports: - - "5432:5432" keycloak: restart: always From 2071ee8d1cc14b26772c2bb480d540f7cf4d81af Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Thu, 3 Apr 2025 09:32:59 +0200 Subject: [PATCH 8/9] remove vars --- .../or-dashboard-builder/src/widgets/html-widget.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts index 03e11cbb36..4bda70e08e 100644 --- a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -79,15 +79,9 @@ export class HtmlWidget extends OrWidget { protected widgetConfig!: HtmlWidgetConfig; - @state() - protected _loading = false; - @query("#widget-wrapper") protected widgetWrapperElem?: HTMLElement; - @queryAll(".attr-input") - protected attributeInputElems?: NodeList; - protected resizeObserver?: ResizeObserver; static getManifest(): WidgetManifest { From 91b13307f7a866aef13774422d29b780a9f6976e Mon Sep 17 00:00:00 2001 From: Hackerberg43 Date: Fri, 4 Apr 2025 11:51:58 +0200 Subject: [PATCH 9/9] cleanup imports --- ui/component/or-dashboard-builder/src/widgets/html-widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts index 4bda70e08e..a98870c0fe 100644 --- a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -5,7 +5,7 @@ import {WidgetConfig} from "../util/widget-config"; import {WidgetManifest} from "../util/or-widget"; import {WidgetSettings} from "../util/widget-settings"; import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; -import { customElement, query, queryAll, state} from "lit/decorators.js"; +import { customElement, query} from "lit/decorators.js"; import {createRef, Ref, ref } from "lit/directives/ref.js"; import { InputType } from "@openremote/or-mwc-components/or-mwc-input"; import {throttle} from "lodash";