From ccca32a3f2dabab99605536c889bb02256aea8d0 Mon Sep 17 00:00:00 2001
From: catch <6915-catch@users.noreply.drupalcode.org>
Date: Thu, 21 Nov 2024 12:19:12 +0000
Subject: [PATCH] Issue #3487031 by larowlan, alexpott, themodularlab,
 ericgsmith, longwave, spokje: Performance Degraded after update to twig
 3.14.2

(cherry picked from commit 457b1dfb538dc1fe7a9ef4b1d26848904878d855)
---
 .../RemoveCheckToStringNodeVisitor.php        | 56 +++++++++++++++++++
 .../Drupal/Core/Template/TwigExtension.php    | 10 +++-
 .../Core/Template/TwigSandboxPolicy.php       | 28 +++++++---
 .../Template/TwigSimpleCheckToStringNode.php  | 33 +++++++++++
 4 files changed, 117 insertions(+), 10 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Template/RemoveCheckToStringNodeVisitor.php
 create mode 100644 core/lib/Drupal/Core/Template/TwigSimpleCheckToStringNode.php

diff --git a/core/lib/Drupal/Core/Template/RemoveCheckToStringNodeVisitor.php b/core/lib/Drupal/Core/Template/RemoveCheckToStringNodeVisitor.php
new file mode 100644
index 000000000000..568b22ace9d4
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/RemoveCheckToStringNodeVisitor.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Template;
+
+use Twig\Environment;
+use Twig\Node\CheckToStringNode;
+use Twig\Node\Node;
+use Twig\NodeVisitor\NodeVisitorInterface;
+
+/**
+ * Defines a TwigNodeVisitor that replaces CheckToStringNodes.
+ *
+ * Twig 3.14.1 resulted in a performance regression in Drupal due to checking if
+ * __toString is an allowed method on objects. __toString is allowed on all
+ * objects when Drupal's default SandboxPolicy is active. Therefore, Twig's
+ * SandboxExtension checks are unnecessary.
+ */
+final class RemoveCheckToStringNodeVisitor implements NodeVisitorInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enterNode(Node $node, Environment $env): Node {
+    if ($node instanceof CheckToStringNode) {
+      // Replace CheckToStringNode with the faster equivalent, __toString is an
+      // allowed method so any checking of __toString on a per-object basis is
+      // performance overhead.
+      $new = new TwigSimpleCheckToStringNode($node->getNode('expr'));
+      // @todo https://www.drupal.org/project/drupal/issues/3488584 Update for
+      //   Twig 4 as the spread attribute has been removed there.
+      if ($node->hasAttribute('spread')) {
+        $new->setAttribute('spread', $node->getAttribute('spread'));
+      }
+      return $new;
+    }
+    return $node;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function leaveNode(Node $node, Environment $env): ?Node {
+    return $node;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPriority() {
+    // Runs after sandbox visitor.
+    return 1;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index 9200d0254177..6ebbfecbfb9d 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -157,10 +157,18 @@ public function getFilters() {
   public function getNodeVisitors() {
     // The node visitor is needed to wrap all variables with
     // render_var -> TwigExtension->renderVar() function.
-    return [
+    $visitors = [
       new TwigNodeVisitor(),
       new TwigNodeVisitorCheckDeprecations(),
     ];
+    if (\in_array('__toString', TwigSandboxPolicy::getMethodsAllowedOnAllObjects(), TRUE)) {
+      // When __toString is an allowed method, there is no point in running
+      // \Twig\Extension\SandboxExtension::ensureToStringAllowed, so we add a
+      // node visitor to remove any CheckToStringNode nodes added by the
+      // sandbox extension.
+      $visitors[] = new RemoveCheckToStringNodeVisitor();
+    }
+    return $visitors;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php b/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php
index 42d1811877e8..7774f715e4f6 100644
--- a/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php
+++ b/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php
@@ -54,15 +54,7 @@ public function __construct() {
     // Flip the array so we can check using isset().
     $this->allowed_classes = array_flip($allowed_classes);
 
-    $allowed_methods = Settings::get('twig_sandbox_allowed_methods', [
-      // Only allow idempotent methods.
-      'id',
-      'label',
-      'bundle',
-      'get',
-      '__toString',
-      'toString',
-    ]);
+    $allowed_methods = static::getMethodsAllowedOnAllObjects();
     // Flip the array so we can check using isset().
     $this->allowed_methods = array_flip($allowed_methods);
 
@@ -109,4 +101,22 @@ public function checkMethodAllowed($obj, $method): void {
     throw new SecurityError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, get_class($obj)));
   }
 
+  /**
+   * Gets the list of allowed methods on all objects.
+   *
+   * @return string[]
+   *   The list of allowed methods on all objects.
+   */
+  public static function getMethodsAllowedOnAllObjects(): array {
+    return Settings::get('twig_sandbox_allowed_methods', [
+      // Only allow idempotent methods.
+      'id',
+      'label',
+      'bundle',
+      'get',
+      '__toString',
+      'toString',
+    ]);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Template/TwigSimpleCheckToStringNode.php b/core/lib/Drupal/Core/Template/TwigSimpleCheckToStringNode.php
new file mode 100644
index 000000000000..42f32a5d4694
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/TwigSimpleCheckToStringNode.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Template;
+
+use Twig\Compiler;
+use Twig\Node\CheckToStringNode;
+
+/**
+ * Defines a twig node for simplifying CheckToStringNode.
+ *
+ * Drupal's sandbox policy is very permissive with checking whether an object
+ * can be converted to a string. We allow any object with a __toString method.
+ * This means that the array traversal in the default SandboxExtension
+ * implementation added by the parent class is a performance overhead we don't
+ * need.
+ *
+ * @see \Drupal\Core\Template\TwigSandboxPolicy
+ * @see \Drupal\Core\Template\RemoveCheckToStringNodeVisitor
+ */
+final class TwigSimpleCheckToStringNode extends CheckToStringNode {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function compile(Compiler $compiler): void {
+    $expr = $this->getNode('expr');
+    $compiler
+      ->subcompile($expr);
+  }
+
+}
-- 
GitLab