Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Michelf/Markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ public static function defaultTransform(string $text): string {
*/
public bool $enhanced_ordered_list = false;

/**
* Maximum nesting depth of blockquotes. Deeper ">" markers are output
* as text. Each level of nesting re-processes and re-indents everything
* inside it, so without a limit a few kilobytes of ">" characters take
* gigabytes of memory. Set to 0 for no limit.
*/
public int $max_blockquote_depth = 32;

/**
* Parser implementation
*/
Expand Down Expand Up @@ -180,6 +188,11 @@ public function __construct() {
*/
protected bool $in_emphasis_processing = false;

/**
* Nesting depth of the blockquote currently being processed.
*/
protected int $blockquote_depth = 0;

/**
* Called before the transformation process starts to setup parser states.
* @return void
Expand All @@ -191,6 +204,7 @@ protected function setup() {
$this->html_hashes = array();
$this->in_anchor = false;
$this->in_emphasis_processing = false;
$this->blockquote_depth = 0;
}

/**
Expand Down Expand Up @@ -1421,6 +1435,11 @@ protected function doItalicsAndBold($text) {
* @return string
*/
protected function doBlockQuotes($text) {
if ($this->max_blockquote_depth > 0 &&
$this->blockquote_depth >= $this->max_blockquote_depth) {
return $text;
}

$text = preg_replace_callback('/
( # Wrap whole match in $1
(?>
Expand All @@ -1445,7 +1464,9 @@ protected function _doBlockQuotes_callback($matches) {
$bq = $matches[1];
// trim one level of quoting - trim whitespace-only lines
$bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
$this->blockquote_depth++;
$bq = $this->runBlockGamut($bq); // recurse
$this->blockquote_depth--;

$bq = preg_replace('/^/m', " ", $bq);
// These leading spaces cause problem with <pre> content,
Expand Down
66 changes: 66 additions & 0 deletions test/unit/MarkdownBlockQuoteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

class MarkdownBlockQuoteTest extends \PHPUnit\Framework\TestCase
{
/**
* Deepest nesting of blockquotes in the HTML
*/
private static function blockQuoteDepth(string $html): int
{
$depth = $deepest = 0;
preg_match_all('{<(/?)blockquote>}', $html, $tags);
foreach ($tags[1] as $closing) {
$depth += $closing === '' ? 1 : -1;
$deepest = max($deepest, $depth);
}
return $deepest;
}

public function deepBlockQuotes(): array
{
return array(
'markers' => array(str_repeat('>', 2000) . ' x'),
'markers with spaces' => array(str_repeat('> ', 2000) . 'x'),
'one level more per line' => array(implode("\n", array_map(function ($i) {
return str_repeat('>', $i) . ' x';
}, range(1, 300)))),
'in a list item' => array("- a\n\n " . str_repeat('>', 2000) . ' x'),
);
}

/**
* @dataProvider deepBlockQuotes
*/
public function testNestingIsLimited(string $text)
{
// without the limit, these need from hundreds of megabytes to gigabytes of memory
foreach (array(new \Michelf\Markdown(), new \Michelf\MarkdownExtra()) as $parser) {
$html = $parser->transform($text);
$this->assertSame($parser->max_blockquote_depth, self::blockQuoteDepth($html));
}
}

public function testMarkersBeyondTheLimitAreText()
{
$parser = new \Michelf\Markdown();
$parser->max_blockquote_depth = 2;

$this->assertSame(
"<blockquote>\n <blockquote>\n <p>> x</p>\n </blockquote>\n</blockquote>",
trim($parser->transform('>>> x'))
);
$this->assertSame(
"<blockquote>\n <blockquote>\n <p>y</p>\n </blockquote>\n</blockquote>",
trim($parser->transform('>> y')),
'the depth starts again at 0 for each transformation'
);
}

public function testLimitCanBeDisabled()
{
$parser = new \Michelf\Markdown();
$parser->max_blockquote_depth = 0;

$this->assertSame(150, self::blockQuoteDepth($parser->transform(str_repeat('>', 150) . ' x')));
}
}