Commit 3a1e130b 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.

erp5_core: viewer add css

erp5_core: cleanup the HTML

erp5_core: html viewer do not keep the body element

erp5_core: HARDCODED html viewer css example

erp5_core: viewer css

erp5_core: html viewer define the interface

erp5_core: html viewer css

erp5_core: viewer ensure to return a dom element
parent afff175b
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