diff --git a/src/Latte/Compiler/Escaper.php b/src/Latte/Compiler/Escaper.php index d3a077eb06..27f9ee0754 100644 --- a/src/Latte/Compiler/Escaper.php +++ b/src/Latte/Compiler/Escaper.php @@ -27,7 +27,8 @@ final class Escaper JavaScript = 'js', Css = 'css', ICal = 'ical', - Url = 'url'; + Url = 'url', + UrlPart = 'url-part'; public const HtmlText = 'html', @@ -101,13 +102,13 @@ public function enterHtmlTag(string $name): void } - public function enterHtmlAttribute(?string $name = null, string $quote = ''): void + public function enterHtmlAttribute(?string $name = null, string $quote = '', string $subType = ''): void { $this->state = self::HtmlAttribute; $this->quote = $quote; - $this->subType = ''; + $this->subType = $subType; - if ($this->contentType === ContentType::Html && is_string($name)) { + if ($this->contentType === ContentType::Html && !$this->subType && $name) { $name = strtolower($name); if (str_starts_with($name, 'on')) { $this->subType = self::JavaScript; @@ -150,6 +151,7 @@ public function escape(string $str): string self::HtmlAttribute => match ($this->subType) { '', self::Url => $lq . 'LR\Filters::escapeHtmlAttr(' . $str . ')' . $rq, + self::UrlPart => $lq . 'LR\Filters::escapeHtmlAttr(rawurlencode(' . $str . '))' . $rq, self::JavaScript => $lq . 'LR\Filters::escapeHtmlAttr(LR\Filters::escapeJs(' . $str . '))' . $rq, self::Css => $lq . 'LR\Filters::escapeHtmlAttr(LR\Filters::escapeCss(' . $str . '))' . $rq, }, diff --git a/src/Latte/Compiler/Nodes/Html/QuotedValue.php b/src/Latte/Compiler/Nodes/Html/QuotedValue.php index 3a858061c5..853b367601 100644 --- a/src/Latte/Compiler/Nodes/Html/QuotedValue.php +++ b/src/Latte/Compiler/Nodes/Html/QuotedValue.php @@ -34,7 +34,7 @@ public function print(PrintContext $context): string if ($this->value instanceof FragmentNode && $escaper->export() === 'html/attr/url') { foreach ($this->value->children as $child) { $res .= $child->print($context); - $escaper->enterHtmlAttribute(null, $this->quote); + $escaper->enterHtmlAttribute(null, $this->quote, $escaper::UrlPart); } } else { $res .= $this->value->print($context); diff --git a/src/Latte/Compiler/Nodes/Php/ModifierNode.php b/src/Latte/Compiler/Nodes/Php/ModifierNode.php index 340b83b8fa..e6087fedff 100644 --- a/src/Latte/Compiler/Nodes/Php/ModifierNode.php +++ b/src/Latte/Compiler/Nodes/Php/ModifierNode.php @@ -49,12 +49,17 @@ public function printSimple(PrintContext $context, string $expr): string { $escape = $this->escape; $check = $this->check; + $escaper = $context->getEscaper(); + $export = $escaper->export(); + foreach ($this->filters as $filter) { $name = $filter->name->name; if (['nocheck' => 1, 'noCheck' => 1][$name] ?? null) { $check = false; } elseif ($name === 'noescape') { $escape = false; + } elseif ($name === 'escapeUrl' && $export === 'html/attr/url-part') { + // prevent double escaping } else { if (['datastream' => 1, 'dataStream' => 1][$name] ?? null) { $check = false; @@ -63,7 +68,6 @@ public function printSimple(PrintContext $context, string $expr): string } } - $escaper = $context->getEscaper(); if ($check) { $expr = $escaper->check($expr); } diff --git a/tests/common/Safe.url.phpt b/tests/common/Safe.url.phpt index 3679cf9bdf..c947995d2a 100644 --- a/tests/common/Safe.url.phpt +++ b/tests/common/Safe.url.phpt @@ -27,7 +27,7 @@ Assert::match(' - + @@ -77,3 +77,9 @@ Assert::contains( 'LR\Filters::escapeHtmlAttr(LR\Filters::safeUrl(($this->filters->upper)($url1)))', $latte->compile(''), ); + + +Assert::contains( + 'echo LR\Filters::escapeHtmlAttr(rawurlencode($url1)) /* line 1 */', + $latte->compile(''), +);