diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 1ecd819..1296c4c 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -2529,8 +2529,8 @@ function install_configure_form_submit($form, &$form_state) { $account->name = $form_state['values']['account']['name']; $account->save(); // Load global $user and perform final login tasks. - $user = user_load(1); - user_login_finalize(); + $account = user_load(1); + user_login_finalize($account); // Record when this install ran. variable_set('install_time', $_SERVER['REQUEST_TIME']); diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php index b6731ac..d0b8ab0 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php @@ -205,7 +205,7 @@ function testAuthUserUserLogin() { $this->drupalGet('user/login'); // Check that we got to 'user'. - $this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid, array('absolute' => TRUE)), "Logged-in user redirected to user on accessing user/login"); + $this->assertTrue($this->url == url('user', array('absolute' => TRUE)), "Logged-in user redirected to user on accessing user/login"); // user/register should redirect to user/UID/edit. $this->drupalGet('user/register'); diff --git a/core/modules/system/lib/Drupal/system/Tests/Session/SessionHttpsTest.php b/core/modules/system/lib/Drupal/system/Tests/Session/SessionHttpsTest.php index b338b7b..00f4b13 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Session/SessionHttpsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Session/SessionHttpsTest.php @@ -52,7 +52,7 @@ protected function testHttpsSession() { // login form through https.php, which creates a mock HTTPS request. $this->drupalGet('user'); $form = $this->xpath('//form[@id="user-login-form"]'); - $form[0]['action'] = $this->httpsUrl('user'); + $form[0]['action'] = $this->httpsUrl('user/login'); $edit = array('name' => $user->name, 'pass' => $user->pass_raw); $this->drupalPost(NULL, $edit, t('Log in')); @@ -60,7 +60,7 @@ protected function testHttpsSession() { $this->curlClose(); $this->drupalGet('user'); $form = $this->xpath('//form[@id="user-login-form"]'); - $form[0]['action'] = $this->httpsUrl('user'); + $form[0]['action'] = $this->httpsUrl('user/login'); $this->drupalPost(NULL, $edit, t('Log in')); // Check secure cookie on secure page. @@ -143,13 +143,13 @@ protected function testHttpsSession() { $this->drupalGet('user/password'); $form = $this->xpath('//form[@id="user-pass"]'); $this->assertNotEqual(substr($form[0]['action'], 0, 6), 'https:', 'Password request form action is not secure'); - $form[0]['action'] = $this->httpsUrl('user'); + $form[0]['action'] = $this->httpsUrl('user/login'); // Check that user login form action is secure. $this->drupalGet('user'); $form = $this->xpath('//form[@id="user-login-form"]'); $this->assertEqual(substr($form[0]['action'], 0, 6), 'https:', 'Login form action is secure'); - $form[0]['action'] = $this->httpsUrl('user'); + $form[0]['action'] = $this->httpsUrl('user/login'); $edit = array( 'name' => $user->name, @@ -205,7 +205,7 @@ protected function testHttpsSession() { // Mock a login to the secure site using the secure session cookie. $this->drupalGet('user'); $form = $this->xpath('//form[@id="user-login-form"]'); - $form[0]['action'] = $this->httpsUrl('user'); + $form[0]['action'] = $this->httpsUrl('user/login'); $this->drupalPost(NULL, $edit, t('Log in')); // Test that the user is also authenticated on the insecure site. diff --git a/core/modules/user/lib/Drupal/user/Controller/UserController.php b/core/modules/user/lib/Drupal/user/Controller/UserController.php index e782019..abd57e9 100644 --- a/core/modules/user/lib/Drupal/user/Controller/UserController.php +++ b/core/modules/user/lib/Drupal/user/Controller/UserController.php @@ -7,10 +7,12 @@ namespace Drupal\user\Controller; +use Drupal\Core\Controller\ControllerInterface; +use Drupal\user\Form\UserLoginForm; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Drupal\Core\Controller\ControllerInterface; +use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Controller routines for user routes. @@ -18,16 +20,51 @@ class UserController implements ControllerInterface { /** - * Constructs an UserController object. + * The HTTP Kernel. + * + * @var \Symfony\Component\HttpKernel\HttpKernelInterface + */ + protected $httpKernel; + + /** + * Constructs a new UserController. + * + * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel + * The HTTP Kernel. */ - public function __construct() { + public function __construct(HttpKernelInterface $http_kernel) { + $this->httpKernel = $http_kernel; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static(); + return new static( + $container->get('http_kernel') + ); + } + + /** + * Returns the user page. + * + * Displays user profile if user is logged in, or login form for anonymous + * users. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Symfony\Component\HttpFoundation\Response + * Returns a subrequest for either the user page or the login form. + */ + public function userPage(Request $request) { + global $user; + $parameters = array(); + if ($request->query->has('destination')) { + $parameters['destination'] = $request->query->get('destination'); + } + $url = ($user->uid) ? '/user/' . $user->uid : '/user/login'; + return $this->httpKernel->handle(Request::create($url, 'GET', $parameters), HttpKernelInterface::MASTER_REQUEST); } /** diff --git a/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php b/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php new file mode 100644 index 0000000..9927b53 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php @@ -0,0 +1,229 @@ +configFactory = $config_factory; + $this->flood = $flood; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('flood') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'user_login_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, Request $request = NULL) { + $this->request = $request; + // Display login form: + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Username'), + '#size' => 60, + '#maxlength' => USERNAME_MAX_LENGTH, + '#description' => t('Enter your @s username.', array('@s' => $this->configFactory->get('system.site')->get('name'))), + '#required' => TRUE, + '#attributes' => array( + 'autocorrect' => 'off', + 'autocapitalize' => 'off', + 'spellcheck' => 'false', + 'autofocus' => 'autofocus', + ), + ); + + $form['pass'] = array( + '#type' => 'password', + '#title' => t('Password'), + '#size' => 60, + '#description' => t('Enter the password that accompanies your username.'), + '#required' => TRUE, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in')); + + $form['#validate'][] = array($this, 'validateName'); + $form['#validate'][] = array($this, 'validateAuthentication'); + $form['#validate'][] = array($this, 'validateFinal'); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $account = user_load($form_state['uid']); + $form_state['redirect'] = 'user/' . $account->uid; + + user_login_finalize($account); + } + + /** + * A FAPI validate handler. Sets an error if supplied username has been blocked. + */ + public function validateName(array &$form, array &$form_state) { + if (!empty($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) { + // Blocked in user administration. + form_set_error('name', t('The username %name has not been activated or is blocked.', array('%name' => $form_state['values']['name']))); + } + } + + /** + * A validate handler on the login form. Check supplied username/password + * against local users table. If successful, $form_state['uid'] + * is set to the matching user ID. + */ + public function validateAuthentication(array &$form, array &$form_state) { + $password = trim($form_state['values']['pass']); + $flood_config = $this->configFactory->get('user.flood'); + if (!empty($form_state['values']['name']) && !empty($password)) { + // Do not allow any login from the current user's IP if the limit has been + // reached. Default is 50 failed attempts allowed in one hour. This is + // independent of the per-user limit to catch attempts from one IP to log + // in to many different user accounts. We have a reasonably high limit + // since there may be only one apparent IP for all users at an institution. + if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) { + $form_state['flood_control_triggered'] = 'ip'; + return; + } + $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject(); + if ($account) { + if ($flood_config->get('uid_only')) { + // Register flood events based on the uid only, so they apply for any + // IP address. This is the most secure option. + $identifier = $account->uid; + } + else { + // The default identifier is a combination of uid and IP address. This + // is less secure but more resistant to denial-of-service attacks that + // could lock out all users with public user names. + $identifier = $account->uid . '-' . $this->request->getClientIP(); + } + $form_state['flood_control_user_identifier'] = $identifier; + + // Don't allow login if the limit for this user has been reached. + // Default is to allow 5 failed attempts every 6 hours. + if (!$this->flood->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) { + $form_state['flood_control_triggered'] = 'user'; + return; + } + } + // We are not limited by flood control, so try to authenticate. + // Set $form_state['uid'] as a flag for user_login_final_validate(). + $form_state['uid'] = user_authenticate($form_state['values']['name'], $password); + } + } + + /** + * The final validation handler on the login form. + * + * Sets a form error if user has not been authenticated, or if too many + * logins have been attempted. This validation function should always + * be the last one. + */ + public function validateFinal(array &$form, array &$form_state) { + $flood_config = $this->configFactory->get('user.flood'); + if (empty($form_state['uid'])) { + // Always register an IP-based failed login event. + $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window')); + // Register a per-user failed login event. + if (isset($form_state['flood_control_user_identifier'])) { + $this->flood->register('user.failed_login_user', $flood_config->get('user_window'), $form_state['flood_control_user_identifier']); + } + + if (isset($form_state['flood_control_triggered'])) { + if ($form_state['flood_control_triggered'] == 'user') { + form_set_error('name', format_plural($flood_config->get('user_limit'), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or request a new password.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + } + else { + // We did not find a uid, so the limit is IP-based. + form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); + } + } + else { + form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password', array('query' => array('name' => $form_state['values']['name'])))))); + if (user_load_by_name($form_state['values']['name'])) { + watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name'])); + } + else { + // If the username entered is not a valid user, + // only store the IP address. + watchdog('user', 'Login attempt failed from %ip.', array('%ip' => $this->request->getClientIp())); + } + } + } + elseif (isset($form_state['flood_control_user_identifier'])) { + // Clear past failures for this user so as not to block a user who might + // log in and out more than once in an hour. + $this->flood->clear('user.failed_login_user', $form_state['flood_control_user_identifier']); + } + } + +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php b/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php index 0c8aa2a..2aa43f0 100644 --- a/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php +++ b/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php @@ -10,6 +10,7 @@ use Drupal\block\BlockBase; use Drupal\Component\Annotation\Plugin; use Drupal\Core\Annotation\Translation; +use Drupal\user\Form\UserLoginForm; /** * Provides a 'User login' block. @@ -33,7 +34,7 @@ public function access() { * {@inheritdoc} */ public function build() { - $form = drupal_get_form('user_login_form'); + $form = drupal_get_form(UserLoginForm::create(\Drupal::getContainer()), \Drupal::request()); unset($form['name']['#attributes']['autofocus']); unset($form['name']['#description']); unset($form['pass']['#description']); diff --git a/core/modules/user/lib/Drupal/user/RegisterFormController.php b/core/modules/user/lib/Drupal/user/RegisterFormController.php index 7d22cd3..f9f6b67 100644 --- a/core/modules/user/lib/Drupal/user/RegisterFormController.php +++ b/core/modules/user/lib/Drupal/user/RegisterFormController.php @@ -120,8 +120,7 @@ public function save(array $form, array &$form_state) { // No e-mail verification required; log in user immediately. elseif (!$admin && !config('user.settings')->get('verify_mail') && $account->status) { _user_mail_notify('register_no_approval_required', $account); - $form_state['uid'] = $account->uid; - user_login_form_submit(array(), $form_state); + user_login_finalize($account); drupal_set_message(t('Registration successful. You are now logged in.')); $form_state['redirect'] = ''; } diff --git a/core/modules/user/user.module b/core/modules/user/user.module index e7ee37a..09e5bd5 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -855,16 +855,13 @@ function user_menu() { $items['user'] = array( 'title' => 'User account', 'title callback' => 'user_menu_title', - 'page callback' => 'user_page', - 'access callback' => TRUE, - 'file' => 'user.pages.inc', 'weight' => -10, + 'route_name' => 'user_page', 'menu_name' => 'account', ); $items['user/login'] = array( 'title' => 'Log in', - 'access callback' => 'user_is_anonymous', 'type' => MENU_DEFAULT_LOCAL_TASK, ); // Other authentication methods may add pages below user/login/. @@ -1156,44 +1153,6 @@ function user_page_title($account) { } /** - * Form builder; the main user login form. - * - * @ingroup forms - */ -function user_login_form($form, &$form_state) { - // Display login form: - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Username'), - '#size' => 60, - '#maxlength' => USERNAME_MAX_LENGTH, - '#description' => t('Enter your @s username.', array('@s' => config('system.site')->get('name'))), - '#required' => TRUE, - '#attributes' => array( - 'autocorrect' => 'off', - 'autocapitalize' => 'off', - 'spellcheck' => 'false', - 'autofocus' => 'autofocus', - ), - ); - - $form['pass'] = array( - '#type' => 'password', - '#title' => t('Password'), - '#size' => 60, - '#description' => t('Enter the password that accompanies your username.'), - '#required' => TRUE, - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in')); - - $form['#validate'] = user_login_default_validators(); - - return $form; -} - -/** * Set up a series for validators which check for blocked users, * then authenticate against local database, then return an error if * authentication fails. Distributed authentication modules are welcome @@ -1216,109 +1175,6 @@ function user_login_default_validators() { } /** - * A FAPI validate handler. Sets an error if supplied username has been blocked. - */ -function user_login_name_validate($form, &$form_state) { - if (!empty($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) { - // Blocked in user administration. - form_set_error('name', t('The username %name has not been activated or is blocked.', array('%name' => $form_state['values']['name']))); - } -} - -/** - * A validate handler on the login form. Check supplied username/password - * against local users table. If successful, $form_state['uid'] - * is set to the matching user ID. - */ -function user_login_authenticate_validate($form, &$form_state) { - $password = trim($form_state['values']['pass']); - $flood_config = config('user.flood'); - $flood = Drupal::service('flood'); - if (!empty($form_state['values']['name']) && !empty($password)) { - // Do not allow any login from the current user's IP if the limit has been - // reached. Default is 50 failed attempts allowed in one hour. This is - // independent of the per-user limit to catch attempts from one IP to log - // in to many different user accounts. We have a reasonably high limit - // since there may be only one apparent IP for all users at an institution. - if (!$flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) { - $form_state['flood_control_triggered'] = 'ip'; - return; - } - $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject(); - if ($account) { - if ($flood_config->get('uid_only')) { - // Register flood events based on the uid only, so they apply for any - // IP address. This is the most secure option. - $identifier = $account->uid; - } - else { - // The default identifier is a combination of uid and IP address. This - // is less secure but more resistant to denial-of-service attacks that - // could lock out all users with public user names. - $identifier = $account->uid . '-' . Drupal::request()->getClientIP(); - } - $form_state['flood_control_user_identifier'] = $identifier; - - // Don't allow login if the limit for this user has been reached. - // Default is to allow 5 failed attempts every 6 hours. - if (!$flood->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) { - $form_state['flood_control_triggered'] = 'user'; - return; - } - } - // We are not limited by flood control, so try to authenticate. - // Set $form_state['uid'] as a flag for user_login_final_validate(). - $form_state['uid'] = user_authenticate($form_state['values']['name'], $password); - } -} - -/** - * The final validation handler on the login form. - * - * Sets a form error if user has not been authenticated, or if too many - * logins have been attempted. This validation function should always - * be the last one. - */ -function user_login_final_validate($form, &$form_state) { - $flood_config = config('user.flood'); - $flood = Drupal::service('flood'); - if (empty($form_state['uid'])) { - // Always register an IP-based failed login event. - $flood->register('user.failed_login_ip', $flood_config->get('ip_window')); - // Register a per-user failed login event. - if (isset($form_state['flood_control_user_identifier'])) { - $flood->register('user.failed_login_user', $flood_config->get('user_window'), $form_state['flood_control_user_identifier']); - } - - if (isset($form_state['flood_control_triggered'])) { - if ($form_state['flood_control_triggered'] == 'user') { - form_set_error('name', format_plural($flood_config->get('user_limit'), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or request a new password.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); - } - else { - // We did not find a uid, so the limit is IP-based. - form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); - } - } - else { - form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password', array('query' => array('name' => $form_state['values']['name'])))))); - if (user_load_by_name($form_state['values']['name'])) { - watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name'])); - } - else { - // If the username entered is not a valid user, - // only store the IP address. - watchdog('user', 'Login attempt failed from %ip.', array('%ip' => Drupal::request()->getClientIp())); - } - } - } - elseif (isset($form_state['flood_control_user_identifier'])) { - // Clear past failures for this user so as not to block a user who might - // log in and out more than once in an hour. - $flood->clear('user.failed_login_user', $form_state['flood_control_user_identifier']); - } -} - -/** * Try to validate the user's login credentials locally. * * @param $name @@ -1355,13 +1211,14 @@ function user_authenticate($name, $password) { * The function records a watchdog message about the new session, saves the * login timestamp, calls hook_user_login(), and generates a new session. * - * @param array $edit - * The array of form values submitted by the user. + * @param \Drupal\Core\Session\AccountInterface $account + * The account to log in. * * @see hook_user_login() */ -function user_login_finalize(&$edit = array()) { +function user_login_finalize(AccountInterface $account) { global $user; + $user = $account; watchdog('user', 'Session opened for %name.', array('%name' => $user->name)); // Update the user table timestamp noting user has logged in. // This is also used to invalidate one-time login links. @@ -1380,19 +1237,6 @@ function user_login_finalize(&$edit = array()) { } /** - * Submit handler for the login form. Load $user object and perform standard login - * tasks. The user is then redirected to the My Account page. Setting the - * destination in the query string overrides the redirect. - */ -function user_login_form_submit($form, &$form_state) { - global $user; - $user = user_load($form_state['uid']); - $form_state['redirect'] = 'user/' . $user->uid; - - user_login_finalize($form_state); -} - -/** * Implements hook_user_login(). */ function user_user_login($account) { @@ -2435,21 +2279,6 @@ function user_modules_uninstalled($modules) { } /** - * Helper function to rewrite the destination to avoid redirecting to login page after login. - * - * Third-party authentication modules may use this function to determine the - * proper destination after a user has been properly logged in. - */ -function user_login_destination() { - $destination = drupal_get_destination(); - // Modules may provide login pages under the "user/login/" path prefix. - if (preg_match('@^user/login(/.*|)$@', $destination['destination'])) { - $destination['destination'] = 'user'; - } - return $destination; -} - -/** * Saves visitor information as a cookie so it can be reused. * * @param $values diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc index 5025c17..f5f164f 100644 --- a/core/modules/user/user.pages.inc +++ b/core/modules/user/user.pages.inc @@ -53,10 +53,9 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a // First stage is a confirmation form, then login if ($action == 'login') { // Set the new user. - $user = $account; // user_login_finalize() also updates the login timestamp of the // user, which invalidates further use of the one-time login link. - user_login_finalize(); + user_login_finalize($account); watchdog('user', 'User %name used one-time login link at time %timestamp.', array('%name' => $account->name, '%timestamp' => $timestamp)); drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); // Let the user's password be changed without the current password check. @@ -324,19 +323,3 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') { } throw new AccessDeniedHttpException(); } - -/** - * Access callback for path /user. - * - * Displays user profile if user is logged in, or login form for anonymous - * users. - */ -function user_page() { - global $user; - if ($user->uid) { - return new RedirectResponse(url('user/' . $user->uid, array('absolute' => TRUE))); - } - else { - return drupal_get_form('user_login_form'); - } -} diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 3d52545..7f8e98b 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -74,3 +74,17 @@ user_pass: _form: '\Drupal\user\Form\UserPasswordForm' requirements: _access: 'TRUE' + +user_page: + pattern: '/user' + defaults: + _controller: '\Drupal\user\Controller\UserController::userPage' + requirements: + _access: 'TRUE' + +user_login: + pattern: '/user/login' + defaults: + _form: '\Drupal\user\Form\UserLoginForm' + requirements: + _access: 'TRUE'