Commit 0caccf51 authored by catch's avatar catch
Browse files

Issue #3304746 by scott_euser, casey, smustgrave: BigPipe cannot handle (GET)...

Issue #3304746 by scott_euser, casey, smustgrave: BigPipe cannot handle (GET) form redirects (EnforcedResponseException)

(cherry picked from commit ac0fac5e)
parent fb96042d
Loading
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ services:
      - { name: placeholder_strategy, priority: 0 }
  big_pipe:
    class: Drupal\big_pipe\Render\BigPipe
    arguments: ['@renderer', '@session', '@request_stack', '@http_kernel', '@event_dispatcher', '@config.factory', '@messenger']
    arguments: ['@renderer', '@session', '@request_stack', '@http_kernel', '@event_dispatcher', '@config.factory', '@messenger', '@router.request_context', '@logger.channel.php']
  Drupal\big_pipe\Render\BigPipe: '@big_pipe'
  html_response.attachments_processor.big_pipe:
    public: false
+56 −0
Original line number Diff line number Diff line
@@ -2,17 +2,24 @@

namespace Drupal\big_pipe\Render;

use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\LocalRedirectResponse;
use Drupal\Core\Routing\RequestContext;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -173,6 +180,8 @@ public function __construct(
    protected EventDispatcherInterface $eventDispatcher,
    protected ConfigFactoryInterface $configFactory,
    protected MessengerInterface $messenger,
    protected RequestContext $requestContext,
    protected LoggerInterface $logger,
  ) {
  }

@@ -553,6 +562,53 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
            $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries']));
          }
        }
        // Handle enforced redirect responses.
        // A typical use case where this might happen are forms using GET as
        // #method that are build inside a lazy builder.
        catch (EnforcedResponseException $e) {
          $response = $e->getResponse();
          if (!$response instanceof RedirectResponse) {
            throw $e;
          }
          $ajax_response = new AjaxResponse();
          if ($response instanceof SecuredRedirectResponse) {
            // Only redirect to safe locations.
            $ajax_response->addCommand(new RedirectCommand($response->getTargetUrl()));
          }
          else {
            try {
              // SecuredRedirectResponse is an abstract class that requires a
              // concrete implementation. Default to LocalRedirectResponse, which
              // considers only redirects to within the same site as safe.
              $safe_response = LocalRedirectResponse::createFromRedirectResponse($response);
              $safe_response->setRequestContext($this->requestContext);
              $ajax_response->addCommand(new RedirectCommand($safe_response->getTargetUrl()));
            }
            catch (\InvalidArgumentException) {
              // If the above failed, it's because the redirect target wasn't
              // local. Do not follow that redirect. Log an error message
              // instead, then return a 400 response to the client with the
              // error message. We don't throw an exception, because this is a
              // client error rather than a server error.
              $message = 'Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.';
              $this->logger->error($message);
              $ajax_response->addCommand(new MessageCommand($message));
            }
          }
          $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response);

          $json = $ajax_response->getContent();
          $output = <<<EOF
<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="$placeholder_id">
$json
</script>
EOF;
          $this->sendChunk($output);

          // Send the stop signal.
          $this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
          break;
        }
        catch (\Exception $e) {
          unset($fibers[$placeholder_id]);
          if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
+16 −0
Original line number Diff line number Diff line
@@ -31,3 +31,19 @@ big_pipe_test_preview:
    _title: 'Test placeholder previews'
  requirements:
    _access: 'TRUE'

big_pipe_test_trusted_redirect:
  path: '/big_pipe_test_trusted_redirect'
  defaults:
    _controller: '\Drupal\big_pipe_test\BigPipeTestController::trustedRedirectLazyBuilder'
    _title: 'BigPipe test trusted redirect'
  requirements:
    _access: 'TRUE'

big_pipe_test_untrusted_redirect:
  path: '/big_pipe_test_untrusted_redirect'
  defaults:
    _controller: '\Drupal\big_pipe_test\BigPipeTestController::untrustedRedirectLazyBuilder'
    _title: 'BigPipe test untrusted redirect'
  requirements:
    _access: 'TRUE'
+55 −1
Original line number Diff line number Diff line
@@ -4,7 +4,9 @@

use Drupal\big_pipe\Render\BigPipeMarkup;
use Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Security\TrustedCallbackInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Returns responses for Big Pipe routes.
@@ -213,11 +215,63 @@ public static function counter() {
    ];
  }

  /**
   * Route callback to test a trusted lazy builder redirect response.
   *
   * @return array
   *   The lazy builder callback.
   */
  public function trustedRedirectLazyBuilder(): array {
    return [
      'redirect' => [
        '#lazy_builder' => [static::class . '::redirectTrusted', []],
        '#create_placeholder' => TRUE,
      ],
    ];
  }

  /**
   * Supports Big Pipe testing of the enforced redirect response.
   *
   * @throws \Drupal\Core\Form\EnforcedResponseException
   *   Trigger catch of Big Pipe enforced redirect response exception.
   */
  public static function redirectTrusted(): void {
    $response = new RedirectResponse('/big_pipe_test');
    throw new EnforcedResponseException($response);
  }

  /**
   * Route callback to test an untrusted lazy builder redirect response.
   *
   * @return array
   *   The lazy builder callback.
   */
  public function untrustedRedirectLazyBuilder(): array {
    return [
      'redirect' => [
        '#lazy_builder' => [static::class . '::redirectUntrusted', []],
        '#create_placeholder' => TRUE,
      ],
    ];
  }

  /**
   * Supports Big Pipe testing of an untrusted external URL.
   *
   * @throws \Drupal\Core\Form\EnforcedResponseException
   *   Trigger catch of Big Pipe enforced redirect response exception.
   */
  public static function redirectUntrusted(): void {
    $response = new RedirectResponse('https://example.com');
    throw new EnforcedResponseException($response);
  }

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['currentTime', 'piggy', 'helloOrHi', 'exception', 'responseException', 'counter'];
    return ['currentTime', 'piggy', 'helloOrHi', 'exception', 'responseException', 'counter', 'redirectTrusted', 'redirectUntrusted'];
  }

}
+10 −0
Original line number Diff line number Diff line
@@ -219,6 +219,16 @@ public function testBigPipe(): void {
    $this->assertSession()->responseNotContains('</body>');
    // The exception is expected. Do not interpret it as a test failure.
    unlink($this->root . '/' . $this->siteDirectory . '/error.log');

    // Tests the enforced redirect response exception handles redirecting to
    // a trusted redirect.
    $this->drupalGet(Url::fromRoute('big_pipe_test_trusted_redirect'));
    $this->assertSession()->responseContains('application/vnd.drupal-ajax');
    $this->assertSession()->responseContains('[{"command":"redirect","url":"\/big_pipe_test"}]');

    // Test that it rejects an untrusted redirect.
    $this->drupalGet(Url::fromRoute('big_pipe_test_untrusted_redirect'));
    $this->assertSession()->responseContains('Redirects to external URLs are not allowed by default');
  }

  /**
Loading