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\Error; 16use Twig\Error\RuntimeError; 17 18/** 19 * Default base class for compiled templates. 20 * 21 * This class is an implementation detail of how template compilation currently 22 * works, which might change. It should never be used directly. Use $twig->load() 23 * instead, which returns an instance of \Twig\TemplateWrapper. 24 * 25 * @author Fabien Potencier <fabien@symfony.com> 26 * 27 * @internal 28 */ 29abstract class Template 30{ 31 public const ANY_CALL = 'any'; 32 public const ARRAY_CALL = 'array'; 33 public const METHOD_CALL = 'method'; 34 35 protected $parent; 36 protected $parents = []; 37 protected $blocks = []; 38 protected $traits = []; 39 protected $traitAliases = []; 40 protected $extensions = []; 41 protected $sandbox; 42 43 private $useYield; 44 45 public function __construct( 46 protected Environment $env, 47 ) { 48 $this->useYield = $env->useYield(); 49 $this->extensions = $env->getExtensions(); 50 } 51 52 /** 53 * Returns the template name. 54 */ 55 abstract public function getTemplateName(): string; 56 57 /** 58 * Returns debug information about the template. 59 * 60 * @return array<int, int> Debug information 61 */ 62 abstract public function getDebugInfo(): array; 63 64 /** 65 * Returns information about the original template source code. 66 */ 67 abstract public function getSourceContext(): Source; 68 69 /** 70 * Returns the parent template. 71 * 72 * This method is for internal use only and should never be called 73 * directly. 74 * 75 * @return self|TemplateWrapper|false The parent template or false if there is no parent 76 */ 77 public function getParent(array $context): self|TemplateWrapper|false 78 { 79 if (null !== $this->parent) { 80 return $this->parent; 81 } 82 83 // The compiled doGetParent() may evaluate user expressions (filters, 84 // functions, method calls) when the parent name is dynamic. Make sure 85 // the sandbox security check runs first so those expressions cannot 86 // bypass the allow-list when getParent() is reached before the first 87 // ensureSecurityChecked() call on this template (e.g. via 88 // getTemplateForMacro() or yieldBlock() into a pre-warmed instance). 89 $this->ensureSecurityChecked(); 90 91 if (!$parent = $this->doGetParent($context)) { 92 return false; 93 } 94 95 if ($parent instanceof self || $parent instanceof TemplateWrapper) { 96 return $this->parents[$parent->getSourceContext()->getName()] = $parent; 97 } 98 99 if (!isset($this->parents[$parent])) { 100 $this->parents[$parent] = $this->load($parent, -1); 101 } 102 103 return $this->parents[$parent]; 104 } 105 106 protected function doGetParent(array $context): bool|string|self|TemplateWrapper 107 { 108 return false; 109 } 110 111 public function isTraitable(): bool 112 { 113 return true; 114 } 115 116 /** 117 * Displays a parent block. 118 * 119 * This method is for internal use only and should never be called 120 * directly. 121 * 122 * @param string $name The block name to display from the parent 123 * @param array $context The context 124 * @param array $blocks The current set of blocks 125 */ 126 public function displayParentBlock($name, array $context, array $blocks = []): void 127 { 128 foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { 129 echo $data; 130 } 131 } 132 133 /** 134 * Displays a block. 135 * 136 * This method is for internal use only and should never be called 137 * directly. 138 * 139 * @param string $name The block name to display 140 * @param array $context The context 141 * @param array $blocks The current set of blocks 142 * @param bool $useBlocks Whether to use the current set of blocks 143 */ 144 public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void 145 { 146 foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { 147 echo $data; 148 } 149 } 150 151 /** 152 * Renders a parent block. 153 * 154 * This method is for internal use only and should never be called 155 * directly. 156 * 157 * @param string $name The block name to render from the parent 158 * @param array $context The context 159 * @param array $blocks The current set of blocks 160 * 161 * @return string The rendered block 162 */ 163 public function renderParentBlock($name, array $context, array $blocks = []): string 164 { 165 if (!$this->useYield) { 166 if ($this->env->isDebug()) { 167 ob_start(); 168 } else { 169 ob_start(static function () { return ''; }); 170 } 171 $this->displayParentBlock($name, $context, $blocks); 172 173 return ob_get_clean(); 174 } 175 176 $content = ''; 177 foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { 178 $content .= $data; 179 } 180 181 return $content; 182 } 183 184 /** 185 * Renders a block. 186 * 187 * This method is for internal use only and should never be called 188 * directly. 189 * 190 * @param string $name The block name to render 191 * @param array $context The context 192 * @param array $blocks The current set of blocks 193 * @param bool $useBlocks Whether to use the current set of blocks 194 * 195 * @return string The rendered block 196 */ 197 public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string 198 { 199 if (!$this->useYield) { 200 $level = ob_get_level(); 201 if ($this->env->isDebug()) { 202 ob_start(); 203 } else { 204 ob_start(static function () { return ''; }); 205 } 206 try { 207 $this->displayBlock($name, $context, $blocks, $useBlocks); 208 } catch (\Throwable $e) { 209 while (ob_get_level() > $level) { 210 ob_end_clean(); 211 } 212 213 throw $e; 214 } 215 216 return ob_get_clean(); 217 } 218 219 $content = ''; 220 foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { 221 $content .= $data; 222 } 223 224 return $content; 225 } 226 227 /** 228 * Returns whether a block exists or not in the current context of the template. 229 * 230 * This method checks blocks defined in the current template 231 * or defined in "used" traits or defined in parent templates. 232 * 233 * @param string $name The block name 234 * @param array $context The context 235 * @param array $blocks The current set of blocks 236 * 237 * @return bool true if the block exists, false otherwise 238 */ 239 public function hasBlock($name, array $context, array $blocks = []): bool 240 { 241 if (isset($blocks[$name])) { 242 return $blocks[$name][0] instanceof self; 243 } 244 245 if (isset($this->blocks[$name])) { 246 return true; 247 } 248 249 if ($parent = $this->getParent($context)) { 250 return $parent->hasBlock($name, $context); 251 } 252 253 return false; 254 } 255 256 /** 257 * Returns all block names in the current context of the template. 258 * 259 * This method checks blocks defined in the current template 260 * or defined in "used" traits or defined in parent templates. 261 * 262 * @param array $context The context 263 * @param array $blocks The current set of blocks 264 * 265 * @return array<string> An array of block names 266 */ 267 public function getBlockNames(array $context, array $blocks = []): array 268 { 269 $names = array_merge(array_keys($blocks), array_keys($this->blocks)); 270 271 if ($parent = $this->getParent($context)) { 272 $names = array_merge($names, $parent->getBlockNames($context)); 273 } 274 275 return array_unique($names); 276 } 277 278 /** 279 * @param string|TemplateWrapper|array<string|TemplateWrapper> $template 280 */ 281 protected function load(string|TemplateWrapper|array $template, int $line, ?int $index = null): self 282 { 283 try { 284 if (\is_array($template)) { 285 return $this->env->resolveTemplate($template)->unwrap(); 286 } 287 288 if ($template instanceof TemplateWrapper) { 289 return $template->unwrap(); 290 } 291 292 if ($template === $this->getTemplateName()) { 293 $class = static::class; 294 if (false !== $pos = strrpos($class, '___', -1)) { 295 $class = substr($class, 0, $pos); 296 } 297 } else { 298 $class = $this->env->getTemplateClass($template); 299 } 300 301 return $this->env->loadTemplate($class, $template, $index); 302 } catch (Error $e) { 303 if (!$e->getSourceContext()) { 304 $e->setSourceContext($this->getSourceContext()); 305 } 306 307 if ($e->getTemplateLine() > 0) { 308 throw $e; 309 } 310 311 if (-1 === $line) { 312 $e->guess(); 313 } else { 314 $e->setTemplateLine($line); 315 } 316 317 throw $e; 318 } 319 } 320 321 /** 322 * @param string|TemplateWrapper|array<string|TemplateWrapper> $template 323 * 324 * @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead. 325 */ 326 protected function loadTemplate($template, $templateName = null, ?int $line = null, ?int $index = null): self|TemplateWrapper 327 { 328 trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__); 329 330 if (null === $line) { 331 $line = -1; 332 } 333 334 if ($template instanceof self) { 335 return $template; 336 } 337 338 return $this->load($template, $line, $index); 339 } 340 341 /** 342 * @internal 343 * 344 * @return $this 345 */ 346 public function unwrap(): self 347 { 348 return $this; 349 } 350 351 /** 352 * Returns all blocks. 353 * 354 * This method is for internal use only and should never be called 355 * directly. 356 * 357 * @return array An array of blocks 358 */ 359 public function getBlocks(): array 360 { 361 return $this->blocks; 362 } 363 364 public function display(array $context, array $blocks = []): void 365 { 366 foreach ($this->yield($context, $blocks) as $data) { 367 echo $data; 368 } 369 } 370 371 public function render(array $context): string 372 { 373 if (!$this->useYield) { 374 $level = ob_get_level(); 375 if ($this->env->isDebug()) { 376 ob_start(); 377 } else { 378 ob_start(static function () { return ''; }); 379 } 380 try { 381 $this->display($context); 382 } catch (\Throwable $e) { 383 while (ob_get_level() > $level) { 384 ob_end_clean(); 385 } 386 387 throw $e; 388 } 389 390 return ob_get_clean(); 391 } 392 393 $content = ''; 394 foreach ($this->yield($context) as $data) { 395 $content .= $data; 396 } 397 398 return $content; 399 } 400 401 /** 402 * @return iterable<scalar|\Stringable|null> 403 */ 404 public function yield(array $context, array $blocks = []): iterable 405 { 406 $context += $this->env->getGlobals(); 407 $blocks = array_merge($this->blocks, $blocks); 408 409 try { 410 $this->ensureSecurityChecked(); 411 yield from $this->doDisplay($context, $blocks); 412 } catch (Error $e) { 413 if (!$e->getSourceContext()) { 414 $e->setSourceContext($this->getSourceContext()); 415 } 416 417 // this is mostly useful for \Twig\Error\LoaderError exceptions 418 // see \Twig\Error\LoaderError 419 if (-1 === $e->getTemplateLine()) { 420 $e->guess(); 421 } 422 423 throw $e; 424 } catch (\Throwable $e) { 425 $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); 426 $e->guess(); 427 428 throw $e; 429 } 430 } 431 432 /** 433 * @return iterable<scalar|\Stringable|null> 434 */ 435 public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable 436 { 437 if ($useBlocks && isset($blocks[$name])) { 438 $template = $blocks[$name][0]; 439 $block = $blocks[$name][1]; 440 } elseif (isset($this->blocks[$name])) { 441 $template = $this->blocks[$name][0]; 442 $block = $this->blocks[$name][1]; 443 } else { 444 $template = null; 445 $block = null; 446 } 447 448 // avoid RCEs when sandbox is enabled 449 if (null !== $template && !$template instanceof self) { 450 throw new \LogicException('A block must be a method on a \Twig\Template instance.'); 451 } 452 453 if (null !== $template) { 454 try { 455 $template->ensureSecurityChecked(); 456 yield from $template->$block($context, $blocks); 457 } catch (Error $e) { 458 if (!$e->getSourceContext()) { 459 $e->setSourceContext($template->getSourceContext()); 460 } 461 462 // this is mostly useful for \Twig\Error\LoaderError exceptions 463 // see \Twig\Error\LoaderError 464 if (-1 === $e->getTemplateLine()) { 465 $e->guess(); 466 } 467 468 throw $e; 469 } catch (\Throwable $e) { 470 $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); 471 $e->guess(); 472 473 throw $e; 474 } 475 } elseif ($parent = $this->getParent($context)) { 476 yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); 477 } elseif (isset($blocks[$name])) { 478 throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); 479 } else { 480 throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); 481 } 482 } 483 484 /** 485 * Yields a parent block. 486 * 487 * This method is for internal use only and should never be called 488 * directly. 489 * 490 * @param string $name The block name to display from the parent 491 * @param array $context The context 492 * @param array $blocks The current set of blocks 493 * 494 * @return iterable<scalar|\Stringable|null> 495 */ 496 public function yieldParentBlock($name, array $context, array $blocks = []): iterable 497 { 498 if (isset($this->traits[$name])) { 499 yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false); 500 } elseif ($parent = $this->getParent($context)) { 501 yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); 502 } else { 503 throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); 504 } 505 } 506 507 protected function hasMacro(string $name, array $context): bool 508 { 509 if (method_exists($this, $name)) { 510 return true; 511 } 512 513 if (!$parent = $this->getParent($context)) { 514 return false; 515 } 516 517 return $parent->hasMacro($name, $context); 518 } 519 520 protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self 521 { 522 if (method_exists($this, $name)) { 523 $this->ensureSecurityChecked(); 524 525 return $this; 526 } 527 528 $parent = $this; 529 while ($parent = $parent->getParent($context)) { 530 if (method_exists($parent, $name)) { 531 $parent->ensureSecurityChecked(); 532 533 return $parent; 534 } 535 } 536 537 throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source); 538 } 539 540 /** 541 * Runs the sandbox security check against the current sandbox state. 542 * 543 * @internal 544 */ 545 public function ensureSecurityChecked(): void 546 { 547 } 548 549 /** 550 * Auto-generated method to display the template with the given context. 551 * 552 * @param array $context An array of parameters to pass to the template 553 * @param array $blocks An array of blocks to pass to the template 554 * 555 * @return iterable<scalar|\Stringable|null> 556 */ 557 abstract protected function doDisplay(array $context, array $blocks = []): iterable; 558} 559