1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 * (c) Armin Ronacher
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13namespace Twig\TokenParser;
14
15use Twig\Error\SyntaxError;
16use Twig\Node\Expression\AssignNameExpression;
17use Twig\Node\Expression\ConstantExpression;
18use Twig\Node\Expression\GetAttrExpression;
19use Twig\Node\Expression\NameExpression;
20use Twig\Node\ForNode;
21use Twig\Node\Node;
22use Twig\Token;
23use Twig\TokenStream;
24
25/**
26 * Loops over each item of a sequence.
27 *
28 *   <ul>
29 *    {% for user in users %}
30 *      <li>{{ user.username|e }}</li>
31 *    {% endfor %}
32 *   </ul>
33 */
34final class ForTokenParser extends AbstractTokenParser
35{
36    public function parse(Token $token)
37    {
38        $lineno = $token->getLine();
39        $stream = $this->parser->getStream();
40        $targets = $this->parser->getExpressionParser()->parseAssignmentExpression();
41        $stream->expect(/* Token::OPERATOR_TYPE */ 8, 'in');
42        $seq = $this->parser->getExpressionParser()->parseExpression();
43
44        $ifexpr = null;
45        if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'if')) {
46            @trigger_error(sprintf('Using an "if" condition on "for" tag in "%s" at line %d is deprecated since Twig 2.10.0, use a "filter" filter or an "if" condition inside the "for" body instead (if your condition depends on a variable updated inside the loop).', $stream->getSourceContext()->getName(), $lineno), \E_USER_DEPRECATED);
47
48            $ifexpr = $this->parser->getExpressionParser()->parseExpression();
49        }
50
51        $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
52        $body = $this->parser->subparse([$this, 'decideForFork']);
53        if ('else' == $stream->next()->getValue()) {
54            $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
55            $else = $this->parser->subparse([$this, 'decideForEnd'], true);
56        } else {
57            $else = null;
58        }
59        $stream->expect(/* Token::BLOCK_END_TYPE */ 3);
60
61        if (\count($targets) > 1) {
62            $keyTarget = $targets->getNode(0);
63            $keyTarget = new AssignNameExpression($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine());
64            $valueTarget = $targets->getNode(1);
65            $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine());
66        } else {
67            $keyTarget = new AssignNameExpression('_key', $lineno);
68            $valueTarget = $targets->getNode(0);
69            $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine());
70        }
71
72        if ($ifexpr) {
73            $this->checkLoopUsageCondition($stream, $ifexpr);
74            $this->checkLoopUsageBody($stream, $body);
75        }
76
77        return new ForNode($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, $lineno, $this->getTag());
78    }
79
80    public function decideForFork(Token $token)
81    {
82        return $token->test(['else', 'endfor']);
83    }
84
85    public function decideForEnd(Token $token)
86    {
87        return $token->test('endfor');
88    }
89
90    // the loop variable cannot be used in the condition
91    private function checkLoopUsageCondition(TokenStream $stream, Node $node)
92    {
93        if ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression && 'loop' == $node->getNode('node')->getAttribute('name')) {
94            throw new SyntaxError('The "loop" variable cannot be used in a looping condition.', $node->getTemplateLine(), $stream->getSourceContext());
95        }
96
97        foreach ($node as $n) {
98            if (!$n) {
99                continue;
100            }
101
102            $this->checkLoopUsageCondition($stream, $n);
103        }
104    }
105
106    // check usage of non-defined loop-items
107    // it does not catch all problems (for instance when a for is included into another or when the variable is used in an include)
108    private function checkLoopUsageBody(TokenStream $stream, Node $node)
109    {
110        if ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression && 'loop' == $node->getNode('node')->getAttribute('name')) {
111            $attribute = $node->getNode('attribute');
112            if ($attribute instanceof ConstantExpression && \in_array($attribute->getAttribute('value'), ['length', 'revindex0', 'revindex', 'last'])) {
113                throw new SyntaxError(sprintf('The "loop.%s" variable is not defined when looping with a condition.', $attribute->getAttribute('value')), $node->getTemplateLine(), $stream->getSourceContext());
114            }
115        }
116
117        // should check for parent.loop.XXX usage
118        if ($node instanceof ForNode) {
119            return;
120        }
121
122        foreach ($node as $n) {
123            if (!$n) {
124                continue;
125            }
126
127            $this->checkLoopUsageBody($stream, $n);
128        }
129    }
130
131    public function getTag()
132    {
133        return 'for';
134    }
135}
136
137class_alias('Twig\TokenParser\ForTokenParser', 'Twig_TokenParser_For');
138