}
})
})
+
+ QUnit.test('should disable sanitizer', function (assert) {
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ sanitize: false
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+ assert.strictEqual(tooltip.options.sanitize, false)
+ })
+
+ QUnit.test('should sanitize template by removing disallowed tags', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<div>',
+ ' <script>console.log("oups script inserted")</script>',
+ ' <span>Some content</span>',
+ '</div>'
+ ].join('')
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+ assert.strictEqual(tooltip.options.template.indexOf('script'), -1)
+ })
+
+ QUnit.test('should sanitize template by removing disallowed attributes', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<div>',
+ ' <img src="x" onError="alert(\'test\')">Some content</img>',
+ '</div>'
+ ].join('')
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+ assert.strictEqual(tooltip.options.template.indexOf('onError'), -1)
+ })
+
+ QUnit.test('should sanitize template by removing tags with XSS', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<div>',
+ ' <a href="javascript:alert(7)">Click me</a>',
+ ' <span>Some content</span>',
+ '</div>'
+ ].join('')
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+ assert.strictEqual(tooltip.options.template.indexOf('javascript'), -1)
+ })
+
+ QUnit.test('should allow custom sanitization rules', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(2)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<a href="javascript:alert(7)">Click me</a>',
+ '<span>Some content</span>'
+ ].join(''),
+ whiteList: {
+ span: null
+ }
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+
+ assert.strictEqual(tooltip.options.template.indexOf('<a'), -1)
+ assert.ok(tooltip.options.template.indexOf('span') !== -1)
+ })
+
+ QUnit.test('should allow passing a custom function for sanitization', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<span>Some content</span>'
+ ].join(''),
+ sanitizeFn: function (input) {
+ return input
+ }
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+
+ assert.ok(tooltip.options.template.indexOf('span') !== -1)
+ })
+
+ QUnit.test('should allow passing aria attributes', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<span aria-pressed="true">Some content</span>'
+ ].join('')
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+
+ assert.ok(tooltip.options.template.indexOf('aria-pressed') !== -1)
+ })
+
+ QUnit.test('should not take into account sanitize in data attributes', function (assert) {
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ assert.expect(0)
+
+ return
+ }
+
+ assert.expect(1)
+
+ var $trigger = $('<a href="#" rel="tooltip" data-sanitize="false" data-trigger="click" title="Another tooltip"/>')
+ .appendTo('#qunit-fixture')
+ .bootstrapTooltip({
+ template: [
+ '<span aria-pressed="true">Some content</span>'
+ ].join('')
+ })
+
+ var tooltip = $trigger.data('bs.tooltip')
+
+ assert.strictEqual(tooltip.options.sanitize, true)
+ })
})
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-
+function ($) {
'use strict';
+ var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
+
+ var uriAttrs = [
+ 'background',
+ 'cite',
+ 'href',
+ 'itemtype',
+ 'longdesc',
+ 'poster',
+ 'src',
+ 'xlink:href'
+ ]
+
+ var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
+
+ var DefaultWhitelist = {
+ // Global attributes allowed on any supplied element below.
+ '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
+ a: ['target', 'href', 'title', 'rel'],
+ area: [],
+ b: [],
+ br: [],
+ col: [],
+ code: [],
+ div: [],
+ em: [],
+ hr: [],
+ h1: [],
+ h2: [],
+ h3: [],
+ h4: [],
+ h5: [],
+ h6: [],
+ i: [],
+ img: ['src', 'alt', 'title', 'width', 'height'],
+ li: [],
+ ol: [],
+ p: [],
+ pre: [],
+ s: [],
+ small: [],
+ span: [],
+ sub: [],
+ sup: [],
+ strong: [],
+ u: [],
+ ul: []
+ }
+
+ /**
+ * A pattern that recognizes a commonly useful subset of URLs that are safe.
+ *
+ * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+ */
+ var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
+
+ /**
+ * A pattern that matches safe data URLs. Only matches image, video and audio types.
+ *
+ * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+ */
+ var 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
+
+ function allowedAttribute(attr, allowedAttributeList) {
+ const attrName = attr.nodeName.toLowerCase()
+
+ if ($.inArray(attrName, allowedAttributeList) !== -1) {
+ if ($.inArray(attrName, uriAttrs) !== -1) {
+ return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
+ }
+
+ return true
+ }
+
+ var regExp = $(allowedAttributeList).filter(function (index, value) {
+ return value instanceof RegExp
+ })
+
+ // Check if a regular expression validates the attribute.
+ for (var i = 0, l = regExp.length; i < l; i++) {
+ if (attrName.match(regExp[i])) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
+ if (unsafeHtml.length === 0) {
+ return unsafeHtml
+ }
+
+ if (sanitizeFn && typeof sanitizeFn === 'function') {
+ return sanitizeFn(unsafeHtml)
+ }
+
+ // IE 8 and below don't support createHTMLDocument
+ if (!document.implementation || !document.implementation.createHTMLDocument) {
+ return unsafeHtml
+ }
+
+ var createdDocument = document.implementation.createHTMLDocument('sanitization')
+ createdDocument.body.innerHTML = unsafeHtml
+
+ var whitelistKeys = Object.keys(whiteList)
+ var elements = $(createdDocument.body).find('*')
+
+ for (var i = 0, len = elements.length; i < len; i++) {
+ var el = elements[i]
+ var elName = el.nodeName.toLowerCase()
+
+ if ($.inArray(elName, whitelistKeys) === -1) {
+ el.parentNode.removeChild(el)
+
+ continue
+ }
+
+ var attributeList = $.map(el.attributes, function (el) { return el })
+ var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
+
+ attributeList.forEach((attr) => {
+ if (!allowedAttribute(attr, whitelistedAttributes)) {
+ el.removeAttribute(attr.nodeName)
+ }
+ })
+ }
+
+ return createdDocument.body.innerHTML
+ }
+
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
viewport: {
selector: 'body',
padding: 0
- }
+ },
+ sanitize : true,
+ sanitizeFn : null,
+ whiteList : DefaultWhitelist
}
Tooltip.prototype.init = function (type, element, options) {
}
Tooltip.prototype.getOptions = function (options) {
- options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+ const dataAttributes = this.$element.data()
+
+ for (var dataAttr in dataAttributes) {
+ if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
+ delete dataAttributes[dataAttr]
+ }
+ }
+
+ options = $.extend({}, this.getDefaults(), dataAttributes, options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
}
}
+ if (options.sanitize) {
+ config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
+ }
+
return options
}
var $tip = this.tip()
var title = this.getTitle()
- $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+ if (this.options.html) {
+ if (this.options.sanitize) {
+ title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn)
+ }
+
+ $tip.find('.tooltip-inner').html(title)
+ } else {
+ $tip.find('.tooltip-inner').text(title)
+ }
+
$tip.removeClass('fade in top bottom left right')
}
})
}
+ Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
+ return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
+ }
// TOOLTIP PLUGIN DEFINITION
// =========================