vendor/twig/twig/src/Template.php line 454

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