Skip to content

Commit

Permalink
Merge pull request #3 from jakubboucek/jb-xml
Browse files Browse the repository at this point in the history
Add Xml escape and sanitized Html content
  • Loading branch information
jakubboucek authored Sep 11, 2021
2 parents 39af2c5 + 564f7f1 commit 60b4dab
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 52 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/code_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ jobs:
strategy:
fail-fast: false
matrix:
php: ['7.3', '7.4', '8.0', '8.1']
php:
# - '7.1' # incompatible tester
- '7.2'
- '7.3'
- '7.4'
- '8.0'
# - '8.1' # not yet compatible (PHP 8.1 RC2)
actions:
- name: PHPStan
run: composer phpstan

- name: Easy Coding Standard
run: composer ecs

- name: Unit tests
run: vendor/bin/tester tests -s -C

Expand Down Expand Up @@ -48,8 +51,6 @@ jobs:
${{ steps.composer-cache.outputs.dir }}
**/composer.lock
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
${{ runner.os }}-${{ matrix.php }}-composer-


- name: Install Composer
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Package is substrate of [Latte package](https://github.com/nette/latte/)
- Escape HTML
- Escape HTML attributes
- Escape HTML comments
- Escape XML
- Escape JS
- Escape URL
- Escape CSS
Expand Down Expand Up @@ -52,6 +53,10 @@ echo '<style>color: ' . \JakubBoucek\Escape\EscapeCss::color($cssColor) . ';</st
It's prevent attact by escaping color value context.
## Safe HTML content
Package supports escaping HTML with included [safe HTML content](https://doc.nette.org/en/3.1/html-elements).
## Output without any escaping
In some cases you intentionally want to output variable without any escaping, but somebody other or your future self may
Expand Down
9 changes: 3 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@
],
"require": {
"php": ">= 7.1",
"nette/utils": "^3.1"
"nette/utils": "^3.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.83",
"symplify/easy-coding-standard": "^9.2",
"phpstan/phpstan": "^0.12.98",
"nette/tester": "^2.4"
},
"autoload": {
Expand All @@ -29,9 +28,7 @@
}
},
"scripts": {
"phpstan": "phpstan analyze src --level 7",
"ecs": "ecs check",
"ecs-fix": "ecs check --fix",
"phpstan": "phpstan analyze src -c phpstan.neon --level 7",
"tester": "tester tests"
}
}
37 changes: 0 additions & 37 deletions ecs.php

This file was deleted.

6 changes: 6 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: '#Instanceof between mixed and Nette\\HtmlStringable will always evaluate to false\.#'
path: src/Escape.php
count: 2
27 changes: 26 additions & 1 deletion src/Escape.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

namespace JakubBoucek\Escape;

use Nette\HtmlStringable;
use Nette\Utils\IHtmlString;
use Nette\Utils\Json;

/**
Expand All @@ -18,13 +20,16 @@ class Escape
{
/**
* Escapes string for use everywhere inside HTML (except for comments)
* @param string|mixed $data
* @param string|HtmlStringable|IHtmlString|mixed $data
* @return string
*
* @link https://api.nette.org/2.4/source-Latte.Runtime.Filters.php.html#27-35
*/
public static function html($data): string
{
if ($data instanceof HtmlStringable || $data instanceof IHtmlString) {
return $data->__toString();
}
return htmlspecialchars((string)$data, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE);
}

Expand Down Expand Up @@ -64,6 +69,22 @@ public static function htmlComment($data): string
return $data;
}

/**
* Escapes string for use everywhere inside XML (except for comments).
* @param string|mixed $data
* @return string XML
*
* @link https://api.nette.org/2.4/source-Latte.Runtime.Filters.php.html#_escapeXml
*/
public static function xml($data): string
{
// XML 1.0: \x09 \x0A \x0D and C1 allowed directly, C0 forbidden
// XML 1.1: \x00 forbidden directly and as a character reference,
// \x09 \x0A \x0D \x85 allowed directly, C0, C1 and \x7F allowed as character references
$data = preg_replace('#[\x00-\x08\x0B\x0C\x0E-\x1F]#', "\u{FFFD}", (string)$data);
return htmlspecialchars($data, ENT_QUOTES | ENT_XML1 | ENT_SUBSTITUTE, 'UTF-8');
}

/**
* Escapes string for use inside JS code
* @param mixed $data
Expand All @@ -73,6 +94,10 @@ public static function htmlComment($data): string
*/
public static function js($data): string
{
if ($data instanceof HtmlStringable || $data instanceof IHtmlString) {
$data = $data->__toString();
}

$json = Json::encode($data);

return str_replace([']]>', '<!', '</'], [']]\u003E', '\u003C!', '<\/'], $json);
Expand Down
3 changes: 2 additions & 1 deletion src/EscapeCss.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

declare(strict_types=1);
// Intentionally no strict – package is most used to secure legacy projects
// declare(strict_types=1);

namespace JakubBoucek\Escape;

Expand Down
3 changes: 2 additions & 1 deletion tests/EscapeCssTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use JakubBoucek\Escape\Escape;
declare(strict_types=1);

use JakubBoucek\Escape\EscapeCss;
use Tester\Assert;
use Tester\Environment;
Expand Down
78 changes: 78 additions & 0 deletions tests/EscapeTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?php

declare(strict_types=1);

use JakubBoucek\Escape\Escape;
use Nette\Utils\Html;
use Tester\Assert;
use Tester\Environment;
use Tester\TestCase;
Expand Down Expand Up @@ -30,6 +33,7 @@ public function getHtmlArgs(): array
['&quot; &apos; &lt; &gt; &amp; �', "\" ' < > & \x8F"],
['`hello`', '`hello`'],
['` &lt;br&gt; `', '` <br> `'],
['Foo<br>bar', Html::fromHtml('Foo<br>bar')]
];
}

Expand Down Expand Up @@ -61,6 +65,7 @@ public function getHtmlAttrArgs(): array
['`hello` ', '`hello`'],
['``onmouseover=alert(1) ', '``onmouseover=alert(1)'],
['` &lt;br&gt; `', '` <br> `'],
['Foo&lt;br&gt;bar', Html::fromHtml('Foo<br>bar')]
];
}

Expand Down Expand Up @@ -94,6 +99,7 @@ public function getHtmlCommentArgs(): array
['`hello`', '`hello`'],
['``onmouseover=alert(1)', '``onmouseover=alert(1)'],
['` <br> `', '` <br> `'],
['Foo<br>bar', Html::fromHtml('Foo<br>bar')]
];
}

Expand All @@ -105,6 +111,43 @@ public function testHtmlComment(string $expected, $data): void
Assert::same($expected, Escape::htmlComment($data));
}

public function getXmlArgs(): array
{
return [

['', null],
['', ''],
['1', 1],
['string', 'string'],
['&lt; &amp; &apos; &quot; &gt;', '< & \' " >'],
['&lt;br&gt;', Html::fromHtml('<br>')],
[
"\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\x09\x0a\u{FFFD}\u{FFFD}\x0d\u{FFFD}\u{FFFD}",
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
],
[
"\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}",
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
],
// invalid UTF-8
["foo \u{FFFD} bar", "foo \u{D800} bar"], // invalid codepoint high surrogates
["foo \u{FFFD}&quot; bar", "foo \xE3\x80\x22 bar"], // stripped UTF
['&amp;quot;', '&quot;'],
['`hello', '`hello'],
['Hello &lt;World&gt;', 'Hello <World>'],
['` &lt;br&gt; `', '` <br> `'],
['Foo&lt;br&gt;bar', Html::fromHtml('Foo<br>bar')]
];
}

/**
* @dataProvider getXmlArgs
*/
public function testXml(string $expected, $data): void
{
Assert::same($expected, Escape::xml($data));
}

public function getJsArgs(): array
{
return [
Expand All @@ -118,6 +161,7 @@ public function getJsArgs(): array
['["0","1"]', ['0', '1']],
['{"a":"0","b":"1"}', ['a' => '0', 'b' => '1']],
['"<\\/script>"', '</script>'],
['"Foo<br>bar"', Html::fromHtml('Foo<br>bar')]
];
}

Expand All @@ -140,6 +184,8 @@ public function getCssArgs(): array
["foo \u{D800} bar", "foo \u{D800} bar"], // invalid codepoint high surrogates
["foo \xE3\x80\\\x22 bar", "foo \xE3\x80\x22 bar"], // stripped UTF
['\\<\\/style\\>', '</style>'],
['Foo\\<br\\>bar', Html::fromHtml('Foo<br>bar')]

];
}

Expand All @@ -165,6 +211,7 @@ public function getUrlArgs(): array
['a+b', 'a b'],
['a%27b', 'a\'b'],
['a%22b', 'a"b'],
['Foo%3Cbr%3Ebar', Html::fromHtml('Foo<br>bar')]
];
}

Expand All @@ -175,6 +222,37 @@ public function testUrl(string $expected, $data): void
{
Assert::same($expected, Escape::url($data));
}

public function getNoescapeArgs(): array
{
return [
['', null],
['', ''],
['1', 1],
['string', 'string'],
['<br>', '<br>'],
['< & \' " >', '< & \' " >'],
['&quot;', '&quot;'],
['`hello', '`hello'],
["foo \u{D800} bar", "foo \u{D800} bar"], // invalid codepoint high surrogates
["foo \xE3\x80\x22 bar", "foo \xE3\x80\x22 bar"], // stripped UTF
['Hello World', 'Hello World'],
['Hello <World>', 'Hello <World>'],
["\" ' < > & \x8F", "\" ' < > & \x8F"],
['`hello`', '`hello`'],
['` <br> `', '` <br> `'],
['Foo<br>bar', Html::fromHtml('Foo<br>bar')]
];
}

/**
* @dataProvider getNoescapeArgs
*/
public function testNoescape(string $expected, $data): void
{
Assert::same($expected, Escape::noescape($data));
}

}

(new EscapeTest())->run();

0 comments on commit 60b4dab

Please sign in to comment.