| 1 |
// Utilities for Photonic |
| 2 |
export const hasClass = (element, className) => { |
| 3 |
if (element.classList) { |
| 4 |
return element.classList.contains(className); |
| 5 |
} |
| 6 |
else { |
| 7 |
return new RegExp('(^| )' + className + '( |$)', 'gi').test(element.className); |
| 8 |
} |
| 9 |
}; |
| 10 |
|
| 11 |
function ajax(method, url, args, callback) { |
| 12 |
const xhr = new XMLHttpRequest(); |
| 13 |
xhr.open(method, url); |
| 14 |
xhr.onreadystatechange = function() { |
| 15 |
if (xhr.readyState === 4) { |
| 16 |
if (xhr.status === 200) { |
| 17 |
const data = xhr.responseText; |
| 18 |
callback(data); |
| 19 |
} |
| 20 |
} |
| 21 |
}; |
| 22 |
let form = new FormData(); |
| 23 |
for (const [key, value] of Object.entries(args)) { |
| 24 |
form.append(key, value); |
| 25 |
} |
| 26 |
xhr.send(form); |
| 27 |
} |
| 28 |
|
| 29 |
export const post = (url, args, callback) => { |
| 30 |
ajax('POST', url, args, callback); |
| 31 |
}; |
| 32 |
|
| 33 |
export const get = (url, args, callback) => { |
| 34 |
ajax('GET', url, args, callback); |
| 35 |
}; |
| 36 |
|
| 37 |
export const next = (elem, selector) => { |
| 38 |
let sibling = elem.nextElementSibling; |
| 39 |
|
| 40 |
if (!selector) return sibling; |
| 41 |
while (sibling) { |
| 42 |
if (sibling.matches(selector)) return sibling; |
| 43 |
sibling = sibling.nextElementSibling; |
| 44 |
} |
| 45 |
}; |
| 46 |
|
| 47 |
export const getElement = value => { |
| 48 |
const parser = new DOMParser(); |
| 49 |
const doc = parser.parseFromString(value, 'text/html'); |
| 50 |
return doc.body; |
| 51 |
}; |
| 52 |
|
| 53 |
export const getText = value => { |
| 54 |
// Not using innerHTML because of vulnerability to XSS |
| 55 |
if (value == null) { |
| 56 |
return ''; |
| 57 |
} |
| 58 |
return value.replace(/<[^>]+>/g, ''); |
| 59 |
}; |
| 60 |
|
| 61 |
export const slideUpDown = (element, state) => { |
| 62 |
if (element != null && element.classList) { |
| 63 |
if (!element.classList.contains('photonic-can-slide')) { |
| 64 |
element.classList.add('photonic-can-slide'); |
| 65 |
} |
| 66 |
if ('show' === state) { |
| 67 |
element.classList.remove('photonic-can-slide-hide'); |
| 68 |
element.style.height = `${element.scrollHeight}px`; |
| 69 |
} |
| 70 |
else { |
| 71 |
element.classList.add('photonic-can-slide-hide'); |
| 72 |
element.style.height = 0 |
| 73 |
} |
| 74 |
} |
| 75 |
}; |
| 76 |
|
| 77 |
export const slideUpTitle = (element, state) => { |
| 78 |
if (element && element.classList) { |
| 79 |
if ('show' === state) { |
| 80 |
let currentPadding = 0; |
| 81 |
if (element.offsetHeight) { |
| 82 |
currentPadding = parseInt(getComputedStyle(element).paddingTop.slice(0, -2)) * 2; |
| 83 |
} |
| 84 |
element.style.height = (element.scrollHeight + 6 - currentPadding) + 'px'; |
| 85 |
element.classList.add('slideup-show'); |
| 86 |
} |
| 87 |
else { |
| 88 |
element.style.height = ''; |
| 89 |
element.classList.remove('slideup-show'); |
| 90 |
} |
| 91 |
} |
| 92 |
} |
| 93 |
|
| 94 |
export const fadeIn = (el) => { |
| 95 |
if (!hasClass(el, 'fade-in')) { |
| 96 |
el.style.display = 'block'; |
| 97 |
el.style.visibility = 'visible'; |
| 98 |
el.classList.add('fade-in'); |
| 99 |
} |
| 100 |
} |
| 101 |
|
| 102 |
export const fadeOut = (el, duration) => { |
| 103 |
let s = el.style, |
| 104 |
step = 25/(duration || 500); |
| 105 |
s.opacity = s.opacity || 1; |
| 106 |
(function fade() { |
| 107 |
s.opacity -= step; |
| 108 |
if (s.opacity < 0) { |
| 109 |
s.display = "none"; |
| 110 |
el.classList.remove('fade-in'); |
| 111 |
} |
| 112 |
else { |
| 113 |
setTimeout(fade, 25); |
| 114 |
} |
| 115 |
})(); |
| 116 |
} |
| 117 |
|
| 118 |
// get the default display style of an element |
| 119 |
const defaultDisplay = tag => { |
| 120 |
const iframe = document.createElement('iframe'); |
| 121 |
iframe.setAttribute('frameborder', 0); |
| 122 |
iframe.setAttribute('width', 0); |
| 123 |
iframe.setAttribute('height', 0); |
| 124 |
document.documentElement.appendChild(iframe); |
| 125 |
|
| 126 |
const doc = (iframe.contentWindow || iframe.contentDocument).document; |
| 127 |
|
| 128 |
// IE support |
| 129 |
doc.write(); |
| 130 |
doc.close(); |
| 131 |
|
| 132 |
const testEl = doc.createElement(tag); |
| 133 |
doc.documentElement.appendChild(testEl); |
| 134 |
const display = (window.getComputedStyle ? getComputedStyle(testEl, null) : testEl.currentStyle).display; |
| 135 |
iframe.parentNode.removeChild(iframe); |
| 136 |
return display; |
| 137 |
}; |
| 138 |
|
| 139 |
// actual show/hide function used by show() and hide() below |
| 140 |
const showHide = (el, show) => { |
| 141 |
let value = el.getAttribute('data-olddisplay'), |
| 142 |
display = el.style.display, |
| 143 |
computedDisplay = (window.getComputedStyle ? getComputedStyle(el, null) : el.currentStyle).display; |
| 144 |
|
| 145 |
if (show) { |
| 146 |
if (!value && display === 'none') el.style.display = ''; |
| 147 |
if (el.style.display === '' && (computedDisplay === 'none')) value = value || defaultDisplay(el.nodeName); |
| 148 |
} |
| 149 |
else { |
| 150 |
if (display && display !== 'none' || !(computedDisplay === 'none')) |
| 151 |
el.setAttribute('data-olddisplay', (computedDisplay === 'none') ? display : computedDisplay); |
| 152 |
} |
| 153 |
if (!show || el.style.display === 'none' || el.style.display === '') |
| 154 |
el.style.display = show ? value || '' : 'none'; |
| 155 |
}; |
| 156 |
|
| 157 |
// helper functions |
| 158 |
export const show = (el) => showHide(el, true); |
| 159 |
export const hide = (el) => showHide(el); |
| 160 |
|
| 161 |
//JavaScript HTML Sanitizer v2.0.3, (c) Alexander Yumashev, Jitbit Software. |
| 162 |
//homepage https://github.com/jitbit/HtmlSanitizer |
| 163 |
//License: MIT https://github.com/jitbit/HtmlSanitizer/blob/master/LICENSE |
| 164 |
export const HTMLSanitizer = new (function () { |
| 165 |
const _tagWhitelist = { |
| 166 |
'A': true, 'ABBR': true, 'B': true, 'BLOCKQUOTE': true, 'BODY': true, 'BR': true, 'CENTER': true, 'CODE': true, 'DD': true, 'DIV': true, 'DL': true, 'DT': true, 'EM': true, 'FONT': true, |
| 167 |
'H1': true, 'H2': true, 'H3': true, 'H4': true, 'H5': true, 'H6': true, 'HR': true, 'I': true, 'IMG': true, 'LABEL': true, 'LI': true, 'OL': true, 'P': true, 'PRE': true, |
| 168 |
'SMALL': true, 'SOURCE': true, 'SPAN': true, 'STRONG': true, 'SUB': true, 'SUP': true, 'TABLE': true, 'TBODY': true, 'TR': true, 'TD': true, 'TH': true, 'THEAD': true, 'UL': true, 'U': true, 'VIDEO': true |
| 169 |
}; |
| 170 |
|
| 171 |
const _contentTagWhiteList = { 'FORM': true, 'GOOGLE-SHEETS-HTML-ORIGIN': true }; //tags that will be converted to DIVs |
| 172 |
const _attributeWhitelist = { 'align': true, 'color': true, 'controls': true, 'height': true, 'href': true, 'id': true, 'src': true, 'style': true, 'target': true, 'title': true, 'type': true, 'width': true }; |
| 173 |
const _cssWhitelist = { 'background-color': true, 'color': true, 'font-size': true, 'font-weight': true, 'text-align': true, 'text-decoration': true, 'width': true }; |
| 174 |
const _schemaWhiteList = [ 'http:', 'https:', 'data:', 'm-files:', 'file:', 'ftp:', 'mailto:', 'pw:' ]; //which "protocols" are allowed in "href", "src" etc |
| 175 |
const _uriAttributes = { 'href': true, 'action': true }; |
| 176 |
const _parser = new DOMParser(); |
| 177 |
|
| 178 |
this.SanitizeHTML = (input, extraSelector) => { |
| 179 |
if (input == null) return null; |
| 180 |
|
| 181 |
input = input.trim(); |
| 182 |
if (input === "") return ""; //to save performance |
| 183 |
|
| 184 |
//firefox "bogus node" workaround for wysiwyg's |
| 185 |
if (input === "<br>") return ""; |
| 186 |
|
| 187 |
if (input.indexOf("<body") === -1) input = "<body>" + input + "</body>"; //add "body" otherwise some tags are skipped, like <style> |
| 188 |
|
| 189 |
let doc = _parser.parseFromString(input, "text/html"); |
| 190 |
|
| 191 |
//DOM clobbering check (damn you firefox) |
| 192 |
if (doc.body.tagName !== 'BODY') |
| 193 |
doc.body.remove(); |
| 194 |
if (typeof doc.createElement !== 'function') |
| 195 |
doc.createElement.remove(); |
| 196 |
|
| 197 |
function makeSanitizedCopy(node) { |
| 198 |
let newNode; |
| 199 |
if (node.nodeType === Node.TEXT_NODE) { |
| 200 |
newNode = node.cloneNode(true); |
| 201 |
} else if (node.nodeType === Node.ELEMENT_NODE && (_tagWhitelist[node.tagName] || _contentTagWhiteList[node.tagName] || (extraSelector && node.matches(extraSelector)))) { //is tag allowed? |
| 202 |
|
| 203 |
if (_contentTagWhiteList[node.tagName]) |
| 204 |
newNode = doc.createElement('DIV'); //convert to DIV |
| 205 |
else |
| 206 |
newNode = doc.createElement(node.tagName); |
| 207 |
|
| 208 |
for (let i = 0; i < node.attributes.length; i++) { |
| 209 |
let attr = node.attributes[i]; |
| 210 |
if (_attributeWhitelist[attr.name]) { |
| 211 |
if (attr.name === "style") { |
| 212 |
for (let s = 0; s < node.style.length; s++) { |
| 213 |
let styleName = node.style[s]; |
| 214 |
if (_cssWhitelist[styleName]) |
| 215 |
newNode.style.setProperty(styleName, node.style.getPropertyValue(styleName)); |
| 216 |
} |
| 217 |
} |
| 218 |
else { |
| 219 |
if (_uriAttributes[attr.name]) { //if this is a "uri" attribute, that can have "javascript:" or something |
| 220 |
if (attr.value.indexOf(":") > -1 && !startsWithAny(attr.value, _schemaWhiteList)) |
| 221 |
continue; |
| 222 |
} |
| 223 |
newNode.setAttribute(attr.name, attr.value); |
| 224 |
} |
| 225 |
} |
| 226 |
} |
| 227 |
for (let i = 0; i < node.childNodes.length; i++) { |
| 228 |
let subCopy = makeSanitizedCopy(node.childNodes[i]); |
| 229 |
newNode.appendChild(subCopy, false); |
| 230 |
} |
| 231 |
|
| 232 |
//remove useless empty spans (lots of those when pasting from MS Outlook) |
| 233 |
if ((newNode.tagName === "SPAN" || newNode.tagName === "B" || newNode.tagName === "I" || newNode.tagName === "U") |
| 234 |
&& newNode.innerHTML.trim() === "") { |
| 235 |
return doc.createDocumentFragment(); |
| 236 |
} |
| 237 |
|
| 238 |
} else { |
| 239 |
newNode = doc.createDocumentFragment(); |
| 240 |
} |
| 241 |
return newNode; |
| 242 |
} |
| 243 |
|
| 244 |
let resultElement = makeSanitizedCopy(doc.body); |
| 245 |
|
| 246 |
return resultElement.innerHTML |
| 247 |
.replace(/div><div/g, "div>\n<div"); //replace is just for cleaner code |
| 248 |
} |
| 249 |
|
| 250 |
function startsWithAny(str, substrings) { |
| 251 |
for (let i = 0; i < substrings.length; i++) { |
| 252 |
if (str.indexOf(substrings[i]) === 0) { |
| 253 |
return true; |
| 254 |
} |
| 255 |
} |
| 256 |
return false; |
| 257 |
} |
| 258 |
|
| 259 |
this.AllowedTags = _tagWhitelist; |
| 260 |
this.AllowedAttributes = _attributeWhitelist; |
| 261 |
this.AllowedCssStyles = _cssWhitelist; |
| 262 |
this.AllowedSchemas = _schemaWhiteList; |
| 263 |
}); |
| 264 |
|