From 3abe2315cd9810266466c1419d70e784f493ee47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sat, 11 Sep 2021 14:00:24 +0200 Subject: [PATCH 1/5] Remove symplify/easy-coding-standard because its too huge and buggy for long time --- .github/workflows/code_analysis.yaml | 3 --- composer.json | 3 --- ecs.php | 37 ---------------------------- 3 files changed, 43 deletions(-) delete mode 100644 ecs.php diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index a0ae72a..6c1a966 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -14,9 +14,6 @@ jobs: - name: PHPStan run: composer phpstan - - name: Easy Coding Standard - run: composer ecs - - name: Unit tests run: vendor/bin/tester tests -s -C diff --git a/composer.json b/composer.json index d2a31fb..a977a39 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ }, "require-dev": { "phpstan/phpstan": "^0.12.83", - "symplify/easy-coding-standard": "^9.2", "nette/tester": "^2.4" }, "autoload": { @@ -30,8 +29,6 @@ }, "scripts": { "phpstan": "phpstan analyze src --level 7", - "ecs": "ecs check", - "ecs-fix": "ecs check --fix", "tester": "tester tests" } } diff --git a/ecs.php b/ecs.php deleted file mode 100644 index 122450d..0000000 --- a/ecs.php +++ /dev/null @@ -1,37 +0,0 @@ -services(); - $services->set(ArraySyntaxFixer::class) - ->call( - 'configure', - [ - [ - 'syntax' => 'short', - ] - ] - ); - - $parameters = $containerConfigurator->parameters(); - $parameters->set( - Option::PATHS, - [ - __DIR__ . '/src', - __DIR__ . '/tests', - ] - ); - - $parameters->set( - Option::SETS, - [ - SetList::PSR_12, - ] - ); -}; From 07d6c4f7e2871fe9d8fafaf80b1cc7d6578f51bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sat, 11 Sep 2021 14:00:55 +0200 Subject: [PATCH 2/5] Composer: Extend support to nette/utils 3.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a977a39..b95f6e7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">= 7.1", - "nette/utils": "^3.1" + "nette/utils": "^3.0" }, "require-dev": { "phpstan/phpstan": "^0.12.83", From 31e8290fb1f7a9c2f0bf486842265406d3174a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sat, 11 Sep 2021 14:02:00 +0200 Subject: [PATCH 3/5] Add XML escaping, add support for Html object (Nette impl.) --- composer.json | 2 +- phpstan.neon | 6 ++++ src/Escape.php | 27 +++++++++++++- src/EscapeCss.php | 3 +- tests/EscapeCssTest.php | 3 +- tests/EscapeTest.php | 78 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index b95f6e7..57222af 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ } }, "scripts": { - "phpstan": "phpstan analyze src --level 7", + "phpstan": "phpstan analyze src -c phpstan.neon --level 7", "tester": "tester tests" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..57ea1e8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: '#Instanceof between mixed and Nette\\HtmlStringable will always evaluate to false\.#' + path: src/Escape.php + count: 2 diff --git a/src/Escape.php b/src/Escape.php index ed15b00..3ba327f 100644 --- a/src/Escape.php +++ b/src/Escape.php @@ -5,6 +5,8 @@ namespace JakubBoucek\Escape; +use Nette\HtmlStringable; +use Nette\Utils\IHtmlString; use Nette\Utils\Json; /** @@ -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); } @@ -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 @@ -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([']]>', ' & \x8F"], ['`hello`', '`hello`'], ['` <br> `', '`
`'], + ['Foo
bar', Html::fromHtml('Foo
bar')] ]; } @@ -61,6 +65,7 @@ public function getHtmlAttrArgs(): array ['`hello` ', '`hello`'], ['``onmouseover=alert(1) ', '``onmouseover=alert(1)'], ['` <br> `', '`
`'], + ['Foo<br>bar', Html::fromHtml('Foo
bar')] ]; } @@ -94,6 +99,7 @@ public function getHtmlCommentArgs(): array ['`hello`', '`hello`'], ['``onmouseover=alert(1)', '``onmouseover=alert(1)'], ['`
`', '`
`'], + ['Foo
bar', Html::fromHtml('Foo
bar')] ]; } @@ -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'], + ['< & ' " >', '< & \' " >'], + ['<br>', Html::fromHtml('
')], + [ + "\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}" bar", "foo \xE3\x80\x22 bar"], // stripped UTF + ['&quot;', '"'], + ['`hello', '`hello'], + ['Hello <World>', 'Hello '], + ['` <br> `', '`
`'], + ['Foo<br>bar', Html::fromHtml('Foo
bar')] + ]; + } + + /** + * @dataProvider getXmlArgs + */ + public function testXml(string $expected, $data): void + { + Assert::same($expected, Escape::xml($data)); + } + public function getJsArgs(): array { return [ @@ -118,6 +161,7 @@ public function getJsArgs(): array ['["0","1"]', ['0', '1']], ['{"a":"0","b":"1"}', ['a' => '0', 'b' => '1']], ['"<\\/script>"', ''], + ['"Foo
bar"', Html::fromHtml('Foo
bar')] ]; } @@ -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\\>', ''], + ['Foo\\bar', Html::fromHtml('Foo
bar')] + ]; } @@ -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
bar')] ]; } @@ -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'], + ['
', '
'], + ['< & \' " >', '< & \' " >'], + ['"', '"'], + ['`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 ', 'Hello '], + ["\" ' < > & \x8F", "\" ' < > & \x8F"], + ['`hello`', '`hello`'], + ['`
`', '`
`'], + ['Foo
bar', Html::fromHtml('Foo
bar')] + ]; + } + + /** + * @dataProvider getNoescapeArgs + */ + public function testNoescape(string $expected, $data): void + { + Assert::same($expected, Escape::noescape($data)); + } + } (new EscapeTest())->run(); From 511ea1b536ef60af6e78e1ff6e0364d08c6f42b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sat, 11 Sep 2021 14:11:40 +0200 Subject: [PATCH 4/5] Tests: Fix GitHub Workflow cache --- .github/workflows/code_analysis.yaml | 10 +++++++--- composer.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 6c1a966..04d7d95 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -9,7 +9,13 @@ 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 @@ -45,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 diff --git a/composer.json b/composer.json index 57222af..728c001 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "nette/utils": "^3.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.83", + "phpstan/phpstan": "^0.12.98", "nette/tester": "^2.4" }, "autoload": { From 564f7f1bf39a3b10921e04e1159d1bc11aee0797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bouc=CC=8Cek?= Date: Sat, 11 Sep 2021 14:32:00 +0200 Subject: [PATCH 5/5] Update Readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a11fa73..cb79a66 100644 --- a/README.md +++ b/README.md @@ -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 @@ -52,6 +53,10 @@ echo '