@hellotext/hellotext 2.5.0 → 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,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.0",
3
+ "version": "2.5.2",
4
4
  "description": "Hellotext JavaScript Client",
5
5
  "source": "src/index.js",
6
6
  "main": "lib/index.cjs",
@@ -55,7 +55,7 @@
55
55
  "jest-environment-jsdom": "^29.3.1",
56
56
  "prettier": "2.8.2",
57
57
  "style-loader": "^3.3.1",
58
- "webpack": "^5.75.0",
58
+ "webpack": "^5.107.2",
59
59
  "webpack-cli": "^5.0.1",
60
60
  "whatwg-fetch": "^3.6.20"
61
61
  },
@@ -94,7 +94,11 @@
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
- "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
100
+ "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
101
+ "resolutions": {
102
+ "@babel/traverse": "7.23.2"
103
+ }
100
104
  }
@@ -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)