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