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

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