Commit eb5b42a5 authored by Youri van Koppen's avatar Youri van Koppen Committed by Greg Knaddison
Browse files

Issue #3016156 by MegaChriz, jidrone, jonathanshaw, Martijn de Wit, lkacenja:...

Issue #3016156 by MegaChriz, jidrone, jonathanshaw, Martijn de Wit, lkacenja: Multiple issues with Commerce 2 login checkout pane
parent d5e3db6b
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -38,6 +38,9 @@ $settings['locale_custom_strings_en'][''] = [
  'Unrecognized username or password. <a href=":password">Forgot your password?</a>' => 'Unrecognized e-mail address or password. <a href=":password">Forgot your password?</a>',
];

* If you use Drupal Commerce, adjust your checkout flow to use the alternative 
  login pane provided by this module.

BUGS, FEATURES, QUESTIONS
-------------------------
Post any bugs, features or questions to the issue queue:
+3 −0
Original line number Diff line number Diff line
@@ -30,5 +30,8 @@
    "support": {
        "issues": "http://drupal.org/project/issues/email_registration",
        "source": "http://cgit.drupalcode.org/email_registration"
    },
    "require-dev": {
        "drupal/commerce": "^2.0"
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -5,3 +5,14 @@ email_registration.settings:
    login_with_username:
      type: boolean
      label: 'Allow users to log in with email address or username.'

# Commerce.
commerce_checkout.commerce_checkout_pane.email_registration_login:
  type: commerce_checkout_pane_configuration
  mapping:
    allow_guest_checkout:
      type: boolean
      label: 'Allow guest checkout'
    allow_registration:
      type: boolean
      label: 'Allow registration'
+109 −45
Original line number Diff line number Diff line
@@ -2,10 +2,18 @@

namespace Drupal\email_registration\Plugin\Commerce\CheckoutPane;

use Drupal\commerce\CredentialsCheckFloodInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\Login;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Email;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\user\UserAuthInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Config\ImmutableConfig;

/**
 * Provides the email registration login pane.
@@ -18,14 +26,62 @@ use Drupal\Core\Url;
 */
class EmailRegistrationLogin extends Login {

  /**
   * Constructs a new EmailRegistrationLogin object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow
   *   The parent checkout flow.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\commerce\CredentialsCheckFloodInterface $credentials_check_flood
   *   The credentials check flood controller.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\user\UserAuthInterface $user_auth
   *   The user authentication object.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\Config\ImmutableConfig $config
   *   The email registration settings.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, CredentialsCheckFloodInterface $credentials_check_flood, AccountInterface $current_user, UserAuthInterface $user_auth, RequestStack $request_stack, ImmutableConfig $config) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager, $credentials_check_flood, $current_user, $user_auth, $request_stack);

    $this->config = $config;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $checkout_flow,
      $container->get('entity_type.manager'),
      $container->get('commerce.credentials_check_flood'),
      $container->get('current_user'),
      $container->get('user.auth'),
      $container->get('request_stack'),
      $container->get('config.factory')->get('email_registration.settings')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
    $pane_form = parent::buildPaneForm($pane_form, $form_state, $complete_form);

    $config = \Drupal::config('email_registration.settings');
    $login_with_username = $config->get('login_with_username');
    $login_with_username = $this->config->get('login_with_username');

    $pane_form['returning_customer']['name']['#title'] = $login_with_username ? t('Email address or username') : t('Email address');
    $pane_form['returning_customer']['name']['#description'] = $login_with_username ? t('Enter your email address or username.') : t('Enter your email address.');
    $pane_form['returning_customer']['name']['#element_validate'][] = 'email_registration_user_login_validate';
@@ -45,34 +101,43 @@ class EmailRegistrationLogin extends Login {
   * {@inheritdoc}
   */
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $subformId = $pane_form['#parents'][0];
    $values = $form_state->getValue($pane_form['#parents']);
    $triggering_element = $form_state->getTriggeringElement();
    if ($triggering_element['#op'] === 'login') {
      $mail = $form_state->getValue([
        'email_registration_login',
        'returning_customer',
        'name',
      ]);
      $mail = $values['returning_customer']['name'];
      if (!empty($mail)) {
        $config = \Drupal::config('email_registration.settings');
        if ($user = user_load_by_mail($mail)) {
          $username = $user->getAccountName();
          $form_state->setValue([
            'email_registration_login',
            'returning_customer',
            'name',
          ], $username);
        // Try to load the user by mail.
        $user = user_load_by_mail($mail);
      }
        elseif (!$config->get('login_with_username')) {

      if (empty($user)) {
        // Check if users are allowed to login with username as well.
        if (!$this->config->get('login_with_username')) {
          // Users are not allowed to login with username. Since no user was
          // found with the specified mail address, fail with an error and
          // bail out.
          $user_input = $form_state->getUserInput();
          $query = isset($user_input['email_registration_login']['returning_customer']['name']) ? ['name' => $user_input['email_registration_login']['returning_customer']['name']] : [];
          $form_state->setError($pane_form['returning_customer'], t('Unrecognized email address or password. <a href=":password">Forgot your password?</a>', [
          $query = isset($user_input[$subformId]['returning_customer']['name']) ? ['name' => $user_input[$subformId]['returning_customer']['name']] : [];
          $form_state->setError($pane_form['returning_customer'], $this->t('Unrecognized email address or password. <a href=":password">Forgot your password?</a>', [
            ':password' => Url::fromRoute('user.pass', [], ['query' => $query])
              ->toString(),
          ]));
          return;
        }
      }
      else {
        // We have found an user! Save username on the form state, as that is
        // what the parent class expects in their submit handler.
        $username = $user->getAccountName();
        $form_state->setValue([
          $subformId,
          'returning_customer',
          'name',
        ], $username);

        // Perform several checks for this user account
        // Copied from parent to override error messages.
        $name_element = $pane_form['returning_customer']['name'];
        $password = trim($values['returning_customer']['password']);
        // Generate the "reset password" url.
@@ -84,24 +149,23 @@ class EmailRegistrationLogin extends Login {
          $form_state->setError($name_element, $this->t('The account with email address %mail has not been activated or is blocked.', ['%mail' => $mail]));
          return;
        }
      if (!$this->credentialsCheckFlood->isAllowedHost($this->clientIp)) {
        $form_state->setErrorByName($name_element, $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')]));
        $this->credentialsCheckFlood->register($this->clientIp, $username);
        return;
      }
      elseif (!$this->credentialsCheckFlood->isAllowedAccount($this->clientIp, $username)) {
        $form_state->setErrorByName($name_element, $this->t('Too many failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')]));
        $this->credentialsCheckFlood->register($this->clientIp, $username);
        return;
      }

        $uid = $this->userAuth->authenticate($username, $password);
        if (!$uid) {
          $this->credentialsCheckFlood->register($this->clientIp, $username);
          // Changing the wrong credentials error message.
          if (!$this->config->get('login_with_username')) {
            $form_state->setError($name_element, $this->t('Unrecognized email address or password. <a href=":password">Forgot your password?</a>', [':url' => $password_url]));
            // Adding return to avoid the parent error when password is empty.
            return;
          }
      $form_state->set('logged_in_uid', $uid);
          else {
            $form_state->setError($name_element, $this->t('Unrecognized username, email, or password. <a href=":url">Have you forgotten your password?</a>', [':url' => $password_url]));
          }
        }
      }
    }

    parent::validatePaneForm($pane_form, $form_state, $complete_form);
  }

+308 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\email_registration\Functional\Plugin\Commerce\CheckoutPane;

use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;

/**
 * Tests the login checkout pane.
 *
 * @group email_registration
 */
class EmailRegistrationLoginTest extends CommerceBrowserTestBase {

  /**
   * The product.
   *
   * @var \Drupal\commerce_product\Entity\ProductInterface
   */
  protected $product;

  /**
   * {@inheritdoc}
   */
  public static $modules = [
    'commerce_product',
    'commerce_order',
    'commerce_cart',
    'commerce_checkout',
    'email_registration',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();

    // Place commerce blocks.
    $this->placeBlock('commerce_cart');
    $this->placeBlock('commerce_checkout_progress');

    // Create a product with variation.
    $variation = $this->createEntity('commerce_product_variation', [
      'type' => 'default',
      'sku' => strtolower($this->randomMachineName()),
      'price' => [
        'number' => 9.99,
        'currency_code' => 'USD',
      ],
    ]);

    /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
    $this->product = $this->createEntity('commerce_product', [
      'type' => 'default',
      'title' => 'My product',
      'variations' => [$variation],
      'stores' => [$this->store],
    ]);

    // Enable the email_registration_login pane and disable the default login
    // pane.
    /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */
    $checkout_flow = $this->container
      ->get('entity_type.manager')
      ->getStorage('commerce_checkout_flow')
      ->load('default');
    /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow_plugin */
    $checkout_flow_plugin = $checkout_flow->getPlugin();
    /** @var \Drupal\email_registration\Plugin\Commerce\CheckoutPane\EmailRegistrationLogin $pane */
    $er_login_pane = $checkout_flow_plugin->getPane('email_registration_login');
    $er_login_pane->setConfiguration([]);
    $er_login_pane->setStepId('login');

    /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\Login $pane */
    $login_pane = $checkout_flow_plugin->getPane('login');
    $login_pane->setStepId('_disabled');

    // Save pane settings.
    $checkout_flow_plugin_configuration = $checkout_flow_plugin->getConfiguration();
    $checkout_flow_plugin_configuration['panes']['email_registration_login'] = $er_login_pane->getConfiguration();
    $checkout_flow_plugin_configuration['panes']['login'] = $login_pane->getConfiguration();
    $checkout_flow_plugin->setConfiguration($checkout_flow_plugin_configuration);
    $checkout_flow->save();

    $this->drupalLogout();
    $this->addProductToCart($this->product);
  }

  /**
   * Tests if an user can login with their email address.
   */
  public function testLoginWithMailAddress() {
    // Create an user to login with.
    $account = $this->drupalCreateUser();

    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');

    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getEmail(),
      'email_registration_login[returning_customer][password]' => $account->passRaw,
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertCheckoutProgressStep('Order information');
    $this->assertLoggedIn($account);
  }

  /**
   * Tests if an user can login with their username.
   */
  public function testLoginWithUsername() {
    // Allow users to login with their username.
    $this->config('email_registration.settings')
      ->set('login_with_username', TRUE)
      ->save();

    // Create an user to login with.
    $account = $this->drupalCreateUser();

    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');
    $this->assertSession()->pageTextContains('Email address or username');

    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getUsername(),
      'email_registration_login[returning_customer][password]' => $account->passRaw,
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertCheckoutProgressStep('Order information');
    $this->assertLoggedIn($account);
  }

  /**
   * Tests trying to login without entering name or mail address.
   */
  public function testLoginWithoutValues() {
    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');
    $this->submitForm([], 'Log in');
    $this->assertSession()->pageTextContains('Unrecognized email address or password. Forgot your password?');
  }

  /**
   * Tests trying to login with wrong password.
   */
  public function testFailedLogin() {
    // Create an user to login with.
    $account = $this->drupalCreateUser();

    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');

    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getEmail(),
      'email_registration_login[returning_customer][password]' => $account->passRaw . 'foo',
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertSession()->pageTextContains('Unrecognized email address or password. Forgot your password?');
  }

  /**
   * Tests if an user cannot login with their username when that is forbidden.
   *
   * Thus when the option 'login_with_username' is disabled.
   */
  public function testFailedLoginWithUsername() {
    // Create an user to login with.
    $account = $this->drupalCreateUser();

    // Don't allow users to login with their username.
    $this->config('email_registration.settings')
      ->set('login_with_username', FALSE)
      ->save();

    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');
    $this->assertSession()->pageTextContains('Enter your email address.');

    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getUsername(),
      'email_registration_login[returning_customer][password]' => $account->passRaw,
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertSession()->pageTextContains('Unrecognized email address or password. Forgot your password?');
  }

  /**
   * Tests failed login using mail when logging in with username is allowed.
   *
   * When the option 'login_with_username' is enabled and trying to login with
   * an existing mail address, the error message should say "Unrecognized
   * username or password."
   */
  public function testFailedLoginWithMailAddressWhenUsernameIsAllowed() {
    // Create an user to login with.
    $account = $this->drupalCreateUser();

    // Don't allow users to login with their username.
    $this->config('email_registration.settings')
      ->set('login_with_username', TRUE)
      ->save();

    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');

    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getEmail(),
      'email_registration_login[returning_customer][password]' => $account->passRaw . 'foo',
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertSession()->pageTextContains('Unrecognized username, email, or password. Have you forgotten your password?');
  }

  /**
   * Tests trying to login in with an mail address belonging to a blocked user.
   */
  public function testFailedLoginWithMailAddressWithBlockedUser() {
    // Create an user to login with.
    $account = $this->drupalCreateUser();
    $account->status = FALSE;
    $account->save();

    $this->goToCheckout();
    $this->assertCheckoutProgressStep('Login');

    // Try logging in with wrong pass first.
    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getEmail(),
      'email_registration_login[returning_customer][password]' => $account->passRaw . 'foo',
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertSession()->pageTextContains('The account with email address ' . $account->getEmail() . ' has not been activated or is blocked.');

    // Now try to login with right password. Same error message should come up.
    $edit = [
      'email_registration_login[returning_customer][name]' => $account->getEmail(),
      'email_registration_login[returning_customer][password]' => $account->passRaw,
    ];
    $this->submitForm($edit, 'Log in');
    $this->assertSession()->pageTextContains('The account with email address ' . $account->getEmail() . ' has not been activated or is blocked.');
  }

  /**
   * {@inheritdoc}
   */
  protected function drupalLogin(AccountInterface $account) {
    if ($this->loggedInUser) {
      $this->drupalLogout();
    }

    $this->drupalGet('user/login');
    $this->submitForm([
      'mail' => $account->getEmail(),
      'pass' => $account->passRaw,
    ], t('Log in'));

    $this->assertLoggedIn($account);
  }

  /**
   * Adds the given product to the cart.
   *
   * @param \Drupal\commerce_product\Entity\ProductInterface $product
   *   The product to add to the cart.
   */
  protected function addProductToCart(ProductInterface $product) {
    $this->drupalGet($product->toUrl()->toString());
    $this->submitForm([], 'Add to cart');
  }

  /**
   * Proceeds to checkout.
   */
  protected function goToCheckout() {
    $cart_link = $this->getSession()->getPage()->findLink('your cart');
    $cart_link->click();
    $this->submitForm([], 'Checkout');
  }

  /**
   * Asserts the current step in the checkout progress block.
   *
   * @param string $expected
   *   The expected value.
   */
  protected function assertCheckoutProgressStep($expected) {
    $current_step = $this->getSession()->getPage()->find('css', '.checkout-progress--step__current')->getText();
    $this->assertEquals($expected, $current_step);
  }

  /**
   * Asserts that a particular user is logged in.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account to check for being logged in.
   */
  protected function assertLoggedIn(AccountInterface $account) {
    $account->sessionId = $this->getSession()->getCookie(\Drupal::service('session_configuration')->getOptions(\Drupal::request())['name']);
    $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()]));

    $this->loggedInUser = $account;
    $this->container->get('current_user')->setAccount($account);
  }

}