mirror of
https://github.com/erusev/parsedown.git
synced 2023-08-10 21:13:06 +03:00
Merge branch 'master' into fix/consistency_follow
This commit is contained in:
commit
d86d839677
31
.travis.yml
31
.travis.yml
@ -1,18 +1,29 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 7.1
|
||||
- 7.0
|
||||
- 5.6
|
||||
- 5.5
|
||||
- 5.4
|
||||
- 5.3
|
||||
- hhvm
|
||||
- nightly
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.3
|
||||
dist: precise
|
||||
- php: 5.4
|
||||
- php: 5.5
|
||||
- php: 5.6
|
||||
- php: 7.0
|
||||
- php: 7.1
|
||||
- php: nightly
|
||||
- php: hhvm
|
||||
- php: hhvm-nightly
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
- php: hhvm-nightly
|
||||
|
||||
sudo: false
|
||||
install:
|
||||
- composer install --prefer-dist --no-interaction --no-progress
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit
|
||||
- vendor/bin/phpunit test/CommonMarkTestWeak.php || true
|
||||
- '[ -z "$TRAVIS_TAG" ] || [ "$TRAVIS_TAG" == "$(php -r "require(\"Parsedown.php\"); echo Parsedown::version;")" ]'
|
||||
|
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Emanuil Rusev, erusev.com
|
||||
Copyright (c) 2013-2018 Emanuil Rusev, erusev.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
234
Parsedown.php
234
Parsedown.php
@ -17,7 +17,7 @@ class Parsedown
|
||||
{
|
||||
# ~
|
||||
|
||||
const version = '1.6.0';
|
||||
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,7 +718,7 @@ class Parsedown
|
||||
|
||||
protected function blockMarkup($Line)
|
||||
{
|
||||
if ($this->markupEscaped)
|
||||
if ($this->markupEscaped or $this->safeMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -946,7 +986,7 @@ class Parsedown
|
||||
# ~
|
||||
#
|
||||
|
||||
public function line($text)
|
||||
public function line($text, $nonNestables=array())
|
||||
{
|
||||
$markup = '';
|
||||
|
||||
@ -962,6 +1002,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))
|
||||
@ -983,6 +1030,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']);
|
||||
|
||||
@ -1023,7 +1077,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(
|
||||
@ -1038,8 +1091,14 @@ class Parsedown
|
||||
|
||||
protected function inlineEmailTag($Excerpt)
|
||||
{
|
||||
if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
|
||||
{
|
||||
$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
|
||||
|
||||
$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
|
||||
. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
|
||||
|
||||
if (strpos($Excerpt['text'], '>') !== false
|
||||
and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
|
||||
){
|
||||
$url = $matches[1];
|
||||
|
||||
if ( ! isset($matches[2]))
|
||||
@ -1142,6 +1201,7 @@ class Parsedown
|
||||
$Element = array(
|
||||
'name' => 'a',
|
||||
'handler' => 'line',
|
||||
'nonNestables' => array('Url', 'Link'),
|
||||
'text' => null,
|
||||
'attributes' => array(
|
||||
'href' => null,
|
||||
@ -1202,8 +1262,6 @@ class Parsedown
|
||||
$Element['attributes']['title'] = $Definition['title'];
|
||||
}
|
||||
|
||||
$Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']);
|
||||
|
||||
return array(
|
||||
'extent' => $extent,
|
||||
'element' => $Element,
|
||||
@ -1212,12 +1270,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],
|
||||
@ -1233,7 +1291,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],
|
||||
@ -1292,14 +1350,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,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -1312,7 +1372,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]),
|
||||
@ -1350,37 +1410,74 @@ class Parsedown
|
||||
|
||||
protected function element(array $Element)
|
||||
{
|
||||
$markup = '<'.$Element['name'];
|
||||
|
||||
if (isset($Element['attributes']))
|
||||
if ($this->safeMode)
|
||||
{
|
||||
foreach ($Element['attributes'] as $name => $value)
|
||||
{
|
||||
if ($value === null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$Element = $this->sanitiseElement($Element);
|
||||
}
|
||||
|
||||
$markup .= ' '.$name.'="'.$value.'"';
|
||||
$hasName = isset($Element['name']);
|
||||
|
||||
$markup = '';
|
||||
|
||||
if ($hasName)
|
||||
{
|
||||
$markup .= '<'.$Element['name'];
|
||||
|
||||
if (isset($Element['attributes']))
|
||||
{
|
||||
foreach ($Element['attributes'] as $name => $value)
|
||||
{
|
||||
if ($value === null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$markup .= ' '.$name.'="'.self::escape($value).'"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$permitRawHtml = false;
|
||||
|
||||
if (isset($Element['text']))
|
||||
{
|
||||
$markup .= '>';
|
||||
$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 .= $hasName ? '>' : '';
|
||||
|
||||
if (!isset($Element['nonNestables']))
|
||||
{
|
||||
$Element['nonNestables'] = array();
|
||||
}
|
||||
|
||||
if (isset($Element['handler']))
|
||||
{
|
||||
$markup .= $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'].'>';
|
||||
$markup .= $hasName ? '</'.$Element['name'].'>' : '';
|
||||
}
|
||||
else
|
||||
elseif ($hasName)
|
||||
{
|
||||
$markup .= ' />';
|
||||
}
|
||||
@ -1434,10 +1531,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]))
|
||||
|
36
README.md
36
README.md
@ -1,4 +1,4 @@
|
||||
> You might also like [Caret](https://caret.io?ref=parsedown) - our Markdown editor for Mac / Windows / Linux.
|
||||
> I also make [Caret](https://caret.io?ref=parsedown) - a Markdown editor for Mac and PC.
|
||||
|
||||
## Parsedown
|
||||
|
||||
@ -15,6 +15,7 @@ Better Markdown Parser in PHP
|
||||
### Features
|
||||
|
||||
* One File
|
||||
* No Dependencies
|
||||
* Super Fast
|
||||
* Extensible
|
||||
* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown)
|
||||
@ -31,10 +32,41 @@ Include `Parsedown.php` or install [the composer package](https://packagist.org/
|
||||
$Parsedown = new Parsedown();
|
||||
|
||||
echo $Parsedown->text('Hello _Parsedown_!'); # prints: <p>Hello <em>Parsedown</em>!</p>
|
||||
// you can also parse inline markdown only
|
||||
echo $Parsedown->line('Hello _Parsedown_!'); # prints: Hello <em>Parsedown</em>!
|
||||
```
|
||||
|
||||
More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI).
|
||||
|
||||
### Security
|
||||
|
||||
Parsedown is capable of escaping user-input within the HTML that it generates. Additionally Parsedown will apply sanitisation to additional scripting vectors (such as scripting link destinations) that are introduced by the markdown syntax itself.
|
||||
|
||||
To tell Parsedown that it is processing untrusted user-input, use the following:
|
||||
```php
|
||||
$parsedown = new Parsedown;
|
||||
$parsedown->setSafeMode(true);
|
||||
```
|
||||
|
||||
If instead, you wish to allow HTML within untrusted user-input, but still want output to be free from XSS it is recommended that you make use of a HTML sanitiser that allows HTML tags to be whitelisted, like [HTML Purifier](http://htmlpurifier.org/).
|
||||
|
||||
In both cases you should strongly consider employing defence-in-depth measures, like [deploying a Content-Security-Policy](https://scotthelme.co.uk/content-security-policy-an-introduction/) (a browser security feature) so that your page is likely to be safe even if an attacker finds a vulnerability in one of the first lines of defence above.
|
||||
|
||||
#### Security of Parsedown Extensions
|
||||
|
||||
Safe mode does not necessarily yield safe results when using extensions to Parsedown. Extensions should be evaluated on their own to determine their specific safety against XSS.
|
||||
|
||||
### Escaping HTML
|
||||
> ⚠️ **WARNING:** This method isn't safe from XSS!
|
||||
|
||||
If you wish to escape HTML **in trusted input**, you can use the following:
|
||||
```php
|
||||
$parsedown = new Parsedown;
|
||||
$parsedown->setMarkupEscaped(true);
|
||||
```
|
||||
|
||||
Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`.
|
||||
|
||||
### Questions
|
||||
|
||||
**How does Parsedown work?**
|
||||
@ -49,7 +81,7 @@ It passes most of the CommonMark tests. Most of the tests that don't pass deal w
|
||||
|
||||
**Who uses it?**
|
||||
|
||||
[phpDocumentor](http://www.phpdoc.org/), [October CMS](http://octobercms.com/), [Bolt CMS](http://bolt.cm/), [Kirby CMS](http://getkirby.com/), [Grav CMS](http://getgrav.org/), [Statamic CMS](http://www.statamic.com/), [Herbie CMS](http://www.getherbie.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents).
|
||||
[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents).
|
||||
|
||||
**How can I help?**
|
||||
|
||||
|
@ -13,9 +13,21 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
"php": ">=5.3.0",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {"Parsedown": ""}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-0": {
|
||||
"TestParsedown": "test/",
|
||||
"ParsedownTest": "test/",
|
||||
"CommonMarkTest": "test/",
|
||||
"CommonMarkTestWeak": "test/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="test/bootstrap.php" colors="true">
|
||||
<phpunit bootstrap="vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite>
|
||||
<file>test/ParsedownTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
</phpunit>
|
||||
|
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test Parsedown against the CommonMark spec.
|
||||
*
|
||||
* Some code based on the original JavaScript test runner by jgm.
|
||||
*
|
||||
* @link http://commonmark.org/ CommonMark
|
||||
* @link http://git.io/8WtRvQ JavaScript test runner
|
||||
*/
|
||||
class CommonMarkTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
const SPEC_URL = 'https://raw.githubusercontent.com/jgm/stmd/master/spec.txt';
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param $section
|
||||
* @param $markdown
|
||||
* @param $expectedHtml
|
||||
*/
|
||||
function test_($section, $markdown, $expectedHtml)
|
||||
{
|
||||
$Parsedown = new Parsedown();
|
||||
$Parsedown->setUrlsLinked(false);
|
||||
|
||||
$actualHtml = $Parsedown->text($markdown);
|
||||
$actualHtml = $this->normalizeMarkup($actualHtml);
|
||||
|
||||
$this->assertEquals($expectedHtml, $actualHtml);
|
||||
}
|
||||
|
||||
function data()
|
||||
{
|
||||
$spec = file_get_contents(self::SPEC_URL);
|
||||
$spec = strstr($spec, '<!-- END TESTS -->', true);
|
||||
|
||||
$tests = array();
|
||||
$currentSection = '';
|
||||
|
||||
preg_replace_callback(
|
||||
'/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$|^#{1,6} *(.*)$/m',
|
||||
function($matches) use ( & $tests, & $currentSection, & $testCount) {
|
||||
if (isset($matches[3]) and $matches[3]) {
|
||||
$currentSection = $matches[3];
|
||||
} else {
|
||||
$testCount++;
|
||||
$markdown = $matches[1];
|
||||
$markdown = preg_replace('/→/', "\t", $markdown);
|
||||
$expectedHtml = $matches[2];
|
||||
$expectedHtml = $this->normalizeMarkup($expectedHtml);
|
||||
$tests []= array(
|
||||
$currentSection, # section
|
||||
$markdown, # markdown
|
||||
$expectedHtml, # html
|
||||
);
|
||||
}
|
||||
},
|
||||
$spec
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
private function normalizeMarkup($markup)
|
||||
{
|
||||
$markup = preg_replace("/\n+/", "\n", $markup);
|
||||
$markup = preg_replace('/^\s+/m', '', $markup);
|
||||
$markup = preg_replace('/^((?:<[\w]+>)+)\n/m', '$1', $markup);
|
||||
$markup = preg_replace('/\n((?:<\/[\w]+>)+)$/m', '$1', $markup);
|
||||
$markup = trim($markup);
|
||||
|
||||
return $markup;
|
||||
}
|
||||
}
|
71
test/CommonMarkTestStrict.php
Normal file
71
test/CommonMarkTestStrict.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test Parsedown against the CommonMark spec
|
||||
*
|
||||
* @link http://commonmark.org/ CommonMark
|
||||
*/
|
||||
class CommonMarkTestStrict extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
const SPEC_URL = 'https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt';
|
||||
|
||||
protected $parsedown;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->parsedown = new TestParsedown();
|
||||
$this->parsedown->setUrlsLinked(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param $id
|
||||
* @param $section
|
||||
* @param $markdown
|
||||
* @param $expectedHtml
|
||||
*/
|
||||
public function testExample($id, $section, $markdown, $expectedHtml)
|
||||
{
|
||||
$actualHtml = $this->parsedown->text($markdown);
|
||||
$this->assertEquals($expectedHtml, $actualHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function data()
|
||||
{
|
||||
$spec = file_get_contents(self::SPEC_URL);
|
||||
if ($spec === false) {
|
||||
$this->fail('Unable to load CommonMark spec from ' . self::SPEC_URL);
|
||||
}
|
||||
|
||||
$spec = str_replace("\r\n", "\n", $spec);
|
||||
$spec = strstr($spec, '<!-- END TESTS -->', true);
|
||||
|
||||
$matches = array();
|
||||
preg_match_all('/^`{32} example\n((?s).*?)\n\.\n(?:|((?s).*?)\n)`{32}$|^#{1,6} *(.*?)$/m', $spec, $matches, PREG_SET_ORDER);
|
||||
|
||||
$data = array();
|
||||
$currentId = 0;
|
||||
$currentSection = '';
|
||||
foreach ($matches as $match) {
|
||||
if (isset($match[3])) {
|
||||
$currentSection = $match[3];
|
||||
} else {
|
||||
$currentId++;
|
||||
$markdown = str_replace('→', "\t", $match[1]);
|
||||
$expectedHtml = isset($match[2]) ? str_replace('→', "\t", $match[2]) : '';
|
||||
|
||||
$data[$currentId] = array(
|
||||
'id' => $currentId,
|
||||
'section' => $currentSection,
|
||||
'markdown' => $markdown,
|
||||
'expectedHtml' => $expectedHtml
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
63
test/CommonMarkTestWeak.php
Normal file
63
test/CommonMarkTestWeak.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/CommonMarkTestStrict.php');
|
||||
|
||||
/**
|
||||
* Test Parsedown against the CommonMark spec, but less aggressive
|
||||
*
|
||||
* The resulting HTML markup is cleaned up before comparison, so examples
|
||||
* which would normally fail due to actually invisible differences (e.g.
|
||||
* superfluous whitespaces), don't fail. However, cleanup relies on block
|
||||
* element detection. The detection doesn't work correctly when a element's
|
||||
* `display` CSS property is manipulated. According to that this test is only
|
||||
* a interim solution on Parsedown's way to full CommonMark compatibility.
|
||||
*
|
||||
* @link http://commonmark.org/ CommonMark
|
||||
*/
|
||||
class CommonMarkTestWeak extends CommonMarkTestStrict
|
||||
{
|
||||
protected $textLevelElementRegex;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$textLevelElements = $this->parsedown->getTextLevelElements();
|
||||
array_walk($textLevelElements, function (&$element) {
|
||||
$element = preg_quote($element, '/');
|
||||
});
|
||||
$this->textLevelElementRegex = '\b(?:' . implode('|', $textLevelElements) . ')\b';
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data
|
||||
* @param $id
|
||||
* @param $section
|
||||
* @param $markdown
|
||||
* @param $expectedHtml
|
||||
*/
|
||||
public function testExample($id, $section, $markdown, $expectedHtml)
|
||||
{
|
||||
$expectedHtml = $this->cleanupHtml($expectedHtml);
|
||||
|
||||
$actualHtml = $this->parsedown->text($markdown);
|
||||
$actualHtml = $this->cleanupHtml($actualHtml);
|
||||
|
||||
$this->assertEquals($expectedHtml, $actualHtml);
|
||||
}
|
||||
|
||||
protected function cleanupHtml($markup)
|
||||
{
|
||||
// invisible whitespaces at the beginning and end of block elements
|
||||
// however, whitespaces at the beginning of <pre> elements do matter
|
||||
$markup = preg_replace(
|
||||
array(
|
||||
'/(<(?!(?:' . $this->textLevelElementRegex . '|\bpre\b))\w+\b[^>]*>(?:<' . $this->textLevelElementRegex . '[^>]*>)*)\s+/s',
|
||||
'/\s+((?:<\/' . $this->textLevelElementRegex . '>)*<\/(?!' . $this->textLevelElementRegex . ')\w+\b>)/s'
|
||||
),
|
||||
'$1',
|
||||
$markup
|
||||
);
|
||||
|
||||
return $markup;
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
<?php
|
||||
require 'SampleExtensions.php';
|
||||
|
||||
class ParsedownTest extends PHPUnit_Framework_TestCase
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ParsedownTest extends TestCase
|
||||
{
|
||||
final function __construct($name = null, array $data = array(), $dataName = '')
|
||||
{
|
||||
@ -28,7 +31,7 @@ class ParsedownTest extends PHPUnit_Framework_TestCase
|
||||
*/
|
||||
protected function initParsedown()
|
||||
{
|
||||
$Parsedown = new Parsedown();
|
||||
$Parsedown = new TestParsedown();
|
||||
|
||||
return $Parsedown;
|
||||
}
|
||||
@ -47,11 +50,47 @@ class ParsedownTest extends PHPUnit_Framework_TestCase
|
||||
$expectedMarkup = str_replace("\r\n", "\n", $expectedMarkup);
|
||||
$expectedMarkup = str_replace("\r", "\n", $expectedMarkup);
|
||||
|
||||
$this->Parsedown->setSafeMode(substr($test, 0, 3) === 'xss');
|
||||
|
||||
$actualMarkup = $this->Parsedown->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
}
|
||||
|
||||
function testRawHtml()
|
||||
{
|
||||
$markdown = "```php\nfoobar\n```";
|
||||
$expectedMarkup = '<pre><code class="language-php"><p>foobar</p></code></pre>';
|
||||
$expectedSafeMarkup = '<pre><code class="language-php"><p>foobar</p></code></pre>';
|
||||
|
||||
$unsafeExtension = new UnsafeExtension;
|
||||
$actualMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
|
||||
$unsafeExtension->setSafeMode(true);
|
||||
$actualSafeMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedSafeMarkup, $actualSafeMarkup);
|
||||
}
|
||||
|
||||
function testTrustDelegatedRawHtml()
|
||||
{
|
||||
$markdown = "```php\nfoobar\n```";
|
||||
$expectedMarkup = '<pre><code class="language-php"><p>foobar</p></code></pre>';
|
||||
$expectedSafeMarkup = $expectedMarkup;
|
||||
|
||||
$unsafeExtension = new TrustDelegatedExtension;
|
||||
$actualMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedMarkup, $actualMarkup);
|
||||
|
||||
$unsafeExtension->setSafeMode(true);
|
||||
$actualSafeMarkup = $unsafeExtension->text($markdown);
|
||||
|
||||
$this->assertEquals($expectedSafeMarkup, $actualSafeMarkup);
|
||||
}
|
||||
|
||||
function data()
|
||||
{
|
||||
$data = array();
|
||||
@ -133,15 +172,14 @@ color: red;
|
||||
<p>comment</p>
|
||||
<p><!-- html comment --></p>
|
||||
EXPECTED_HTML;
|
||||
$parsedownWithNoMarkup = new Parsedown();
|
||||
|
||||
$parsedownWithNoMarkup = new TestParsedown();
|
||||
$parsedownWithNoMarkup->setMarkupEscaped(true);
|
||||
$this->assertEquals($expectedHtml, $parsedownWithNoMarkup->text($markdownWithHtml));
|
||||
}
|
||||
|
||||
public function testLateStaticBinding()
|
||||
{
|
||||
include __DIR__ . '/TestParsedown.php';
|
||||
|
||||
$parsedown = Parsedown::instance();
|
||||
$this->assertInstanceOf('Parsedown', $parsedown);
|
||||
|
||||
|
40
test/SampleExtensions.php
Normal file
40
test/SampleExtensions.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
class UnsafeExtension extends Parsedown
|
||||
{
|
||||
protected function blockFencedCodeComplete($Block)
|
||||
{
|
||||
$text = $Block['element']['text']['text'];
|
||||
unset($Block['element']['text']['text']);
|
||||
|
||||
// WARNING: There is almost always a better way of doing things!
|
||||
//
|
||||
// This example is one of them, unsafe behaviour is NOT needed here.
|
||||
// Only use this if you trust the input and have no idea what
|
||||
// the output HTML will look like (e.g. using an external parser).
|
||||
$Block['element']['text']['rawHtml'] = "<p>$text</p>";
|
||||
|
||||
return $Block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TrustDelegatedExtension extends Parsedown
|
||||
{
|
||||
protected function blockFencedCodeComplete($Block)
|
||||
{
|
||||
$text = $Block['element']['text']['text'];
|
||||
unset($Block['element']['text']['text']);
|
||||
|
||||
// WARNING: There is almost always a better way of doing things!
|
||||
//
|
||||
// This behaviour is NOT needed in the demonstrated case.
|
||||
// Only use this if you are sure that the result being added into
|
||||
// rawHtml is safe.
|
||||
// (e.g. using an external parser with escaping capabilities).
|
||||
$Block['element']['text']['rawHtml'] = "<p>$text</p>";
|
||||
$Block['element']['text']['allowRawHtmlInSafeMode'] = true;
|
||||
|
||||
return $Block;
|
||||
}
|
||||
}
|
@ -2,4 +2,8 @@
|
||||
|
||||
class TestParsedown extends Parsedown
|
||||
{
|
||||
public function getTextLevelElements()
|
||||
{
|
||||
return $this->textLevelElements;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
include 'Parsedown.php';
|
||||
|
||||
if ( ! class_exists('\PHPUnit_Framework_TestCase')) {
|
||||
class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase');
|
||||
}
|
@ -1 +1,2 @@
|
||||
<p>my email is <a href="mailto:me@example.com">me@example.com</a></p>
|
||||
<p>my email is <a href="mailto:me@example.com">me@example.com</a></p>
|
||||
<p>html tags shouldn't start an email autolink <strong>first.last@example.com</strong></p>
|
@ -1 +1,3 @@
|
||||
my email is <me@example.com>
|
||||
my email is <me@example.com>
|
||||
|
||||
html tags shouldn't start an email autolink <strong>first.last@example.com</strong>
|
@ -3,4 +3,9 @@
|
||||
$message = 'fenced code block';
|
||||
echo $message;</code></pre>
|
||||
<pre><code>tilde</code></pre>
|
||||
<pre><code class="language-php">echo 'language identifier';</code></pre>
|
||||
<pre><code class="language-php">echo 'language identifier';</code></pre>
|
||||
<pre><code class="language-c#">echo 'language identifier with non words';</code></pre>
|
||||
<pre><code class="language-html+php"><?php
|
||||
echo "Hello World";
|
||||
?>
|
||||
<a href="http://auraphp.com" >Aura Project</a></code></pre>
|
@ -11,4 +11,15 @@ tilde
|
||||
|
||||
```php
|
||||
echo 'language identifier';
|
||||
```
|
||||
|
||||
```c#
|
||||
echo 'language identifier with non words';
|
||||
```
|
||||
|
||||
```html+php
|
||||
<?php
|
||||
echo "Hello World";
|
||||
?>
|
||||
<a href="http://auraphp.com" >Aura Project</a>
|
||||
```
|
10
test/data/multiline_lists.html
Normal file
10
test/data/multiline_lists.html
Normal file
@ -0,0 +1,10 @@
|
||||
<ol>
|
||||
<li>
|
||||
<p>One
|
||||
First body copy</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Two
|
||||
Last body copy</p>
|
||||
</li>
|
||||
</ol>
|
5
test/data/multiline_lists.md
Normal file
5
test/data/multiline_lists.md
Normal file
@ -0,0 +1,5 @@
|
||||
1. One
|
||||
First body copy
|
||||
|
||||
2. Two
|
||||
Last body copy
|
@ -8,5 +8,7 @@
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
<li>li</li>
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
</ul>
|
@ -2,6 +2,10 @@
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
<li>li</li>
|
||||
<li>li</li>
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
</ul>
|
@ -2,7 +2,9 @@
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
<li>li</li>
|
||||
<li>
|
||||
<p>li</p>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<ul>
|
||||
|
6
test/data/xss_attribute_encoding.html
Normal file
6
test/data/xss_attribute_encoding.html
Normal file
@ -0,0 +1,6 @@
|
||||
<p><a href="https://www.example.com"">xss</a></p>
|
||||
<p><img src="https://www.example.com"" alt="xss" /></p>
|
||||
<p><a href="https://www.example.com'">xss</a></p>
|
||||
<p><img src="https://www.example.com'" alt="xss" /></p>
|
||||
<p><img src="https://www.example.com" alt="xss"" /></p>
|
||||
<p><img src="https://www.example.com" alt="xss'" /></p>
|
11
test/data/xss_attribute_encoding.md
Normal file
11
test/data/xss_attribute_encoding.md
Normal file
@ -0,0 +1,11 @@
|
||||
[xss](https://www.example.com")
|
||||
|
||||
![xss](https://www.example.com")
|
||||
|
||||
[xss](https://www.example.com')
|
||||
|
||||
![xss](https://www.example.com')
|
||||
|
||||
![xss"](https://www.example.com)
|
||||
|
||||
![xss'](https://www.example.com)
|
16
test/data/xss_bad_url.html
Normal file
16
test/data/xss_bad_url.html
Normal file
@ -0,0 +1,16 @@
|
||||
<p><a href="javascript%3Aalert(1)">xss</a></p>
|
||||
<p><a href="javascript%3Aalert(1)">xss</a></p>
|
||||
<p><a href="javascript%3A//alert(1)">xss</a></p>
|
||||
<p><a href="javascript&colon;alert(1)">xss</a></p>
|
||||
<p><img src="javascript%3Aalert(1)" alt="xss" /></p>
|
||||
<p><img src="javascript%3Aalert(1)" alt="xss" /></p>
|
||||
<p><img src="javascript%3A//alert(1)" alt="xss" /></p>
|
||||
<p><img src="javascript&colon;alert(1)" alt="xss" /></p>
|
||||
<p><a href="data%3Atext/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">xss</a></p>
|
||||
<p><a href="data%3Atext/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">xss</a></p>
|
||||
<p><a href="data%3A//text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">xss</a></p>
|
||||
<p><a href="data&colon;text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">xss</a></p>
|
||||
<p><img src="data%3Atext/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" alt="xss" /></p>
|
||||
<p><img src="data%3Atext/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" alt="xss" /></p>
|
||||
<p><img src="data%3A//text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" alt="xss" /></p>
|
||||
<p><img src="data&colon;text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" alt="xss" /></p>
|
31
test/data/xss_bad_url.md
Normal file
31
test/data/xss_bad_url.md
Normal file
@ -0,0 +1,31 @@
|
||||
[xss](javascript:alert(1))
|
||||
|
||||
[xss]( javascript:alert(1))
|
||||
|
||||
[xss](javascript://alert(1))
|
||||
|
||||
[xss](javascript:alert(1))
|
||||
|
||||
![xss](javascript:alert(1))
|
||||
|
||||
![xss]( javascript:alert(1))
|
||||
|
||||
![xss](javascript://alert(1))
|
||||
|
||||
![xss](javascript:alert(1))
|
||||
|
||||
[xss](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
[xss]( data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
[xss](data://text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
[xss](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
![xss](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
![xss]( data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
![xss](data://text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
||||
|
||||
![xss](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
|
7
test/data/xss_text_encoding.html
Normal file
7
test/data/xss_text_encoding.html
Normal file
@ -0,0 +1,7 @@
|
||||
<p><script>alert(1)</script></p>
|
||||
<p><script></p>
|
||||
<p>alert(1)</p>
|
||||
<p></script></p>
|
||||
<p><script>
|
||||
alert(1)
|
||||
</script></p>
|
12
test/data/xss_text_encoding.md
Normal file
12
test/data/xss_text_encoding.md
Normal file
@ -0,0 +1,12 @@
|
||||
<script>alert(1)</script>
|
||||
|
||||
<script>
|
||||
|
||||
alert(1)
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
alert(1)
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user