diff --git a/bl-kernel/parsedown.class.php b/bl-kernel/parsedown.class.php index 42375525..87d612ab 100644 --- a/bl-kernel/parsedown.class.php +++ b/bl-kernel/parsedown.class.php @@ -17,7 +17,7 @@ class Parsedown { # ~ - const version = '1.6.2'; + const version = '1.7.1'; # ~ @@ -75,6 +75,32 @@ class Parsedown protected $urlsLinked = true; + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + # # Lines # @@ -342,8 +368,6 @@ class Parsedown { $text = $Block['element']['text']['text']; - $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); - $Block['element']['text']['text'] = $text; return $Block; @@ -354,7 +378,7 @@ class Parsedown protected function blockComment($Line) { - if ($this->markupEscaped) + if ($this->markupEscaped or $this->safeMode) { return; } @@ -396,7 +420,7 @@ class Parsedown protected function blockFencedCode($Line) { - if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) { $Element = array( 'name' => 'code', @@ -457,8 +481,6 @@ class Parsedown { $text = $Block['element']['text']['text']; - $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); - $Block['element']['text']['text'] = $text; return $Block; @@ -547,6 +569,8 @@ class Parsedown { $Block['li']['text'] []= ''; + $Block['loose'] = true; + unset($Block['interrupted']); } @@ -595,6 +619,22 @@ class Parsedown } } + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['text'] as &$li) + { + if (end($li['text']) !== '') + { + $li['text'] []= ''; + } + } + } + + return $Block; + } + # # Quote @@ -678,12 +718,12 @@ class Parsedown protected function blockMarkup($Line) { - if ($this->markupEscaped) + if ($this->markupEscaped or $this->safeMode) { return; } - if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) { $element = strtolower($matches[1]); @@ -997,7 +1037,7 @@ class Parsedown # ~ # - public function line($text) + public function line($text, $nonNestables=array()) { $markup = ''; @@ -1013,6 +1053,13 @@ class Parsedown foreach ($this->InlineTypes[$marker] as $inlineType) { + # check to see if the current inline type is nestable in the current context + + if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + { + continue; + } + $Inline = $this->{'inline'.$inlineType}($Excerpt); if ( ! isset($Inline)) @@ -1034,6 +1081,13 @@ class Parsedown $Inline['position'] = $markerPosition; } + # cause the new element to 'inherit' our non nestables + + foreach ($nonNestables as $non_nestable) + { + $Inline['element']['nonNestables'][] = $non_nestable; + } + # the text that comes before the inline $unmarkedText = substr($text, 0, $Inline['position']); @@ -1074,7 +1128,6 @@ class Parsedown if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? 'a', 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), 'text' => null, 'attributes' => array( 'href' => null, @@ -1253,8 +1307,6 @@ class Parsedown $Element['attributes']['title'] = $Definition['title']; } - $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); - return array( 'extent' => $extent, 'element' => $Element, @@ -1263,12 +1315,12 @@ class Parsedown protected function inlineMarkup($Excerpt) { - if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) { return; } - if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) { return array( 'markup' => $matches[0], @@ -1284,7 +1336,7 @@ class Parsedown ); } - if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) { return array( 'markup' => $matches[0], @@ -1343,14 +1395,16 @@ class Parsedown if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) { + $url = $matches[0][0]; + $Inline = array( 'extent' => strlen($matches[0][0]), 'position' => $matches[0][1], 'element' => array( 'name' => 'a', - 'text' => $matches[0][0], + 'text' => $url, 'attributes' => array( - 'href' => $matches[0][0], + 'href' => $url, ), ), ); @@ -1363,7 +1417,7 @@ class Parsedown { if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) { - $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); + $url = $matches[1]; return array( 'extent' => strlen($matches[0]), @@ -1401,6 +1455,11 @@ class Parsedown protected function element(array $Element) { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + $markup = '<'.$Element['name']; if (isset($Element['attributes'])) @@ -1412,7 +1471,7 @@ class Parsedown continue; } - $markup .= ' '.$name.'="'.$value.'"'; + $markup .= ' '.$name.'="'.self::escape($value).'"'; } } @@ -1420,13 +1479,18 @@ class Parsedown { $markup .= '>'; + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + if (isset($Element['handler'])) { - $markup .= $this->{$Element['handler']}($Element['text']); + $markup .= $this->{$Element['handler']}($Element['text'], $Element['nonNestables']); } else { - $markup .= $Element['text']; + $markup .= self::escape($Element['text'], true); } $markup .= ''; @@ -1485,10 +1549,77 @@ class Parsedown return $markup; } + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + # # Static Methods # + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + static function instance($name = 'default') { if (isset(self::$instances[$name])) @@ -1545,4 +1676,4 @@ class Parsedown 'var', 'span', 'wbr', 'time', ); -} \ No newline at end of file +}