Commit 12ea8942 authored by catch's avatar catch
Browse files

fix: #3625969 Twig's 3.30 TypeError: Twig\Runtime\EscaperRuntime::escape():...

fix: #3625969 Twig's 3.30 TypeError: Twig\Runtime\EscaperRuntime::escape(): Argument #4 ($autoescape) must be of type bool, null given

By: gábor hojtsy
By: godotislate
By: catch
By: longwave
By: f0ns
By: ykhalid
parent 57ff5213
Loading
Loading
Loading
Loading
Loading
+31 −9
Changes for core/lib/Drupal/Core/Template/TwigExtension.php: 31 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -116,6 +116,10 @@ public function getFunctions() {
   * {@inheritdoc}
   */
  public function getFilters() {
    $escape_options = [
      'needs_environment' => TRUE,
      'is_safe_callback' => [static::class, 'escapeFilterIsSafe'],
    ];
    return [
      // Translation filters.
      new TwigFilter('t', 't', ['is_safe' => ['html']]),
@@ -127,15 +131,14 @@ public function getFilters() {
      // @see TwigNodeTrans::compileString()
      new TwigFilter('placeholder', [$this, 'escapePlaceholder'], ['is_safe' => ['html'], 'needs_environment' => TRUE]),

      // Replace twig's escape filter with our own.
      new TwigFilter(
        'drupal_escape',
        [$this, 'escapeFilter'],
        [
          'needs_environment' => TRUE,
          'is_safe_callback' => 'twig_escape_filter_is_safe',
        ]
      ),
      // Replace Twig's escape filters with our own MarkupInterface-aware
      // filter. Filters registered by later extensions override those with
      // the same name, and this extension is always added after Twig's
      // EscaperExtension. Twig's auto-escaping always uses the filter
      // named 'escape'.
      new TwigFilter('escape', [$this, 'escapeFilter'], $escape_options),
      new TwigFilter('e', [$this, 'escapeFilter'], $escape_options),
      new TwigFilter('drupal_escape', [$this, 'escapeFilter'], $escape_options),

      // Implements safe joining.
      // @todo Make that the default for |join? Upstream issue:
@@ -402,6 +405,25 @@ public function escapePlaceholder(Environment $env, $string) {
    return $return ? '<em class="placeholder">' . $return . '</em>' : NULL;
  }

  /**
   * Determines which strategies the output of the escape filter is safe for.
   *
   * @param \Twig\Node\Node $filter_args
   *   The arguments passed to the escape filter.
   *
   * @return string[]
   *   The escaping strategies the output is safe for.
   */
  public static function escapeFilterIsSafe(Node $filter_args): array {
    foreach ($filter_args as $arg) {
      if ($arg instanceof ConstantExpression) {
        return [$arg->getAttribute('value')];
      }
      return [];
    }
    return ['html'];
  }

  /**
   * Overrides twig_escape_filter().
   *
+1 −5
Changes for core/lib/Drupal/Core/Template/TwigNodeVisitor.php: 1 added line, 5 removed lines.
Original line number Diff line number Diff line
@@ -57,13 +57,9 @@ public function leaveNode(Node $node, Environment $env): ?Node {
        $line
      );
    }
    // Change the 'escape' filter to our own 'drupal_escape' filter.
    elseif ($node instanceof FilterExpression) {
      $name = $node->getAttribute('twig_callable')->getName();
      if ('escape' == $name || 'e' == $name) {
        // Use our own escape filter that is MarkupInterface aware.
        $node->setAttribute('twig_callable', $env->getFilter('drupal_escape'));

      if (in_array($name, ['escape', 'e', 'drupal_escape'], TRUE)) {
        // Store that we have a filter active already that knows
        // how to deal with render arrays.
        $this->skipRenderVarFunction = TRUE;
+40 −0
Changes for core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php: 40 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -144,6 +144,46 @@ public static function providerTestEscaping(): array {
    ];
  }

  /**
   * Tests that Twig's escape filters are replaced with our own.
   */
  #[DataProvider('providerTestEscapeFilterOverride')]
  public function testEscapeFilterOverride(string $template, string $expected): void {
    $loader = new ArrayLoader(['test' => $template]);
    $twig = new Environment($loader, [
      'cache' => FALSE,
      'autoescape' => 'html',
    ]);
    $twig->addExtension($this->systemUnderTest);

    // Explicit and automatic escaping must both call our filter.
    $source = $twig->compileSource($loader->getSourceContext('test'));
    $this->assertStringContainsString(sprintf("\$this->extensions['%s']->escapeFilter(", TwigExtension::class), $source);

    $this->assertSame($expected, $twig->render('test', [
      'text' => '<b>',
      'markup' => Markup::create('<b>'),
    ]));
  }

  /**
   * Provides test data for testEscapeFilterOverride().
   *
   * @return array
   *   An array of test data, each containing a Twig template string and the
   *   expected rendered output.
   */
  public static function providerTestEscapeFilterOverride(): array {
    return [
      'auto-escaped string' => ['{{ text }}', '&lt;b&gt;'],
      'auto-escaped markup' => ['{{ markup }}', '<b>'],
      'escape filter' => ['{{ text|escape }}', '&lt;b&gt;'],
      'e filter' => ['{{ text|e }}', '&lt;b&gt;'],
      'escape filter with html strategy' => ['{{ text|e("html") }}', '&lt;b&gt;'],
      'filter before auto-escaping' => ['{{ text|upper }}', '&lt;B&gt;'],
    ];
  }

  /**
   * Tests the active_theme function.
   */