From 1bdb6ed2a2caaaf42ed182c10175339b8e856178 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Sat, 21 Oct 2023 08:11:41 +0200
Subject: [PATCH] Issue #3394062 by ReINFaTe: Fiber loops in Renderer and
 BigPipe are never suspended

---
 core/lib/Drupal/Core/Render/Renderer.php      |   2 +-
 core/modules/big_pipe/src/Render/BigPipe.php  |   2 +-
 .../src/Unit/Render/FiberPlaceholderTest.php  | 127 ++++++++++++++++++
 3 files changed, 129 insertions(+), 2 deletions(-)
 create mode 100644 core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php

diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index fec1cf215c5a..afd993bee701 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -708,8 +708,8 @@ protected function replacePlaceholders(array &$elements) {
         });
       }
     }
+    $iterations = 0;
     while (count($fibers) > 0) {
-      $iterations = 0;
       foreach ($fibers as $placeholder => $fiber) {
         if (!$fiber->isStarted()) {
           $fiber->start();
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
index eb4b73109678..4f479d663adb 100644
--- a/core/modules/big_pipe/src/Render/BigPipe.php
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -553,8 +553,8 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
       }
       $fibers[$placeholder_id] = new \Fiber(fn() => $this->renderPlaceholder($placeholder_id, $placeholder_render_array));
     }
+    $iterations = 0;
     while (count($fibers) > 0) {
-      $iterations = 0;
       foreach ($fibers as $placeholder_id => $fiber) {
         // Keep skipping the messages placeholder until it's the only Fiber
         // remaining. @todo https://www.drupal.org/project/drupal/issues/3379885
diff --git a/core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php b/core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php
new file mode 100644
index 000000000000..dd1f0c768832
--- /dev/null
+++ b/core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\Tests\big_pipe\Unit\Render;
+
+use Drupal\big_pipe\Render\BigPipe;
+use Drupal\big_pipe\Render\BigPipeResponse;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\PlaceholderGeneratorInterface;
+use Drupal\Core\Render\RenderCacheInterface;
+use Drupal\Core\Render\Renderer;
+use Drupal\Core\Security\TrustedCallbackInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * @coversDefaultClass \Drupal\big_pipe\Render\BigPipe
+ * @group big_pipe
+ */
+class FiberPlaceholderTest extends UnitTestCase {
+
+  /**
+   * @covers \Drupal\big_pipe\Render\BigPipe::sendPlaceholders
+   */
+  public function testLongPlaceholderFiberSuspendingLoop() {
+    $request_stack = $this->prophesize(RequestStack::class);
+    $request_stack->getMainRequest()
+      ->willReturn(new Request());
+    $request_stack->getCurrentRequest()
+      ->willReturn(new Request());
+
+    $renderer = new Renderer(
+      $this->prophesize(ControllerResolverInterface::class)->reveal(),
+      $this->prophesize(ThemeManagerInterface::class)->reveal(),
+      $this->prophesize(ElementInfoManagerInterface::class)->reveal(),
+      $this->prophesize(PlaceholderGeneratorInterface::class)->reveal(),
+      $this->prophesize(RenderCacheInterface::class)->reveal(),
+      $request_stack->reveal(),
+      [
+        'required_cache_contexts' => [
+          'languages:language_interface',
+          'theme',
+        ],
+      ]
+    );
+
+    $session = $this->prophesize(SessionInterface::class);
+    $session->start()->willReturn(TRUE);
+
+    $bigpipe = new BigPipe(
+      $renderer,
+      $session->reveal(),
+      $request_stack->reveal(),
+      $this->prophesize(HttpKernelInterface::class)->reveal(),
+      $this->createMock(EventDispatcherInterface::class),
+      $this->prophesize(ConfigFactoryInterface::class)->reveal(),
+    );
+    $response = new BigPipeResponse(new HtmlResponse());
+
+    $attachments = [
+      'library' => [],
+      'drupalSettings' => [
+        'ajaxPageState' => [],
+      ],
+      'big_pipe_placeholders' => [
+        'callback=%5CDrupal%5CTests%5Cbig_pipe%5CUnit%5CRender%5CTurtleLazyBuilder%3A%3Aturtle&amp;&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => [
+          '#lazy_builder' => [
+            '\Drupal\Tests\big_pipe\Unit\Render\TurtleLazyBuilder::turtle',
+            [],
+          ],
+        ],
+      ],
+    ];
+    $response->setAttachments($attachments);
+
+    // Construct minimal HTML response.
+    $content = '<html><body><span data-big-pipe-placeholder-id="callback=%5CDrupal%5CTests%5Cbig_pipe%5CUnit%5CRender%5CTurtleLazyBuilder%3A%3Aturtle&amp;&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></body></html>';
+    $response->setContent($content);
+
+    // Capture the result to avoid PHPUnit complaining.
+    ob_start();
+    $fiber = new \Fiber(function () use ($bigpipe, $response) {
+      $bigpipe->sendContent($response);
+    });
+    $fiber->start();
+    $this->assertFalse($fiber->isTerminated(), 'Placeholder fibers with long execution time supposed to return control before terminating');
+    ob_get_clean();
+  }
+
+}
+
+class TurtleLazyBuilder implements TrustedCallbackInterface {
+
+  /**
+   * #lazy_builder callback.
+   *
+   * Suspends its own execution twice to simulate long operation.
+   *
+   * @return array
+   */
+  public static function turtle(): array {
+    if (\Fiber::getCurrent() !== NULL) {
+      \Fiber::suspend();
+    }
+    if (\Fiber::getCurrent() !== NULL) {
+      \Fiber::suspend();
+    }
+    return [
+      '#markup' => '<span>Turtle is finally here. But how?</span>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks() {
+    return ['turtle'];
+  }
+
+}
-- 
GitLab