|
@@ -17,7 +17,7 @@ class Parsedown
|
|
|
{
|
|
|
# ~
|
|
|
|
|
|
- const version = '1.6.0';
|
|
|
+ const version = '1.7.4';
|
|
|
|
|
|
# ~
|
|
|
|
|
@@ -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
|
|
|
#
|
|
@@ -141,11 +167,7 @@ class Parsedown
|
|
|
|
|
|
foreach ($parts as $part)
|
|
|
{
|
|
|
- // @CHANGE LDR Fix when mb_strlen is not available
|
|
|
- //$shortage = 4 - mb_strlen($line, 'utf-8') % 4;
|
|
|
- if (function_exists('mb_strlen')) $len = mb_strlen($line, 'utf-8');
|
|
|
- else $len = strlen($line);
|
|
|
- $shortage = 4 - $len % 4;
|
|
|
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
|
|
|
|
|
|
$line .= str_repeat(' ', $shortage);
|
|
|
$line .= $part;
|
|
@@ -346,8 +368,6 @@ class Parsedown
|
|
|
{
|
|
|
$text = $Block['element']['text']['text'];
|
|
|
|
|
|
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
|
|
|
-
|
|
|
$Block['element']['text']['text'] = $text;
|
|
|
|
|
|
return $Block;
|
|
@@ -358,7 +378,7 @@ class Parsedown
|
|
|
|
|
|
protected function blockComment($Line)
|
|
|
{
|
|
|
- if ($this->markupEscaped)
|
|
|
+ if ($this->markupEscaped or $this->safeMode)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
@@ -400,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',
|
|
@@ -409,7 +429,21 @@ class Parsedown
|
|
|
|
|
|
if (isset($matches[1]))
|
|
|
{
|
|
|
- $class = 'language-'.$matches[1];
|
|
|
+ /**
|
|
|
+ * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
|
|
|
+ * Every HTML element may have a class attribute specified.
|
|
|
+ * The attribute, if specified, must have a value that is a set
|
|
|
+ * of space-separated tokens representing the various classes
|
|
|
+ * that the element belongs to.
|
|
|
+ * [...]
|
|
|
+ * The space characters, for the purposes of this specification,
|
|
|
+ * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
|
|
|
+ * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
|
|
|
+ * U+000D CARRIAGE RETURN (CR).
|
|
|
+ */
|
|
|
+ $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
|
|
|
+
|
|
|
+ $class = 'language-'.$language;
|
|
|
|
|
|
$Element['attributes'] = array(
|
|
|
'class' => $class,
|
|
@@ -461,8 +495,6 @@ class Parsedown
|
|
|
{
|
|
|
$text = $Block['element']['text']['text'];
|
|
|
|
|
|
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
|
|
|
-
|
|
|
$Block['element']['text']['text'] = $text;
|
|
|
|
|
|
return $Block;
|
|
@@ -551,6 +583,8 @@ class Parsedown
|
|
|
{
|
|
|
$Block['li']['text'] []= '';
|
|
|
|
|
|
+ $Block['loose'] = true;
|
|
|
+
|
|
|
unset($Block['interrupted']);
|
|
|
}
|
|
|
|
|
@@ -599,6 +633,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
|
|
|
|
|
@@ -682,12 +732,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]);
|
|
|
|
|
@@ -1001,7 +1051,7 @@ class Parsedown
|
|
|
# ~
|
|
|
#
|
|
|
|
|
|
- public function line($text)
|
|
|
+ public function line($text, $nonNestables=array())
|
|
|
{
|
|
|
$markup = '';
|
|
|
|
|
@@ -1017,6 +1067,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))
|
|
@@ -1038,6 +1095,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']);
|
|
|
|
|
@@ -1078,7 +1142,6 @@ class Parsedown
|
|
|
if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
|
|
|
{
|
|
|
$text = $matches[2];
|
|
|
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
|
|
|
$text = preg_replace("/[ ]*\n/", ' ', $text);
|
|
|
|
|
|
return array(
|
|
@@ -1180,9 +1243,7 @@ class Parsedown
|
|
|
'name' => 'img',
|
|
|
'attributes' => array(
|
|
|
'src' => $Link['element']['attributes']['href'],
|
|
|
- 'alt' => $Link['element']['text'],
|
|
|
- // @CHANGE LDR
|
|
|
- 'class' => (!empty($Link['element']['attributes']['class']) ? $Link['element']['attributes']['class'] : '')
|
|
|
+ 'alt' => $Link['element']['text'],
|
|
|
),
|
|
|
),
|
|
|
);
|
|
@@ -1199,6 +1260,7 @@ class Parsedown
|
|
|
$Element = array(
|
|
|
'name' => 'a',
|
|
|
'handler' => 'line',
|
|
|
+ 'nonNestables' => array('Url', 'Link'),
|
|
|
'text' => null,
|
|
|
'attributes' => array(
|
|
|
'href' => null,
|
|
@@ -1233,13 +1295,6 @@ class Parsedown
|
|
|
}
|
|
|
|
|
|
$extent += strlen($matches[0]);
|
|
|
-
|
|
|
- // @CHANGE LDR
|
|
|
- if (preg_match('/{([^}]+)}/', $remainder, $matches2))
|
|
|
- {
|
|
|
- $Element['attributes']['class'] = $matches2[1];
|
|
|
- $remainder = preg_replace('/{'.preg_quote($matches2[1],'/').'}/', '', $remainder);
|
|
|
- }
|
|
|
}
|
|
|
else
|
|
|
{
|
|
@@ -1266,8 +1321,6 @@ class Parsedown
|
|
|
$Element['attributes']['title'] = $Definition['title'];
|
|
|
}
|
|
|
|
|
|
- $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']);
|
|
|
-
|
|
|
return array(
|
|
|
'extent' => $extent,
|
|
|
'element' => $Element,
|
|
@@ -1276,12 +1329,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],
|
|
@@ -1297,7 +1350,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],
|
|
@@ -1356,14 +1409,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,
|
|
|
),
|
|
|
),
|
|
|
);
|
|
@@ -1376,7 +1431,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]),
|
|
@@ -1414,6 +1469,11 @@ class Parsedown
|
|
|
|
|
|
protected function element(array $Element)
|
|
|
{
|
|
|
+ if ($this->safeMode)
|
|
|
+ {
|
|
|
+ $Element = $this->sanitiseElement($Element);
|
|
|
+ }
|
|
|
+
|
|
|
$markup = '<'.$Element['name'];
|
|
|
|
|
|
if (isset($Element['attributes']))
|
|
@@ -1425,23 +1485,45 @@ class Parsedown
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- $markup .= ' '.$name.'="'.$value.'"';
|
|
|
+ $markup .= ' '.$name.'="'.self::escape($value).'"';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ $permitRawHtml = false;
|
|
|
+
|
|
|
if (isset($Element['text']))
|
|
|
+ {
|
|
|
+ $text = $Element['text'];
|
|
|
+ }
|
|
|
+ // very strongly consider an alternative if you're writing an
|
|
|
+ // extension
|
|
|
+ elseif (isset($Element['rawHtml']))
|
|
|
+ {
|
|
|
+ $text = $Element['rawHtml'];
|
|
|
+ $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
|
|
|
+ $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($text))
|
|
|
{
|
|
|
$markup .= '>';
|
|
|
|
|
|
+ if (!isset($Element['nonNestables']))
|
|
|
+ {
|
|
|
+ $Element['nonNestables'] = array();
|
|
|
+ }
|
|
|
+
|
|
|
if (isset($Element['handler']))
|
|
|
{
|
|
|
- // @CHANGE LDR
|
|
|
- //$markup .= $this->{$Element['handler']}($Element['text']);
|
|
|
- $markup .= preg_replace('/>{[^}]+}/', '>', $this->{$Element['handler']}($Element['text']));
|
|
|
+ $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
|
|
|
+ }
|
|
|
+ elseif (!$permitRawHtml)
|
|
|
+ {
|
|
|
+ $markup .= self::escape($text, true);
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- $markup .= $Element['text'];
|
|
|
+ $markup .= $text;
|
|
|
}
|
|
|
|
|
|
$markup .= '</'.$Element['name'].'>';
|
|
@@ -1500,10 +1582,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]))
|
|
@@ -1554,10 +1703,10 @@ class Parsedown
|
|
|
'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
|
|
|
'i', 'rp', 'del', 'code', 'strike', 'marquee',
|
|
|
'q', 'rt', 'ins', 'font', 'strong',
|
|
|
- 's', 'tt', 'sub', 'mark',
|
|
|
- 'u', 'xm', 'sup', 'nobr',
|
|
|
- 'var', 'ruby',
|
|
|
- 'wbr', 'span',
|
|
|
- 'time',
|
|
|
+ 's', 'tt', 'kbd', 'mark',
|
|
|
+ 'u', 'xm', 'sub', 'nobr',
|
|
|
+ 'sup', 'ruby',
|
|
|
+ 'var', 'span',
|
|
|
+ 'wbr', 'time',
|
|
|
);
|
|
|
}
|