Advertisement
Guest User

rcube_washtml

a guest
Sep 11th, 2017
452
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 30.29 KB | None | 0 0
  1. <?php
  2. class rcube_washtml
  3. {
  4.     /* Allowed HTML elements (default) */
  5.     static $html_elements = array('a', 'abbr', 'acronym', 'address', 'area', 'b',
  6.         'basefont', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center',
  7.         'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl',
  8.         'dt', 'em', 'fieldset', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i',
  9.         'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q',
  10.         's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
  11.         'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img',
  12.         'video', 'source',
  13.         // form elements
  14.         'button', 'input', 'textarea', 'select', 'option', 'optgroup',
  15.         // SVG
  16.         'svg', 'altglyph', 'altglyphdef', 'altglyphitem', 'animate',
  17.         'animatecolor', 'animatetransform', 'circle', 'clippath', 'defs', 'desc',
  18.         'ellipse', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line',
  19.         'lineargradient', 'marker', 'mask', 'mpath', 'path', 'pattern',
  20.         'polygon', 'polyline', 'radialgradient', 'rect', 'set', 'stop', 'switch', 'symbol',
  21.         'text', 'textpath', 'tref', 'tspan', 'use', 'view', 'vkern', 'filter',
  22.          // SVG Filters
  23.         'feblend', 'fecolormatrix', 'fecomponenttransfer', 'fecomposite',
  24.         'feconvolvematrix', 'fediffuselighting', 'fedisplacementmap',
  25.         'feflood', 'fefunca', 'fefuncb', 'fefuncg', 'fefuncr', 'fegaussianblur',
  26.         'feimage', 'femerge', 'femergenode', 'femorphology', 'feoffset',
  27.         'fespecularlighting', 'fetile', 'feturbulence',
  28.         // MathML
  29.         'math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr',
  30.         'mmuliscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow',
  31.         'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd',
  32.         'mtext', 'mtr', 'munder', 'munderover', 'maligngroup', 'malignmark',
  33.         'mprescripts', 'semantics', 'annotation', 'annotation-xml', 'none',
  34.         'infinity', 'matrix', 'matrixrow', 'ci', 'cn', 'sep', 'apply',
  35.         'plus', 'minus', 'eq', 'power', 'times', 'divide', 'csymbol', 'root',
  36.         'bvar', 'lowlimit', 'uplimit',
  37.     );
  38.     /* Ignore these HTML tags and their content */
  39.     static $ignore_elements = array('script', 'applet', 'embed', 'object', 'style');
  40.     /* Allowed HTML attributes */
  41.     static $html_attribs = array('name', 'class', 'title', 'alt', 'width', 'height',
  42.         'align', 'nowrap', 'col', 'row', 'id', 'rowspan', 'colspan', 'cellspacing',
  43.         'cellpadding', 'valign', 'bgcolor', 'color', 'border', 'bordercolorlight',
  44.         'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border',
  45.         'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace',
  46.         'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media',
  47.         'background', 'src', 'poster', 'href',
  48.         // attributes of form elements
  49.         'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value',
  50.         // SVG
  51.         'accent-height', 'accumulate', 'additive', 'alignment-baseline', 'alphabetic',
  52.         'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseprofile',
  53.         'baseline-shift', 'begin', 'bias', 'by', 'clip', 'clip-path', 'clip-rule',
  54.         'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile',
  55.         'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction',
  56.         'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity',
  57.         'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size',
  58.         'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from',
  59.         'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform',
  60.         'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints',
  61.         'keysplines', 'keytimes', 'lengthadjust', 'letter-spacing', 'kernelmatrix',
  62.         'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid',
  63.         'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits',
  64.         'maskunits', 'max', 'mask', 'mode', 'min', 'numoctaves', 'offset', 'operator',
  65.         'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order',
  66.         'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits',
  67.         'points', 'preservealpha', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount',
  68.         'repeatdur', 'restart', 'rotate', 'scale', 'seed', 'shape-rendering', 'show', 'specularconstant',
  69.         'specularexponent', 'spreadmethod', 'stddeviation', 'stitchtiles', 'stop-color',
  70.         'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap',
  71.         'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width',
  72.         'surfacescale', 'targetx', 'targety', 'transform', 'text-anchor', 'text-decoration',
  73.         'text-rendering', 'textlength', 'to', 'u1', 'u2', 'unicode', 'values', 'viewbox',
  74.         'visibility', 'vert-adv-y', 'version', 'vert-origin-x', 'vert-origin-y', 'word-spacing',
  75.         'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2',
  76.         'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan',
  77.         // MathML
  78.         'accent', 'accentunder', 'bevelled', 'close', 'columnalign', 'columnlines',
  79.         'columnspan', 'denomalign', 'depth', 'display', 'displaystyle', 'encoding', 'fence',
  80.         'frame', 'largeop', 'length', 'linethickness', 'lspace', 'lquote',
  81.         'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize',
  82.         'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign',
  83.         'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel',
  84.         'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator',
  85.         'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset',
  86.         'fontsize', 'fontweight', 'fontstyle', 'fontfamily', 'groupalign', 'edge', 'side',
  87.     );
  88.     /* Elements which could be empty and be returned in short form (<tag />) */
  89.     static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr',
  90.         'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr',
  91.         // MathML
  92.         'sep', 'infinity', 'in', 'plus', 'eq', 'power', 'times', 'divide', 'root',
  93.         'maligngroup', 'none', 'mprescripts',
  94.     );
  95.     /* State for linked objects in HTML */
  96.     public $extlinks = false;
  97.     /* Current settings */
  98.     private $config = array();
  99.     /* Registered callback functions for tags */
  100.     private $handlers = array();
  101.     /* Allowed HTML elements */
  102.     private $_html_elements = array();
  103.     /* Ignore these HTML tags but process their content */
  104.     private $_ignore_elements = array();
  105.     /* Elements which could be empty and be returned in short form (<tag />) */
  106.     private $_void_elements = array();
  107.     /* Allowed HTML attributes */
  108.     private $_html_attribs = array();
  109.     /* Max nesting level */
  110.     private $max_nesting_level;
  111.     private $is_xml = false;
  112.     /**
  113.      * Class constructor
  114.      */
  115.     public function __construct($p = array())
  116.     {
  117.         $this->_html_elements   = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements);
  118.         $this->_html_attribs    = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs);
  119.         $this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements);
  120.         $this->_void_elements   = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements);
  121.         unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']);
  122.         $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array());
  123.     }
  124.     /**
  125.      * Register a callback function for a certain tag
  126.      */
  127.     public function add_callback($tagName, $callback)
  128.     {
  129.         $this->handlers[$tagName] = $callback;
  130.     }
  131.     /**
  132.      * Check CSS style
  133.      */
  134.     private function wash_style($style)
  135.     {
  136.         $result = array();
  137.         // Remove unwanted white-space characters so regular expressions below work better
  138.         $style = preg_replace('/[\n\r\s\t]+/', ' ', $style);
  139.         foreach (explode(';', $style) as $declaration) {
  140.             if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
  141.                 $cssid = $match[1];
  142.                 $str   = $match[2];
  143.                 $value = '';
  144.                 foreach ($this->explode_style($str) as $val) {
  145.                     if (preg_match('/^url\(/i', $val)) {
  146.                         if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
  147.                             if ($url = $this->wash_uri($match[1])) {
  148.                                 $value .= ' url(' . htmlspecialchars($url, ENT_QUOTES) . ')';
  149.                             }
  150.                         }
  151.                     }
  152.                     else if (!preg_match('/^(behavior|expression)/i', $val)) {
  153.                         // Set position:fixed to position:absolute for security (#5264)
  154.                         if (!strcasecmp($cssid, 'position') && !strcasecmp($val, 'fixed')) {
  155.                             $val = 'absolute';
  156.                         }
  157.                         // whitelist ?
  158.                         $value .= ' ' . $val;
  159.                         // #1488535: Fix size units, so width:800 would be changed to width:800px
  160.                         if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
  161.                             && preg_match('/^[0-9]+$/', $val)
  162.                         ) {
  163.                             $value .= 'px';
  164.                         }
  165.                     }
  166.                 }
  167.                 if (isset($value[0])) {
  168.                     $result[] = $cssid . ':' . $value;
  169.                 }
  170.             }
  171.         }
  172.         return implode('; ', $result);
  173.     }
  174.     /**
  175.      * Take a node and return allowed attributes and check values
  176.      */
  177.     private function wash_attribs($node)
  178.     {
  179.         $result = '';
  180.         $washed = array();
  181.         foreach ($node->attributes as $name => $attr) {
  182.             $key   = strtolower($name);
  183.             $value = $attr->nodeValue;
  184.             if ($key == 'style' && ($style = $this->wash_style($value))) {
  185.                 // replace double quotes to prevent syntax error and XSS issues (#1490227)
  186.                 $result .= ' style="' . str_replace('"', '&quot;', $style) . '"';
  187.             }
  188.             else if (isset($this->_html_attribs[$key])) {
  189.                 $value = trim($value);
  190.                 $out   = null;
  191.                 // in SVG to/from attribs may contain anything, including URIs
  192.                 if ($key == 'to' || $key == 'from') {
  193.                     $key = strtolower($node->getAttribute('attributeName'));
  194.                     if ($key && !isset($this->_html_attribs[$key])) {
  195.                         $key = null;
  196.                     }
  197.                 }
  198.                 if ($this->is_image_attribute($node->tagName, $key)) {
  199.                     $out = $this->wash_uri($value, true);
  200.                 }
  201.                 else if ($this->is_link_attribute($node->tagName, $key)) {
  202.                     if (!preg_match('!^(javascript|vbscript|data:text)!i', $value)
  203.                         && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value)
  204.                     ) {
  205.                         $out = $value;
  206.                     }
  207.                 }
  208.                 else if ($this->is_funciri_attribute($node->tagName, $key)) {
  209.                     if (preg_match('/^[a-z:]*url\(/i', $val)) {
  210.                         if (preg_match('/^([a-z:]*url)\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $value, $match)) {
  211.                             if ($url = $this->wash_uri($match[2])) {
  212.                                 $result .= ' ' . $attr->nodeName . '="' . $match[1] . '(' . htmlspecialchars($url, ENT_QUOTES) . ')'
  213.                                      . substr($val, strlen($match[0])) . '"';
  214.                                 continue;
  215.                             }
  216.                         }
  217.                         else {
  218.                             $out = $value;
  219.                         }
  220.                     }
  221.                     else {
  222.                         $out = $value;
  223.                     }
  224.                 }
  225.                 else if ($key) {
  226.                    $out = $value;
  227.                 }
  228.                 if ($out !== null && $out !== '') {
  229.                     $result .= ' ' . $attr->nodeName . '="' . htmlspecialchars($out, ENT_QUOTES) . '"';
  230.                 }
  231.                 else if ($value) {
  232.                     $washed[] = htmlspecialchars($attr->nodeName, ENT_QUOTES);
  233.                 }
  234.             }
  235.             else {
  236.                 $washed[] = htmlspecialchars($attr->nodeName, ENT_QUOTES);
  237.             }
  238.         }
  239.         if (!empty($washed) && $this->config['show_washed']) {
  240.             $result .= ' x-washed="' . implode(' ', $washed) . '"';
  241.         }
  242.         return $result;
  243.     }
  244.     /**
  245.      * Wash URI value
  246.      */
  247.     private function wash_uri($uri, $blocked_source = false)
  248.     {
  249.         if ((isset($this->config['cid_map'][$uri]) && $src = $this->config['cid_map'][$uri])
  250.             || (isset($this->config['cid_map'][$this->config['base_url'].$uri]) && $src = $this->config['cid_map'][$this->config['base_url'].$uri])
  251.         ) {
  252.             return $src;
  253.         }
  254.         // allow url(#id) used in SVG
  255.         if ($uri[0] == '#') {
  256.             return $uri;
  257.         }
  258.         if (preg_match('/^(http|https|ftp):.+/i', $uri)) {
  259.             if ($this->config['allow_remote']) {
  260.                 return $uri;
  261.             }
  262.             $this->extlinks = true;
  263.             if ($blocked_source && $this->config['blocked_src']) {
  264.                 return $this->config['blocked_src'];
  265.             }
  266.         }
  267.         else if (preg_match('/^data:image.+/i', $uri)) { // RFC2397
  268.             return $uri;
  269.         }
  270.     }
  271.     /**
  272.      * Check it the tag/attribute may contain an URI
  273.      */
  274.     private function is_link_attribute($tag, $attr)
  275.     {
  276.         return ($tag == 'a' || $tag == 'area') && $attr == 'href';
  277.     }
  278.     /**
  279.      * Check it the tag/attribute may contain an image URI
  280.      */
  281.     private function is_image_attribute($tag, $attr)
  282.     {
  283.         return $attr == 'background'
  284.             || $attr == 'color-profile' // SVG
  285.             || ($attr == 'poster' && $tag == 'video')
  286.             || ($attr == 'src' && preg_match('/^(img|source|input|video|audio)$/i', $tag))
  287.             || ($tag == 'image' && $attr == 'href'); // SVG
  288.     }
  289.     /**
  290.      * Check it the tag/attribute may contain a FUNCIRI value
  291.      */
  292.     private function is_funciri_attribute($tag, $attr)
  293.     {
  294.         return in_array($attr, array('fill', 'filter', 'stroke', 'marker-start',
  295.             'marker-end', 'marker-mid', 'clip-path', 'mask', 'cursor'));
  296.     }
  297.     /**
  298.      * The main loop that recurse on a node tree.
  299.      * It output only allowed tags with allowed attributes and allowed inline styles
  300.      *
  301.      * @param DOMNode $node  HTML element
  302.      * @param int     $level Recurrence level (safe initial value found empirically)
  303.      */
  304.     private function dumpHtml($node, $level = 20)
  305.     {
  306.         if (!$node->hasChildNodes()) {
  307.             return '';
  308.         }
  309.         $level++;
  310.         if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) {
  311.             // log error message once
  312.             if (!$this->max_nesting_level_error) {
  313.                 $this->max_nesting_level_error = true;
  314.                 var_dump("Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})");
  315.             }
  316.             return '<!-- ignored -->';
  317.         }
  318.         $node = $node->firstChild;
  319.         $dump = '';
  320.         do {
  321.             switch ($node->nodeType) {
  322.             case XML_ELEMENT_NODE: //Check element
  323.                 var_dump($node->tagName);
  324.                 $tagName = strtolower($node->tagName);
  325.                 if (isset($this->handlers[$tagName]) && $callback = $this->handlers[$tagName]) {
  326.                     $dump .= call_user_func($callback, $tagName,
  327.                         $this->wash_attribs($node), $this->dumpHtml($node, $level), $this);
  328.                 } else if (isset($this->_html_elements[$tagName])) {
  329.                     $content = $this->dumpHtml($node, $level);
  330.                     $dump .= '<' . $node->tagName;
  331.                     if ($tagName == 'svg') {
  332.                         $xpath = new DOMXPath($node->ownerDocument);
  333.                         foreach ($xpath->query('namespace::*') as $ns) {
  334.                             if ($ns->nodeName != 'xmlns:xml') {
  335.                                 $dump .= ' ' . $ns->nodeName . '="' . $ns->nodeValue . '"';
  336.                             }
  337.                         }
  338.                     }
  339.                     else if ($tagName == 'textarea' && strpos($content, '<') !== false) {
  340.                         $content = htmlspecialchars($content, ENT_QUOTES);
  341.                     }
  342.                     $dump .= $this->wash_attribs($node);
  343.                     if ($content === '' && ($this->is_xml || isset($this->_void_elements[$tagName]))) {
  344.                         $dump .= ' />';
  345.                     }
  346.                     else {
  347.                         $dump .= '>' . $content . '</' . $node->tagName . '>';
  348.                     }
  349.                 }
  350.                 else if (isset($this->_ignore_elements[$tagName])) {
  351.                     $dump .= '<!-- ' . htmlspecialchars($node->tagName, ENT_QUOTES) . ' not allowed -->';
  352.                 }
  353.                 else {
  354.                     $dump .= '<!-- ' . htmlspecialchars($node->tagName, ENT_QUOTES) . ' ignored -->';
  355.                     $dump .= $this->dumpHtml($node, $level); // ignore tags not its content
  356.                 }
  357.                 break;
  358.             case XML_CDATA_SECTION_NODE:
  359.                 $dump .= $node->nodeValue;
  360.                 break;
  361.             case XML_TEXT_NODE:
  362.                 $dump .= htmlspecialchars($node->nodeValue);
  363.                 break;
  364.             case XML_HTML_DOCUMENT_NODE:
  365.                 $dump .= $this->dumpHtml($node, $level);
  366.                 break;
  367.             }
  368.         }
  369.         while($node = $node->nextSibling);
  370.         return $dump;
  371.     }
  372.     /**
  373.      * Main function, give it untrusted HTML, tell it if you allow loading
  374.      * remote images and give it a map to convert "cid:" urls.
  375.      */
  376.     public function wash($html)
  377.     {
  378.         // Charset seems to be ignored (probably if defined in the HTML document)
  379.         $node = new DOMDocument('1.0', $this->config['charset']);
  380.         $this->extlinks = false;
  381.         $html = $this->cleanup($html);
  382.        
  383.         // Find base URL for images
  384.         if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) {
  385.             $this->config['base_url'] = $matches[1];
  386.         }
  387.         else {
  388.             $this->config['base_url'] = '';
  389.         }
  390.         // Detect max nesting level (for dumpHTML) (#1489110)
  391.         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
  392.         // SVG need to be parsed as XML
  393.         $this->is_xml = stripos($html, '<html') === false && stripos($html, '<svg') !== false;
  394.         $method       = $this->is_xml ? 'loadXML' : 'loadHTML';
  395.         $options      = 0;
  396.         // Use optimizations if supported
  397.         if (PHP_VERSION_ID >= 50400) {
  398.             $options = LIBXML_PARSEHUGE | LIBXML_COMPACT | LIBXML_NONET;
  399.             @$node->{$method}($html, $options);
  400.         }
  401.         else {
  402.             @$node->{$method}($html);
  403.         }
  404.        
  405.         return $this->dumpHtml($node);
  406.     }
  407.     /**
  408.      * Getter for config parameters
  409.      */
  410.     public function get_config($prop)
  411.     {
  412.         return $this->config[$prop];
  413.     }
  414.     /**
  415.      * Clean HTML input
  416.      */
  417.     private function cleanup($html)
  418.     {
  419.         $html = trim($html);
  420.         // special replacements (not properly handled by washtml class)
  421.         $html_search = array(
  422.             // space(s) between <NOBR>
  423.             '/(<\/nobr>)(\s+)(<nobr>)/i',
  424.             // PHP bug #32547 workaround: remove title tag
  425.             '/<title[^>]*>[^<]*<\/title>/i',
  426.             // remove <!doctype> before BOM (#1490291)
  427.             '/<\!doctype[^>]+>[^<]*/im',
  428.             // byte-order mark (only outlook?)
  429.             '/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/',
  430.             // washtml/DOMDocument cannot handle xml namespaces
  431.             '/<html\s[^>]+>/i',
  432.         );
  433.         $html_replace = array(
  434.             '\\1'.' &nbsp; '.'\\3',
  435.             '',
  436.             '',
  437.             '',
  438.             '<html>',
  439.         );
  440.         $html = preg_replace($html_search, $html_replace, trim($html));
  441.         // Replace all of those weird MS Word quotes and other high characters
  442.         $badwordchars = array(
  443.             "\xe2\x80\x98", // left single quote
  444.             "\xe2\x80\x99", // right single quote
  445.             "\xe2\x80\x9c", // left double quote
  446.             "\xe2\x80\x9d", // right double quote
  447.             "\xe2\x80\x94", // em dash
  448.             "\xe2\x80\xa6"  // elipses
  449.         );
  450.         $fixedwordchars = array(
  451.             "'",
  452.             "'",
  453.             '"',
  454.             '"',
  455.             '&mdash;',
  456.             '...'
  457.         );
  458.         $html = str_replace($badwordchars, $fixedwordchars, $html);
  459.         // PCRE errors handling (#1486856), should we use something like for every preg_* use?
  460.         if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
  461.             $errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
  462.             if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
  463.                 $errstr .= " Consider raising pcre.backtrack_limit!";
  464.             }
  465.             if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
  466.                 $errstr .= " Consider raising pcre.recursion_limit!";
  467.             }
  468.             var_dump($errstr);
  469.             return '';
  470.         }
  471.         // fix (unknown/malformed) HTML tags before "wash"
  472.         $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
  473.         // Remove invalid HTML comments (#1487759)
  474.         // Don't remove valid conditional comments
  475.         // Don't remove MSOutlook (<!-->) conditional comments (#1489004)
  476.         $html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html);
  477.         // fix broken nested lists
  478.         self::fix_broken_lists($html);
  479.         // turn relative into absolute urls
  480.         $html = self::resolve_base($html);
  481.         return $html;
  482.     }
  483.     /**
  484.      * Callback function for HTML tags fixing
  485.      */
  486.     public static function html_tag_callback($matches)
  487.     {
  488.         $tagname = $matches[2];
  489.         $tagname = preg_replace(array(
  490.             '/:.*$/',               // Microsoft's Smart Tags <st1:xxxx>
  491.             '/[^a-z0-9_\[\]\!?-]/i', // forbidden characters
  492.         ), '', $tagname);
  493.         // fix invalid closing tags - remove any attributes (#1489446)
  494.         if ($matches[1] == '</') {
  495.             $matches[3] = '';
  496.         }
  497.         return $matches[1] . $tagname . $matches[3];
  498.     }
  499.     /**
  500.      * Convert all relative URLs according to a <base> in HTML
  501.      */
  502.     public static function resolve_base($body)
  503.     {
  504.         // check for <base href=...>
  505.         if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
  506.             $replacer = new rcube_base_replacer($regs[2]);
  507.             $body     = $replacer->replace($body);
  508.         }
  509.         return $body;
  510.     }
  511.     /**
  512.      * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
  513.      */
  514.     public static function fix_broken_lists(&$html)
  515.     {
  516.         // do two rounds, one for <ol>, one for <ul>
  517.         foreach (array('ol', 'ul') as $tag) {
  518.             $pos = 0;
  519.             while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
  520.                 $pos++;
  521.                 // make sure this is an ol/ul tag
  522.                 if (!in_array($html[$pos+2], array(' ', '>'))) {
  523.                     continue;
  524.                 }
  525.                 $p      = $pos;
  526.                 $in_li  = false;
  527.                 $li_pos = 0;
  528.                 while (($p = strpos($html, '<', $p)) !== false) {
  529.                     $tt = strtolower(substr($html, $p, 4));
  530.                     // li open tag
  531.                     if ($tt == '<li>' || $tt == '<li ') {
  532.                         $in_li = true;
  533.                         $p += 4;
  534.                     }
  535.                     // li close tag
  536.                     else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
  537.                         $li_pos = $p;
  538.                         $p += 4;
  539.                         $in_li = false;
  540.                     }
  541.                     // ul/ol closing tag
  542.                     else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
  543.                         break;
  544.                     }
  545.                     // nested ol/ul element out of li
  546.                     else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
  547.                         // find closing tag of this ul/ol element
  548.                         $element = substr($tt, 1, 2);
  549.                         $cpos    = $p;
  550.                         do {
  551.                             $tpos = stripos($html, '<' . $element, $cpos+1);
  552.                             $cpos = stripos($html, '</' . $element, $cpos+1);
  553.                         }
  554.                         while ($tpos !== false && $cpos !== false && $cpos > $tpos);
  555.                         // not found, this is invalid HTML, skip it
  556.                         if ($cpos === false) {
  557.                             break;
  558.                         }
  559.                         // get element content
  560.                         $end     = strpos($html, '>', $cpos);
  561.                         $len     = $end - $p + 1;
  562.                         $element = substr($html, $p, $len);
  563.                         // move element to the end of the last li
  564.                         $html    = substr_replace($html, '', $p, $len);
  565.                         $html    = substr_replace($html, $element, $li_pos, 0);
  566.                         $p = $end;
  567.                     }
  568.                     else {
  569.                         $p++;
  570.                     }
  571.                 }
  572.             }
  573.         }
  574.     }
  575.     /**
  576.      * Explode css style value
  577.      */
  578.     protected function explode_style($style)
  579.     {
  580.         $pos = 0;
  581.         // first remove comments
  582.         while (($pos = strpos($style, '/*', $pos)) !== false) {
  583.             $end = strpos($style, '*/', $pos+2);
  584.             if ($end === false) {
  585.                 $style = substr($style, 0, $pos);
  586.             }
  587.             else {
  588.                 $style = substr_replace($style, '', $pos, $end - $pos + 2);
  589.             }
  590.         }
  591.         $style  = trim($style);
  592.         $strlen = strlen($style);
  593.         $result = array();
  594.         // explode value
  595.         $q = false;
  596.         for ($p=$i=0; $i < $strlen; $i++) {
  597.             if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
  598.                 if ($q == $style[$i]) {
  599.                     $q = false;
  600.                 }
  601.                 else if (!$q) {
  602.                     $q = $style[$i];
  603.                 }
  604.             }
  605.             if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
  606.                 $result[] = substr($style, $p, $i - $p);
  607.                 $p = $i + 1;
  608.             }
  609.         }
  610.         $result[] = (string) substr($style, $p);
  611.         return $result;
  612.     }
  613. }
  614.  
  615. class rcube_base_replacer
  616. {
  617.     private $base_url;
  618.     /**
  619.      * Class constructor
  620.      *
  621.      * @param string $base Base URL
  622.      */
  623.     public function __construct($base)
  624.     {
  625.         $this->base_url = $base;
  626.     }
  627.     /**
  628.      * Replace callback
  629.      *
  630.      * @param array $matches Matching entries
  631.      *
  632.      * @return string Replaced text with absolute URL
  633.      */
  634.     public function callback($matches)
  635.     {
  636.         return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"';
  637.     }
  638.     /**
  639.      * Convert base URLs to absolute ones
  640.      *
  641.      * @param string $body Text body
  642.      *
  643.      * @return string Replaced text
  644.      */
  645.     public function replace($body)
  646.     {
  647.         $regexp = array(
  648.             '/(src|background|href)=(["\']?)([^"\'\s>]+)(\2|\s|>)/i',
  649.             '/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/i',
  650.         );
  651.         return preg_replace_callback($regexp, array($this, 'callback'), $body);
  652.     }
  653.     /**
  654.      * Convert paths like ../xxx to an absolute path using a base url
  655.      *
  656.      * @param string $path     Relative path
  657.      * @param string $base_url Base URL
  658.      *
  659.      * @return string Absolute URL
  660.      */
  661.     public static function absolute_url($path, $base_url)
  662.     {
  663.         // check if path is an absolute URL
  664.         if (preg_match('/^[fhtps]+:\/\//', $path)) {
  665.             return $path;
  666.         }
  667.         // check if path is a content-id scheme
  668.         if (strpos($path, 'cid:') === 0) {
  669.             return $path;
  670.         }
  671.         $host_url = $base_url;
  672.         $abs_path = $path;
  673.         // cut base_url to the last directory
  674.         if (strrpos($base_url, '/') > 7) {
  675.             $host_url = substr($base_url, 0, strpos($base_url, '/', 7));
  676.             $base_url = substr($base_url, 0, strrpos($base_url, '/'));
  677.         }
  678.         // $path is absolute
  679.         if ($path[0] == '/') {
  680.             $abs_path = $host_url.$path;
  681.         }
  682.         else {
  683.             // strip './' because its the same as ''
  684.             $path = preg_replace('/^\.\//', '', $path);
  685.             if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
  686.                 $cnt = count($matches);
  687.                 while ($cnt--) {
  688.                     if ($pos = strrpos($base_url, '/')) {
  689.                         $base_url = substr($base_url, 0, $pos);
  690.                     }
  691.                     $path = substr($path, 3);
  692.                 }
  693.             }
  694.             $abs_path = $base_url.'/'.$path;
  695.         }
  696.         return $abs_path;
  697.     }
  698. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement