diff --git a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php index 9f2e7d4092..da93e4fc92 100644 --- a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php @@ -33,7 +33,7 @@ public function __construct(CsrfTokenGenerator $csrf_token) { * {@inheritdoc} */ public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { - if ($route->hasRequirement('_csrf_token')) { + if ($route->hasRequirement('_csrf_token') || $route->hasOption('_csrf_token_or_confirm')) { $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 986410d197..29b63b081b 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -377,7 +377,7 @@ protected function drupalLogout() { // Make a request to the logout page, and redirect to the user page, the // idea being if you were properly logged out you should be seeing a login // screen. - $this->drupalGet('user/logout', ['query' => ['destination' => 'user/login']]); + $this->drupalPostForm('user/logout', [], 'Log out', ['query' => ['destination' => 'user/login']]); $this->assertResponse(200, 'User was logged out.'); $pass = $this->assertField('name', 'Username field found.', 'Logout'); $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout'); diff --git a/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php b/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php index 6995d5985c..0e2da6a65d 100644 --- a/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php +++ b/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php @@ -64,7 +64,7 @@ protected function setUp() { */ public function testToolbarCacheContextsCaller() { // Test with default combination and permission to see toolbar. - $this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found for default combination and permission to see toolbar.'); + $this->assertToolbarCacheContexts(['user', 'session'], 'Expected cache contexts found for default combination and permission to see toolbar.'); // Test without user toolbar tab. User module is a required module so we have to // manually remove the user toolbar tab. diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php index be15fac4ab..dd4513c121 100644 --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -4,8 +4,10 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Xss; +use Drupal\Core\Access\CsrfTokenGenerator; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\user\Form\UserLogoutConfirm; use Drupal\user\Form\UserPasswordResetForm; use Drupal\user\UserDataInterface; use Drupal\user\UserInterface; @@ -49,6 +51,13 @@ class UserController extends ControllerBase { protected $logger; /** + * The csrf token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator + */ + protected $csrfToken; + + /** * Constructs a UserController object. * * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter @@ -59,12 +68,15 @@ class UserController extends ControllerBase { * The user data service. * @param \Psr\Log\LoggerInterface $logger * A logger instance. + * @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator + * The csrf token generator. */ - public function __construct(DateFormatterInterface $date_formatter, UserStorageInterface $user_storage, UserDataInterface $user_data, LoggerInterface $logger) { + public function __construct(DateFormatterInterface $date_formatter, UserStorageInterface $user_storage, UserDataInterface $user_data, LoggerInterface $logger, CsrfTokenGenerator $token_generator) { $this->dateFormatter = $date_formatter; $this->userStorage = $user_storage; $this->userData = $user_data; $this->logger = $logger; + $this->csrfToken = $token_generator; } /** @@ -75,7 +87,8 @@ public static function create(ContainerInterface $container) { $container->get('date.formatter'), $container->get('entity.manager')->getStorage('user'), $container->get('user.data'), - $container->get('logger.factory')->get('user') + $container->get('logger.factory')->get('user'), + $container->get('csrf_token') ); } @@ -274,10 +287,20 @@ public function userTitle(UserInterface $user = NULL) { /** * Logs the current user out. * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * * @return \Symfony\Component\HttpFoundation\RedirectResponse * A redirection to home page. */ - public function logout() { + public function logout(Request $request) { + $token = $request->query->get('token'); + + // Show confirm form when no$this->drupalPostForm('user/logout', [], 'Log out', ['query' => ['destination' => 'user']]); valid csrf token is present. + if (!$token || !$this->csrfToken->validate($token, 'user/logout')) { + return $this->formBuilder() + ->getForm(UserLogoutConfirm::class); + } user_logout(); return $this->redirect(''); } diff --git a/core/modules/user/src/Form/UserLogoutConfirm.php b/core/modules/user/src/Form/UserLogoutConfirm.php new file mode 100644 index 0000000000..725b374d1f --- /dev/null +++ b/core/modules/user/src/Form/UserLogoutConfirm.php @@ -0,0 +1,58 @@ +t('Log out'); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to log out?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url(''); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'user_logout_confirm'; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + user_logout(); + + $form_state->setRedirect(''); + } + +} diff --git a/core/modules/user/tests/src/Functional/UserLogoutTest.php b/core/modules/user/tests/src/Functional/UserLogoutTest.php new file mode 100644 index 0000000000..5883bbf072 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserLogoutTest.php @@ -0,0 +1,46 @@ +placeBlock('system_menu_block:account'); + } + + /** + * Tests user logout functionality. + */ + public function testLogout() { + $account = $this->createUser([]); + $this->drupalLogin($account); + + // Test invalid csrf token. + $this->drupalGet('user/logout', ['query' => ['token' => '123']]); + $this->assertSession()->buttonExists('Log out'); + + $this->drupalGet('user'); + $this->getSession()->getPage()->clickLink('Log out'); + // Make sure the user gets logged out. + $this->drupalGet('user/login'); + $this->assertSession()->fieldExists('name'); + } + +} diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 319f219e8c..1ac567181a 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -12,6 +12,8 @@ user.logout: _controller: '\Drupal\user\Controller\UserController::logout' requirements: _user_is_logged_in: 'TRUE' + options: + _csrf_token_or_confirm: 'TRUE' user.admin_index: path: '/admin/config/people' diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 754f9d9838..1e25a87ee2 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -738,7 +738,7 @@ protected function drupalLogout() { // idea being if you were properly logged out you should be seeing a login // screen. $assert_session = $this->assertSession(); - $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]); + $this->drupalPostForm('user/logout', [], 'Log out', ['query' => ['destination' => 'user']]); $assert_session->statusCodeEquals(200); $assert_session->fieldExists('name'); $assert_session->fieldExists('pass');