@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.
- package/dist/hellotext.js +2 -1
- package/dist/hellotext.js.LICENSE.txt +1 -0
- package/dist/webchat-emoji-en.js +1 -1
- package/dist/webchat-emoji-es.js +1 -1
- package/dist/webchat-emoji.js +1 -1
- package/lib/controllers/webchat_controller.cjs +5 -8
- package/lib/controllers/webchat_controller.js +5 -8
- package/lib/core/sanitize_html.cjs +46 -0
- package/lib/core/sanitize_html.js +37 -0
- package/lib/models/form.cjs +3 -2
- package/lib/models/form.js +3 -2
- package/package.json +7 -3
- package/src/controllers/webchat_controller.js +8 -10
- package/src/core/sanitize_html.js +40 -0
- package/src/models/form.js +3 -2
|
@@ -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
|
+
}
|
package/lib/models/form.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
package/lib/models/form.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
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]')
|
|
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]')
|
|
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.
|
|
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 =
|
|
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
|
+
}
|
package/src/models/form.js
CHANGED
|
@@ -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
|
|
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
|
|
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)
|