From dcec8637914897589f9e0c736b71d5169e7ccfc7 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Tue, 18 Apr 2017 13:33:14 +0100 Subject: [PATCH] Issue #2867703 by Berdir: Change #action in UserLoginBlock to a #lazy_builder to avoid the route cache context --- .../user/src/Plugin/Block/UserLoginBlock.php | 37 +++++++++++++++++-- .../modules/user/src/Tests/UserBlocksTest.php | 23 +++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/core/modules/user/src/Plugin/Block/UserLoginBlock.php b/core/modules/user/src/Plugin/Block/UserLoginBlock.php index ed0e2eaf1a8..a9341f9b7f7 100644 --- a/core/modules/user/src/Plugin/Block/UserLoginBlock.php +++ b/core/modules/user/src/Plugin/Block/UserLoginBlock.php @@ -6,7 +6,6 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\RedirectDestinationTrait; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Routing\UrlGeneratorTrait; use Drupal\Core\Url; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Block\BlockBase; @@ -23,7 +22,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface { - use UrlGeneratorTrait; use RedirectDestinationTrait; /** @@ -94,7 +92,24 @@ class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterfac unset($form['pass']['#attributes']['aria-describedby']); $form['name']['#size'] = 15; $form['pass']['#size'] = 15; - $form['#action'] = $this->url('', [], ['query' => $this->getDestinationArray(), 'external' => FALSE]); + + // Instead of setting an actual action URL, we set the placeholder, which + // will be replaced at the very last moment. This ensures forms with + // dynamically generated action URLs don't have poor cacheability. + // Use the proper API to generate the placeholder, when we have one. See + // https://www.drupal.org/node/2562341. The placholder uses a fixed string + // that is + // Crypt::hashBase64('\Drupal\user\Plugin\Block\UserLoginBlock::build'); + // This is based on the implementation in + // \Drupal\Core\Form\FormBuilder::prepareForm(), but the user login block + // requires different behavior for the destination query argument. + $placeholder = 'form_action_p_4r8ITd22yaUvXM6SzwrSe9rnQWe48hz9k1Sxto3pBvE'; + + $form['#attached']['placeholders'][$placeholder] = [ + '#lazy_builder' => ['\Drupal\user\Plugin\Block\UserLoginBlock::renderPlaceholderFormAction', []], + ]; + $form['#action'] = $placeholder; + // Build action links. $items = []; if (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) { @@ -128,4 +143,20 @@ class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterfac ]; } + /** + * #lazy_builder callback; renders a form action URL including destination. + * + * @return array + * A renderable array representing the form action. + * + * @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction() + */ + public static function renderPlaceholderFormAction() { + return [ + '#type' => 'markup', + '#markup' => Url::fromRoute('', [], ['query' => \Drupal::destination()->getAsArray(), 'external' => FALSE])->toString(), + '#cache' => ['contexts' => ['url.path', 'url.query_args']], + ]; + } + } diff --git a/core/modules/user/src/Tests/UserBlocksTest.php b/core/modules/user/src/Tests/UserBlocksTest.php index 1495b20b08b..0a4c619da6c 100644 --- a/core/modules/user/src/Tests/UserBlocksTest.php +++ b/core/modules/user/src/Tests/UserBlocksTest.php @@ -2,6 +2,7 @@ namespace Drupal\user\Tests; +use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber; use Drupal\simpletest\WebTestBase; /** @@ -77,10 +78,30 @@ class UserBlocksTest extends WebTestBase { // Now, log out and repeat with a non-403 page. $this->drupalLogout(); - $this->drupalPostForm('filter/tips', $edit, t('Log in')); + $this->drupalGet('filter/tips'); + $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalPostForm(NULL, $edit, t('Log in')); $this->assertNoText(t('User login'), 'Logged in.'); $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + // Log out again and repeat with a non-403 page including query arguments. + $this->drupalLogout(); + $this->drupalGet('filter/tips', ['query' => ['foo' => 'bar']]); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalPostForm(NULL, $edit, t('Log in')); + $this->assertNoText(t('User login'), 'Logged in.'); + $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + $this->assertTrue(strpos($this->getUrl(), '/filter/tips?foo=bar') !== FALSE, 'Correct query arguments are displayed after login'); + + // Repeat with different query arguments. + $this->drupalLogout(); + $this->drupalGet('filter/tips', ['query' => ['foo' => 'baz']]); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalPostForm(NULL, $edit, t('Log in')); + $this->assertNoText(t('User login'), 'Logged in.'); + $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + $this->assertTrue(strpos($this->getUrl(), '/filter/tips?foo=baz') !== FALSE, 'Correct query arguments are displayed after login'); + // Check that the user login block is not vulnerable to information // disclosure to third party sites. $this->drupalLogout();