@hellotext/hellotext 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ /*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
@@ -11,6 +11,7 @@ var _webchat_channel = _interopRequireDefault(require("../channels/webchat_chann
11
11
  var _hellotext = _interopRequireDefault(require("../hellotext"));
12
12
  var _core = require("../core");
13
13
  var _webchat = require("../core/configuration/webchat");
14
+ var _sanitize_html = require("../core/sanitize_html");
14
15
  var _usePopover = require("./mixins/usePopover");
15
16
  var _useBehaviour = require("./webchat/useBehaviour");
16
17
  var _useOpeningSequence = require("./webchat/useOpeningSequence");
@@ -265,8 +266,6 @@ let _default = /*#__PURE__*/function (_Controller) {
265
266
  attachments
266
267
  } = message;
267
268
  const createdAt = message.created_at || message.createdAt;
268
- const div = document.createElement('div');
269
- div.innerHTML = body;
270
269
  const element = this.messageTemplateTarget.cloneNode(true);
271
270
  element.classList.add('hellotext--webchat-message');
272
271
  element.setAttribute('data-hellotext--webchat-target', 'message');
@@ -275,7 +274,7 @@ let _default = /*#__PURE__*/function (_Controller) {
275
274
  element.setAttribute('data-created-at', createdAt);
276
275
  }
277
276
  element.style.removeProperty('display');
278
- element.querySelector('[data-body]').innerHTML = div.innerHTML;
277
+ (0, _sanitize_html.setSanitizedRichText)(element.querySelector('[data-body]'), body);
279
278
  if (message.state === 'received') {
280
279
  element.classList.add('received');
281
280
  } else {
@@ -412,12 +411,10 @@ let _default = /*#__PURE__*/function (_Controller) {
412
411
  if (message.carousel) {
413
412
  return this.insertCarouselMessage(message, options);
414
413
  }
415
- const div = document.createElement('div');
416
- div.innerHTML = body;
417
414
  const element = this.messageTemplateTarget.cloneNode(true);
418
415
  element.classList.add('hellotext--webchat-message');
419
416
  element.style.display = 'flex';
420
- element.querySelector('[data-body]').innerHTML = div.innerHTML;
417
+ (0, _sanitize_html.setSanitizedRichText)(element.querySelector('[data-body]'), body);
421
418
  element.setAttribute('data-id', id);
422
419
  element.setAttribute('data-hellotext--webchat-target', 'message');
423
420
  this.setMessageCreatedAt(element, createdAt);
@@ -533,7 +530,7 @@ let _default = /*#__PURE__*/function (_Controller) {
533
530
  return;
534
531
  }
535
532
  this.teaserMessageTargets.forEach(teaserMessage => teaserMessage.classList.add('hidden'));
536
- this.inboundMessageTeaserBodyTarget.innerHTML = this.messageTeaserValue;
533
+ this.inboundMessageTeaserBodyTarget.textContent = this.messageTeaserValue;
537
534
  this.inboundMessageTeaserTarget.classList.remove('hidden');
538
535
  this.teaserTarget.classList.toggle('invisible', this.openValue);
539
536
  }
@@ -543,7 +540,7 @@ let _default = /*#__PURE__*/function (_Controller) {
543
540
  var _element$querySelecto;
544
541
  const html = message.html;
545
542
  const createdAt = message.created_at || message.createdAt;
546
- const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
543
+ const element = (0, _sanitize_html.sanitizedWebchatComponentFragment)(html).firstElementChild;
547
544
  element.classList.add('hellotext--webchat-message');
548
545
  element.setAttribute('data-id', message.id);
549
546
  element.setAttribute('data-hellotext--webchat-target', 'message');
@@ -24,6 +24,7 @@ import WebchatChannel from '../channels/webchat_channel';
24
24
  import Hellotext from '../hellotext';
25
25
  import { Locale } from '../core';
26
26
  import { Webchat as WebchatConfiguration, modes } from '../core/configuration/webchat';
27
+ import { sanitizedWebchatComponentFragment, setSanitizedRichText } from '../core/sanitize_html';
27
28
  import { usePopover } from './mixins/usePopover';
28
29
  import { useBehaviour } from './webchat/useBehaviour';
29
30
  import { useOpeningSequence } from './webchat/useOpeningSequence';
@@ -264,8 +265,6 @@ var _default = /*#__PURE__*/function (_Controller) {
264
265
  attachments
265
266
  } = message;
266
267
  var createdAt = message.created_at || message.createdAt;
267
- var div = document.createElement('div');
268
- div.innerHTML = body;
269
268
  var element = this.messageTemplateTarget.cloneNode(true);
270
269
  element.classList.add('hellotext--webchat-message');
271
270
  element.setAttribute('data-hellotext--webchat-target', 'message');
@@ -274,7 +273,7 @@ var _default = /*#__PURE__*/function (_Controller) {
274
273
  element.setAttribute('data-created-at', createdAt);
275
274
  }
276
275
  element.style.removeProperty('display');
277
- element.querySelector('[data-body]').innerHTML = div.innerHTML;
276
+ setSanitizedRichText(element.querySelector('[data-body]'), body);
278
277
  if (message.state === 'received') {
279
278
  element.classList.add('received');
280
279
  } else {
@@ -417,12 +416,10 @@ var _default = /*#__PURE__*/function (_Controller) {
417
416
  if (message.carousel) {
418
417
  return this.insertCarouselMessage(message, options);
419
418
  }
420
- var div = document.createElement('div');
421
- div.innerHTML = body;
422
419
  var element = this.messageTemplateTarget.cloneNode(true);
423
420
  element.classList.add('hellotext--webchat-message');
424
421
  element.style.display = 'flex';
425
- element.querySelector('[data-body]').innerHTML = div.innerHTML;
422
+ setSanitizedRichText(element.querySelector('[data-body]'), body);
426
423
  element.setAttribute('data-id', id);
427
424
  element.setAttribute('data-hellotext--webchat-target', 'message');
428
425
  this.setMessageCreatedAt(element, createdAt);
@@ -543,7 +540,7 @@ var _default = /*#__PURE__*/function (_Controller) {
543
540
  return;
544
541
  }
545
542
  this.teaserMessageTargets.forEach(teaserMessage => teaserMessage.classList.add('hidden'));
546
- this.inboundMessageTeaserBodyTarget.innerHTML = this.messageTeaserValue;
543
+ this.inboundMessageTeaserBodyTarget.textContent = this.messageTeaserValue;
547
544
  this.inboundMessageTeaserTarget.classList.remove('hidden');
548
545
  this.teaserTarget.classList.toggle('invisible', this.openValue);
549
546
  }
@@ -554,7 +551,7 @@ var _default = /*#__PURE__*/function (_Controller) {
554
551
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
555
552
  var html = message.html;
556
553
  var createdAt = message.created_at || message.createdAt;
557
- var element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild;
554
+ var element = sanitizedWebchatComponentFragment(html).firstElementChild;
558
555
  element.classList.add('hellotext--webchat-message');
559
556
  element.setAttribute('data-id', message.id);
560
557
  element.setAttribute('data-hellotext--webchat-target', 'message');
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.sanitizedRichTextFragment = sanitizedRichTextFragment;
7
+ exports.sanitizedWebchatComponentFragment = sanitizedWebchatComponentFragment;
8
+ exports.setSanitizedRichText = setSanitizedRichText;
9
+ var _dompurify = _interopRequireDefault(require("dompurify"));
10
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
+ const RICH_TEXT_SANITIZER_OPTIONS = {
12
+ ADD_ATTR: ['target'],
13
+ ALLOW_DATA_ATTR: false,
14
+ RETURN_DOM_FRAGMENT: true,
15
+ USE_PROFILES: {
16
+ html: true
17
+ }
18
+ };
19
+ const WEBCHAT_COMPONENT_SANITIZER_OPTIONS = {
20
+ ADD_ATTR: ['target'],
21
+ ADD_TAGS: ['hellotext-icon'],
22
+ RETURN_DOM_FRAGMENT: true,
23
+ USE_PROFILES: {
24
+ html: true,
25
+ svg: true
26
+ }
27
+ };
28
+ function sanitizedFragment(html, options) {
29
+ const fragment = _dompurify.default.sanitize(html, options);
30
+ fragment.querySelectorAll('a[target="_blank"]').forEach(link => {
31
+ const rel = new Set(link.rel.split(/\s+/).filter(Boolean));
32
+ rel.add('noopener');
33
+ rel.add('noreferrer');
34
+ link.rel = Array.from(rel).join(' ');
35
+ });
36
+ return fragment;
37
+ }
38
+ function sanitizedRichTextFragment(html) {
39
+ return sanitizedFragment(html, RICH_TEXT_SANITIZER_OPTIONS);
40
+ }
41
+ function sanitizedWebchatComponentFragment(html) {
42
+ return sanitizedFragment(html, WEBCHAT_COMPONENT_SANITIZER_OPTIONS);
43
+ }
44
+ function setSanitizedRichText(element, html) {
45
+ element.replaceChildren(sanitizedRichTextFragment(html));
46
+ }
@@ -0,0 +1,37 @@
1
+ import DOMPurify from 'dompurify';
2
+ var RICH_TEXT_SANITIZER_OPTIONS = {
3
+ ADD_ATTR: ['target'],
4
+ ALLOW_DATA_ATTR: false,
5
+ RETURN_DOM_FRAGMENT: true,
6
+ USE_PROFILES: {
7
+ html: true
8
+ }
9
+ };
10
+ var WEBCHAT_COMPONENT_SANITIZER_OPTIONS = {
11
+ ADD_ATTR: ['target'],
12
+ ADD_TAGS: ['hellotext-icon'],
13
+ RETURN_DOM_FRAGMENT: true,
14
+ USE_PROFILES: {
15
+ html: true,
16
+ svg: true
17
+ }
18
+ };
19
+ function sanitizedFragment(html, options) {
20
+ var fragment = DOMPurify.sanitize(html, options);
21
+ fragment.querySelectorAll('a[target="_blank"]').forEach(link => {
22
+ var rel = new Set(link.rel.split(/\s+/).filter(Boolean));
23
+ rel.add('noopener');
24
+ rel.add('noreferrer');
25
+ link.rel = Array.from(rel).join(' ');
26
+ });
27
+ return fragment;
28
+ }
29
+ export function sanitizedRichTextFragment(html) {
30
+ return sanitizedFragment(html, RICH_TEXT_SANITIZER_OPTIONS);
31
+ }
32
+ export function sanitizedWebchatComponentFragment(html) {
33
+ return sanitizedFragment(html, WEBCHAT_COMPONENT_SANITIZER_OPTIONS);
34
+ }
35
+ export function setSanitizedRichText(element, html) {
36
+ element.replaceChildren(sanitizedRichTextFragment(html));
37
+ }
@@ -7,6 +7,7 @@ exports.Form = void 0;
7
7
  var _hellotext = _interopRequireDefault(require("../hellotext"));
8
8
  var _input_builder = require("../builders/input_builder");
9
9
  var _logo_builder = require("../builders/logo_builder");
10
+ var _sanitize_html = require("../core/sanitize_html");
10
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
12
  function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
12
13
  function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
@@ -58,7 +59,7 @@ let Form = /*#__PURE__*/function () {
58
59
  key: "buildHeader",
59
60
  value: function buildHeader(header) {
60
61
  const headerElement = _classPrivateFieldLooseBase(this, _findOrCreateComponent)[_findOrCreateComponent]('[data-form-header]', 'header');
61
- headerElement.innerHTML = header.content;
62
+ (0, _sanitize_html.setSanitizedRichText)(headerElement, header.content);
62
63
  if (this.element.querySelector('[data-form-header]')) {
63
64
  this.element.querySelector('[data-form-header]').replaceWith(headerElement);
64
65
  } else {
@@ -94,7 +95,7 @@ let Form = /*#__PURE__*/function () {
94
95
  key: "buildFooter",
95
96
  value: function buildFooter(footer) {
96
97
  const element = _classPrivateFieldLooseBase(this, _findOrCreateComponent)[_findOrCreateComponent]('[data-form-footer]', 'footer');
97
- element.innerHTML = footer.content;
98
+ (0, _sanitize_html.setSanitizedRichText)(element, footer.content);
98
99
  if (this.element.querySelector('[data-form-footer]')) {
99
100
  this.element.querySelector('[data-form-footer]').replaceWith(element);
100
101
  } else {
@@ -14,6 +14,7 @@ function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + n
14
14
  import Hellotext from '../hellotext';
15
15
  import { InputBuilder } from '../builders/input_builder';
16
16
  import { LogoBuilder } from '../builders/logo_builder';
17
+ import { setSanitizedRichText } from '../core/sanitize_html';
17
18
  var _findOrCreateComponent = /*#__PURE__*/_classPrivateFieldLooseKey("findOrCreateComponent");
18
19
  var Form = /*#__PURE__*/function () {
19
20
  function Form(data) {
@@ -63,7 +64,7 @@ var Form = /*#__PURE__*/function () {
63
64
  key: "buildHeader",
64
65
  value: function buildHeader(header) {
65
66
  var headerElement = _classPrivateFieldLooseBase(this, _findOrCreateComponent)[_findOrCreateComponent]('[data-form-header]', 'header');
66
- headerElement.innerHTML = header.content;
67
+ setSanitizedRichText(headerElement, header.content);
67
68
  if (this.element.querySelector('[data-form-header]')) {
68
69
  this.element.querySelector('[data-form-header]').replaceWith(headerElement);
69
70
  } else {
@@ -99,7 +100,7 @@ var Form = /*#__PURE__*/function () {
99
100
  key: "buildFooter",
100
101
  value: function buildFooter(footer) {
101
102
  var element = _classPrivateFieldLooseBase(this, _findOrCreateComponent)[_findOrCreateComponent]('[data-form-footer]', 'footer');
102
- element.innerHTML = footer.content;
103
+ setSanitizedRichText(element, footer.content);
103
104
  if (this.element.querySelector('[data-form-footer]')) {
104
105
  this.element.querySelector('[data-form-footer]').replaceWith(element);
105
106
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellotext/hellotext",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
@@ -94,6 +94,7 @@
94
94
  "@emoji-mart/data": "^1.2.1",
95
95
  "@floating-ui/dom": "^1.7.3",
96
96
  "@hotwired/stimulus": "^3.0.0",
97
+ "dompurify": "^3.4.13",
97
98
  "emoji-mart": "^5.6.0"
98
99
  },
99
100
  "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
@@ -7,6 +7,10 @@ import Hellotext from '../hellotext'
7
7
 
8
8
  import { Locale } from '../core'
9
9
  import { Webchat as WebchatConfiguration, modes } from '../core/configuration/webchat'
10
+ import {
11
+ sanitizedWebchatComponentFragment,
12
+ setSanitizedRichText,
13
+ } from '../core/sanitize_html'
10
14
 
11
15
  import { usePopover } from './mixins/usePopover'
12
16
  import { useBehaviour } from './webchat/useBehaviour'
@@ -333,9 +337,6 @@ export default class extends Controller {
333
337
  const { body, attachments } = message
334
338
  const createdAt = message.created_at || message.createdAt
335
339
 
336
- const div = document.createElement('div')
337
- div.innerHTML = body
338
-
339
340
  const element = this.messageTemplateTarget.cloneNode(true)
340
341
 
341
342
  element.classList.add('hellotext--webchat-message')
@@ -348,7 +349,7 @@ export default class extends Controller {
348
349
 
349
350
  element.style.removeProperty('display')
350
351
 
351
- element.querySelector('[data-body]').innerHTML = div.innerHTML
352
+ setSanitizedRichText(element.querySelector('[data-body]'), body)
352
353
 
353
354
  if (message.state === 'received') {
354
355
  element.classList.add('received')
@@ -497,14 +498,11 @@ export default class extends Controller {
497
498
  return this.insertCarouselMessage(message, options)
498
499
  }
499
500
 
500
- const div = document.createElement('div')
501
- div.innerHTML = body
502
-
503
501
  const element = this.messageTemplateTarget.cloneNode(true)
504
502
  element.classList.add('hellotext--webchat-message')
505
503
  element.style.display = 'flex'
506
504
 
507
- element.querySelector('[data-body]').innerHTML = div.innerHTML
505
+ setSanitizedRichText(element.querySelector('[data-body]'), body)
508
506
 
509
507
  element.setAttribute('data-id', id)
510
508
  element.setAttribute('data-hellotext--webchat-target', 'message')
@@ -633,7 +631,7 @@ export default class extends Controller {
633
631
  }
634
632
 
635
633
  this.teaserMessageTargets.forEach(teaserMessage => teaserMessage.classList.add('hidden'))
636
- this.inboundMessageTeaserBodyTarget.innerHTML = this.messageTeaserValue
634
+ this.inboundMessageTeaserBodyTarget.textContent = this.messageTeaserValue
637
635
  this.inboundMessageTeaserTarget.classList.remove('hidden')
638
636
 
639
637
  this.teaserTarget.classList.toggle('invisible', this.openValue)
@@ -642,7 +640,7 @@ export default class extends Controller {
642
640
  insertCarouselMessage(message, options = {}) {
643
641
  const html = message.html
644
642
  const createdAt = message.created_at || message.createdAt
645
- const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild
643
+ const element = sanitizedWebchatComponentFragment(html).firstElementChild
646
644
 
647
645
  element.classList.add('hellotext--webchat-message')
648
646
  element.setAttribute('data-id', message.id)
@@ -0,0 +1,40 @@
1
+ import DOMPurify from 'dompurify'
2
+
3
+ const RICH_TEXT_SANITIZER_OPTIONS = {
4
+ ADD_ATTR: ['target'],
5
+ ALLOW_DATA_ATTR: false,
6
+ RETURN_DOM_FRAGMENT: true,
7
+ USE_PROFILES: { html: true },
8
+ }
9
+
10
+ const WEBCHAT_COMPONENT_SANITIZER_OPTIONS = {
11
+ ADD_ATTR: ['target'],
12
+ ADD_TAGS: ['hellotext-icon'],
13
+ RETURN_DOM_FRAGMENT: true,
14
+ USE_PROFILES: { html: true, svg: true },
15
+ }
16
+
17
+ function sanitizedFragment(html, options) {
18
+ const fragment = DOMPurify.sanitize(html, options)
19
+
20
+ fragment.querySelectorAll('a[target="_blank"]').forEach(link => {
21
+ const rel = new Set(link.rel.split(/\s+/).filter(Boolean))
22
+ rel.add('noopener')
23
+ rel.add('noreferrer')
24
+ link.rel = Array.from(rel).join(' ')
25
+ })
26
+
27
+ return fragment
28
+ }
29
+
30
+ export function sanitizedRichTextFragment(html) {
31
+ return sanitizedFragment(html, RICH_TEXT_SANITIZER_OPTIONS)
32
+ }
33
+
34
+ export function sanitizedWebchatComponentFragment(html) {
35
+ return sanitizedFragment(html, WEBCHAT_COMPONENT_SANITIZER_OPTIONS)
36
+ }
37
+
38
+ export function setSanitizedRichText(element, html) {
39
+ element.replaceChildren(sanitizedRichTextFragment(html))
40
+ }
@@ -2,6 +2,7 @@ import Hellotext from '../hellotext'
2
2
 
3
3
  import { InputBuilder } from '../builders/input_builder'
4
4
  import { LogoBuilder } from '../builders/logo_builder'
5
+ import { setSanitizedRichText } from '../core/sanitize_html'
5
6
 
6
7
  class Form {
7
8
  constructor(data, element = null) {
@@ -44,7 +45,7 @@ class Form {
44
45
 
45
46
  buildHeader(header) {
46
47
  const headerElement = this.#findOrCreateComponent('[data-form-header]', 'header')
47
- headerElement.innerHTML = header.content
48
+ setSanitizedRichText(headerElement, header.content)
48
49
 
49
50
  if (this.element.querySelector('[data-form-header]')) {
50
51
  this.element.querySelector('[data-form-header]').replaceWith(headerElement)
@@ -86,7 +87,7 @@ class Form {
86
87
 
87
88
  buildFooter(footer) {
88
89
  const element = this.#findOrCreateComponent('[data-form-footer]', 'footer')
89
- element.innerHTML = footer.content
90
+ setSanitizedRichText(element, footer.content)
90
91
 
91
92
  if (this.element.querySelector('[data-form-footer]')) {
92
93
  this.element.querySelector('[data-form-footer]').replaceWith(element)