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