Commit e0eed9a0 authored by Romain Courteaud's avatar Romain Courteaud

erp5_core: add a html viewer gadget

This gadget take an HTML string as parameter.

It first cleans it up (with hardcoded behaviour currently) by dropping unknown tag elements, unknown/unsafe tag attributes.
It is another protection layer on top of asStrippedHTML inside ERP5.

Then, it displays the output HTML and style it with an hardcoded set of rules.
parent 28c0a7fa
div[data-gadget-url$="gadget_html_viewer.html"] {
max-width: 50em;
display: block;
word-wrap: break-word;
font-family: Cambria, "Hoefler Text", Utopia, "Liberation Serif", "Nimbus Roman No9 L Regular", Times, "Times New Roman", serif;
text-align: justify;
}
div[data-gadget-url$="gadget_html_viewer.html"] canvas,
div[data-gadget-url$="gadget_html_viewer.html"] img,
div[data-gadget-url$="gadget_html_viewer.html"] iframe,
div[data-gadget-url$="gadget_html_viewer.html"] svg {
max-width: 100%;
max-height: 100vh;
}
div[data-gadget-url$="gadget_html_viewer.html"] video {
max-width: 100%;
height: auto;
max-height: 100vh;
}
div[data-gadget-url$="gadget_html_viewer.html"] h1 {
font-family: Cambria, "Hoefler Text", Utopia, "Liberation Serif", "Nimbus Roman No9 L Regular", Times, "Times New Roman", serif;
font-size: 1.5em;
text-transform: capitalize;
text-align: center;
}
div[data-gadget-url$="gadget_html_viewer.html"] h2 {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 1.3em;
text-transform: capitalize;
}
div[data-gadget-url$="gadget_html_viewer.html"] h3 {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 1.1em;
text-transform: uppercase;
}
div[data-gadget-url$="gadget_html_viewer.html"] h4 {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 1.1em;
}
div[data-gadget-url$="gadget_html_viewer.html"] blockquote {
margin: 6pt 6pt 6pt 0;
padding-left: 6pt;
border-left: 2px solid #0E81C2;
}
div[data-gadget-url$="gadget_html_viewer.html"] i,
div[data-gadget-url$="gadget_html_viewer.html"] cite,
div[data-gadget-url$="gadget_html_viewer.html"] em,
div[data-gadget-url$="gadget_html_viewer.html"] var,
div[data-gadget-url$="gadget_html_viewer.html"] address,
div[data-gadget-url$="gadget_html_viewer.html"] dfn {
font-style: italic;
}
div[data-gadget-url$="gadget_html_viewer.html"] strong,
div[data-gadget-url$="gadget_html_viewer.html"] b,
div[data-gadget-url$="gadget_html_viewer.html"] figcaption {
font-weight: 600;
}
div[data-gadget-url$="gadget_html_viewer.html"] u,
div[data-gadget-url$="gadget_html_viewer.html"] ins {
text-decoration: underline;
}
div[data-gadget-url$="gadget_html_viewer.html"] s,
div[data-gadget-url$="gadget_html_viewer.html"] strike,
div[data-gadget-url$="gadget_html_viewer.html"] del {
text-decoration: line-through;
}
div[data-gadget-url$="gadget_html_viewer.html"] tt,
div[data-gadget-url$="gadget_html_viewer.html"] code,
div[data-gadget-url$="gadget_html_viewer.html"] kbd,
div[data-gadget-url$="gadget_html_viewer.html"] samp {
font-family: "Courier New", Courier, monospace;
}
div[data-gadget-url$="gadget_html_viewer.html"] code,
div[data-gadget-url$="gadget_html_viewer.html"] kbd {
color: #2CC32C;
}
div[data-gadget-url$="gadget_html_viewer.html"] q {
display: inline;
quotes: initial;
}
div[data-gadget-url$="gadget_html_viewer.html"] q:before {
content: open-quote;
}
div[data-gadget-url$="gadget_html_viewer.html"] q:after {
content: close-quote;
}
div[data-gadget-url$="gadget_html_viewer.html"] pre,
div[data-gadget-url$="gadget_html_viewer.html"] xmp,
div[data-gadget-url$="gadget_html_viewer.html"] plaintext,
div[data-gadget-url$="gadget_html_viewer.html"] listing {
display: block;
white-space: pre-wrap;
font-family: "Courier New", Courier, monospace;
}
div[data-gadget-url$="gadget_html_viewer.html"] table {
border: 1px solid #1F1F1F;
width: 100%;
margin: 0;
padding: 0;
border-collapse: collapse;
border-spacing: 0;
}
div[data-gadget-url$="gadget_html_viewer.html"] table tr {
border: 1px solid #1F1F1F;
padding-top: 6pt;
padding-bottom: 6pt;
}
div[data-gadget-url$="gadget_html_viewer.html"] table th,
div[data-gadget-url$="gadget_html_viewer.html"] table td {
text-align: center;
padding-top: 6pt;
padding-bottom: 6pt;
}
div[data-gadget-url$="gadget_html_viewer.html"] table th {
text-transform: uppercase;
}
div[data-gadget-url$="gadget_html_viewer.html"] ul {
list-style: disc;
}
div[data-gadget-url$="gadget_html_viewer.html"] ul li {
margin-left: 2em;
}
div[data-gadget-url$="gadget_html_viewer.html"] ol {
list-style: decimal;
}
div[data-gadget-url$="gadget_html_viewer.html"] ol li {
margin-left: 2em;
}
div[data-gadget-url$="gadget_html_viewer.html"] dl {
display: grid;
grid-template-columns: max-content auto;
}
div[data-gadget-url$="gadget_html_viewer.html"] dl dt {
grid-column-start: 1;
}
div[data-gadget-url$="gadget_html_viewer.html"] dl dd,
div[data-gadget-url$="gadget_html_viewer.html"] dl dl {
grid-column-start: 2;
}
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_html_viewer.css</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/css</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>gadget_html_viewer.css</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML viewer gadget</title>
<link rel="http://www.renderjs.org/rel/interface" href="interface_editor.html">
<link rel="stylesheet" href="gadget_html_viewer.css">
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="domsugar.js"></script>
<script src="gadget_html_viewer.js"></script>
</head>
<body>
</body>
</html>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_html_viewer.html</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
/*jslint nomen: true, indent: 2 */
/*global window, rJS, domsugar, document, DOMParser, NodeFilter*/
(function (window, rJS, domsugar, document, DOMParser, NodeFilter) {
"use strict";
function startsWithOneOf(str, prefix_list) {
var i;
for (i = prefix_list.length - 1; i >= 0; i -= 1) {
if (str.substr(0, prefix_list[i].length) === prefix_list[i]) {
return true;
}
}
return false;
}
var whitelist = {
node_list: {
BODY: true,
P: true,
H1: true,
H2: true,
H3: true,
H4: true,
H5: true,
H6: true,
UL: true,
OL: true,
LI: true,
DL: true,
DT: true,
DD: true,
BLOCKQUOTE: true,
Q: true,
I: true,
B: true,
CITE: true,
EM: true,
VAR: true,
ADDRESS: true,
DFN: true,
U: true,
INS: true,
S: true,
STRIKE: true,
DEL: true,
SUP: true,
SUB: true,
MARK: true,
TT: true,
PRE: true,
CODE: true,
KBD: true,
SAMP: true,
STRONG: true,
SMALL: true,
A: true,
HR: true,
TABLE: true,
THEAD: true,
TFOOT: true,
TR: true,
TH: true,
TD: true,
BR: true,
IMG: true,
FIGURE: true,
FIGCAPTION: true,
PICTURE: true,
SOURCE: true,
TIME: true,
ARTICLE: true,
ASIDE: true,
NAV: true,
FOOTER: true
},
attribute_list: {
alt: true,
rel: true,
href: true,
src: true,
srcset: true,
media: true,
datetime: true,
class: true
},
link_node_list: {
A: true,
IMG: true,
FIGURE: true,
PICTURE: true
},
link_list: {
href: true,
src: true,
srcset: true
}
},
emptylist = {
BR: true,
HR: true
},
blacklist = {
SCRIPT: true,
STYLE: true,
NOSCRIPT: true,
FORM: true,
FIELDSET: true,
INPUT: true,
SELECT: true,
TEXTAREA: true,
BUTTON: true,
IFRAME: true,
SVG: true
};
function keepOnlyChildren(current_node) {
var fragment = document.createDocumentFragment();
while (current_node.firstChild) {
fragment.appendChild(current_node.firstChild);
}
current_node.parentNode.replaceChild(
fragment,
current_node
);
}
function cleanup(html) {
var html_doc = (new DOMParser()).parseFromString(html,
'text/html'),
iterator,
current_node,
attribute,
attribute_list,
len,
link_len,
already_dropped,
finished = false;
iterator = document.createNodeIterator(
html_doc.body,
NodeFilter.SHOW_ELEMENT,
function () {
return NodeFilter.FILTER_ACCEPT;
}
);
while (!finished) {
current_node = iterator.nextNode();
finished = (current_node === null);
if (!finished) {
if (blacklist[current_node.nodeName]) {
// Drop element
current_node.parentNode.removeChild(current_node);
} else if (!whitelist.node_list[current_node.nodeName]) {
// Only keep children
keepOnlyChildren(current_node);
} else {
// Cleanup attributes
attribute_list = current_node.attributes;
len = attribute_list.length;
while (len !== 0) {
len = len - 1;
attribute = attribute_list[len].name;
if (!whitelist.attribute_list[attribute]) {
current_node.removeAttribute(attribute);
}
}
// Cleanup links
attribute_list = current_node.attributes;
len = attribute_list.length;
link_len = 0;
already_dropped = false;
while (len !== 0) {
len = len - 1;
attribute = attribute_list[len].name;
if (whitelist.link_list[attribute]) {
if (startsWithOneOf(current_node.getAttribute(attribute),
['http://', 'https://', '//', 'data:'])) {
link_len += 1;
} else {
keepOnlyChildren(current_node);
already_dropped = true;
break;
}
}
}
// Lazy img load
if (current_node.nodeName === 'IMG') {
current_node.setAttribute('loading', 'lazy');
}
// Drop link node without url
if (whitelist.link_node_list[current_node.nodeName]) {
if ((link_len === 0) && (!already_dropped)) {
already_dropped = true;
keepOnlyChildren(current_node);
}
}
// Drop element if no text or link
if ((link_len === 0) && (!already_dropped) &&
(!current_node.textContent) &&
(!emptylist[current_node.nodeName])) {
current_node.parentNode.removeChild(current_node);
}
}
}
}
return html_doc.querySelector('body') || domsugar(null);
}
rJS(window)
.declareMethod('render', function (options) {
domsugar(this.element, Array.from(cleanup(options.value || '').childNodes));
});
}(window, rJS, domsugar, document, DOMParser, NodeFilter));
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_html_viewer.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
@sans-serif: 'Open Sans', Helvetica, Arial, sans-serif;
@monospace: "Courier New", Courier, monospace;
@serif: Cambria, "Hoefler Text", Utopia, "Liberation Serif", "Nimbus Roman No9 L Regular", Times, "Times New Roman", serif;
@margin-size: 6pt;
@border-size: 2px;
@border-type: solid;
@colorsubheaderbackground: #0E81C2;
@bold-font-weight: 600;
@black: #1F1F1F;
@colorforeground: @black;
div[data-gadget-url$="gadget_html_viewer.html"] {
max-width: 50em;
display: block;
word-wrap: break-word;
font-family: @serif;
text-align: justify;
canvas, img, iframe, svg {
max-width: 100%;
max-height: 100vh;
}
video {
max-width: 100%;
height: auto;
max-height: 100vh;
}
h1 {
font-family: @serif;
font-size: 1.5em;
text-transform: capitalize;
text-align: center;
}
h2 {
font-family: @sans-serif;
font-size: 1.3em;
text-transform: capitalize;
}
h3 {
font-family: @sans-serif;
font-size: 1.1em;
text-transform: uppercase;
}
h4 {
font-family: @sans-serif;
font-size: 1.1em;
}
blockquote {
margin: @margin-size @margin-size @margin-size 0;
padding-left: @margin-size;
border-left: @border-size @border-type @colorsubheaderbackground;
}
i, cite, em, var, address, dfn {
font-style: italic;
}
strong, b, figcaption {
font-weight: @bold-font-weight;
}
u, ins {
text-decoration: underline;
}
s, strike, del {
text-decoration: line-through;
}
tt, code, kbd, samp {
font-family: @monospace;
}
code, kbd {
color: #2CC32C;
}
q {
display: inline;
&:before {
content: open-quote;
}
&:after {
content: close-quote;
}
quotes: initial;
}
pre, xmp, plaintext, listing {
display: block;
white-space: pre-wrap;
font-family: @monospace;
}
table {
border: 1px solid @colorforeground;
width: 100%;
margin:0;
padding:0;
border-collapse: collapse;
border-spacing: 0;
tr {
border: 1px solid @colorforeground;
padding-top: @margin-size;
padding-bottom: @margin-size;
}
th, td {
text-align: center;
padding-top: @margin-size;
padding-bottom: @margin-size;
}
th {
text-transform: uppercase;
}
}
ul {
list-style: disc;
li {
margin-left: 2em;
}
}
ol {
list-style: decimal;
li {
margin-left: 2em;
}
}
dl {
display: grid;
grid-template-columns: max-content auto;
dt {
grid-column-start: 1;
}
dd, dl {
grid-column-start: 2;
}
}
}
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_html_viewer.less</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/plain</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment