Skip to content

Instantly share code, notes, and snippets.

@lmeurs
Last active August 29, 2015 14:11
Show Gist options
  • Save lmeurs/de7e5cda42fd2e73577a to your computer and use it in GitHub Desktop.
Save lmeurs/de7e5cda42fd2e73577a to your computer and use it in GitHub Desktop.
Overhauled version of Drupal's Commerce Checkout Redirect module
<?php
/**
* @file
* Force anonymous users to login before being able to checkout.
*/
/**
* Implements hook_menu().
*/
function commerce_checkout_redirect_menu() {
$items = array();
$items['admin/commerce/config/checkout_redirect'] = array(
'title' => t('Checkout Redirect'),
'description' => t('Checkout redirect module settings'),
'page callback' => 'drupal_get_form',
'page arguments' => array('commerce_checkout_redirect_settings'),
'access arguments' => array('configure store'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
/**
* Implements hook_entity_property_info_alter().
*/
function commerce_checkout_redirect_entity_property_info_alter(&$info) {
// Add the commerce_checkout_redirect property to the user, useful for
// resolving redirect conflicts.
$info['user']['properties']['commerce_checkout_redirect'] = array(
'label' => t('User is in the checkout redirect process'),
'description' => t('The user is in the checkout redirect process.'),
'type' => 'boolean',
'computed' => TRUE,
);
}
/**
* Creates settings form for module.
*/
function commerce_checkout_redirect_settings($form, &$form_state) {
$form = array();
$form['commerce_checkout_redirect_path'] = array(
'#type' => 'textfield',
'#title' => t('Checkout redirect path'),
'#description' => t("Set the checkout redirect path for anonymous users. Leave blank to use the default 'user/login' page. If you redirect to another page, make sure you add a user login block to that page."),
'#default_value' => variable_get('commerce_checkout_redirect_path')
);
$form['commerce_checkout_redirect_message'] = array(
'#type' => 'textarea',
'#title' => t('Checkout redirect message'),
'#description' => t('The optional message that will be displayed at the login page in the checkout process.'),
'#default_value' => variable_get('commerce_checkout_redirect_message', t('You need to be logged in to be able to checkout.')),
'#rows' => 2,
);
$form['commerce_checkout_redirect_anonymous'] = array(
'#type' => 'checkbox',
'#title' => t('Continue without register'),
'#description' => t('Allow anonymous checkout.'),
'#default_value' => variable_get('commerce_checkout_redirect_anonymous', FALSE),
);
$form['commerce_checkout_redirect_username_as_order_email'] = array(
'#type' => 'checkbox',
'#title' => t('Require an e-mail address'),
'#description' => t("The username field will be turned into an e-mail field. It's value will be used as the order's e-mail address and at login forms the user can login using his e-mail address."),
'#default_value' => variable_get('commerce_checkout_redirect_username_as_order_email', FALSE),
'#states' => array (
'visible' => array(
':input[name="commerce_checkout_redirect_anonymous"]' => array('checked' => true),
),
),
);
$form['commerce_checkout_redirect_anonymous_as_login_option'] = array(
'#type' => 'checkbox',
'#title' => t('Hide fields'),
'#description' => t('Initially hide fields that are irrelevant for anonymous checkout.'),
'#default_value' => variable_get('commerce_checkout_redirect_anonymous_as_login_option', FALSE),
'#states' => array (
'visible' => array(
':input[name="commerce_checkout_redirect_anonymous"]' => array('checked' => true),
),
),
);
// Setting which holds the keys of user forms that may contain anonymous
// checkout functionality.
$form['commerce_checkout_redirect_user_forms'] = array(
'#type' => 'checkboxes',
'#title' => t('User forms that may contain anonymous checkout functionality.'),
'#description' => t('Select the forms which should contain the option to checkout anonymously.'),
'#default_value' => variable_get('commerce_checkout_redirect_user_forms', array_keys(commerce_checkout_redirect_get_user_forms())),
'#options' => commerce_checkout_redirect_get_user_forms(),
'#states' => array (
'visible' => array(
':input[name="commerce_checkout_redirect_anonymous"]' => array('checked' => true),
),
),
);
// Chekout button help text for e-mail verification when a visitor creates an account.
if (variable_get('user_email_verification', TRUE)) {
$form['commerce_checkout_redirect_reset_password_message'] = array(
'#type' => 'textarea',
'#title' => t('Reset password checkout redirect message'),
'#description' => t('The message that should be displayed for the reset password page for new account in the checkout process.'),
'#default_value' => variable_get('commerce_checkout_redirect_reset_password_message', t('You can also continue with the checkout process.')),
'#rows' => 2,
);
}
return system_settings_form($form);
}
/**
* Implements hook_commerce_checkout_router().
*/
function commerce_checkout_redirect_commerce_checkout_router($order, $checkout_page) {
// Get keys of first and last checkout pages.
$checkout_pages = array_keys(commerce_checkout_pages());
$first_checkout_page = $checkout_pages[0];
$last_checkout_page = end($checkout_pages);
// If user is at the first checkout page.
if ($checkout_page['page_id'] == $first_checkout_page) {
// If user is anonymous, does not have an empty cart and has not chosen to
// checkout anonymously.
if (user_is_anonymous() && !commerce_checkout_redirect_is_cart_empty() && empty($_SESSION['commerce_checkout_redirect_bypass'])) {
// Unset session variable that indicates that the user is being
// redirected.
$_SESSION['commerce_checkout_redirect_anonymous'] = TRUE;
// Show redirect message.
if (variable_get('commerce_checkout_redirect_message')) {
drupal_set_message(variable_get('commerce_checkout_redirect_message', t('You need to be logged in to be able to checkout.')));
}
// If no redirect path has been set.
if (!($redirect_path = variable_get('commerce_checkout_redirect_path', 'user/login'))) {
// Set default redirect path.
$redirect_path = 'user/login';
}
// Redirect user.
return drupal_goto($redirect_path);
}
}
// If user is at the last checkout page.
elseif ($checkout_page['page_id'] == $last_checkout_page) {
// Unset bypass session variable.
unset($_SESSION['commerce_checkout_redirect_bypass']);
}
}
/**
* Implements hook_commerce_checkout_pane_info_alter().
*
*/
function commerce_checkout_redirect_commerce_checkout_pane_info_alter(&$checkout_panes) {
// If the user is anonymous and an e-mail address is required to checkout
// anonymously.
if (user_is_anonymous() && variable_get('commerce_checkout_redirect_username_as_order_email', FALSE)) {
// Disable the Account Information pane if the username it will be used for order email.
$checkout_panes['account']['page'] = 'disabled';
$checkout_panes['account']['enabled'] = FALSE;
}
}
/**
* Implements hook_module_implements_alter().
*/
function commerce_checkout_redirect_module_implements_alter(&$implementations, $hook) {
// Return if the hook is not for hook_user_login.
if ($hook !== 'user_login') {
return;
}
// Move our hook implementation to the top, so the commerce_checkout_redirect
// user property could be used by other hook implementations.
$module = 'commerce_checkout_redirect';
$group = array($module => $implementations[$module]);
unset($implementations[$module]);
$implementations = $group + $implementations;
}
/**
* Implements hook_user_login().
*/
function commerce_checkout_redirect_user_login(&$edit, &$account) {
// If the user has been redirected.
if (!empty($_SESSION['commerce_checkout_redirect_anonymous'])) {
// Add the commerce_checkout_redirect property to the account object.
$account->commerce_checkout_redirect = TRUE;
}
}
/**
* Implements hook_form_alter().
*/
function commerce_checkout_redirect_form_alter(&$form, &$form_state, $form_id) {
// If form is one of the user forms that could be anonymous checkout enabled.
if (in_array($form_id, array_keys(commerce_checkout_redirect_get_user_forms()))) {
// Return if the cart is empty, the user has chosen to checkout anonymously
// or the user has already been redirected.
if (commerce_checkout_redirect_is_cart_empty() || !empty($_SESSION['commerce_checkout_redirect_bypass']) || empty($_SESSION['commerce_checkout_redirect_anonymous'])) {
return;
}
// Get some settings to improve readability of this script.
$setting_anonymous_checkout_enabled = variable_get('commerce_checkout_redirect_anonymous', FALSE);
$setting_email_required = variable_get('commerce_checkout_redirect_username_as_order_email', FALSE);
$setting_initially_hide_fields = variable_get('commerce_checkout_redirect_anonymous_as_login_option', FALSE);
// If this is the first time this form is being built.
if (!isset($form_state['build_info']['ajax_options'])) {
// Define AJAX options and store them in build_info so we can re-use the
// random wrapper ID.
$form_state['build_info']['ajax_options'] = array(
'effect' => 'fade',
'callback' => 'commerce_checkout_redirect_form_ajax_callback',
'wrapper' => drupal_html_class($form_state['build_info']['form_id'] . '-wrapper-' . user_password()),
);
// Store the form's original path.
$form_state['build_info']['original_path'] = url(current_path());
}
// Append the checkout redirect function to all user forms.
$form['#submit'][] = 'commerce_checkout_redirect_redirect_anonymous_submit';
// We used to unset the action property to use submit form functions
// instead. Now we set the form's original path to prevent Drupal from
// setting an AJAX path when no path is provided on an AJAX call.
$form['#action'] = $form_state['build_info']['original_path'];
// Get anonymous checkout enabled user forms.
$anonymous_checkout_enabled_user_form_keys = variable_get('commerce_checkout_redirect_user_forms', commerce_checkout_redirect_get_user_forms());
// Return if form is not anonymous checkout enabled.
if (empty($anonymous_checkout_enabled_user_form_keys[$form_id])) {
return;
}
// If form is not the password reset form (it is a login, register or user
// profile form) and anonymous checkout is enabled.
if ($form_id != 'user_pass_reset' && $setting_anonymous_checkout_enabled) {
// Store order in form state array.
$form_state['#order'] = commerce_cart_order_load();
// Set flag: hide fields?
$hide_fields = FALSE;
// Get parent form element.
if (isset($form['account'])) $parent_form_element = &$form['account'];
else $parent_form_element = &$form;
// Get key of name or mail field.
$name_or_mail_field_key = empty($parent_form_element['mail']) ? 'name' : 'mail';
// If irrelevant fields are supposed to be hidden initially.
if ($setting_initially_hide_fields) {
// Wrap form (while respecting possible earlier set prefix and suffix
// properties) so AJAX can replace the wrapper's contents.
$form['#prefix'] = '<div id="' . $form_state['build_info']['ajax_options']['wrapper'] . '">' . (empty($form['#prefix']) ? '' : $form['#prefix']);
$form['#suffix'] = (empty($form['#suffix']) ? '' : $form['#suffix']) . '</div>';
// Add radio buttons that show / hide irrelevant fields.
$parent_form_element['have_pass'] = array(
'#type' => 'radios',
'#title' => t('Do you have an account?'),
'#options' => array(
0 => t('No (You can create an account later)'),
1 => t('Yes'),
),
'#default_value' => 0,
'#weight' => -1000,
'#ajax' => $form_state['build_info']['ajax_options'],
);
// Alter hide fields flag depending on user input or have_pass's default
// value.
$hide_fields = (isset($form_state['values']['have_pass']) ? $form_state['values']['have_pass'] : $parent_form_element['have_pass']['#default_value']) == 0;
}
// Add continue anonymously button.
$form['actions']['continue_button'] = array(
'#name' => 'continue_button',
'#type' => 'submit',
'#value' => t('Checkout without an account'),
'#limit_validation_errors' => array(),
'#submit' => array('commerce_checkout_redirect_anonymous_continue_checkout'),
'#access' => !$setting_initially_hide_fields || $hide_fields,
);
// If fields need to be hidden.
if ($hide_fields) {
// Show / hide the name or mail and password fields on login forms.
// @todo: Add alter hook to easily hide other fields?
if (strpos($form_id, 'user_login') === 0) {
// Show / hide the name or mail field depending on whether an e-mail
// address is required to continue anonymous checkout.
$parent_form_element[$name_or_mail_field_key]['#access'] = $setting_email_required;
$parent_form_element['pass']['#access'] = FALSE;
}
elseif ($form_id === 'user_register_form') {
// Show / hide the name or mail field depending on whether an e-mail
// address is required to continue anonymous checkout.
$parent_form_element['name']['#access'] = FALSE;
$parent_form_element['mail']['#access'] = $setting_email_required;
}
// Hide the default submit button.
$form['actions']['submit']['#access'] = FALSE;
// Alter the continue anonymously button's title.
$form['actions']['continue_button']['#value'] = strpos($form_id, 'user_login') === 0 ? t('Continue without logging in') : t('Continue without an account');
}
// If an e-mail address is required.
if ($setting_email_required) {
// If the form does not have a dedicated e-mail field and the Email
// Registration module is not enabled.
if ($name_or_mail_field_key === 'name') {
// Alter name and pass fields' properties.
$parent_form_element['name']['#title'] = t('E-mail');
$parent_form_element['name']['#description'] = t('Enter your e-mail address.');
}
// Set weight of name or mail field to move it to top of the form.
$parent_form_element[$name_or_mail_field_key]['#weight'] = -1001;
// Set validation options for continue anonymously button.
$form['actions']['continue_button']['#limit_validation_errors'] = array(array($name_or_mail_field_key));
$form['actions']['continue_button']['#validate'][] = 'commerce_checkout_redirect_username_as_order_email_form_validate';
// Add separate element validation callback that is also called when the
// form has not been submitted by continue anonymously button.
$parent_form_element[$name_or_mail_field_key]['#element_validate'][] = 'commerce_checkout_redirect_username_as_order_email_element_validate';
// If an e-mail address has already been defined for this order.
if ($form_state['#order']->mail) {
// Set the order's e-mail address as a default value.
$parent_form_element[$name_or_mail_field_key]['#default_value'] = $form_state['#order']->mail;
}
}
}
// If form is a password reset form and any actions are defined, provide the
// option to checkout anonymously.
elseif ($form_id == 'user_pass_reset' && !empty($form['actions'])) {
// Add help text for continue anonymously button.
if (variable_get('commerce_checkout_redirect_reset_password_message')) {
$form['actions']['checkout_message']['#markup'] = '<p>' . variable_get('commerce_checkout_redirect_reset_password_message', t('You can also continue with the checkout process.')) . '</p>';
}
// Add continue anonymously button.
$form['actions']['checkout'] = array(
'#type' => 'submit',
'#value' => t('Continue with checkout'),
);
}
}
// For resetting the session variables for back to cart checkout button.
elseif (strpos($form_id, 'commerce_checkout_form_') === 0 && !empty($form['buttons']['cancel'])) {
$form['buttons']['cancel']['#submit'][] = 'commerce_checkout_redirect_checkout_form_cancel_submit';
}
}
/**
* AJAX form callback function.
*/
function commerce_checkout_redirect_form_ajax_callback($form, &$form_state) {
// Return the full form.
return $form;
}
/**
* Submit callback for the user forms that will perform the redirection.
*/
function commerce_checkout_redirect_redirect_anonymous_submit($form, &$form_state) {
// Because the user in the order may have been updated (from uid 0 to the real
// uid for example), clear static cache before trying to get the order.
drupal_static_reset('commerce_cart_order_id');
// If the user has been redirected.
if (!empty($_SESSION['commerce_checkout_redirect_anonymous'])) {
// If form is password reset form (after e-mail verification for new
// accounts or after an existing user requested to change his password.
if ($form['#form_id'] == 'user_pass_reset') {
// Login to account to continue with the checkout process.
// @see user_pass_reset()
if ($form_state['clicked_button']['#id'] == 'edit-checkout') {
global $user;
$users = user_load_multiple(array($form_state['build_info']['args'][0]), array('status' => '1'));
$user = reset($users);
$GLOBALS['user'] = $user;
// User login with user_login_finalize().
user_login_finalize();
// Let the user's password be changed without the current password check.
$token = drupal_random_key();
$_SESSION['pass_reset_' . $GLOBALS['user']->uid] = $token;
}
// Continue the reset password process.
else {
drupal_goto('user/reset/' . implode('/', $form_state['build_info']['args']) . '/login');
}
}
// If user is logged in and an order ID can be retrieved.
if (user_is_logged_in() && ($order_id = commerce_cart_order_id($GLOBALS['user']->uid))) {
// Unset session variable and user property.
unset($_SESSION['commerce_checkout_redirect_anonymous']);
unset($GLOBALS['user']->commerce_checkout_redirect);
// Set redirect path.
$form_state['redirect'] = 'checkout/' . $order_id;
}
}
}
/**
* Validation callback for name field when it is used as a mail field.
*
* This function is solely used to trim the user input and swap the e-mail
* address for an actual username if the form has not been submitted by the
* anonymous checkout button (inspired by the Email Registration module).
*/
function commerce_checkout_redirect_username_as_order_email_element_validate($element, &$form_state, $form) {
// Get parent form element.
if (isset($form['account'])) $parent_form_element = &$form['account'];
else $parent_form_element = &$form;
// Get key of name or mail field.
$name_or_mail_field_key = empty($parent_form_element['mail']) ? 'name' : 'mail';
// Return if the field contains an actual mail field or if the field has no
// value.
if ($name_or_mail_field_key === 'mail' || empty($form_state['values']['name'])) {
return;
}
// Store the field's trimmed value.
form_set_value($parent_form_element['name'], trim($form_state['values']['name']), $form_state);
// Swap the e-mail address for an actual username if the form has not been
// submitted by the anonymous checkout button.
if ($form_state['triggering_element']['#name'] !== 'continue_button' && !module_exists('email_registration')) {
// Keep the e-mail value in form state for possible further validation.
$form_state['values']['mail'] = $form_state['values']['name'];
// Try to find the actual username.
if ($name = db_query('SELECT name FROM {users} WHERE LOWER(mail) = LOWER(:name)', array(':name' => $form_state['values']['mail']))->fetchField()) {
$form_state['values']['name'] = $name;
}
}
}
/**
* Validation callback for anonymous checkout enabled user forms when an e-mail
* address is required.
*/
function commerce_checkout_redirect_username_as_order_email_form_validate($form, &$form_state) {
// Get parent form element.
if (isset($form['account'])) $parent_form_element = &$form['account'];
else $parent_form_element = &$form;
// Get key of name or mail field.
$name_or_mail_field_key = empty($parent_form_element['mail']) ? 'name' : 'mail';
// The Email Registration module might have looked up the user's actual
// username and set it as the name field's value. Set the e-mail address as
// the name field's value if the form was submitted by the checkout anonymous
// button.
if ($form_state['triggering_element']['#name'] === 'continue_button' && module_exists('email_registration') && $name_or_mail_field_key == 'name') {
// If mail value exists.
if (isset($form_state['values']['mail'])) {
$form_state['values']['name'] = $form_state['values']['mail'];
}
// If email value exists (Email Registration sets the email value which
// seems errornous since Drupal only uses the mail property).
// @link https://www.drupal.org/node/2393189
elseif (isset($form_state['values']['email'])) {
$form_state['values']['name'] = $form_state['values']['email'];
}
}
// Return if the field has no value.
if (empty($form_state['values'][$name_or_mail_field_key])) {
return;
}
// Validate the e-mail address.
if ($error = user_validate_mail($form_state['values'][$name_or_mail_field_key])) {
form_set_error($name_or_mail_field_key, $error);
return;
}
// Check if the e-mail address is taken by an existing user.
if ($form_state['triggering_element']['#name'] === 'continue_button') {
// Try to look up the user ID for a user with the given e-mail address.
$mail_taken = db_select('users')
->fields('users', array('uid'))
->condition('uid', $GLOBALS['user']->uid, '<>')
// Why was a like pattern used? Couldn't this return a false positive if
// e-mail address joe@gmail.com was given and superjoe@gmail.com existed
// in the database?
// ->condition('mail', db_like($form_state['values'][$name_or_mail_field_key]), 'LIKE')
->condition('mail', $form_state['values'][$name_or_mail_field_key])
->range(0, 1)
->execute()
->fetchField();
// If an account exists with the givern e-mail address.
if ($mail_taken) {
// Format error message depending on whether the user is logged in.
if (user_is_logged_in()) {
form_set_error($name_or_mail_field_key, t('The e-mail address %email is already taken.', array('%email' => $form_state['values'][$name_or_mail_field_key])));
}
else {
form_set_error($name_or_mail_field_key, t('The e-mail address %email is already registered. <a href="@password">Have you forgotten your password?</a>', array('%email' => $form_state['values'][$name_or_mail_field_key], '@password' => url('user/password'))));
}
}
}
}
/**
* Submit callback to allow anonymous users to continue without logging in.
*/
function commerce_checkout_redirect_anonymous_continue_checkout($form, &$form_state) {
// Get parent form element.
if (isset($form['account'])) $parent_form_element = &$form['account'];
else $parent_form_element = &$form;
// Get key of name or mail field.
$name_or_mail_field_key = empty($parent_form_element['mail']) ? 'name' : 'mail';
// If the user chooses to continue without an account, set this variable so user
// is only redirected once per order (or once per session if the session expires first).
$_SESSION['commerce_checkout_redirect_bypass'] = TRUE;
// Using the username as order email.
if (variable_get('commerce_checkout_redirect_username_as_order_email', FALSE)) {
// Set order's e-mail address.
$form_state['#order']->mail = $form_state['values'][$name_or_mail_field_key];
// Save order.
commerce_order_save($form_state['#order']);
}
// Set redirect path.
$form_state['redirect'] = 'checkout';
}
/**
* Submit callback for the cancel checkout button.
*/
function commerce_checkout_redirect_checkout_form_cancel_submit($form, &$form_state) {
unset($_SESSION['commerce_checkout_redirect_anonymous']);
unset($_SESSION['commerce_checkout_redirect_bypass']);
}
/**
* Helper function to check if the current user's cart is empty.
*
* Based on proposal for commerce_cart_is_cart_empty().
* @link https://www.drupal.org/node/2372223
*/
function commerce_checkout_redirect_is_cart_empty() {
global $user;
if ($order = commerce_cart_order_load($user->uid)) {
$wrapper = entity_metadata_wrapper('commerce_order', $order);
// If there are one or more products in the cart...
if (commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
return FALSE;
}
}
return TRUE;
}
/**
* Helper function to get user forms which can be anonymous checkout enabled.
*/
function commerce_checkout_redirect_get_user_forms() {
return array(
'user_login' => t('Login form'),
'user_login_block' => t('Login form block'),
'user_register_form' => t('Registration form'),
'user_profile_form' => t('User profile form'),
'user_pass_reset' => t('Password reset form'),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment