CVE-2023-0643: Improper Handling of Additional Special Element (CWE-167)
value: Improper Handling of Additional Special Element in GitHub repository squidex/squidex prior to 7.4.0.
CWE description: The product receives input from an upstream component, but it does not handle or incorrectly handles when an additional unexpected special element is provided.
CVSS v3 base score: 6.1
Published: 2023-02-02T12:15Z
Repository: https://github.com/squidex/squidex
Language: TypeScript
Files changed: frontend/src/app/declarations.d.ts, frontend/src/app/features/rules/pages/rule/rule-page.component.ts, frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts, frontend/src/app/features/rules/shared/actions/generic-action.component.html, frontend/src/app/framework/angular/markdown.directive.spec.ts, frontend/src/app/framework/angular/markdown.directive.ts, frontend/src/app/framework/angular/pager.component.ts, frontend/src/app/framework/angular/pipes/markdown.pipe.spec.ts, frontend/src/app/framework/angular/pipes/markdown.pipe.ts, frontend/src/app/framework/internal.ts, frontend/src/app/framework/services/localizer.service.ts, frontend/src/app/framework/utils/date-time.spec.ts, frontend/src/app/framework/utils/markdown.ts, frontend/src/app/framework/utils/text-measurer.ts, frontend/src/app/shared/services/history.service.spec.ts, frontend/src/app/shared/services/history.service.ts, frontend/src/app/shared/services/rules.service.spec.ts, frontend/src/app/shared/services/schemas.service.spec.ts, frontend/src/app/shared/state/asset-scripts.state.spec.ts, frontend/src/app/shared/state/asset-scripts.state.ts, frontend/src/app/shared/state/resolvers.spec.ts, frontend/src/app/shared/state/rules.forms.ts, frontend/src/app/shared/state/table-settings.spec.ts
Commit date: 2022-12-13 13:19:46 +0100
Commit message: Escape HTML in markdown. (#955)

* Escape HTML in markdown.

* Escape variables for message history.

* Rename escape method.
Diff: diff --git a/frontend/src/app/declarations.d.ts b/frontend/src/app/declarations.d.ts
index 43716dcf2..1f0d94e0e 100644
--- a/frontend/src/app/declarations.d.ts
+++ b/frontend/src/app/declarations.d.ts
@@ -16,5 +16,9 @@ declare module 'sortablejs' {
         public option(property: string, value: any): any;
     }
 
     export function create(element: any, options: any): Ref;
 }
+
+declare namespace marked {
+    export function escape(input: string): string;
+}
\ No newline at end of file
diff --git a/frontend/src/app/features/rules/pages/rule/rule-page.component.ts b/frontend/src/app/features/rules/pages/rule/rule-page.component.ts
index d816942fd..cc275b7bc 100644
--- a/frontend/src/app/features/rules/pages/rule/rule-page.component.ts
+++ b/frontend/src/app/features/rules/pages/rule/rule-page.component.ts
@@ -8,11 +8,11 @@
 import { Component, OnInit } from '@angular/core';
 import { AbstractControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { debounceTime, Subscription } from 'rxjs';
 import { ActionForm, ALL_TRIGGERS, MessageBus, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm, value$ } from '@app/shared';
-import { RuleConfigured } from '../messages';
+import { RuleConfigured } from './../messages';
 
 @Component({
     selector: 'sqx-rule-page',
     styleUrls: ['./rule-page.component.scss'],
     templateUrl: './rule-page.component.html',
diff --git a/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts b/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts
index dce788290..6b920699b 100644
--- a/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts
+++ b/frontend/src/app/features/rules/pages/simulator/rule-simulator-page.component.ts
@@ -6,11 +6,11 @@
  */
 
 import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { MessageBus, ResourceOwner, RuleSimulatorState, SimulatedRuleEventDto } from '@app/shared';
-import { RuleConfigured } from '../messages';
+import { RuleConfigured } from './../messages';
 
 @Component({
     selector: 'sqx-simulator-events-page',
     styleUrls: ['./rule-simulator-page.component.scss'],
     templateUrl: './rule-simulator-page.component.html',
diff --git a/frontend/src/app/features/rules/shared/actions/generic-action.component.html b/frontend/src/app/features/rules/shared/actions/generic-action.component.html
index c519b96ae..dd78bf94e 100644
--- a/frontend/src/app/features/rules/shared/actions/generic-action.component.html
+++ b/frontend/src/app/features/rules/shared/actions/generic-action.component.html
@@ -45,11 +45,11 @@
                     <input type="{{property.editor | lowercase}}" class="form-control" id="{{property.name}}" [formControlName]="property.name">
                 </ng-container>
             </ng-container>
 
             <sqx-form-hint>
-                <span [sqxMarkdown]="property.description" [inline]="true" [html]="true"></span>
+                <span [sqxMarkdown]="property.description" [inline]="true"></span>
 
                 <div *ngIf="property.isFormattable">
                     {{ 'rules.advancedFormattingHint' | sqxTranslate }}: <a tabindex="-1" href="https://docs.squidex.io/concepts/rules#3-formatting" sqxExternalLink>{{ 'common.documentation' | sqxTranslate }}</a>
                 </div>
             </sqx-form-hint>
diff --git a/frontend/src/app/framework/angular/markdown.directive.spec.ts b/frontend/src/app/framework/angular/markdown.directive.spec.ts
new file mode 100644
index 000000000..87f59a115
--- /dev/null
+++ b/frontend/src/app/framework/angular/markdown.directive.spec.ts
@@ -0,0 +1,89 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { Renderer2 } from '@angular/core';
+import { IMock, It, Mock, Times } from 'typemoq';
+import { MarkdownDirective } from './markdown.directive';
+
+describe('MarkdownDirective', () => {
+    let renderer: IMock<Renderer2>;
+    let markdownElement = {};
+    let markdownDirective: MarkdownDirective;
+
+    beforeEach(() => {
+        renderer = Mock.ofType<Renderer2>();
+
+        markdownElement = {};
+        markdownDirective = new MarkdownDirective(markdownElement as any, renderer.object);
+    });
+
+    it('should render empty text as text', () => {
+        markdownDirective.markdown = '';
+        markdownDirective.ngOnChanges();
+
+        verifyTextRender('');
+    });
+
+    it('should render as text if result has no tags', () => {
+        markdownDirective.inline = true;
+        markdownDirective.markdown = 'markdown';
+        markdownDirective.ngOnChanges();
+
+        verifyTextRender('markdown');
+    });
+
+    it('should render as text if optional', () => {
+        markdownDirective.optional = true;
+        markdownDirective.markdown = '**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyTextRender('**bold**');
+    });
+
+    it('should render if optional with exclamation', () => {
+        markdownDirective.optional = true;
+        markdownDirective.markdown = '!**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<strong>bold</strong>');
+    });
+
+    it('should render as HTML if allowed', () => {
+        markdownDirective.inline = false;
+        markdownDirective.markdown = '**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<p><strong>bold</strong></p>\n');
+    });
+
+    it('should render as inline HTML if allowed', () => {
+        markdownDirective.markdown = '!**bold**';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<strong>bold</strong>');
+    });
+
+    it('should render HTML escaped', () => {
+        markdownDirective.inline = false;
+        markdownDirective.markdown = '<h1>Header</h1>';
+        markdownDirective.ngOnChanges();
+
+        verifyHtmlRender('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
+    });
+
+    function verifyTextRender(text: string) {
+        renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once());
+
+        expect().nothing();
+    }
+
+    function verifyHtmlRender(text: string) {
+        renderer.verify(x => x.setProperty(It.isAny(), 'innerHTML', text), Times.once());
+
+        expect().nothing();
+    }
+});
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/markdown.directive.ts b/frontend/src/app/framework/angular/markdown.directive.ts
index 48dd250ca..2fefb6d7a 100644
--- a/frontend/src/app/framework/angular/markdown.directive.ts
+++ b/frontend/src/app/framework/angular/markdown.directive.ts
@@ -4,28 +4,11 @@
  * @license
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { Directive, ElementRef, Input, OnChanges, Renderer2 } from '@angular/core';
-import { marked } from 'marked';
-
-const RENDERER_DEFAULT = new marked.Renderer();
-const RENDERER_INLINE = new marked.Renderer();
-
-RENDERER_DEFAULT.link = (href, _, text) => {
-    if (href && href.startsWith('mailto')) {
-        return text;
-    } else {
-        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
-    }
-};
-
-RENDERER_INLINE.paragraph = (text) => {
-    return text;
-};
-
-RENDERER_INLINE.link = RENDERER_DEFAULT.link;
+import { renderMarkdown } from '@app/framework/internal';
 
 @Directive({
     selector: '[sqxMarkdown]',
 })
 export class MarkdownDirective implements OnChanges {
@@ -33,13 +16,10 @@ export class MarkdownDirective implements OnChanges {
     public markdown!: string;
 
     @Input()
     public inline = true;
 
-    @Input()
-    public html = false;
-
     @Input()
     public optional = false;
 
     constructor(
         private readonly element: ElementRef,
@@ -48,24 +28,30 @@ export class MarkdownDirective implements OnChanges {
     }
 
     public ngOnChanges() {
         let html = '';
 
-        const markdown = this.markdown;
+        let markdown = this.markdown;
+
+        const hasExclamation = markdown.indexOf('!') === 0;
+
+        if (hasExclamation) {
+            markdown = markdown.substring(1);
+        }
 
         if (!markdown) {
             html = markdown;
-        } else if (this.optional && markdown.indexOf('!') !== 0) {
+        } else if (this.optional && !hasExclamation) {
             html = markdown;
         } else if (this.markdown) {
-            const renderer = this.inline ? RENDERER_INLINE : RENDERER_DEFAULT;
-
-            html = marked(this.markdown, { renderer });
+            html = renderMarkdown(markdown, this.inline);
         }
 
-        if (!this.html && (!html || html === this.markdown || html.indexOf('<') < 0)) {
-            this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
-        } else {
+        const hasHtml = html.indexOf('<') >= 0;
+
+        if (hasHtml) {
             this.renderer.setProperty(this.element.nativeElement, 'innerHTML', html);
+        } else {
+            this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
         }
     }
 }
diff --git a/frontend/src/app/framework/angular/pager.component.ts b/frontend/src/app/framework/angular/pager.component.ts
index 8e79a3d5a..27895531c 100644
--- a/frontend/src/app/framework/angular/pager.component.ts
+++ b/frontend/src/app/framework/angular/pager.component.ts
@@ -4,11 +4,11 @@
  * @license
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
-import { PagingInfo } from '../state';
+import { PagingInfo } from './../state';
 
 export const PAGE_SIZES: ReadonlyArray<number> = [10, 20, 30, 50];
 
 @Component({
     selector: 'sqx-pager[paging]',
diff --git a/frontend/src/app/framework/angular/pipes/markdown.pipe.spec.ts b/frontend/src/app/framework/angular/pipes/markdown.pipe.spec.ts
index b1338502f..2ba2308cb 100644
--- a/frontend/src/app/framework/angular/pipes/markdown.pipe.spec.ts
+++ b/frontend/src/app/framework/angular/pipes/markdown.pipe.spec.ts
@@ -6,47 +6,63 @@
  */
 
 import { MarkdownInlinePipe, MarkdownPipe } from './markdown.pipe';
 
 describe('MarkdownInlinePipe', () => {
+    const pipe = new MarkdownInlinePipe();
+
     it('should convert link to html', () => {
-        const actual = new MarkdownInlinePipe().transform('[link-name](link-url)');
+        const actual = pipe.transform('[link-name](link-url)');
 
         expect(actual).toBe('<a href="link-url" target="_blank", rel="noopener">link-name <i class="icon-external-link"></i></a>');
     });
 
     it('should convert markdown to html', () => {
-        const actual = new MarkdownInlinePipe().transform('*bold*');
+        const actual = pipe.transform('*bold*');
 
         expect(actual).toBe('<em>bold</em>');
     });
 
+    it('should escape input html', () => {
+        const actual = pipe.transform('<h1>Header</h1>');
+
+        expect(actual).toBe('&lt;h1&gt;Header&lt;/h1&gt;');
+    });
+
     [null, undefined, ''].forEach(x => {
         it('should return empty string for invalid value', () => {
-            const actual = new MarkdownInlinePipe().transform(x);
+            const actual = pipe.transform(x);
 
             expect(actual).toBe('');
         });
     });
 });
 
 describe('MarkdownPipe', () => {
+    const pipe = new MarkdownPipe();
+
     it('should convert link to html', () => {
-        const actual = new MarkdownPipe().transform('[link-name](link-url)');
+        const actual = pipe.transform('[link-name](link-url)');
 
         expect(actual).toBe('<p><a href="link-url" target="_blank", rel="noopener">link-name <i class="icon-external-link"></i></a></p>\n');
     });
 
     it('should convert markdown to html', () => {
-        const actual = new MarkdownPipe().transform('*bold*');
+        const actual = pipe.transform('*bold*');
 
         expect(actual).toBe('<p><em>bold</em></p>\n');
     });
 
+    it('should escape input html', () => {
+        const actual = pipe.transform('<h1>Header</h1>');
+
+        expect(actual).toBe('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
+    });
+
     [null, undefined, ''].forEach(x => {
         it('should return empty string for invalid value', () => {
-            const actual = new MarkdownPipe().transform(x);
+            const actual = pipe.transform(x);
 
             expect(actual).toBe('');
         });
     });
 });
diff --git a/frontend/src/app/framework/angular/pipes/markdown.pipe.ts b/frontend/src/app/framework/angular/pipes/markdown.pipe.ts
index c78ffaa62..d47942017 100644
--- a/frontend/src/app/framework/angular/pipes/markdown.pipe.ts
+++ b/frontend/src/app/framework/angular/pipes/markdown.pipe.ts
@@ -4,52 +4,26 @@
  * @license
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { Pipe, PipeTransform } from '@angular/core';
-import { marked } from 'marked';
-
-const renderer = new marked.Renderer();
-
-renderer.link = (href, _, text) => {
-    if (href && href.startsWith('mailto')) {
-        return text;
-    } else {
-        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
-    }
-};
-
-const inlinerRenderer = new marked.Renderer();
-
-inlinerRenderer.paragraph = (text) => {
-    return text;
-};
-
-inlinerRenderer.link = renderer.link;
+import { renderMarkdown } from '@app/framework/internal';
 
 @Pipe({
     name: 'sqxMarkdown',
     pure: true,
 })
 export class MarkdownPipe implements PipeTransform {
     public transform(text: string | undefined | null): string {
-        if (text) {
-            return marked(text, { renderer });
-        } else {
-            return '';
-        }
+        return renderMarkdown(text, false);
     }
 }
 
 @Pipe({
     name: 'sqxMarkdownInline',
     pure: true,
 })
 export class MarkdownInlinePipe implements PipeTransform {
     public transform(text: string | undefined | null): string {
-        if (text) {
-            return marked(text, { renderer: inlinerRenderer });
-        } else {
-            return '';
-        }
+        return renderMarkdown(text, true);
     }
-}
+}
\ No newline at end of file
diff --git a/frontend/src/app/framework/internal.ts b/frontend/src/app/framework/internal.ts
index 166b4700d..39759d4c4 100644
--- a/frontend/src/app/framework/internal.ts
+++ b/frontend/src/app/framework/internal.ts
@@ -31,10 +31,11 @@ export * from './utils/duration';
 export * from './utils/error';
 export * from './utils/hateos';
 export * from './utils/interpolator';
 export * from './utils/keys';
 export * from './utils/math-helper';
+export * from './utils/markdown';
 export * from './utils/modal-positioner';
 export * from './utils/modal-view';
 export * from './utils/picasso';
 export * from './utils/rxjs-extensions';
 export * from './utils/string-helper';
diff --git a/frontend/src/app/framework/services/localizer.service.ts b/frontend/src/app/framework/services/localizer.service.ts
index ca58ff8a5..6539d04c1 100644
--- a/frontend/src/app/framework/services/localizer.service.ts
+++ b/frontend/src/app/framework/services/localizer.service.ts
@@ -4,11 +4,11 @@
  * @license
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { Injectable } from '@angular/core';
-import { compareStrings } from '../utils/array-helper';
+import { compareStrings } from './../utils/array-helper';
 
 @Injectable()
 export class LocalizerService {
     private shouldLog = false;
 
diff --git a/frontend/src/app/framework/utils/date-time.spec.ts b/frontend/src/app/framework/utils/date-time.spec.ts
index dfabb261b..e69e57bda 100644
--- a/frontend/src/app/framework/utils/date-time.spec.ts
+++ b/frontend/src/app/framework/utils/date-time.spec.ts
@@ -3,11 +3,11 @@
  *
  * @license
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
-import { DateHelper } from '..';
+import { DateHelper } from './..';
 import { DateTime } from './date-time';
 
 describe('DateTime', () => {
     const today = DateTime.today();
     const today2 = DateTime.today();
diff --git a/frontend/src/app/framework/utils/markdown.ts b/frontend/src/app/framework/utils/markdown.ts
new file mode 100644
index 000000000..aa9cf7df7
--- /dev/null
+++ b/frontend/src/app/framework/utils/markdown.ts
@@ -0,0 +1,61 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { marked } from 'marked';
+
+function renderLink(href: string, _: string, text: string) {
+    if (href && href.startsWith('mailto')) {
+        return text;
+    } else {
+        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
+    }
+}
+
+function renderInlineParagraph(text: string) {
+    return text;
+}
+
+const RENDERER_DEFAULT = new marked.Renderer();
+const RENDERER_INLINE = new marked.Renderer();
+
+RENDERER_INLINE.paragraph = renderInlineParagraph;
+RENDERER_INLINE.link = renderLink;
+RENDERER_DEFAULT.link = renderLink;
+
+export function renderMarkdown(input: string | undefined | null, inline: boolean) {
+    if (!input) {
+        return '';
+    }
+
+    input = escapeHTML(input);
+
+    if (inline) {
+        return marked(input, { renderer: RENDERER_INLINE });
+    } else {
+        return marked(input, { renderer: RENDERER_DEFAULT });
+    }
+}
+
+const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
+const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
+const escapeReplacements = {
+  '&' : '&amp;',
+  '<' : '&lt;',
+  '>' : '&gt;',
+  '"' : '&quot;',
+  '\'': '&#39;',
+};
+
+const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
+
+export function escapeHTML(html: string) {
+    if (escapeTestNoEncode.test(html)) {
+        return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
+    }
+
+    return html;
+}
\ No newline at end of file
diff --git a/frontend/src/app/framework/utils/text-measurer.ts b/frontend/src/app/framework/utils/text-measurer.ts
index 74653d57b..ffc493bd9 100644
--- a/frontend/src/app/framework/utils/text-measurer.ts
+++ b/frontend/src/app/framework/utils/text-measurer.ts
@@ -4,11 +4,11 @@
  * @license
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { ElementRef } from '@angular/core';
-import { Types } from '../internal';
+import { Types } from './../internal';
 
 let CANVAS: HTMLCanvasElement | null = null;
 
 export class TextMeasurer {
     private font?: string;
diff --git a/frontend/src/app/shared/services/history.service.spec.ts b/frontend/src/app/shared/services/history.service.spec.ts
index e5279c27e..c06ef02d5 100644
--- a/frontend/src/app/shared/services/history.service.spec.ts
+++ b/frontend/src/app/shared/services/history.service.spec.ts
@@ -5,11 +5,75 @@
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { inject, TestBed } from '@angular/core/testing';
-import { ApiUrlConfig, DateTime, HistoryEventDto, HistoryService, Version } from '@app/shared/internal';
+import { firstValueFrom, of } from 'rxjs';
+import { IMock, Mock } from 'typemoq';
+import { ApiUrlConfig, DateTime, formatHistoryMessage, HistoryEventDto, HistoryService, UsersProviderService, Version } from '@app/shared/internal';
+
+describe('formatHistoryMessage', () => {
+    let userProvider: IMock<UsersProviderService>;
+
+    beforeEach(() => {
+        userProvider = Mock.ofType<UsersProviderService>();
+    });
+
+    it('should provide simple message', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('message', userProvider.object));
+
+        expect(message).toEqual('message');
+    });
+
+    it('should embed marker', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{Name}', userProvider.object));
+
+        expect(message).toEqual('<span class="marker-ref">Name</span>');
+    });
+
+    it('should embed marker ref and escape HTML', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{<h1>HTML</h1>}', userProvider.object));
+
+        expect(message).toEqual('<span class="marker-ref">&lt;h1&gt;HTML&lt;/h1&gt;</span>');
+    });
+
+    it('should embed user ref with unknown type', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{unknown:User1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">User1</span>');
+    });
+
+    it('should embed user ref with client', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{user:unknown:Client1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">Client1-client</span>');
+    });
+
+    it('should embed user ref with client ending with client', async () => {
+        const message = await firstValueFrom(formatHistoryMessage('{user:client:Sample-Client}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">Sample-Client</span>');
+    });
+
+    it('should embed user ref with subject', async () => {
+        userProvider.setup(x => x.getUser('1', null))
+            .returns(() => of({ id: '1', displayName: 'User1' }));
+
+        const message = await firstValueFrom(formatHistoryMessage('{user:subject:1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">User1</span>');
+    });
+
+    it('should embed user ref with id', async () => {
+        userProvider.setup(x => x.getUser('1', null))
+            .returns(() => of({ id: '1', displayName: 'User1' }));
+
+        const message = await firstValueFrom(formatHistoryMessage('{user:1}', userProvider.object));
+
+        expect(message).toEqual('<span class="user-ref">User1</span>');
+    });
+});
 
 describe('HistoryService', () => {
     beforeEach(() => {
         TestBed.configureTestingModule({
             imports: [
diff --git a/frontend/src/app/shared/services/history.service.ts b/frontend/src/app/shared/services/history.service.ts
index 486eb2e34..883ee3b4a 100644
--- a/frontend/src/app/shared/services/history.service.ts
+++ b/frontend/src/app/shared/services/history.service.ts
@@ -5,13 +5,13 @@
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { HttpClient, HttpHeaders } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { Observable, of } from 'rxjs';
+import { firstValueFrom, from, Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
-import { ApiUrlConfig, DateTime, pretifyError, Version } from '@app/framework';
+import { ApiUrlConfig, DateTime, escapeHTML, pretifyError, Version } from '@app/framework';
 import { UsersProviderService } from './users-provider.service';
 
 export class HistoryEventDto {
     constructor(
         public readonly eventId: string,
@@ -22,47 +22,54 @@ export class HistoryEventDto {
         public readonly version: Version,
     ) {
     }
 }
 
-const REPLACEMENT_TEMP = '$TEMP$';
-
 export function formatHistoryMessage(message: string, users: UsersProviderService): Observable<string> {
-    const userName = (userId: string) => {
-        const parts = userId.split(':');
-
-        if (parts.length === 1) {
-            return users.getUser(parts[0], null).pipe(map(u => u.displayName));
-        } else if (parts[0] === 'subject') {
-            return users.getUser(parts[1], null).pipe(map(u => u.displayName));
-        } else if (parts[1].endsWith('client')) {
-            return of(parts[1]);
-        } else {
-            return of(`${parts[1]}-client`);
-        }
-    };
+    async function getUserName(id: string): Promise<string> {
+        const user = await firstValueFrom(users.getUser(id, null));
 
-    let foundUserId: string | null = null;
+        return user.displayName;
+    }
 
-    message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => {
-        if (type === 'user') {
-            foundUserId = id;
-            return REPLACEMENT_TEMP;
-        } else {
-            return id;
+    async function format(message: string): Promise<string> {
+        const placeholderMatches = message.matchAll(/{(?<type>[^\s:]*):(?<id>[^}]*)}/g) || [];
+        const placeholderValues: string[] = [];
+
+        for (const match of placeholderMatches) {
+            const { id, type } = match.groups!;
+
+            if (type !== 'user') {
+                placeholderValues.push(id);
+                continue;
+            }
+
+            const parts = id.split(':');
+
+            if (parts.length === 1) {
+                placeholderValues.push(await getUserName(parts[0]));
+            } else if (parts[0] === 'subject') {
+                placeholderValues.push(await getUserName(parts[1]));
+            } else if (parts[1].toLowerCase().endsWith('client')) {
+                placeholderValues.push(parts[1]);
+            } else {
+                placeholderValues.push(`${parts[1]}-client`);
+            }
         }
-    });
 
-    message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
-        return `<span class="marker-ref">${marker}</span>`;
-    });
+        message = message.replace(/{([^\s:]*):([^}]*)}/, () => {
+            return `<span class="user-ref">${escapeHTML(placeholderValues.shift() || '')}</span>`;
+        });
+
+        message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
+            return `<span class="marker-ref">${escapeHTML(marker)}</span>`;
+        });
 
-    if (foundUserId) {
-        return userName(foundUserId).pipe(map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`)));
+        return message;
     }
 
-    return of(message);
+    return from(format(message));
 }
 
 @Injectable()
 export class HistoryService {
     constructor(
diff --git a/frontend/src/app/shared/services/rules.service.spec.ts b/frontend/src/app/shared/services/rules.service.spec.ts
index 65d190898..65379e143 100644
--- a/frontend/src/app/shared/services/rules.service.spec.ts
+++ b/frontend/src/app/shared/services/rules.service.spec.ts
@@ -6,11 +6,11 @@
  */
 
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { inject, TestBed } from '@angular/core/testing';
 import { ApiUrlConfig, DateTime, Resource, ResourceLinks, RuleDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesDto, RulesService, Version } from '@app/shared/internal';
-import { RuleCompletions } from '..';
+import { RuleCompletions } from './..';
 import { SimulatedRuleEventDto, SimulatedRuleEventsDto } from './rules.service';
 
 describe('RulesService', () => {
     const version = new Version('1');
 
diff --git a/frontend/src/app/shared/services/schemas.service.spec.ts b/frontend/src/app/shared/services/schemas.service.spec.ts
index e42a19e57..cb7a886dc 100644
--- a/frontend/src/app/shared/services/schemas.service.spec.ts
+++ b/frontend/src/app/shared/services/schemas.service.spec.ts
@@ -6,11 +6,11 @@
  */
 
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { inject, TestBed } from '@angular/core/testing';
 import { ApiUrlConfig, createProperties, DateTime, FieldRule, NestedFieldDto, Resource, ResourceLinks, RootFieldDto, SchemaDto, SchemaPropertiesDto, SchemasDto, SchemasService, Version } from '@app/shared/internal';
-import { SchemaCompletions } from '..';
+import { SchemaCompletions } from './..';
 
 describe('SchemasService', () => {
     const version = new Version('1');
 
     beforeEach(() => {
diff --git a/frontend/src/app/shared/state/asset-scripts.state.spec.ts b/frontend/src/app/shared/state/asset-scripts.state.spec.ts
index 32d526c76..ddddd8a3f 100644
--- a/frontend/src/app/shared/state/asset-scripts.state.spec.ts
+++ b/frontend/src/app/shared/state/asset-scripts.state.spec.ts
@@ -7,12 +7,12 @@
 
 import { of, throwError } from 'rxjs';
 import { onErrorResumeNext } from 'rxjs/operators';
 import { IMock, It, Mock, Times } from 'typemoq';
 import { DialogService, versioned } from '@app/shared/internal';
-import { AppsService, AssetScriptsPayload } from '../services/apps.service';
-import { createAssetScripts } from '../services/apps.service.spec';
+import { AppsService, AssetScriptsPayload } from './../services/apps.service';
+import { createAssetScripts } from './../services/apps.service.spec';
 import { TestValues } from './_test-helpers';
 import { AssetScriptsState } from './asset-scripts.state';
 
 describe('AssetScriptsState', () => {
     const {
diff --git a/frontend/src/app/shared/state/asset-scripts.state.ts b/frontend/src/app/shared/state/asset-scripts.state.ts
index fa57fd87e..2f27e4da9 100644
--- a/frontend/src/app/shared/state/asset-scripts.state.ts
+++ b/frontend/src/app/shared/state/asset-scripts.state.ts
@@ -7,11 +7,11 @@
 
 import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs';
 import { finalize, tap } from 'rxjs/operators';
 import { DialogService, LoadingState, Resource, shareSubscribed, State, Version } from '@app/framework';
-import { AppsService, AssetScripts, AssetScriptsPayload } from '../services/apps.service';
+import { AppsService, AssetScripts, AssetScriptsPayload } from './../services/apps.service';
 import { AppsState } from './apps.state';
 
 interface Snapshot extends LoadingState {
     // The current scripts.
     scripts: AssetScripts;
diff --git a/frontend/src/app/shared/state/resolvers.spec.ts b/frontend/src/app/shared/state/resolvers.spec.ts
index 8e0ce5a5e..5f92e3335 100644
--- a/frontend/src/app/shared/state/resolvers.spec.ts
+++ b/frontend/src/app/shared/state/resolvers.spec.ts
@@ -6,12 +6,12 @@
  */
 
 import { firstValueFrom, of, throwError } from 'rxjs';
 import { IMock, Mock, Times } from 'typemoq';
 import { UIOptions } from '@app/framework';
-import { ContentsService } from '../services/contents.service';
-import { createContent } from '../services/contents.service.spec';
+import { ContentsService } from './../services/contents.service';
+import { createContent } from './../services/contents.service.spec';
 import { TestValues } from './_test-helpers';
 import { ResolveContents } from './resolvers';
 
 describe('ResolveContents', () => {
     const {
diff --git a/frontend/src/app/shared/state/rules.forms.ts b/frontend/src/app/shared/state/rules.forms.ts
index ed72d8b11..5b095aa00 100644
--- a/frontend/src/app/shared/state/rules.forms.ts
+++ b/frontend/src/app/shared/state/rules.forms.ts
@@ -5,11 +5,11 @@
  * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
  */
 
 import { AbstractControl, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
 import { ExtendedFormGroup, Form, ValidatorsEx } from '@app/framework';
-import { RuleElementDto } from '../services/rules.service';
+import { RuleElementDto } from './../services/rules.service';
 
 export class ActionForm extends Form<any, UntypedFormGroup> {
     constructor(public readonly definition: RuleElementDto,
         public readonly actionType: string,
     ) {
diff --git a/frontend/src/app/shared/state/table-settings.spec.ts b/frontend/src/app/shared/state/table-settings.spec.ts
index 415146d1d..26ce1b3f8 100644
--- a/frontend/src/app/shared/state/table-settings.spec.ts
+++ b/frontend/src/app/shared/state/table-settings.spec.ts
@@ -7,11 +7,11 @@
 
 import { of } from 'rxjs';
 import { IMock, It, Mock, Times } from 'typemoq';
 import { DateTime, Version } from '@app/framework';
 import { createProperties, FieldSizes, META_FIELDS, RootFieldDto, SchemaDto, TableSettings, UIState } from '@app/shared/internal';
-import { FieldWrappings } from '..';
+import { FieldWrappings } from './..';
 
 describe('TableSettings', () => {
     let uiState: IMock<UIState>;
 
     const schema =

Vulnerable code: import { RuleConfigured } from '../messages';
import { RuleConfigured } from '../messages';
                <span [sqxMarkdown]="property.description" [inline]="true" [html]="true"></span>
import { marked } from 'marked';

const RENDERER_DEFAULT = new marked.Renderer();
const RENDERER_INLINE = new marked.Renderer();

RENDERER_DEFAULT.link = (href, _, text) => {
    if (href && href.startsWith('mailto')) {
        return text;
    } else {
        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
    }
};

RENDERER_INLINE.paragraph = (text) => {
    return text;
};

RENDERER_INLINE.link = RENDERER_DEFAULT.link;
    @Input()
    public html = false;

        const markdown = this.markdown;
        } else if (this.optional && markdown.indexOf('!') !== 0) {
            const renderer = this.inline ? RENDERER_INLINE : RENDERER_DEFAULT;

            html = marked(this.markdown, { renderer });
        if (!this.html && (!html || html === this.markdown || html.indexOf('<') < 0)) {
            this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
        } else {
import { PagingInfo } from '../state';
        const actual = new MarkdownInlinePipe().transform('[link-name](link-url)');
        const actual = new MarkdownInlinePipe().transform('*bold*');
            const actual = new MarkdownInlinePipe().transform(x);
        const actual = new MarkdownPipe().transform('[link-name](link-url)');
        const actual = new MarkdownPipe().transform('*bold*');
            const actual = new MarkdownPipe().transform(x);
import { marked } from 'marked';

const renderer = new marked.Renderer();

renderer.link = (href, _, text) => {
    if (href && href.startsWith('mailto')) {
        return text;
    } else {
        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
    }
};

const inlinerRenderer = new marked.Renderer();

inlinerRenderer.paragraph = (text) => {
    return text;
};

inlinerRenderer.link = renderer.link;
        if (text) {
            return marked(text, { renderer });
        } else {
            return '';
        }
        if (text) {
            return marked(text, { renderer: inlinerRenderer });
        } else {
            return '';
        }
}
import { compareStrings } from '../utils/array-helper';
import { DateHelper } from '..';
import { Types } from '../internal';
import { ApiUrlConfig, DateTime, HistoryEventDto, HistoryService, Version } from '@app/shared/internal';
import { Observable, of } from 'rxjs';
import { ApiUrlConfig, DateTime, pretifyError, Version } from '@app/framework';
const REPLACEMENT_TEMP = '$TEMP$';

    const userName = (userId: string) => {
        const parts = userId.split(':');

        if (parts.length === 1) {
            return users.getUser(parts[0], null).pipe(map(u => u.displayName));
        } else if (parts[0] === 'subject') {
            return users.getUser(parts[1], null).pipe(map(u => u.displayName));
        } else if (parts[1].endsWith('client')) {
            return of(parts[1]);
        } else {
            return of(`${parts[1]}-client`);
        }
    };
    let foundUserId: string | null = null;
    message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => {
        if (type === 'user') {
            foundUserId = id;
            return REPLACEMENT_TEMP;
        } else {
            return id;
    });
    message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
        return `<span class="marker-ref">${marker}</span>`;
    });
    if (foundUserId) {
        return userName(foundUserId).pipe(map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`)));
    return of(message);
import { RuleCompletions } from '..';
import { SchemaCompletions } from '..';
import { AppsService, AssetScriptsPayload } from '../services/apps.service';
import { createAssetScripts } from '../services/apps.service.spec';
import { AppsService, AssetScripts, AssetScriptsPayload } from '../services/apps.service';
import { ContentsService } from '../services/contents.service';
import { createContent } from '../services/contents.service.spec';
import { RuleElementDto } from '../services/rules.service';
import { FieldWrappings } from '..';
Fixed code: 
declare namespace marked {
    export function escape(input: string): string;
}
import { RuleConfigured } from './../messages';
import { RuleConfigured } from './../messages';
                <span [sqxMarkdown]="property.description" [inline]="true"></span>
/*
 * Squidex Headless CMS
 *
 * @license
 * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
 */

import { Renderer2 } from '@angular/core';
import { IMock, It, Mock, Times } from 'typemoq';
import { MarkdownDirective } from './markdown.directive';

describe('MarkdownDirective', () => {
    let renderer: IMock<Renderer2>;
    let markdownElement = {};
    let markdownDirective: MarkdownDirective;

    beforeEach(() => {
        renderer = Mock.ofType<Renderer2>();

        markdownElement = {};
        markdownDirective = new MarkdownDirective(markdownElement as any, renderer.object);
    });

    it('should render empty text as text', () => {
        markdownDirective.markdown = '';
        markdownDirective.ngOnChanges();

        verifyTextRender('');
    });

    it('should render as text if result has no tags', () => {
        markdownDirective.inline = true;
        markdownDirective.markdown = 'markdown';
        markdownDirective.ngOnChanges();

        verifyTextRender('markdown');
    });

    it('should render as text if optional', () => {
        markdownDirective.optional = true;
        markdownDirective.markdown = '**bold**';
        markdownDirective.ngOnChanges();

        verifyTextRender('**bold**');
    });

    it('should render if optional with exclamation', () => {
        markdownDirective.optional = true;
        markdownDirective.markdown = '!**bold**';
        markdownDirective.ngOnChanges();

        verifyHtmlRender('<strong>bold</strong>');
    });

    it('should render as HTML if allowed', () => {
        markdownDirective.inline = false;
        markdownDirective.markdown = '**bold**';
        markdownDirective.ngOnChanges();

        verifyHtmlRender('<p><strong>bold</strong></p>\n');
    });

    it('should render as inline HTML if allowed', () => {
        markdownDirective.markdown = '!**bold**';
        markdownDirective.ngOnChanges();

        verifyHtmlRender('<strong>bold</strong>');
    });

    it('should render HTML escaped', () => {
        markdownDirective.inline = false;
        markdownDirective.markdown = '<h1>Header</h1>';
        markdownDirective.ngOnChanges();

        verifyHtmlRender('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
    });

    function verifyTextRender(text: string) {
        renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once());

        expect().nothing();
    }

    function verifyHtmlRender(text: string) {
        renderer.verify(x => x.setProperty(It.isAny(), 'innerHTML', text), Times.once());

        expect().nothing();
    }
});
import { renderMarkdown } from '@app/framework/internal';
        let markdown = this.markdown;

        const hasExclamation = markdown.indexOf('!') === 0;

        if (hasExclamation) {
            markdown = markdown.substring(1);
        }
        } else if (this.optional && !hasExclamation) {
            html = renderMarkdown(markdown, this.inline);
        const hasHtml = html.indexOf('<') >= 0;

        if (hasHtml) {
        } else {
            this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
import { PagingInfo } from './../state';
    const pipe = new MarkdownInlinePipe();

        const actual = pipe.transform('[link-name](link-url)');
        const actual = pipe.transform('*bold*');
    it('should escape input html', () => {
        const actual = pipe.transform('<h1>Header</h1>');

        expect(actual).toBe('&lt;h1&gt;Header&lt;/h1&gt;');
    });

            const actual = pipe.transform(x);
    const pipe = new MarkdownPipe();

        const actual = pipe.transform('[link-name](link-url)');
        const actual = pipe.transform('*bold*');
    it('should escape input html', () => {
        const actual = pipe.transform('<h1>Header</h1>');

        expect(actual).toBe('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
    });

            const actual = pipe.transform(x);
import { renderMarkdown } from '@app/framework/internal';
        return renderMarkdown(text, false);
        return renderMarkdown(text, true);
}
export * from './utils/markdown';
import { compareStrings } from './../utils/array-helper';
import { DateHelper } from './..';
/*
 * Squidex Headless CMS
 *
 * @license
 * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
 */

import { marked } from 'marked';

function renderLink(href: string, _: string, text: string) {
    if (href && href.startsWith('mailto')) {
        return text;
    } else {
        return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
    }
}

function renderInlineParagraph(text: string) {
    return text;
}

const RENDERER_DEFAULT = new marked.Renderer();
const RENDERER_INLINE = new marked.Renderer();

RENDERER_INLINE.paragraph = renderInlineParagraph;
RENDERER_INLINE.link = renderLink;
RENDERER_DEFAULT.link = renderLink;

export function renderMarkdown(input: string | undefined | null, inline: boolean) {
    if (!input) {
        return '';
    }

    input = escapeHTML(input);

    if (inline) {
        return marked(input, { renderer: RENDERER_INLINE });
    } else {
        return marked(input, { renderer: RENDERER_DEFAULT });
    }
}

const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
const escapeReplacements = {
  '&' : '&amp;',
  '<' : '&lt;',
  '>' : '&gt;',
  '"' : '&quot;',
  '\'': '&#39;',
};

const getEscapeReplacement = (ch: string) => escapeReplacements[ch];

export function escapeHTML(html: string) {
    if (escapeTestNoEncode.test(html)) {
        return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
    }

    return html;
}
import { Types } from './../internal';
import { firstValueFrom, of } from 'rxjs';
import { IMock, Mock } from 'typemoq';
import { ApiUrlConfig, DateTime, formatHistoryMessage, HistoryEventDto, HistoryService, UsersProviderService, Version } from '@app/shared/internal';

describe('formatHistoryMessage', () => {
    let userProvider: IMock<UsersProviderService>;

    beforeEach(() => {
        userProvider = Mock.ofType<UsersProviderService>();
    });

    it('should provide simple message', async () => {
        const message = await firstValueFrom(formatHistoryMessage('message', userProvider.object));

        expect(message).toEqual('message');
    });

    it('should embed marker', async () => {
        const message = await firstValueFrom(formatHistoryMessage('{Name}', userProvider.object));

        expect(message).toEqual('<span class="marker-ref">Name</span>');
    });

    it('should embed marker ref and escape HTML', async () => {
        const message = await firstValueFrom(formatHistoryMessage('{<h1>HTML</h1>}', userProvider.object));

        expect(message).toEqual('<span class="marker-ref">&lt;h1&gt;HTML&lt;/h1&gt;</span>');
    });

    it('should embed user ref with unknown type', async () => {
        const message = await firstValueFrom(formatHistoryMessage('{unknown:User1}', userProvider.object));

        expect(message).toEqual('<span class="user-ref">User1</span>');
    });

    it('should embed user ref with client', async () => {
        const message = await firstValueFrom(formatHistoryMessage('{user:unknown:Client1}', userProvider.object));

        expect(message).toEqual('<span class="user-ref">Client1-client</span>');
    });

    it('should embed user ref with client ending with client', async () => {
        const message = await firstValueFrom(formatHistoryMessage('{user:client:Sample-Client}', userProvider.object));

        expect(message).toEqual('<span class="user-ref">Sample-Client</span>');
    });

    it('should embed user ref with subject', async () => {
        userProvider.setup(x => x.getUser('1', null))
            .returns(() => of({ id: '1', displayName: 'User1' }));

        const message = await firstValueFrom(formatHistoryMessage('{user:subject:1}', userProvider.object));

        expect(message).toEqual('<span class="user-ref">User1</span>');
    });

    it('should embed user ref with id', async () => {
        userProvider.setup(x => x.getUser('1', null))
            .returns(() => of({ id: '1', displayName: 'User1' }));

        const message = await firstValueFrom(formatHistoryMessage('{user:1}', userProvider.object));

        expect(message).toEqual('<span class="user-ref">User1</span>');
    });
});
import { firstValueFrom, from, Observable } from 'rxjs';
import { ApiUrlConfig, DateTime, escapeHTML, pretifyError, Version } from '@app/framework';
    async function getUserName(id: string): Promise<string> {
        const user = await firstValueFrom(users.getUser(id, null));
        return user.displayName;
    }
    async function format(message: string): Promise<string> {
        const placeholderMatches = message.matchAll(/{(?<type>[^\s:]*):(?<id>[^}]*)}/g) || [];
        const placeholderValues: string[] = [];

        for (const match of placeholderMatches) {
            const { id, type } = match.groups!;

            if (type !== 'user') {
                placeholderValues.push(id);
                continue;
            }

            const parts = id.split(':');

            if (parts.length === 1) {
                placeholderValues.push(await getUserName(parts[0]));
            } else if (parts[0] === 'subject') {
                placeholderValues.push(await getUserName(parts[1]));
            } else if (parts[1].toLowerCase().endsWith('client')) {
                placeholderValues.push(parts[1]);
            } else {
                placeholderValues.push(`${parts[1]}-client`);
            }
        message = message.replace(/{([^\s:]*):([^}]*)}/, () => {
            return `<span class="user-ref">${escapeHTML(placeholderValues.shift() || '')}</span>`;
        });

        message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
            return `<span class="marker-ref">${escapeHTML(marker)}</span>`;
        });
        return message;
    return from(format(message));
import { RuleCompletions } from './..';
import { SchemaCompletions } from './..';
import { AppsService, AssetScriptsPayload } from './../services/apps.service';
import { createAssetScripts } from './../services/apps.service.spec';
import { AppsService, AssetScripts, AssetScriptsPayload } from './../services/apps.service';
import { ContentsService } from './../services/contents.service';
import { createContent } from './../services/contents.service.spec';
import { RuleElementDto } from './../services/rules.service';
import { FieldWrappings } from './..';