Skip to content

Commit c7f59e2

Browse files
committed
added ExpressionAttributeNode for <tag name="{$foo}"> or <tag name={$foo}>
(always uses double quotes)
1 parent fec0dfc commit c7f59e2

File tree

11 files changed

+135
-37
lines changed

11 files changed

+135
-37
lines changed

src/Latte/Compiler/Nodes/Html/ElementNode.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public function getAttribute(string $name): string|Node|bool|null
5353
&& $this->matchesIdentifier($name, $child->name->content)
5454
) {
5555
return NodeHelpers::toText($child->value) ?? $child->value ?? true;
56+
} elseif ($child instanceof ExpressionAttributeNode
57+
&& $this->matchesIdentifier($name, $child->name)
58+
) {
59+
return true;
5660
}
5761
}
5862

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Latte (https://latte.nette.org)
5+
* Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Latte\Compiler\Nodes\Html;
11+
12+
use Latte\Compiler\Nodes\AreaNode;
13+
use Latte\Compiler\Nodes\Php\ExpressionNode;
14+
use Latte\Compiler\Nodes\Php\ModifierNode;
15+
use Latte\Compiler\Position;
16+
use Latte\Compiler\PrintContext;
17+
18+
19+
class ExpressionAttributeNode extends AreaNode
20+
{
21+
public function __construct(
22+
public string $name,
23+
public ExpressionNode $value,
24+
public ModifierNode $modifier,
25+
public ?Position $position = null,
26+
public ?string $indentation = null,
27+
) {
28+
}
29+
30+
31+
public function print(PrintContext $context): string
32+
{
33+
$escaper = $context->beginEscape();
34+
$escaper->enterHtmlAttribute($this->name);
35+
$res = $context->format(
36+
'echo %dump; echo %modify(%node) %line; echo \'"\';',
37+
$this->indentation . $this->name . '="',
38+
$this->modifier,
39+
$this->value,
40+
$this->value->position,
41+
);
42+
$context->restoreEscape();
43+
return $res;
44+
}
45+
46+
47+
public function &getIterator(): \Generator
48+
{
49+
yield $this->value;
50+
yield $this->modifier;
51+
}
52+
}

src/Latte/Compiler/TemplateParserHtml.php

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ private function parseStartTag(&$elem = null): Html\ElementNode
210210
'textualName' => $textual,
211211
];
212212
$elem->attributes = $this->parser->parseFragment($this->inTagResolve(...));
213+
$this->relocateAttributeIndentation($elem->attributes);
213214
$elem->selfClosing = (bool) $stream->tryConsume(Token::Slash);
214215
if ($variable) {
215216
$elem->dynamicTag = new Nodes\Html\TagNode($elem, $variable);
@@ -223,6 +224,19 @@ private function parseStartTag(&$elem = null): Html\ElementNode
223224
}
224225

225226

227+
private function relocateAttributeIndentation(FragmentNode $node): void
228+
{
229+
foreach ($node->children as $i => $child) {
230+
$next = $node->children[$i + 1] ?? null;
231+
if ($child instanceof Nodes\TextNode && $child->isWhitespace() && $next instanceof Html\ExpressionAttributeNode) {
232+
$next->indentation = $child->content;
233+
unset($node->children[$i]);
234+
}
235+
}
236+
$node->children = array_values($node->children);
237+
}
238+
239+
226240
/** @return array{string, ?Nodes\Php\ExpressionNode} */
227241
private function parseEndTag(): array
228242
{
@@ -363,12 +377,19 @@ private function parseAttribute(): ?Node
363377
}
364378

365379
[$value, $quote] = $this->parseAttributeValue();
366-
return new Html\AttributeNode(
367-
name: $name,
368-
value: $value,
369-
quote: $quote,
370-
position: $name->position,
371-
);
380+
return $name instanceof Nodes\TextNode && $value instanceof Nodes\PrintNode
381+
? new Html\ExpressionAttributeNode(
382+
name: $name->content,
383+
value: $value->expression,
384+
modifier: $value->modifier,
385+
position: $name->position,
386+
)
387+
: new Html\AttributeNode(
388+
name: $name,
389+
value: $value,
390+
quote: $quote,
391+
position: $name->position,
392+
);
372393
}
373394

374395

src/Latte/Essential/Passes.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111

1212
use Latte\CompileException;
1313
use Latte\Compiler\Node;
14-
use Latte\Compiler\Nodes\FragmentNode;
15-
use Latte\Compiler\Nodes\Html\AttributeNode;
1614
use Latte\Compiler\Nodes\Html\ElementNode;
15+
use Latte\Compiler\Nodes\Html\ExpressionAttributeNode;
1716
use Latte\Compiler\Nodes\Php;
1817
use Latte\Compiler\Nodes\Php\Expression;
1918
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
@@ -123,20 +122,12 @@ public function checkUrlsPass(TemplateNode $node): void
123122
if ($node instanceof ElementNode) {
124123
$elem = $node;
125124

126-
} elseif ($node instanceof AttributeNode
127-
&& $node->name instanceof TextNode
128-
&& HtmlHelpers::isUrlAttribute($elem->name, $node->name->content)
125+
} elseif ($node instanceof ExpressionAttributeNode
126+
&& HtmlHelpers::isUrlAttribute($elem->name, $node->name)
127+
&& !$node->modifier->removeFilter('nocheck') && !$node->modifier->removeFilter('noCheck')
128+
&& !$node->modifier->hasFilter('datastream') && !$node->modifier->hasFilter('dataStream')
129129
) {
130-
$attrValue = $node->value instanceof FragmentNode && $node->value->children
131-
? $node->value->children[0]
132-
: $node->value;
133-
134-
if ($attrValue instanceof PrintNode && ($modifier = $attrValue->modifier)
135-
&& !$modifier->removeFilter('nocheck') && !$modifier->removeFilter('noCheck')
136-
&& !$modifier->hasFilter('datastream') && !$modifier->hasFilter('dataStream')
137-
) {
138-
$attrValue->modifier->filters[] = new Php\FilterNode(new Php\IdentifierNode('checkUrl'));
139-
}
130+
$node->modifier->filters[] = new Php\FilterNode(new Php\IdentifierNode('checkUrl'));
140131
}
141132
});
142133
}

tests/common/ElementNode.getAttribute().phpt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ declare(strict_types=1);
55
use Latte\Compiler\Nodes\FragmentNode;
66
use Latte\Compiler\Nodes\Html\AttributeNode;
77
use Latte\Compiler\Nodes\Html\ElementNode;
8+
use Latte\Compiler\Nodes\Html\ExpressionAttributeNode;
9+
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
10+
use Latte\Compiler\Nodes\Php\ModifierNode;
811
use Latte\Compiler\Nodes\TextNode;
912
use Latte\ContentType;
1013
use Tester\Assert;
@@ -113,3 +116,35 @@ test('ElementNode::getAttribute() - non-existent attribute', function () {
113116
Assert::null($element->getAttribute('id'));
114117
Assert::null($element->getAttribute('nonexistent'));
115118
});
119+
120+
121+
test('ElementNode::getAttribute() - ExpressionAttributeNode HTML case insensitive', function () {
122+
$element = new ElementNode('div', contentType: ContentType::Html);
123+
$element->attributes = new FragmentNode([
124+
new ExpressionAttributeNode(
125+
'CLASS',
126+
new VariableNode('foo'),
127+
new ModifierNode([]),
128+
),
129+
]);
130+
131+
Assert::same(true, $element->getAttribute('class'));
132+
Assert::same(true, $element->getAttribute('CLASS'));
133+
Assert::same(true, $element->getAttribute('Class'));
134+
});
135+
136+
137+
test('ElementNode::getAttribute() - ExpressionAttributeNode non-HTML case sensitive', function () {
138+
$element = new ElementNode('element', contentType: ContentType::Xml);
139+
$element->attributes = new FragmentNode([
140+
new ExpressionAttributeNode(
141+
'CLASS',
142+
new VariableNode('foo'),
143+
new ModifierNode([]),
144+
),
145+
]);
146+
147+
Assert::same(true, $element->getAttribute('CLASS'));
148+
Assert::null($element->getAttribute('class'));
149+
Assert::null($element->getAttribute('Class'));
150+
});

tests/common/expected/contentType.xml.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<p onclick='alert(:/item);alert("hello");'
3333
title=':/item"'
3434
style="color::/item;'"
35-
alt=''
35+
alt=""
3636
onfocus="alert()"
3737
>click on me</p>
3838

tests/common/expected/contentType.xml.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ public function main(array $ʟ_args): void
7272
style="color:';
7373
echo LR\XmlHelpers::escapeAttr($id) /* line %d%:%d% */;
7474
echo ';\'"
75-
alt=\'';
75+
alt="';
7676
echo LR\XmlHelpers::escapeAttr($el) /* line %d%:%d% */;
77-
echo '\'
77+
echo '"
7878
onfocus="alert(';
7979
echo LR\XmlHelpers::escapeAttr($el) /* line %d%:%d% */;
8080
echo ')"

tests/tags/expected/ifchanged.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
ob_start(fn() => '');
128128
try /* line 22:55 */ {
129129
echo '<span class="';
130-
echo LR\HtmlHelpers::escapeAttr($i) /* line 22:49 */;
130+
echo LR\HtmlHelpers::escapeAttr($i) /* line 22:50 */;
131131
echo '"></span>';
132132
} finally {
133133
$ʟ_tmp = ob_get_clean();

tests/tags/expected/print.xss.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</SCRIPT>
3535

3636
<p onclick='alert(&quot;some&amp;&lt;&gt;\&quot;&apos;/chars&quot;);alert("hello");'
37-
title='some&amp;&lt;&gt;&quot;&apos;/chars'
37+
title="some&amp;&lt;&gt;&quot;&apos;/chars"
3838
STYLE="color:some\&amp;\&lt;\&gt;\&quot;\&apos;\/chars;"
3939
rel="some&amp;&lt;&gt;&quot;&apos;/chars"
4040
onblur="alert(&quot;some&amp;&lt;&gt;\&quot;&apos;/chars&quot;)"

tests/tags/expected/print.xss.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@
5959
<p onclick=\'alert(';
6060
echo LR\HtmlHelpers::escapeAttr(LR\Helpers::escapeJs($xss)) /* line %d%:%d% */;
6161
echo ');alert("hello");\'
62-
title=\'';
62+
title="';
6363
echo LR\HtmlHelpers::escapeAttr($xss) /* line %d%:%d% */;
64-
echo '\'
64+
echo '"
6565
STYLE="color:';
6666
echo LR\HtmlHelpers::escapeAttr(LR\Helpers::escapeCss($xss)) /* line %d%:%d% */;
6767
echo ';"

0 commit comments

Comments
 (0)