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

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 $environmentContaoFilesystemLoader $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->contaoEscaperFilterRulestrue)) {
  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 [$valuesubstr($value7)];
  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. }