Skip to content

Commit 7bc4d2e

Browse files
Johann-SXhmikosR
authored andcommitted
Add sanitize template option for tooltip/popover plugins.
1 parent bf2515a commit 7bc4d2e

File tree

7 files changed

+453
-17
lines changed

7 files changed

+453
-17
lines changed

js/src/tools/sanitizer.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* --------------------------------------------------------------------------
3+
* Bootstrap (v4.3.0): tools/sanitizer.js
4+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5+
* --------------------------------------------------------------------------
6+
*/
7+
8+
const uriAttrs = [
9+
'background',
10+
'cite',
11+
'href',
12+
'itemtype',
13+
'longdesc',
14+
'poster',
15+
'src',
16+
'xlink:href'
17+
]
18+
19+
const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
20+
21+
export const DefaultWhitelist = {
22+
// Global attributes allowed on any supplied element below.
23+
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
24+
a: ['target', 'href', 'title', 'rel'],
25+
area: [],
26+
b: [],
27+
br: [],
28+
col: [],
29+
code: [],
30+
div: [],
31+
em: [],
32+
hr: [],
33+
h1: [],
34+
h2: [],
35+
h3: [],
36+
h4: [],
37+
h5: [],
38+
h6: [],
39+
i: [],
40+
img: ['src', 'alt', 'title', 'width', 'height'],
41+
li: [],
42+
ol: [],
43+
p: [],
44+
pre: [],
45+
s: [],
46+
small: [],
47+
span: [],
48+
sub: [],
49+
sup: [],
50+
strong: [],
51+
u: [],
52+
ul: []
53+
}
54+
55+
/**
56+
* A pattern that recognizes a commonly useful subset of URLs that are safe.
57+
*
58+
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
59+
*/
60+
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
61+
62+
/**
63+
* A pattern that matches safe data URLs. Only matches image, video and audio types.
64+
*
65+
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
66+
*/
67+
const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
68+
69+
function allowedAttribute(attr, allowedAttributeList) {
70+
const attrName = attr.nodeName.toLowerCase()
71+
72+
if (allowedAttributeList.indexOf(attrName) !== -1) {
73+
if (uriAttrs.indexOf(attrName) !== -1) {
74+
return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
75+
}
76+
77+
return true
78+
}
79+
80+
const regExp = allowedAttributeList.filter((attrRegex) => attrRegex instanceof RegExp)
81+
82+
// Check if a regular expression validates the attribute.
83+
for (let i = 0, l = regExp.length; i < l; i++) {
84+
if (attrName.match(regExp[i])) {
85+
return true
86+
}
87+
}
88+
89+
return false
90+
}
91+
92+
export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
93+
if (unsafeHtml.length === 0) {
94+
return unsafeHtml
95+
}
96+
97+
if (sanitizeFn && typeof sanitizeFn === 'function') {
98+
return sanitizeFn(unsafeHtml)
99+
}
100+
101+
const domParser = new window.DOMParser()
102+
const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
103+
const whitelistKeys = Object.keys(whiteList)
104+
const elements = [].slice.call(createdDocument.body.querySelectorAll('*'))
105+
106+
for (let i = 0, len = elements.length; i < len; i++) {
107+
const el = elements[i]
108+
const elName = el.nodeName.toLowerCase()
109+
110+
if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {
111+
el.parentNode.removeChild(el)
112+
113+
continue
114+
}
115+
116+
const attributeList = [].slice.call(el.attributes)
117+
const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
118+
119+
attributeList.forEach((attr) => {
120+
if (!allowedAttribute(attr, whitelistedAttributes)) {
121+
el.removeAttribute(attr.nodeName)
122+
}
123+
})
124+
}
125+
126+
return createdDocument.body.innerHTML
127+
}

js/src/tooltip.js

+46-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* --------------------------------------------------------------------------
66
*/
77

8+
import {
9+
DefaultWhitelist,
10+
sanitizeHtml
11+
} from './tools/sanitizer'
812
import $ from 'jquery'
913
import Popper from 'popper.js'
1014
import Util from './util'
@@ -15,13 +19,14 @@ import Util from './util'
1519
* ------------------------------------------------------------------------
1620
*/
1721

18-
const NAME = 'tooltip'
19-
const VERSION = '4.3.0'
20-
const DATA_KEY = 'bs.tooltip'
21-
const EVENT_KEY = `.${DATA_KEY}`
22-
const JQUERY_NO_CONFLICT = $.fn[NAME]
23-
const CLASS_PREFIX = 'bs-tooltip'
24-
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
22+
const NAME = 'tooltip'
23+
const VERSION = '4.3.0'
24+
const DATA_KEY = 'bs.tooltip'
25+
const EVENT_KEY = `.${DATA_KEY}`
26+
const JQUERY_NO_CONFLICT = $.fn[NAME]
27+
const CLASS_PREFIX = 'bs-tooltip'
28+
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
29+
const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
2530

2631
const DefaultType = {
2732
animation : 'boolean',
@@ -35,7 +40,10 @@ const DefaultType = {
3540
offset : '(number|string|function)',
3641
container : '(string|element|boolean)',
3742
fallbackPlacement : '(string|array)',
38-
boundary : '(string|element)'
43+
boundary : '(string|element)',
44+
sanitize : 'boolean',
45+
sanitizeFn : '(null|function)',
46+
whiteList : 'object'
3947
}
4048

4149
const AttachmentMap = {
@@ -60,7 +68,10 @@ const Default = {
6068
offset : 0,
6169
container : false,
6270
fallbackPlacement : 'flip',
63-
boundary : 'scrollParent'
71+
boundary : 'scrollParent',
72+
sanitize : true,
73+
sanitizeFn : null,
74+
whiteList : DefaultWhitelist
6475
}
6576

6677
const HoverState = {
@@ -419,18 +430,27 @@ class Tooltip {
419430
}
420431

421432
setElementContent($element, content) {
422-
const html = this.config.html
423433
if (typeof content === 'object' && (content.nodeType || content.jquery)) {
424434
// Content is a DOM node or a jQuery
425-
if (html) {
435+
if (this.config.html) {
426436
if (!$(content).parent().is($element)) {
427437
$element.empty().append(content)
428438
}
429439
} else {
430440
$element.text($(content).text())
431441
}
442+
443+
return
444+
}
445+
446+
if (this.config.html) {
447+
if (this.config.sanitize) {
448+
content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)
449+
}
450+
451+
$element.html(content)
432452
} else {
433-
$element[html ? 'html' : 'text'](content)
453+
$element.text(content)
434454
}
435455
}
436456

@@ -636,9 +656,18 @@ class Tooltip {
636656
}
637657

638658
_getConfig(config) {
659+
const dataAttributes = $(this.element).data()
660+
661+
Object.keys(dataAttributes)
662+
.forEach((dataAttr) => {
663+
if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
664+
delete dataAttributes[dataAttr]
665+
}
666+
})
667+
639668
config = {
640669
...this.constructor.Default,
641-
...$(this.element).data(),
670+
...dataAttributes,
642671
...typeof config === 'object' && config ? config : {}
643672
}
644673

@@ -663,6 +692,10 @@ class Tooltip {
663692
this.constructor.DefaultType
664693
)
665694

695+
if (config.sanitize) {
696+
config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
697+
}
698+
666699
return config
667700
}
668701

0 commit comments

Comments
 (0)