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\NodeVisitor\NodeVisitorInterface;
28use Twig\TokenParser\TokenParserInterface;
29
30/**
31 * Default parser implementation.
32 *
33 * @author Fabien Potencier <fabien@symfony.com>
34 */
35class Parser implements \Twig_ParserInterface
36{
37    protected $stack = [];
38    protected $stream;
39    protected $parent;
40    protected $handlers;
41    protected $visitors;
42    protected $expressionParser;
43    protected $blocks;
44    protected $blockStack;
45    protected $macros;
46    protected $env;
47    protected $reservedMacroNames;
48    protected $importedSymbols;
49    protected $traits;
50    protected $embeddedTemplates = [];
51    private $varNameSalt = 0;
52
53    public function __construct(Environment $env)
54    {
55        $this->env = $env;
56    }
57
58    /**
59     * @deprecated since 1.27 (to be removed in 2.0)
60     */
61    public function getEnvironment()
62    {
63        @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
64
65        return $this->env;
66    }
67
68    public function getVarName()
69    {
70        return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->stream->getSourceContext()->getCode().$this->varNameSalt++));
71    }
72
73    /**
74     * @deprecated since 1.27 (to be removed in 2.0). Use $parser->getStream()->getSourceContext()->getPath() instead.
75     */
76    public function getFilename()
77    {
78        @trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use $parser->getStream()->getSourceContext()->getPath() instead.', __METHOD__), E_USER_DEPRECATED);
79
80        return $this->stream->getSourceContext()->getName();
81    }
82
83    public function parse(TokenStream $stream, $test = null, $dropNeedle = false)
84    {
85        // push all variables into the stack to keep the current state of the parser
86        // using get_object_vars() instead of foreach would lead to https://bugs.php.net/71336
87        // This hack can be removed when min version if PHP 7.0
88        $vars = [];
89        foreach ($this as $k => $v) {
90            $vars[$k] = $v;
91        }
92
93        unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']);
94        $this->stack[] = $vars;
95
96        // tag handlers
97        if (null === $this->handlers) {
98            $this->handlers = $this->env->getTokenParsers();
99            $this->handlers->setParser($this);
100        }
101
102        // node visitors
103        if (null === $this->visitors) {
104            $this->visitors = $this->env->getNodeVisitors();
105        }
106
107        if (null === $this->expressionParser) {
108            $this->expressionParser = new ExpressionParser($this, $this->env);
109        }
110
111        $this->stream = $stream;
112        $this->parent = null;
113        $this->blocks = [];
114        $this->macros = [];
115        $this->traits = [];
116        $this->blockStack = [];
117        $this->importedSymbols = [[]];
118        $this->embeddedTemplates = [];
119        $this->varNameSalt = 0;
120
121        try {
122            $body = $this->subparse($test, $dropNeedle);
123
124            if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
125                $body = new Node();
126            }
127        } catch (SyntaxError $e) {
128            if (!$e->getSourceContext()) {
129                $e->setSourceContext($this->stream->getSourceContext());
130            }
131
132            if (!$e->getTemplateLine()) {
133                $e->setTemplateLine($this->stream->getCurrent()->getLine());
134            }
135
136            throw $e;
137        }
138
139        $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
140
141        $traverser = new NodeTraverser($this->env, $this->visitors);
142
143        $node = $traverser->traverse($node);
144
145        // restore previous stack so previous parse() call can resume working
146        foreach (array_pop($this->stack) as $key => $val) {
147            $this->$key = $val;
148        }
149
150        return $node;
151    }
152
153    public function subparse($test, $dropNeedle = false)
154    {
155        $lineno = $this->getCurrentToken()->getLine();
156        $rv = [];
157        while (!$this->stream->isEOF()) {
158            switch ($this->getCurrentToken()->getType()) {
159                case Token::TEXT_TYPE:
160                    $token = $this->stream->next();
161                    $rv[] = new TextNode($token->getValue(), $token->getLine());
162                    break;
163
164                case Token::VAR_START_TYPE:
165                    $token = $this->stream->next();
166                    $expr = $this->expressionParser->parseExpression();
167                    $this->stream->expect(Token::VAR_END_TYPE);
168                    $rv[] = new PrintNode($expr, $token->getLine());
169                    break;
170
171                case Token::BLOCK_START_TYPE:
172                    $this->stream->next();
173                    $token = $this->getCurrentToken();
174
175                    if (Token::NAME_TYPE !== $token->getType()) {
176                        throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
177                    }
178
179                    if (null !== $test && \call_user_func($test, $token)) {
180                        if ($dropNeedle) {
181                            $this->stream->next();
182                        }
183
184                        if (1 === \count($rv)) {
185                            return $rv[0];
186                        }
187
188                        return new Node($rv, [], $lineno);
189                    }
190
191                    $subparser = $this->handlers->getTokenParser($token->getValue());
192                    if (null === $subparser) {
193                        if (null !== $test) {
194                            $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
195
196                            if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) {
197                                $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
198                            }
199                        } else {
200                            $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
201                            $e->addSuggestions($token->getValue(), array_keys($this->env->getTags()));
202                        }
203
204                        throw $e;
205                    }
206
207                    $this->stream->next();
208
209                    $node = $subparser->parse($token);
210                    if (null !== $node) {
211                        $rv[] = $node;
212                    }
213                    break;
214
215                default:
216                    throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
217            }
218        }
219
220        if (1 === \count($rv)) {
221            return $rv[0];
222        }
223
224        return new Node($rv, [], $lineno);
225    }
226
227    /**
228     * @deprecated since 1.27 (to be removed in 2.0)
229     */
230    public function addHandler($name, $class)
231    {
232        @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
233
234        $this->handlers[$name] = $class;
235    }
236
237    /**
238     * @deprecated since 1.27 (to be removed in 2.0)
239     */
240    public function addNodeVisitor(NodeVisitorInterface $visitor)
241    {
242        @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
243
244        $this->visitors[] = $visitor;
245    }
246
247    public function getBlockStack()
248    {
249        return $this->blockStack;
250    }
251
252    public function peekBlockStack()
253    {
254        return $this->blockStack[\count($this->blockStack) - 1];
255    }
256
257    public function popBlockStack()
258    {
259        array_pop($this->blockStack);
260    }
261
262    public function pushBlockStack($name)
263    {
264        $this->blockStack[] = $name;
265    }
266
267    public function hasBlock($name)
268    {
269        return isset($this->blocks[$name]);
270    }
271
272    public function getBlock($name)
273    {
274        return $this->blocks[$name];
275    }
276
277    public function setBlock($name, BlockNode $value)
278    {
279        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
280    }
281
282    public function hasMacro($name)
283    {
284        return isset($this->macros[$name]);
285    }
286
287    public function setMacro($name, MacroNode $node)
288    {
289        if ($this->isReservedMacroName($name)) {
290            throw new SyntaxError(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword.', $name), $node->getTemplateLine(), $this->stream->getSourceContext());
291        }
292
293        $this->macros[$name] = $node;
294    }
295
296    public function isReservedMacroName($name)
297    {
298        if (null === $this->reservedMacroNames) {
299            $this->reservedMacroNames = [];
300            $r = new \ReflectionClass($this->env->getBaseTemplateClass());
301            foreach ($r->getMethods() as $method) {
302                $methodName = strtolower($method->getName());
303
304                if ('get' === substr($methodName, 0, 3) && isset($methodName[3])) {
305                    $this->reservedMacroNames[] = substr($methodName, 3);
306                }
307            }
308        }
309
310        return \in_array(strtolower($name), $this->reservedMacroNames);
311    }
312
313    public function addTrait($trait)
314    {
315        $this->traits[] = $trait;
316    }
317
318    public function hasTraits()
319    {
320        return \count($this->traits) > 0;
321    }
322
323    public function embedTemplate(ModuleNode $template)
324    {
325        $template->setIndex(mt_rand());
326
327        $this->embeddedTemplates[] = $template;
328    }
329
330    public function addImportedSymbol($type, $alias, $name = null, AbstractExpression $node = null)
331    {
332        $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
333    }
334
335    public function getImportedSymbol($type, $alias)
336    {
337        foreach ($this->importedSymbols as $functions) {
338            if (isset($functions[$type][$alias])) {
339                return $functions[$type][$alias];
340            }
341        }
342    }
343
344    public function isMainScope()
345    {
346        return 1 === \count($this->importedSymbols);
347    }
348
349    public function pushLocalScope()
350    {
351        array_unshift($this->importedSymbols, []);
352    }
353
354    public function popLocalScope()
355    {
356        array_shift($this->importedSymbols);
357    }
358
359    /**
360     * @return ExpressionParser
361     */
362    public function getExpressionParser()
363    {
364        return $this->expressionParser;
365    }
366
367    public function getParent()
368    {
369        return $this->parent;
370    }
371
372    public function setParent($parent)
373    {
374        $this->parent = $parent;
375    }
376
377    /**
378     * @return TokenStream
379     */
380    public function getStream()
381    {
382        return $this->stream;
383    }
384
385    /**
386     * @return Token
387     */
388    public function getCurrentToken()
389    {
390        return $this->stream->getCurrent();
391    }
392
393    protected function filterBodyNodes(\Twig_NodeInterface $node)
394    {
395        // check that the body does not contain non-empty output nodes
396        if (
397            ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
398            ||
399            (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface)
400        ) {
401            if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) {
402                $t = substr($node->getAttribute('data'), 3);
403                if ('' === $t || ctype_space($t)) {
404                    // bypass empty nodes starting with a BOM
405                    return;
406                }
407            }
408
409            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());
410        }
411
412        // bypass nodes that will "capture" the output
413        if ($node instanceof NodeCaptureInterface) {
414            return $node;
415        }
416
417        if ($node instanceof NodeOutputInterface) {
418            return;
419        }
420
421        foreach ($node as $k => $n) {
422            if (null !== $n && null === $this->filterBodyNodes($n)) {
423                $node->removeNode($k);
424            }
425        }
426
427        return $node;
428    }
429}
430
431class_alias('Twig\Parser', 'Twig_Parser');
432