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

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. }