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;
14
15use Twig\Error\SyntaxError;
16use Twig\Node\BlockNode;
17use Twig\Node\BlockReferenceNode;
18use Twig\Node\BodyNode;
19use Twig\Node\Expression\AbstractExpression;
20use Twig\Node\MacroNode;
21use Twig\Node\ModuleNode;
22use Twig\Node\Node;
23use Twig\Node\NodeCaptureInterface;
24use Twig\Node\NodeOutputInterface;
25use Twig\Node\PrintNode;
26use Twig\Node\TextNode;
27use Twig\TokenParser\TokenParserInterface;
28
29/**
30 * @author Fabien Potencier <fabien@symfony.com>
31 */
32class Parser
33{
34    private $stack = [];
35    private $stream;
36    private $parent;
37    private $visitors;
38    private $expressionParser;
39    private $blocks;
40    private $blockStack;
41    private $macros;
42    private $env;
43    private $importedSymbols;
44    private $traits;
45    private $embeddedTemplates = [];
46    private $varNameSalt = 0;
47
48    public function __construct(Environment $env)
49    {
50        $this->env = $env;
51    }
52
53    public function getVarName(): string
54    {
55        return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->stream->getSourceContext()->getCode().$this->varNameSalt++));
56    }
57
58    public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
59    {
60        $vars = get_object_vars($this);
61        unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']);
62        $this->stack[] = $vars;
63
64        // node visitors
65        if (null === $this->visitors) {
66            $this->visitors = $this->env->getNodeVisitors();
67        }
68
69        if (null === $this->expressionParser) {
70            $this->expressionParser = new ExpressionParser($this, $this->env);
71        }
72
73        $this->stream = $stream;
74        $this->parent = null;
75        $this->blocks = [];
76        $this->macros = [];
77        $this->traits = [];
78        $this->blockStack = [];
79        $this->importedSymbols = [[]];
80        $this->embeddedTemplates = [];
81        $this->varNameSalt = 0;
82
83        try {
84            $body = $this->subparse($test, $dropNeedle);
85
86            if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
87                $body = new Node();
88            }
89        } catch (SyntaxError $e) {
90            if (!$e->getSourceContext()) {
91                $e->setSourceContext($this->stream->getSourceContext());
92            }
93
94            if (!$e->getTemplateLine()) {
95                $e->setTemplateLine($this->stream->getCurrent()->getLine());
96            }
97
98            throw $e;
99        }
100
101        $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
102
103        $traverser = new NodeTraverser($this->env, $this->visitors);
104
105        $node = $traverser->traverse($node);
106
107        // restore previous stack so previous parse() call can resume working
108        foreach (array_pop($this->stack) as $key => $val) {
109            $this->$key = $val;
110        }
111
112        return $node;
113    }
114
115    public function subparse($test, bool $dropNeedle = false): Node
116    {
117        $lineno = $this->getCurrentToken()->getLine();
118        $rv = [];
119        while (!$this->stream->isEOF()) {
120            switch ($this->getCurrentToken()->getType()) {
121                case /* Token::TEXT_TYPE */ 0:
122                    $token = $this->stream->next();
123                    $rv[] = new TextNode($token->getValue(), $token->getLine());
124                    break;
125
126                case /* Token::VAR_START_TYPE */ 2:
127                    $token = $this->stream->next();
128                    $expr = $this->expressionParser->parseExpression();
129                    $this->stream->expect(/* Token::VAR_END_TYPE */ 4);
130                    $rv[] = new PrintNode($expr, $token->getLine());
131                    break;
132
133                case /* Token::BLOCK_START_TYPE */ 1:
134                    $this->stream->next();
135                    $token = $this->getCurrentToken();
136
137                    if (/* Token::NAME_TYPE */ 5 !== $token->getType()) {
138                        throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
139                    }
140
141                    if (null !== $test && $test($token)) {
142                        if ($dropNeedle) {
143                            $this->stream->next();
144                        }
145
146                        if (1 === \count($rv)) {
147                            return $rv[0];
148                        }
149
150                        return new Node($rv, [], $lineno);
151                    }
152
153                    if (!$subparser = $this->env->getTokenParser($token->getValue())) {
154                        if (null !== $test) {
155                            $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
156
157                            if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) {
158                                $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
159                            }
160                        } else {
161                            $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
162                            $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers()));
163                        }
164
165                        throw $e;
166                    }
167
168                    $this->stream->next();
169
170                    $subparser->setParser($this);
171                    $node = $subparser->parse($token);
172                    if (null !== $node) {
173                        $rv[] = $node;
174                    }
175                    break;
176
177                default:
178                    throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
179            }
180        }
181
182        if (1 === \count($rv)) {
183            return $rv[0];
184        }
185
186        return new Node($rv, [], $lineno);
187    }
188
189    public function getBlockStack(): array
190    {
191        return $this->blockStack;
192    }
193
194    public function peekBlockStack()
195    {
196        return $this->blockStack[\count($this->blockStack) - 1] ?? null;
197    }
198
199    public function popBlockStack(): void
200    {
201        array_pop($this->blockStack);
202    }
203
204    public function pushBlockStack($name): void
205    {
206        $this->blockStack[] = $name;
207    }
208
209    public function hasBlock(string $name): bool
210    {
211        return isset($this->blocks[$name]);
212    }
213
214    public function getBlock(string $name): Node
215    {
216        return $this->blocks[$name];
217    }
218
219    public function setBlock(string $name, BlockNode $value): void
220    {
221        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
222    }
223
224    public function hasMacro(string $name): bool
225    {
226        return isset($this->macros[$name]);
227    }
228
229    public function setMacro(string $name, MacroNode $node): void
230    {
231        $this->macros[$name] = $node;
232    }
233
234    public function addTrait($trait): void
235    {
236        $this->traits[] = $trait;
237    }
238
239    public function hasTraits(): bool
240    {
241        return \count($this->traits) > 0;
242    }
243
244    public function embedTemplate(ModuleNode $template)
245    {
246        $template->setIndex(mt_rand());
247
248        $this->embeddedTemplates[] = $template;
249    }
250
251    public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void
252    {
253        $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
254    }
255
256    public function getImportedSymbol(string $type, string $alias)
257    {
258        // if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
259        return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[\count($this->importedSymbols) - 1][$type][$alias] ?? null);
260    }
261
262    public function isMainScope(): bool
263    {
264        return 1 === \count($this->importedSymbols);
265    }
266
267    public function pushLocalScope(): void
268    {
269        array_unshift($this->importedSymbols, []);
270    }
271
272    public function popLocalScope(): void
273    {
274        array_shift($this->importedSymbols);
275    }
276
277    public function getExpressionParser(): ExpressionParser
278    {
279        return $this->expressionParser;
280    }
281
282    public function getParent(): ?Node
283    {
284        return $this->parent;
285    }
286
287    public function setParent(?Node $parent): void
288    {
289        $this->parent = $parent;
290    }
291
292    public function getStream(): TokenStream
293    {
294        return $this->stream;
295    }
296
297    public function getCurrentToken(): Token
298    {
299        return $this->stream->getCurrent();
300    }
301
302    private function filterBodyNodes(Node $node, bool $nested = false): ?Node
303    {
304        // check that the body does not contain non-empty output nodes
305        if (
306            ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
307            ||
308            (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface)
309        ) {
310            if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) {
311                $t = substr($node->getAttribute('data'), 3);
312                if ('' === $t || ctype_space($t)) {
313                    // bypass empty nodes starting with a BOM
314                    return null;
315                }
316            }
317
318            throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
319        }
320
321        // bypass nodes that "capture" the output
322        if ($node instanceof NodeCaptureInterface) {
323            // a "block" tag in such a node will serve as a block definition AND be displayed in place as well
324            return $node;
325        }
326
327        // "block" tags that are not captured (see above) are only used for defining
328        // the content of the block. In such a case, nesting it does not work as
329        // expected as the definition is not part of the default template code flow.
330        if ($nested && $node instanceof BlockReferenceNode) {
331            throw new SyntaxError('A block definition cannot be nested under non-capturing nodes.', $node->getTemplateLine(), $this->stream->getSourceContext());
332        }
333
334        if ($node instanceof NodeOutputInterface) {
335            return null;
336        }
337
338        // here, $nested means "being at the root level of a child template"
339        // we need to discard the wrapping "Node" for the "body" node
340        $nested = $nested || Node::class !== \get_class($node);
341        foreach ($node as $k => $n) {
342            if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
343                $node->removeNode($k);
344            }
345        }
346
347        return $node;
348    }
349}
350