Loading core/modules/big_pipe/big_pipe.services.yml +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading core/modules/big_pipe/src/Render/BigPipe.php +56 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -173,6 +180,8 @@ public function __construct( protected EventDispatcherInterface $eventDispatcher, protected ConfigFactoryInterface $configFactory, protected MessengerInterface $messenger, protected RequestContext $requestContext, protected LoggerInterface $logger, ) { } Loading Loading @@ -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) { Loading core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml +16 −0 Original line number Diff line number Diff line Loading @@ -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' core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php +55 −1 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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']; } } core/modules/big_pipe/tests/src/Functional/BigPipeTest.php +10 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
core/modules/big_pipe/big_pipe.services.yml +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
core/modules/big_pipe/src/Render/BigPipe.php +56 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -173,6 +180,8 @@ public function __construct( protected EventDispatcherInterface $eventDispatcher, protected ConfigFactoryInterface $configFactory, protected MessengerInterface $messenger, protected RequestContext $requestContext, protected LoggerInterface $logger, ) { } Loading Loading @@ -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) { Loading
core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml +16 −0 Original line number Diff line number Diff line Loading @@ -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'
core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php +55 −1 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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']; } }
core/modules/big_pipe/tests/src/Functional/BigPipeTest.php +10 −0 Original line number Diff line number Diff line Loading @@ -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