diff --git a/README.md b/README.md index afacc90..2d126b2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Package is substrate of [Latte package](https://github.com/nette/latte/) - Escape HTML - Escape HTML attributes +- Escape HTML href attributes - Escape HTML comments - Escape XML - Escape JS diff --git a/src/Escape.php b/src/Escape.php index e41eddc..fc5b8d5 100644 --- a/src/Escape.php +++ b/src/Escape.php @@ -56,6 +56,16 @@ public static function htmlAttr($data): string return self::html($data); } + /** + * Escapes string for use inside HTML attribute `href` or `src` which contains URL string. + * @param string|mixed $data + * @return string + */ + public static function htmlHref($data): string + { + return self::htmlAttr(self::safeUrl($data)); + } + /** * Escapes string for use inside HTML comments. * @param string|mixed $data @@ -140,6 +150,27 @@ public static function url($data): string return urlencode($data); } + /** + * Sanitizes string for use inside href attribute. + * @param string|mixed $data + * @param bool $warning + * @return string + * + * @link https://api.nette.org/2.4/source-Latte.Runtime.Filters.php.html#_safeUrl + */ + public static function safeUrl($data, bool $warning = false):string + { + if (preg_match('~^(?:(?:https?|ftp)://[^@]+(?:/.*)?|(?:mailto|tel|sms):.+|[/?#].*|[^:]+)$~Di', (string)$data)) { + return (string)$data; + } + + if($warning) { + trigger_error('URL was removed because is invalid or unsafe: ' . $data, E_USER_WARNING); + } + + return ''; + } + /** * Just returns argument as is without any escaping. * Method is useful to mark code as intentionally unescaped as opposed to simple neglected. diff --git a/tests/EscapeTest.php b/tests/EscapeTest.php index 8a84a2c..b83647e 100644 --- a/tests/EscapeTest.php +++ b/tests/EscapeTest.php @@ -86,6 +86,23 @@ public function testHtmlAttr(string $expected, $data): void Assert::same($expected, Escape::htmlAttr($data)); } + public function getHtmlHrefArgs(): array + { + return [ + ['', ''], + ['http://example.com/foo/bar.txt?par=var', 'http://example.com/foo/bar.txt?par=var'], + ['', 'javascript:alert(1)'], + ]; + } + + /** + * @dataProvider getHtmlHrefArgs + */ + public function testHtmlHref(string $expected, $data): void + { + Assert::same($expected, Escape::htmlHref($data)); + } + public function getHtmlCommentArgs(): array { return [ @@ -232,6 +249,38 @@ public function testUrl(string $expected, $data): void Assert::same($expected, Escape::url($data)); } + public function getSafeUrlArgs(): array + { + return [ + ['', null], + ['', ''], + ['', 'http://'], + ['http://x', 'http://x'], + ['http://x:80', 'http://x:80'], + ['', 'http://nette.org@1572395127'], + ['https://x', 'https://x'], + ['ftp://x', 'ftp://x'], + ['mailto:x', 'mailto:x'], + ['/', '/'], + ['/a:b', '/a:b'], + ['//x', '//x'], + ['#aa:b', '#aa:b'], + ['', 'data:'], + ['', 'javascript:'], + ['', ' javascript:'], + ['javascript', 'javascript'], + ['http://example.com', Html::fromHtml('http://example.com')], + ]; + } + + /** + * @dataProvider getSafeUrlArgs + */ + public function testSafeUrl(string $expected, $data): void + { + Assert::same($expected, Escape::safeUrl($data)); + } + public function getNoescapeArgs(): array { return [