ponepaste/includes/Parsedown/SecureParsedown.php

338 lines
No EOL
9.8 KiB
PHP

<?php
namespace Aidantwoods\SecureParsedown;
class SecureParsedown extends \Parsedown {
const version = '1.0.1';
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:',
);
protected function blockCodeComplete($Block) {
$text = $Block['element']['text']['text'];
$Block['element']['text']['text'] = $text;
return $Block;
}
protected function blockComment($Line) {
if ($this->markupEscaped or $this->safeMode) {
return;
}
if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') {
$Block = array(
'markup' => $Line['body'],
);
if (preg_match('/-->$/', $Line['text'])) {
$Block['closed'] = true;
}
return $Block;
}
}
protected function blockFencedCodeComplete($Block) {
$text = $Block['element']['text']['text'];
$Block['element']['text']['text'] = $text;
return $Block;
}
protected function blockMarkup($Line) {
if ($this->markupEscaped or $this->safeMode) {
return;
}
if (preg_match('/^<(\w*)(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*(\/)?>/', $Line['text'], $matches)) {
$element = strtolower($matches[1]);
if (in_array($element, $this->textLevelElements)) {
return;
}
$Block = array(
'name' => $matches[1],
'depth' => 0,
'markup' => $Line['text'],
);
$length = strlen($matches[0]);
$remainder = substr($Line['text'], $length);
if (trim($remainder) === '') {
if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) {
$Block['closed'] = true;
$Block['void'] = true;
}
} else {
if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) {
return;
}
if (preg_match('/<\/' . $matches[1] . '>[ ]*$/i', $remainder)) {
$Block['closed'] = true;
}
}
return $Block;
}
}
protected function inlineCode($Excerpt) {
$marker = $Excerpt['text'][0];
if (preg_match('/^(' . $marker . '+)[ ]*(.+?)[ ]*(?<!' . $marker . ')\1(?!' . $marker . ')/s', $Excerpt['text'], $matches)) {
$text = $matches[2];
$text = preg_replace("/[ ]*\n/", ' ', $text);
return array(
'extent' => strlen($matches[0]),
'element' => array(
'name' => 'code',
'text' => $text,
),
);
}
}
protected function inlineLink($Excerpt) {
$Element = array(
'name' => 'a',
'handler' => 'line',
'text' => null,
'attributes' => array(
'href' => null,
'title' => null,
),
);
$extent = 0;
$remainder = $Excerpt['text'];
if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
$Element['text'] = $matches[1];
$extent += strlen($matches[0]);
$remainder = substr($remainder, $extent);
} else {
return;
}
if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) {
$Element['attributes']['href'] = $matches[1];
if (isset($matches[2])) {
$Element['attributes']['title'] = substr($matches[2], 1, -1);
}
$extent += strlen($matches[0]);
} else {
if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
$definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
$definition = strtolower($definition);
$extent += strlen($matches[0]);
} else {
$definition = strtolower($Element['text']);
}
if (!isset($this->DefinitionData['Reference'][$definition])) {
return;
}
$Definition = $this->DefinitionData['Reference'][$definition];
$Element['attributes']['href'] = $Definition['url'];
$Element['attributes']['title'] = $Definition['title'];
}
return array(
'extent' => $extent,
'element' => $Element,
);
}
protected function inlineMarkup($Excerpt) {
if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) {
return;
}
if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) {
return array(
'markup' => $matches[0],
'extent' => strlen($matches[0]),
);
}
if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) {
return array(
'markup' => $matches[0],
'extent' => strlen($matches[0]),
);
}
if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*\/?>/s', $Excerpt['text'], $matches)) {
return array(
'markup' => $matches[0],
'extent' => strlen($matches[0]),
);
}
}
protected function inlineUrl($Excerpt) {
if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
return;
}
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' => $url,
'attributes' => array(
'href' => $url,
),
),
);
return $Inline;
}
}
protected function inlineUrlTag($Excerpt) {
if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) {
$url = $matches[1];
return array(
'extent' => strlen($matches[0]),
'element' => array(
'name' => 'a',
'text' => $url,
'attributes' => array(
'href' => $url,
),
),
);
}
}
protected function element(array $Element) {
if ($this->safeMode) {
$Element = $this->sanitiseElement($Element);
}
$markup = '<' . $Element['name'];
if (isset($Element['attributes'])) {
foreach ($Element['attributes'] as $name => $value) {
if ($value === null) {
continue;
}
$markup .= ' ' . $name . '="' . self::escape($value) . '"';
}
}
if (isset($Element['text'])) {
$markup .= '>';
if (isset($Element['handler'])) {
$markup .= $this->{$Element['handler']}($Element['text']);
} else {
$markup .= self::escape($Element['text'], true);
}
$markup .= '</' . $Element['name'] . '>';
} else {
$markup .= ' />';
}
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;
}
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);
}
}
}