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