vendor/contao/core-bundle/src/Twig/Extension/ContaoExtension.php line 187

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of Contao.
  5. *
  6. * (c) Leo Feyer
  7. *
  8. * @license LGPL-3.0-or-later
  9. */
  10. namespace Contao\CoreBundle\Twig\Extension;
  11. use Contao\BackendTemplateTrait;
  12. use Contao\CoreBundle\InsertTag\ChunkedText;
  13. use Contao\CoreBundle\Twig\Inheritance\DynamicExtendsTokenParser;
  14. use Contao\CoreBundle\Twig\Inheritance\DynamicIncludeTokenParser;
  15. use Contao\CoreBundle\Twig\Interop\ContaoEscaper;
  16. use Contao\CoreBundle\Twig\Interop\ContaoEscaperNodeVisitor;
  17. use Contao\CoreBundle\Twig\Interop\PhpTemplateProxyNode;
  18. use Contao\CoreBundle\Twig\Interop\PhpTemplateProxyNodeVisitor;
  19. use Contao\CoreBundle\Twig\Loader\ContaoFilesystemLoader;
  20. use Contao\CoreBundle\Twig\Runtime\FigureRendererRuntime;
  21. use Contao\CoreBundle\Twig\Runtime\InsertTagRuntime;
  22. use Contao\CoreBundle\Twig\Runtime\LegacyTemplateFunctionsRuntime;
  23. use Contao\CoreBundle\Twig\Runtime\PictureConfigurationRuntime;
  24. use Contao\CoreBundle\Twig\Runtime\SchemaOrgRuntime;
  25. use Contao\FrontendTemplateTrait;
  26. use Contao\Template;
  27. use Symfony\Component\Filesystem\Path;
  28. use Twig\Environment;
  29. use Twig\Extension\AbstractExtension;
  30. use Twig\Extension\CoreExtension;
  31. use Twig\Extension\EscaperExtension;
  32. use Twig\Node\Expression\ConstantExpression;
  33. use Twig\Node\Node;
  34. use Twig\TwigFilter;
  35. use Twig\TwigFunction;
  36. /**
  37. * @experimental
  38. */
  39. final class ContaoExtension extends AbstractExtension
  40. {
  41. private Environment $environment;
  42. private ContaoFilesystemLoader $filesystemLoader;
  43. private array $contaoEscaperFilterRules = [];
  44. public function __construct(Environment $environment, ContaoFilesystemLoader $filesystemLoader)
  45. {
  46. $this->environment = $environment;
  47. $this->filesystemLoader = $filesystemLoader;
  48. $contaoEscaper = new ContaoEscaper();
  49. /** @var EscaperExtension $escaperExtension */
  50. $escaperExtension = $environment->getExtension(EscaperExtension::class);
  51. // Forward compatibility with twig/twig >=3.10.0
  52. if (method_exists($escaperExtension, 'setEnvironment')) {
  53. $escaperExtension->setEnvironment($environment);
  54. }
  55. $escaperExtension->setEscaper('contao_html', [$contaoEscaper, 'escapeHtml']);
  56. $escaperExtension->setEscaper('contao_html_attr', [$contaoEscaper, 'escapeHtmlAttr']);
  57. // Use our escaper on all templates in the "@Contao" and "@Contao_*"
  58. // namespaces, as well as the existing bundle templates we're already
  59. // shipping.
  60. $this->addContaoEscaperRule('%^@Contao(_[a-zA-Z0-9_-]*)?/%');
  61. $this->addContaoEscaperRule('%^@Contao(Core|Installation)/%');
  62. }
  63. /**
  64. * Adds a Contao escaper rule.
  65. *
  66. * If a template name matches any of the defined rules, it will be processed
  67. * with the "contao_html" escaper strategy. Make sure your rule will only
  68. * match templates with input encoded contexts!
  69. */
  70. public function addContaoEscaperRule(string $regularExpression): void
  71. {
  72. if (\in_array($regularExpression, $this->contaoEscaperFilterRules, true)) {
  73. return;
  74. }
  75. $this->contaoEscaperFilterRules[] = $regularExpression;
  76. }
  77. public function getNodeVisitors(): array
  78. {
  79. return [
  80. // Enables the "contao_twig" escaper for Contao templates with
  81. // input encoding
  82. new ContaoEscaperNodeVisitor(
  83. fn () => $this->contaoEscaperFilterRules
  84. ),
  85. // Allows rendering PHP templates with the legacy framework by
  86. // installing proxy nodes
  87. new PhpTemplateProxyNodeVisitor(self::class),
  88. // Triggers PHP deprecations if deprecated constructs are found in
  89. // the parsed templates.
  90. new DeprecationsNodeVisitor(),
  91. ];
  92. }
  93. public function getTokenParsers(): array
  94. {
  95. return [
  96. // Overwrite the parsers for the "extends" and "include" tags to
  97. // additionally support the Contao template hierarchy
  98. new DynamicExtendsTokenParser($this->filesystemLoader),
  99. new DynamicIncludeTokenParser($this->filesystemLoader),
  100. ];
  101. }
  102. public function getFunctions(): array
  103. {
  104. $includeFunctionCallable = $this->getTwigIncludeFunction()->getCallable();
  105. return [
  106. // Overwrite the "include" function to additionally support the
  107. // Contao template hierarchy
  108. new TwigFunction(
  109. 'include',
  110. function (Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false /* we need named arguments here */) use ($includeFunctionCallable) {
  111. $args = \func_get_args();
  112. $args[2] = DynamicIncludeTokenParser::adjustTemplateName($template, $this->filesystemLoader);
  113. return $includeFunctionCallable(...$args);
  114. },
  115. ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]
  116. ),
  117. new TwigFunction(
  118. 'contao_figure',
  119. [FigureRendererRuntime::class, 'render'],
  120. ['is_safe' => ['html']]
  121. ),
  122. new TwigFunction(
  123. 'picture_config',
  124. [PictureConfigurationRuntime::class, 'fromArray']
  125. ),
  126. new TwigFunction(
  127. 'insert_tag',
  128. [InsertTagRuntime::class, 'renderInsertTag'],
  129. ),
  130. new TwigFunction(
  131. 'add_schema_org',
  132. [SchemaOrgRuntime::class, 'add']
  133. ),
  134. new TwigFunction(
  135. 'contao_sections',
  136. [LegacyTemplateFunctionsRuntime::class, 'renderLayoutSections'],
  137. ['needs_context' => true, 'is_safe' => ['html']]
  138. ),
  139. new TwigFunction(
  140. 'contao_section',
  141. [LegacyTemplateFunctionsRuntime::class, 'renderLayoutSection'],
  142. ['needs_context' => true, 'is_safe' => ['html']]
  143. ),
  144. new TwigFunction(
  145. 'render_contao_backend_template',
  146. [LegacyTemplateFunctionsRuntime::class, 'renderContaoBackendTemplate'],
  147. ['is_safe' => ['html']]
  148. ),
  149. ];
  150. }
  151. public function getFilters(): array
  152. {
  153. $escaperFilter = static function (Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) {
  154. if ($string instanceof ChunkedText) {
  155. $parts = [];
  156. foreach ($string as [$type, $chunk]) {
  157. if (ChunkedText::TYPE_RAW === $type) {
  158. $parts[] = $chunk;
  159. } else {
  160. $parts[] = twig_escape_filter($env, $chunk, $strategy, $charset);
  161. }
  162. }
  163. return implode('', $parts);
  164. }
  165. return twig_escape_filter($env, $string, $strategy, $charset, $autoescape);
  166. };
  167. $twigEscaperFilterIsSafe = static function (Node $filterArgs): array {
  168. $expression = iterator_to_array($filterArgs)[0] ?? null;
  169. if ($expression instanceof ConstantExpression) {
  170. $value = $expression->getAttribute('value');
  171. // Our escaper strategy variants that tolerate input encoding are
  172. // also safe in the original context (e.g. for the filter argument
  173. // 'contao_html' we will return ['contao_html', 'html']).
  174. if (\in_array($value, ['contao_html', 'contao_html_attr'], true)) {
  175. return [$value, substr($value, 7)];
  176. }
  177. }
  178. return twig_escape_filter_is_safe($filterArgs);
  179. };
  180. return [
  181. // Overwrite the "escape" filter to additionally support chunked
  182. // text and our escaper strategies
  183. new TwigFilter(
  184. 'escape',
  185. $escaperFilter,
  186. ['needs_environment' => true, 'is_safe_callback' => $twigEscaperFilterIsSafe],
  187. ),
  188. new TwigFilter(
  189. 'e',
  190. $escaperFilter,
  191. ['needs_environment' => true, 'is_safe_callback' => $twigEscaperFilterIsSafe],
  192. ),
  193. new TwigFilter(
  194. 'insert_tag',
  195. [InsertTagRuntime::class, 'replaceInsertTags']
  196. ),
  197. new TwigFilter(
  198. 'insert_tag_raw',
  199. [InsertTagRuntime::class, 'replaceInsertTagsChunkedRaw']
  200. ),
  201. ];
  202. }
  203. /**
  204. * @see PhpTemplateProxyNode
  205. * @see PhpTemplateProxyNodeVisitor
  206. *
  207. * @internal
  208. */
  209. public function renderLegacyTemplate(string $name, array $blocks, array $context): string
  210. {
  211. $template = Path::getFilenameWithoutExtension($name);
  212. $partialTemplate = new class($template) extends Template {
  213. use BackendTemplateTrait;
  214. use FrontendTemplateTrait;
  215. public function setBlocks(array $blocks): void
  216. {
  217. $this->arrBlocks = array_map(static fn ($block) => \is_array($block) ? $block : [$block], $blocks);
  218. }
  219. public function parse(): string
  220. {
  221. return $this->inherit();
  222. }
  223. protected function renderTwigSurrogateIfExists(): ?string
  224. {
  225. return null;
  226. }
  227. };
  228. $partialTemplate->setData($context);
  229. $partialTemplate->setBlocks($blocks);
  230. return $partialTemplate->parse();
  231. }
  232. private function getTwigIncludeFunction(): TwigFunction
  233. {
  234. foreach ($this->environment->getExtension(CoreExtension::class)->getFunctions() as $function) {
  235. if ('include' === $function->getName()) {
  236. return $function;
  237. }
  238. }
  239. throw new \RuntimeException(sprintf('The %s class was expected to register the "include" Twig function but did not.', CoreExtension::class));
  240. }
  241. }