diff -u b/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php --- b/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -407,8 +407,8 @@ // 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->drupalGetRoute('user.logout', array('destination' => 'user/login')); $this->assertResponse(200, 'User was logged out.'); + $this->drupalPostForm('user/logout', [], t('Confirm'), ['query' => ['destination' => 'user/login']]); $pass = $this->assertField('name', 'Username field found.', 'Logout'); $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout'); @@ -1300,52 +1300,6 @@ return $out; } - /** - * Retrieves a Drupal path by route. - * - * @param $path - * Drupal path or URL to load into internal browser - * @param $options - * Options to be forwarded to the url generator. - * @param $headers - * An array containing additional HTTP request headers, each formatted as - * "name: value". - * - * @return - * The retrieved HTML string, also available as $this->getRawContent() - */ - protected function drupalGetRoute($route_name, array $parameters = array(), array $options = array(), array $headers = array()) { - $options['absolute'] = TRUE; - - // The URL generator service is not necessarily available yet; e.g., in - // interactive installer tests. - if ($this->container->has('url_generator')) { - $url = $this->container->get('url_generator')->generateFromRoute($route_name, $parameters, $options); - } - - // We re-using a CURL connection here. If that connection still has certain - // options set, it might change the GET into a POST. Make sure we clear out - // previous options. - $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $url, CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); - // Ensure that any changes to variables in the other thread are picked up. - $this->refreshVariables(); - - // Replace original page output with new output from redirected page(s). - if ($new = $this->checkForMetaRefresh()) { - $out = $new; - } - - $verbose = 'GET request to: ' . $url . - '
Ending URL: ' . $this->getUrl(); - if ($this->dumpHeaders) { - $verbose .= '
Headers:
' . String::checkPlain(var_export(array_map('trim', $this->headers), TRUE)) . '
'; - } - $verbose .= '
' . $out; - - $this->verbose($verbose); - return $out; - } - /** * Retrieves a Drupal path or an absolute path and JSON decodes the result. * diff -u b/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml --- b/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -12,7 +12,7 @@ _controller: '\Drupal\user\Controller\UserController::logout' requirements: _user_is_logged_in: 'TRUE' - _csrf_token: 'TRUE' + _csrf_token_optional: 'TRUE' user.admin_index: path: '/admin/config/people' only in patch2: unchanged: --- a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php @@ -32,7 +32,7 @@ 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->hasRequirement('_csrf_token_optional')) { $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { only in patch2: unchanged: --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -4,6 +4,7 @@ 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\UserPasswordResetForm; @@ -49,6 +50,13 @@ class UserController extends ControllerBase { protected $logger; /** + * The srcrf token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator + */ + protected $csrfToken; + + /** * Constructs a UserController object. * * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter @@ -59,12 +67,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 +86,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 +286,25 @@ 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 csrf token is present. + if (!$token) { + return $this->formBuilder() + ->getForm('\Drupal\user\Form\UserLogoutConfirm'); + } + if (!$this->csrfToken->validate($token, 'user/logout')) { + drupal_set_message($this->t('Invalid csrf token.'), 'error'); + + return $this->redirect(''); + } user_logout(); return $this->redirect(''); } only in patch2: unchanged: --- /dev/null +++ b/core/modules/user/src/Form/UserLogoutConfirm.php @@ -0,0 +1,51 @@ +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(''); + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserLogoutTest.php @@ -0,0 +1,52 @@ +install(['bartik']); + $theme_config = \Drupal::configFactory()->getEditable('system.theme'); + $theme_config->set('default', 'bartik'); + $theme_config->save(); + + $this->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()->pageTextContains('Invalid csrf token.'); + + $this->drupalGet('user'); + $this->getSession()->getPage()->clickLink(t('Log out')); + // Make sure user legs logged out. + $this->drupalGet('user/login'); + $this->assertSession()->fieldExists('name'); + } + +}