integrate marked as markdown parser
authorThomas Lamprecht <t.lamprecht@proxmox.com>
Fri, 18 Jun 2021 12:57:20 +0000 (14:57 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Fri, 18 Jun 2021 13:19:29 +0000 (15:19 +0200)
Define our own, rather minimal interface so that we change the parser
under the hood if ever needed, I already did so once during
evaluating this, as first I checked out Snarkdown[0], which is really
nice for the few lines of code it needs, but is a bit to limited for
the use case.

Currently marked[1] is used, provided by the libjs-marked Debian
package.

For now statically link the marked parser in on built time to avoid
the need to add new directories to serve in our pve/pmg/pbs proxies.
This is a bit ugly but can be cleaned up afterwards transparently
too.

We sanitize the produced HTML ourselves (most MD JS parser/renderer
don't do that) by creating a real, but not active, DOM tree and
recursively prune bad nodes/attrs from it and let it spit out HTML
again at the end. While a tad inefficient it really won't matter for
our use case, as the notes/comments we render are only a few KiB of
text and it's done on the client side anyway.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
debian/control
debian/copyright
src/Makefile
src/Parser.js [new file with mode: 0644]
src/css/ext6-pmx.css

index ad88929..f0794fa 100644 (file)
@@ -3,6 +3,7 @@ Section: web
 Priority: optional
 Maintainer: Proxmox Support Team <support@proxmox.com>
 Build-Depends: debhelper (>= 12~),
+               libjs-marked,
                pve-eslint (>= 7.12.1-1),
 Standards-Version: 4.5.1
 Homepage: https://www.proxmox.com
index 2f3455d..e7310c2 100644 (file)
@@ -15,3 +15,12 @@ License: AGPLv3
  .
  You should have received a copy of the GNU Affero General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Marked:
+ The Marked JavaScript library is shipped through linkage from the Debian
+ package unmodified alongside proxmox-widget-toolkit.
+
+ Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) Copyright (c)
+ 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
+
+ For the license and copyright details see `/usr/share/doc/libjs-marked/copyright`
index afbe2ec..37da480 100644 (file)
@@ -2,10 +2,15 @@ include defines.mk
 
 SUBDIRS= css images
 
+# bundle it for now from the libjs-marked debian package to avoid touching our proxies file mapper,
+# we could also just ship a link to the packages file and load from same path as the widget-toolkit
+MARKEDJS=/usr/share/javascript/marked/marked.min.js
+
 JSSRC=                                 \
        Utils.js                        \
        Toolkit.js                      \
        Logo.js                         \
+       Parser.js                       \
        mixin/CBind.js                  \
        data/reader/JsonObject.js       \
        data/ProxmoxProxy.js            \
@@ -94,7 +99,7 @@ lint: ${JSSRC}
 proxmoxlib.js: .lint-incremental ${JSSRC}
        # add the version as comment in the file
        echo "// ${DEB_VERSION_UPSTREAM_REVISION}" > $@.tmp
-       cat ${JSSRC} >> $@.tmp
+       cat ${JSSRC} ${MARKEDJS}  >> $@.tmp
        mv $@.tmp $@
 
 install: proxmoxlib.js
diff --git a/src/Parser.js b/src/Parser.js
new file mode 100644 (file)
index 0000000..a5e9337
--- /dev/null
@@ -0,0 +1,45 @@
+// NOTE: just relays parsing to markedjs parser
+Ext.define('Proxmox.Markdown', {
+    alternateClassName: 'Px.Markdown', // just trying out something, do NOT copy this line
+    singleton: true,
+
+    // transforms HTML to a DOM tree and recursively descends and prunes every branch with a
+    // "bad" node.type and drops "bad" attributes from the remaining nodes.
+    // "bad" means anything which can do XSS or break the layout of the outer page
+    sanitizeHTML: function(input) {
+       if (!input) {
+           return input;
+       }
+       let _sanitize;
+       _sanitize = (node) => {
+           if (node.nodeType === 3) return;
+           if (node.nodeType !== 1 || /^(script|iframe|object|embed|svg)$/i.test(node.tagName)) {
+               node.remove();
+               return;
+           }
+           for (let i=node.attributes.length; i--;) {
+               const name = node.attributes[i].name;
+               // TODO: we may want to also disallow class and id attrs
+               if (!/^(class|id|name|href|src|alt|align|valign)$/i.test(name)) {
+                   node.attributes.removeNamedItem(name);
+               }
+           }
+           for (let i=node.childNodes.length; i--;) _sanitize(node.childNodes[i]);
+       };
+
+       const doc = new DOMParser().parseFromString(`<!DOCTYPE html><html><body>${input}`, 'text/html');
+       doc.normalize();
+
+       _sanitize(doc.body);
+
+       return doc.body.innerHTML;
+    },
+
+    parse: function(markdown) {
+       /*global marked*/
+       let unsafeHTML = marked(markdown);
+
+       return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
+    },
+
+});
index 0c83d8a..09ec11c 100644 (file)
@@ -127,3 +127,20 @@ div.right-aligned {
 .x-legend-inner {
     padding: 0;
 }
+
+/* rules for the markdown content, prefix with the .pmx-md class */
+.pmx-md code {
+    white-space: pre;
+    background-color: #f5f5f5;
+    padding: 1px;
+}
+
+.pmx-md pre code {
+    display: inline-block;
+    padding: 5px;
+    border-left: 3px solid #e0e0e0;
+}
+
+.pmx-md strong {
+    font-weight: bold;
+}