Explorar el Código

Logging, page overflow fix

Alan Colon hace 7 años
padre
commit
323de89d21
Se han modificado 7 ficheros con 807 adiciones y 76 borrados
  1. 94 0
      lib/sanitize.js
  2. 501 16
      package-lock.json
  3. 6 1
      package.json
  4. 0 0
      public/viewer.css
  5. 3 2
      public/viewer.html
  6. 15 5
      public/viewer.js
  7. 188 52
      server.js

+ 94 - 0
lib/sanitize.js

@@ -0,0 +1,94 @@
+const sanitizeHtml = require('sanitize-html')
+
+const sanitize = html => sanitizeHtml(html, {
+    allowedTags: [
+        // HTML
+        'a', 'abbr', 'acronym', 'address', /*'applet',*/ 'area', 'article', 'aside', /*'audio'*/, 'b', 'base', 'basefont', 'bdi',
+        'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
+        'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', /*'embed',*/ 'fieldset',
+        'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head',
+        'header', 'hr', 'html', 'i', /*'iframe',*/ 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main',
+        'map', 'mark', 'meta', 'meter', 'nav', 'noframes', 'noscript', /*'object',*/ 'ol', 'optgroup', 'option', 'output',
+        'p', /*'param',*/ 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', /*'script',*/ 'section', 'select',
+        'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template',
+        'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', /*'video',*/ 'wbr',
+
+        // SVG
+        'a', 'altGlyph', 'altGlyphDef', 'altGlyphItem', 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'circle',
+        'clipPath', 'color-profile', 'cursor', 'defs', 'desc', 'discard', 'ellipse', 'feBlend', 'feColorMatrix',
+        'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight',
+        'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode',
+        'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence', 'filter', 'font',
+        'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignObject', 'g', 'glyph',
+        'glyphRef', 'hatch', 'hatchpath', 'hkern', 'image', 'line', 'linearGradient', 'marker', 'mask', 'mesh', 'meshgradient',
+        'meshpatch', 'meshrow', 'metadata', 'missing-glyph', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient',
+        'rect', /*'script',*/ 'set', 'solidcolor', 'stop', 'style', 'svg', 'switch', 'symbol', 'text', 'textPath', 'title', 'tref',
+        'tspan', 'unknown', 'use', 'view', 'vkern'
+    ],
+    allowedAttributes: {
+        '*': [
+            // HTML
+            'accept', 'accept-charset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'autofocus', 'autoplay',
+            'bgcolor', 'border', 'charset', 'checked', 'cite', 'class', 'color', 'cols', 'colspan', 'content', 'contenteditable',
+            'controls', 'coords', 'data', 'data-*', 'datetime', 'default', 'defer', 'dir', 'dirname', 'disabled', 'download',
+            'draggable', 'dropzone', 'enctype', 'for', 'form', 'formaction', 'headers', 'height', 'hidden', 'high', 'href',
+            'hreflang', /*'http-equiv'*/, 'id', 'ismap', 'kind', 'label', 'lang', 'list', 'loop', 'low', 'max', 'maxlength', 'media',
+            'method', 'min', 'multiple', 'muted', 'name', 'novalidate', /*'onabort',*/ /*'onafterprint',*/ /*'onbeforeprint',*/
+            /*'onbeforeunload',*/ /*'onblur',*/ /*'oncanplay',*/ /*'oncanplaythrough',*/ /*'onchange',*/ /*'onclick',*/
+            /*'oncontextmenu',*/ /*'oncopy',*/ /*'oncuechange',*/ /*'oncut',*/ /*'ondblclick',*/ /*'ondrag',*/ /*'ondragend',*/
+            /*'ondragenter',*/ /*'ondragleave',*/ /*'ondragover',*/ /*'ondragstart',*/ /*'ondrop',*/ /*'ondurationchange',*/
+            /*'onemptied',*/ /*'onended',*/ /*'onerror',*/ /*'onfocus',*/ /*'onhashchange',*/ /*'oninput',*/ /*'oninvalid',*/
+            /*'onkeydown',*/ /*'onkeypress',*/ /*'onkeyup',*/ /*'onload',*/ /*'onloadeddata',*/ /*'onloadedmetadata',*/
+            /*'onloadstart',*/ /*'onmousedown',*/ /*'onmousemove',*/ /*'onmouseout',*/ /*'onmouseover',*/ /*'onmouseup',*/
+            /*'onmousewheel',*/ /*'onoffline',*/ /*'ononline',*/ /*'onpagehide',*/ /*'onpageshow',*/ /*'onpaste',*/ /*'onpause',*/
+            /*'onplay',*/ /*'onplaying',*/ /*'onpopstate',*/ /*'onprogress',*/ /*'onratechange',*/ /*'onreset',*/ /*'onresize',*/
+            /*'onscroll',*/ /*'onsearch',*/ /*'onseeked',*/ /*'onseeking',*/ /*'onselect',*/ /*'onstalled',*/ /*'onstorage',*/
+            /*'onsubmit',*/ /*'onsuspend',*/ /*'ontimeupdate',*/ /*'ontoggle',*/ /*'onunload',*/ /*'onvolumechange',*/ 
+            /*'onwaiting',*/ /*'onwheel',*/ 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'readonly', 'rel',
+            'required', 'reversed', 'rows', 'rowspan', 'sandbox', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'spellcheck',
+            'src', 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'style', 'tabindex', 'target', 'title', 'translate', 'type',
+            'usemap', 'value', 'width', 'wrap',
+
+            // SVG
+            'accent-height', 'accumulate', 'additive', 'alignment-baseline', 'allowReorder', 'alphabetic', 'amplitude',
+            'arabic-form', 'ascent', 'attributeName', 'attributeType', 'autoReverse', 'azimuth', 'baseFrequency', 'baseline-shift',
+            'baseProfile', 'bbox', 'begin', 'bias', 'by', 'calcMode', 'cap-height', 'class', 'clip', 'clipPathUnits', 'clip-path',
+            'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering',
+            'contentScriptType', 'contentStyleType', 'cursor', 'cx', 'cy', 'd', 'decelerate', 'descent', 'diffuseConstant',
+            'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'dx', 'dy', 'edgeMode', 'elevation', 'enable-background',
+            'end', 'exponent', 'externalResourcesRequired', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterRes', 'filterUnits',
+            'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style',
+            'font-variant', 'font-weight', 'format', 'from', 'fr', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyph-orientation-horizontal',
+            'glyph-orientation-vertical', 'glyphRef', 'gradientTransform', 'gradientUnits', 'hanging', 'height', 'href', 'hreflang',
+            'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3',
+            'k4', 'kernelMatrix', 'kernelUnitLength', 'kerning', 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'lengthAdjust',
+            'letter-spacing', 'lighting-color', 'limitingConeAngle', 'local', 'marker-end', 'marker-mid', 'marker-start',
+            'markerHeight', 'markerUnits', 'markerWidth', 'mask', 'maskContentUnits', 'maskUnits', 'mathematical', 'max', 'media',
+            'method', 'min', 'mode', 'name', 'numOctaves', 'offset', 'opacity', 'operator', 'order', 'orient', 'orientation', 'origin',
+            'overflow', 'overline-position', 'overline-thickness', 'panose-1', 'paint-order', 'path', 'pathLength',
+            'patternContentUnits', 'patternTransform', 'patternUnits', 'ping', 'pointer-events', 'points', 'pointsAtX', 'pointsAtY',
+            'pointsAtZ', 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits', 'r', 'radius', 'referrerPolicy', 'refX', 'refY', 
+            'rel', 'rendering-intent', 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', 'result',
+            'rotate', 'rx', 'ry', 'scale', 'seed', 'shape-rendering', 'slope', 'spacing', 'specularConstant', 'specularExponent',
+            'speed', 'spreadMethod', 'startOffset', 'stdDeviation', 'stemh', 'stemv', 'stitchTiles', 'stop-color', 'stop-opacity',
+            'strikethrough-position', 'strikethrough-thickness', 'string', 'stroke', 'stroke-dasharray', 'stroke-dashoffset',
+            'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'surfaceScale',
+            'systemLanguage', 'tabindex', 'tableValues', 'target', 'targetX', 'targetY', 'text-anchor', 'text-decoration',
+            'text-rendering', 'textLength', 'to', 'transform', 'type', 'u1', 'u2', 'underline-position', 'underline-thickness',
+            'unicode', 'unicode-bidi', 'unicode-range', 'units-per-em', 'v-alphabetic', 'v-hanging', 'v-ideographic',
+            'v-mathematical', 'values', 'vector-effect', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'viewBox',
+            'viewTarget', 'visibility', 'width', 'widths', 'word-spacing', 'writing-mode', 'x', 'x-height', 'x1', 'x2',
+            'xChannelSelector', 'xlink:actuate', 'xlink:arcrole', 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title',
+            'xlink:type', 'xml:base', 'xml:lang', 'xml:space', 'y', 'y1', 'y2', 'yChannelSelector', 'z', 'zoomAndPan'
+        ]
+    },
+    selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
+    allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'data' ],
+    allowedSchemesByTag: {},
+    allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
+    allowProtocolRelative: true,
+    allowedIframeHostnames: [] 
+})
+
+module.exports = sanitize
+

+ 501 - 16
package-lock.json

@@ -42,6 +42,14 @@
         "glob": "7.1.2"
       }
     },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "requires": {
+        "color-convert": "1.9.2"
+      }
+    },
     "append-field": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/append-field/-/append-field-0.1.0.tgz",
@@ -55,11 +63,21 @@
         "iftype": "3.0.2"
       }
     },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ="
+    },
     "array-flatten": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
       "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
     },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY="
+    },
     "asfs": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/asfs/-/asfs-2.0.3.tgz",
@@ -70,6 +88,16 @@
         "mkdirp": "0.5.1"
       }
     },
+    "async-array-reduce": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/async-array-reduce/-/async-array-reduce-0.2.1.tgz",
+      "integrity": "sha1-yL4BCitc0A3qlsgRFgNGk9/dgtE="
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0="
+    },
     "async-limiter": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
@@ -141,6 +169,34 @@
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
       "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
     },
+    "chalk": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+      "requires": {
+        "ansi-styles": "3.2.1",
+        "escape-string-regexp": "1.0.5",
+        "supports-color": "5.5.0"
+      }
+    },
+    "circular-json": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.5.tgz",
+      "integrity": "sha512-13YaR6kiz0kBNmIVM87Io8Hp7bWOo4r61vkEANy8iH9R9bc6avud/1FT0SBpqR1RpIQADOh/Q+yHZDA1iL6ysA=="
+    },
+    "color-convert": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz",
+      "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==",
+      "requires": {
+        "color-name": "1.1.1"
+      }
+    },
+    "color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok="
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -216,22 +272,6 @@
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
     },
-    "cors": {
-      "version": "2.8.4",
-      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
-      "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
-      "requires": {
-        "object-assign": "4.1.1",
-        "vary": "1.1.2"
-      },
-      "dependencies": {
-        "object-assign": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-          "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
-        }
-      }
-    },
     "data-urls": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.1.tgz",
@@ -242,6 +282,11 @@
         "whatwg-url": "7.0.0"
       }
     },
+    "date-format": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
+      "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg="
+    },
     "debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -250,6 +295,17 @@
         "ms": "2.0.0"
       }
     },
+    "delete": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/delete/-/delete-1.1.0.tgz",
+      "integrity": "sha512-bdhJatRNYsJnOhSRx9Eej3ABBtxQQw/uz2RprpYL5R3jCC2XMYVBcQWwvQLl+iNDk4LCLEKhdIP3uZSqRWi/tw==",
+      "requires": {
+        "async-each": "1.0.1",
+        "extend-shallow": "2.0.1",
+        "matched": "1.0.2",
+        "rimraf": "2.6.2"
+      }
+    },
     "depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -269,6 +325,44 @@
         "streamsearch": "0.1.2"
       }
     },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "requires": {
+        "domelementtype": "1.1.3",
+        "entities": "1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
+        }
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+      "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI="
+    },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "requires": {
+        "domelementtype": "1.3.0"
+      }
+    },
+    "domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "requires": {
+        "dom-serializer": "0.1.0",
+        "domelementtype": "1.3.0"
+      }
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -279,6 +373,16 @@
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
       "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
     },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
+    },
+    "env-obj": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/env-obj/-/env-obj-0.0.2.tgz",
+      "integrity": "sha1-ZIwOil2AOqnNQMbORXtAjjak494="
+    },
     "es6-promise": {
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
@@ -297,11 +401,24 @@
       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
       "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
     },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
     "etag": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
     },
+    "expand-tilde": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+      "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+      "requires": {
+        "homedir-polyfill": "1.0.1"
+      }
+    },
     "express": {
       "version": "4.16.3",
       "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
@@ -397,6 +514,22 @@
         }
       }
     },
+    "express-logging": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/express-logging/-/express-logging-1.1.1.tgz",
+      "integrity": "sha1-YoOWGMurW7NhDxocFIU1L+nSbCo=",
+      "requires": {
+        "on-headers": "1.0.1"
+      }
+    },
+    "extend-shallow": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+      "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+      "requires": {
+        "is-extendable": "0.1.1"
+      }
+    },
     "extract-zip": {
       "version": "1.6.7",
       "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
@@ -468,6 +601,96 @@
         "path-is-absolute": "1.0.1"
       }
     },
+    "global-modules": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+      "requires": {
+        "global-prefix": "1.0.2",
+        "is-windows": "1.0.2",
+        "resolve-dir": "1.0.1"
+      }
+    },
+    "global-prefix": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+      "requires": {
+        "expand-tilde": "2.0.2",
+        "homedir-polyfill": "1.0.1",
+        "ini": "1.3.5",
+        "is-windows": "1.0.2",
+        "which": "1.3.1"
+      }
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+    },
+    "has-glob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz",
+      "integrity": "sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=",
+      "requires": {
+        "is-glob": "3.1.0"
+      }
+    },
+    "homedir-polyfill": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
+      "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
+      "requires": {
+        "parse-passwd": "1.0.0"
+      }
+    },
+    "htmlencode": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz",
+      "integrity": "sha1-9+LWr74YqHp45jujMI51N2Z0Dj8="
+    },
+    "htmlparser2": {
+      "version": "3.9.2",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+      "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+      "requires": {
+        "domelementtype": "1.3.0",
+        "domhandler": "2.4.2",
+        "domutils": "1.7.0",
+        "entities": "1.1.1",
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.6"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "2.0.0",
+            "safe-buffer": "5.1.1",
+            "string_decoder": "1.1.1",
+            "util-deprecate": "1.0.2"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "requires": {
+            "safe-buffer": "5.1.1"
+          }
+        }
+      }
+    },
     "http-errors": {
       "version": "1.6.3",
       "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
@@ -528,21 +751,124 @@
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
       "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
     },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+    },
     "ipaddr.js": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
       "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
     },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+    },
+    "is-glob": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+      "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+      "requires": {
+        "is-extglob": "2.1.1"
+      }
+    },
+    "is-valid-glob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
+      "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao="
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
+    },
     "isarray": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
     },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "lodash": {
+      "version": "4.17.10",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+      "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
+    },
+    "lodash.escaperegexp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+      "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
+    },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+    },
+    "lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+    },
+    "lodash.mergewith": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ=="
+    },
     "lodash.sortby": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
       "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
     },
+    "log4js": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.5.tgz",
+      "integrity": "sha512-IX5c3G/7fuTtdr0JjOT2OIR12aTESVhsH6cEsijloYwKgcPRlO6DgOU72v0UFhWcoV1HN6+M3dwT89qVPLXm0w==",
+      "requires": {
+        "circular-json": "0.5.5",
+        "date-format": "1.2.0",
+        "debug": "3.1.0",
+        "rfdc": "1.1.2",
+        "streamroller": "0.7.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "matched": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/matched/-/matched-1.0.2.tgz",
+      "integrity": "sha512-7ivM1jFZVTOOS77QsR+TtYHH0ecdLclMkqbf5qiJdX2RorqfhsL65QHySPZgDE0ZjHoh+mQUNHTanNXIlzXd0Q==",
+      "requires": {
+        "arr-union": "3.1.0",
+        "async-array-reduce": "0.2.1",
+        "glob": "7.1.2",
+        "has-glob": "1.0.0",
+        "is-valid-glob": "1.0.0",
+        "resolve-dir": "1.0.1"
+      }
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -558,6 +884,16 @@
       "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
       "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
     },
+    "microservice-config": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/microservice-config/-/microservice-config-0.0.1.tgz",
+      "integrity": "sha1-YcGNgqOfoyr7aCYjEhlLWkd/F6U=",
+      "requires": {
+        "env-obj": "0.0.2",
+        "lodash": "4.17.10",
+        "subarg": "1.0.0"
+      }
+    },
     "mime": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
@@ -3347,6 +3683,11 @@
         }
       }
     },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
     "object-assign": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
@@ -3360,6 +3701,11 @@
         "ee-first": "1.1.1"
       }
     },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
+    },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3377,6 +3723,11 @@
         "wordwrap": "0.0.3"
       }
     },
+    "parse-passwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+      "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
+    },
     "parseurl": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@@ -3397,6 +3748,16 @@
       "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
       "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
     },
+    "postcss": {
+      "version": "6.0.23",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+      "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+      "requires": {
+        "chalk": "2.4.1",
+        "source-map": "0.6.1",
+        "supports-color": "5.5.0"
+      }
+    },
     "process-nextick-args": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
@@ -3496,6 +3857,20 @@
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
       "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
     },
+    "resolve-dir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+      "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+      "requires": {
+        "expand-tilde": "2.0.2",
+        "global-modules": "1.0.0"
+      }
+    },
+    "rfdc": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz",
+      "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA=="
+    },
     "rimraf": {
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
@@ -3514,6 +3889,23 @@
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
+    "sanitize-html": {
+      "version": "1.18.4",
+      "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.18.4.tgz",
+      "integrity": "sha512-hjyDYCYrQuhnEjq+5lenLlIfdPBtnZ7z0DkQOC8YGxvkuOInH+1SrkNTj30t4f2/SSv9c5kLniB+uCIpBvYuew==",
+      "requires": {
+        "chalk": "2.4.1",
+        "htmlparser2": "3.9.2",
+        "lodash.clonedeep": "4.5.0",
+        "lodash.escaperegexp": "4.1.2",
+        "lodash.isplainobject": "4.0.6",
+        "lodash.isstring": "4.0.1",
+        "lodash.mergewith": "4.6.1",
+        "postcss": "6.0.23",
+        "srcset": "1.0.0",
+        "xtend": "4.0.1"
+      }
+    },
     "send": {
       "version": "0.16.2",
       "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
@@ -3550,11 +3942,73 @@
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
       "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
     },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+    },
+    "srcset": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz",
+      "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=",
+      "requires": {
+        "array-uniq": "1.0.3",
+        "number-is-nan": "1.0.1"
+      }
+    },
     "statuses": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
       "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
     },
+    "streamroller": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
+      "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==",
+      "requires": {
+        "date-format": "1.2.0",
+        "debug": "3.1.0",
+        "mkdirp": "0.5.1",
+        "readable-stream": "2.3.6"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "2.0.0",
+            "safe-buffer": "5.1.1",
+            "string_decoder": "1.1.1",
+            "util-deprecate": "1.0.2"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "requires": {
+            "safe-buffer": "5.1.1"
+          }
+        }
+      }
+    },
     "streamsearch": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
@@ -3565,6 +4019,29 @@
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
       "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
     },
+    "subarg": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
+      "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
+      "requires": {
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "requires": {
+        "has-flag": "3.0.0"
+      }
+    },
     "tr46": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
@@ -3639,6 +4116,14 @@
         "webidl-conversions": "4.0.2"
       }
     },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "requires": {
+        "isexe": "2.0.0"
+      }
+    },
     "wordwrap": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",

+ 6 - 1
package.json

@@ -12,12 +12,17 @@
     "@alancnet/pdf2htmlex": "0.0.2",
     "asfs": "^2.0.3",
     "body-parser": "^1.18.3",
-    "cors": "^2.8.4",
     "data-urls": "^1.0.1",
+    "delete": "^1.1.0",
     "express": "^4.16.3",
+    "express-logging": "^1.1.1",
+    "htmlencode": "0.0.4",
+    "log4js": "^3.0.5",
+    "microservice-config": "0.0.1",
     "multer": "^1.3.1",
     "node-windows": "^0.1.14",
     "puppeteer": "^1.7.0",
+    "sanitize-html": "^1.18.4",
     "uuid": "^3.3.2"
   }
 }

+ 0 - 0
public/edit.css → public/viewer.css


+ 3 - 2
public/edit.html → public/viewer.html

@@ -1,5 +1,5 @@
-<script src="edit.js" type="text/javascript"></script>
-<link rel="stylesheet" href="edit.css" />
+<script src="viewer.js" type="text/javascript"></script>
+<link rel="stylesheet" href="viewer.css" />
 <link rel="stylesheet" href="https://toert.github.io/Isolated-Bootstrap/versions/4.1.0/iso_bootstrap4.1.0min.css" />
 <style>
     @media print {
@@ -25,4 +25,5 @@
 </div>
 <form style="display: none" action="save" method="POST" id="saveForm">
     <input type="hidden" name="html" id="htmlInput" />
+    <input type="hidden" name="filename" id="filenameInput" />
 </form>

+ 15 - 5
public/edit.js → public/viewer.js

@@ -1,10 +1,13 @@
 document.addEventListener('DOMContentLoaded', () => {
+    const viewerMode = document.getElementById('viewerMode').value
+    const filename = document.getElementById('filename').value
     const zoom = 0.25// From the pdf2htmlex parameter --zoom
     const zoomRatio = 1 / zoom
     const saveButton = document.getElementById('saveButton')
     const whiteoutButton = document.getElementById('whiteoutButton')
     const editTextButton = document.getElementById('editTextButton')
     const saveForm = document.getElementById('saveForm')
+    const filenameInput = document.getElementById('filenameInput')
     const htmlInput = document.getElementById('htmlInput')
     const pageContainer = document.getElementById('page-container')
     let host = null
@@ -34,20 +37,21 @@ document.addEventListener('DOMContentLoaded', () => {
     })
 
     saveButton.addEventListener('click', () => {
-        htmlInput.value = ""
+        htmlInput.value = ''
         htmlInput.value = document.documentElement.outerHTML
+        filenameInput.value = filename
         saveForm.submit()
-        htmlInput.value = ""
+        htmlInput.value = ''
     })
 
     const save = () => new Promise((resolve, reject) => {
         document.documentElement.classList.add('saving')
         const req = new XMLHttpRequest()
         req.open('POST', 'save', true)
-        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
+        req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
         req.responseType = 'arraybuffer'
-        htmlInput.value = ""
-        const body = `html=${encodeURIComponent(document.documentElement.outerHTML)}`
+        htmlInput.value = ''
+        const body = `filename=${encodeURIComponent(filename)}&html=${encodeURIComponent(document.documentElement.outerHTML)}`
 
         req.onload = () => {
             if (req.status > 299) {
@@ -267,4 +271,10 @@ document.addEventListener('DOMContentLoaded', () => {
         save,
         changed: false
     }
+
+    // Debugging purposes: Display page CSS rules
+    const rules = Array.from(document.styleSheets).filter(x => !x.href).map(x => Array.from(x.cssRules)).reduce((a, b) => [...a, ...b], [])
+    const pageRules = rules.filter(x => x.constructor.name == 'CSSPageRule').map(x => x.cssText + rules.filter(y => y.parentRule === x).map(x => x.cssText).join('\n')).join('\n')
+    console.log(pageRules)
+    console.log(rules.filter(x => x.cssText.length < 2000).map(x => x.cssText).join('\n'))
 })

+ 188 - 52
server.js

@@ -1,39 +1,115 @@
 const fs = require('fs')
 const asfs = require('asfs')
+const del = require('delete')
 const express = require('express')
 const multer = require('multer')
 const childProcess = require('child_process')
 const bodyParser = require('body-parser')
-const pdf2htmlexPath = `${__dirname}/node_modules/@alancnet/pdf2htmlex/bin-win/pdf2htmlEX.exe`
 const puppeteer = require('puppeteer')
 const uuid = require('uuid')
 const parseDataUrl = require('data-urls')
-const cors = require('cors')
 const path = require('path')
+const sanitize = require('./lib/sanitize')
+const expressLogging = require('express-logging')
+const log4js = require('log4js')
+const getConfig = require('microservice-config')
+const bytes = require('bytes')
+const {htmlEncode} = require('htmlencode')
+const pixelsPerPoint = 1.3333333333333333
 
-const chromePromise = puppeteer.launch()
+const config = getConfig({
+    port: process.env.PORT || 0,
+    logLevel: 'info',
+    tempPath: 'temp',
+    pdf2htmlexPath: `${__dirname}/node_modules/@alancnet/pdf2htmlex/bin-win/pdf2htmlEX.exe`,
+    fileSizeLimit: '10mb',
+    chromeUserDataDir: path.join(__dirname, 'temp/chrome')
+})
+
+
+const logger = log4js.getLogger('Server')
+log4js.getLogger().level = config.logLevel
+
+const chromePromise = (async () => {
+    if (await asfs.existsAsync(config.chromeUserDataDir)) {
+        logger.info('Cleaning Chrome user data directory...')
+        await del.promise(config.chromeUserDataDir)
+    }
+    logger.info('Launching chrome...')
+    const chrome = await puppeteer.launch({
+        userDataDir: config.chromeUserDataDir
+    })
+    logger.info('Chrome is running.')
+    return chrome
+})()
 
-const upload = multer({
-    dest: 'temp/'
+const upload = ext => multer({
+    storage: multer.diskStorage({
+        destination: (req, file, cb) => cb(null, config.tempPath),
+        filename: (req, file, cb) => {
+            file.uuid = uuid()
+            cb(null, `${file.uuid}.${ext}`)
+        }
+    }),
+    limits: {
+        fileSize: bytes(config.fileSizeLimit)
+    }
 })
 
 const app = express()
-app.use(cors({
-    origin: '*'
-}))
-app.use(bodyParser.urlencoded({extended: false, limit: '100mb'}))
-app.use(express.static('./public'))
+let counter = 0
+
+// Provide a unique id and a logger to each request
+app.use((req, res, next) => {
+    req.id = counter++
+    req.logger = log4js.getLogger(`Request ${req.id}`)
+    next()
+})
 
-app.post('/edit', upload.single('document'), async (req, res) => {
+app.use(expressLogging(logger))
+
+const async = (fn) => (req, res, next) => {
+    fn(req, res, next)
+        .then(next)
+        .catch(err => {
+            req.logger.error(err)
+            res.status(500).send(`<pre>${err}</pre>`)
+        })
+}
+
+const exec = (logger, commandLine) => new Promise((resolve, reject) => {
+    logger.debug(`Executing: ${commandLine}`)
+    childProcess.exec(commandLine, (error, stdout, stderr) => {
+        if (error) {
+            logger.error(`Exec error: ${error}\n${stderr}`)
+            reject(stderr)
+        } else {
+            logger.trace(`Exec success`)
+            resolve({stdout, stderr})
+        }
+    })
+})
+
+app.use(bodyParser.urlencoded({extended: false, limit: config.fileSizeLimit}))
+app.get('/', (req, res) => res.redirect('/edit'))
+app.use(express.static('./public'))
+const viewerCommon = mode => async(async (req, res) => {
+    const logger = req.logger
     let pdfFile, htmlFile
     if (req.file) {
+        logger.debug(`Request includes file: ${req.file.originalname}`)
         pdfFile = req.file.path
-        htmlFile = `${req.file.path}.html`
+        htmlFile = `${config.tempPath}/${req.file.uuid}.html`
     } else if (req.body.url) {
-        pdfFile = `temp/${uuid()}.pdf`
+        logger.debug(`Request includes url.`)
+        pdfFile = `${config.tempPath}/${uuid()}.pdf`
         htmlFile = `${pdfFile}.html`
         const pdf = parseDataUrl(req.body.url)
+        logger.debug(`Writing ${pdfFile}...`)
         await asfs.writeFileAsync(pdfFile, pdf.body)
+    } else {
+        logger.debug(`Request includes no data.`)
+        return res.status(400).send('Requires file or url.')
     }
     /*
         Executes pdf2htmlex[.exe] with:
@@ -43,54 +119,114 @@ app.post('/edit', upload.single('document'), async (req, res) => {
         - The path to the source PDF file
         - The path to the output HTML file
     */
-    childProcess.exec(`"${pdf2htmlexPath}" --hdpi 200 --vdpi 200 --font-format ttf --no-drm 1 "${pdfFile}" "${htmlFile}"`, (err, stdout, stderr) => {
-        if (err) {
-            res.status(500).send(`<pre>${stderr}</pre>`)
-        } else {
-            fs.readFile('public/edit.html', (err, editHtml) => {
-                fs.readFile(htmlFile, 'utf8', async (err, data) => {
-                    if (err) {
-                        res.status(500).send(`<pre>${err}</pre>`)
-                    } else {
-                        res.status(200).send(data.replace('</body>', `${editHtml}</body>`))
-                    }
-                    await asfs.unlinkAsync(htmlFile)
-                    await asfs.unlinkAsync(pdfFile)
-                })
-            })
+    await exec(logger, `"${config.pdf2htmlexPath}" --hdpi 200 --vdpi 200 --font-format ttf --no-drm 1 "${pdfFile}" "${htmlFile}"`)
+
+    logger.debug(`Reading public/viewer.html...`)
+    const editHtml = await asfs.readFileAsync('public/viewer.html')
+
+    logger.debug(`Reading ${htmlFile}...`)
+    const html = await asfs.readFileAsync(htmlFile, 'utf8')
+
+    const pageWidth = (/\.w0{width:([\d\.]*)pt/).exec(html)[1] * 1
+    const pageHeight = (/\.h0{height:([\d\.]*)pt/).exec(html)[1] * 1
+    const pageStyle = `
+        html {
+            -webkit-print-color-adjust: exact;
         }
-    })
+        /* Prevent overflow into blank pages */
+        .pf {
+            max-height: ${pageHeight * pixelsPerPoint / 96 * 0.999}in;
+            overflow-y: hidden;
+        }
+        @page {
+            width: ${pageWidth / 96}in;
+            height: ${pageHeight / 96}in;
+            margin: 0;
+        }
+    `
+
+    logger.trace(`Page style: ${pageStyle}`)
+
+    res.status(200).send(html
+        .replace(
+            '</body>', 
+            `<input type="hidden" id="viewerMode" value="${htmlEncode(mode)}" />
+            <input type="hidden" id="filename" value="${htmlEncode(req.file.originalname)}" />
+            ${editHtml}</body>`
+        )
+        .replace(
+            '</head>',
+            `<style type="text/css">${pageStyle}</style></head>`
+        )
+    )
+
+    logger.debug(`Deleting ${htmlFile}...`)
+    await asfs.unlinkAsync(htmlFile)
+
+    logger.debug(`Deleting ${pdfFile}...`)
+    await asfs.unlinkAsync(pdfFile)
+
+    logger.debug(`Done.`)
 })
 
+app.get('/edit', (req, res) => res.sendFile(path.join(__dirname, './public/index.html')))
+app.post('/edit', upload('pdf').single('document'), viewerCommon('edit'))
 
+app.get('/view', (req, res) => res.sendFile(path.join(__dirname, './public/index.html')))
+app.post('/view', upload('pdf').single('document'), viewerCommon('view'))
 
-app.post('/save', (req, res) => {
+app.post('/save', async(async (req, res) => {
+    const logger = req.logger
     const tmpUuid = uuid()
-    const htmlFile = `temp/${tmpUuid}.html`
+    const htmlFile = `${config.tempPath}/${tmpUuid}.html`
     const htmlUrl = `file://${path.join(process.cwd(), htmlFile)}`
-    const pdfFile = `temp/${tmpUuid}.pdf`
+    const pdfFile = `${config.tempPath}/${tmpUuid}.pdf`
+    const html = sanitize(req.body.html)
+    const filename = req.body.filename || 'document.pdf'
+    logger.trace({tmpUuid, htmlFile, htmlUrl, pdfFile})
 
-    const pageWidth = /\.w0{width:([\d\.]*)pt/.exec(req.body.html)[1]
-    const pageHeight = /\.h0{height:([\d\.]*)pt/.exec(req.body.html)[1]
+    const pageWidth = /\.w0{width:([\d\.]*)pt/.exec(html)[1]
+    const pageHeight = /\.h0{height:([\d\.]*)pt/.exec(html)[1]
+    logger.trace({pageWidth, pageHeight})
 
+    logger.debug(`Writing HTML file, ${html.length} chars to ${htmlFile}...`)
+    await asfs.writeFileAsync(htmlFile, html, 'utf8')
+    
+    logger.debug(`Getting chrome...`)
+    const chrome = await chromePromise
+    
+    logger.debug(`Getting new page...`)
+    const page = await chrome.newPage()
 
-    fs.writeFile(htmlFile, req.body.html, 'utf8', async (err) => {
-        const chrome = await chromePromise
-        const page = await chrome.newPage()
-        await page.goto(htmlUrl)
-        await page.pdf({
-            path: pdfFile,
-            preferCSSPageSize: true
-        })
-        await page.close()
-        res.download(pdfFile, 'todo-preserve-filename.pdf', async (err) => {
-            if (err) {
-                console.error(err)
-                res.status(500).send(err)
-            }
-            await asfs.unlinkAsync(htmlFile)
-            await asfs.unlinkAsync(pdfFile)
-        })
+    logger.debug(`Navigating to ${htmlUrl}...`)
+    await page.goto(htmlUrl)
+
+    logger.debug(`Capturing PDF to ${pdfFile}...`)
+    await page.pdf({
+        path: pdfFile,
+        preferCSSPageSize: true
     })
+
+    logger.debug(`Closing page...`)
+    await page.close()
+
+    logger.debug(`Sending ${pdfFile} to client...`)
+    await new Promise((resolve, reject) => res.download(pdfFile, filename, err => err ? reject(err) : resolve()))
+
+    logger.debug(`Deleting ${htmlFile}...`)
+    await asfs.unlinkAsync(htmlFile)
+
+    logger.debug(`Deleting ${pdfFile}...`)
+    await asfs.unlinkAsync(pdfFile)
+
+    logger.debug(`Done.`)
+}))
+
+
+const listener = app.listen(config.port, (err) => {
+    if (err) logger.error(err)
+    else {
+        const address = listener.address()
+        logger.info(`Listening on http://127.0.0.1:${address.port}`)
+    }
 })
-app.listen(process.env.PORT || 3000)