You are here

sagepay_3d_secure.module in Drupal Commerce SagePay Integration 7

File

modules/sagepay_3d_secure/sagepay_3d_secure.module
View source
<?php

/**
 * @file
 * sagepay_3d_secure.module
 */
module_load_include('inc', 'commerce_sagepay', 'includes/commerce_sagepay_common');

/**
 * Implements hook_menu().
 */
function sagepay_3d_secure_menu() {
  $items = array();
  $items['sagepay_3d_secure/3d_secure_waiting_page'] = array(
    'page callback' => 'sagepay_3d_secure_waiting_page',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );

  // Define a path to receive 3D Secure callback. (Direct only).
  $items['commerce-sagepay/3d_secure_callback/%'] = array(
    'page callback' => 'sagepay_3d_secure_callback',
    'page arguments' => array(
      2,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_commerce_checkout_router().
 */
function sagepay_3d_secure_commerce_checkout_router($order, $checkout_page) {

  // Check if we have an active 3D Secure request from a payment module.
  // If we don't, skip this page and move onto the next in the checkout process.
  if ($checkout_page['page_id'] == '3d_secure') {

    // If there is 3D Authentication required, the payment module should have
    // placed an array in an order object field called 'extra_authorisation'.
    // This data won't be stored permanently, just used for this transaction.
    // The array should contain the following 4 fields:
    // 1) PAReq
    // 2) ACSURL
    // 3) MD
    // 4) Term URL
    // If the array is not present in the order object or any of the 4 fields
    // are missing, we skip straight past the checkout page.
    $skip_3d_secure = FALSE;
    if (!isset($order->data['extra_authorisation'])) {
      $skip_3d_secure = TRUE;
    }
    else {
      if (!isset($order->data['extra_authorisation']['PAReq']) || !isset($order->data['extra_authorisation']['ACSURL']) || !isset($order->data['extra_authorisation']['MD']) || !isset($order->data['extra_authorisation']['TermUrl'])) {
        watchdog('commerce_3d_secure', 'Only partial 3D Secure Data present, cannot proceed so skipping this step', array(), WATCHDOG_WARNING);
        $skip_3d_secure = TRUE;
      }
    }
    if ($skip_3d_secure) {

      // If there are no 3d secure markers in the order, we're done.
      // Go to checkout completion page or to the offsite redirect page.
      $checkout_pages = commerce_checkout_pages();
      $next_step = $checkout_pages['3d_secure']['next_page'];
      commerce_order_status_update($order, 'checkout_' . $next_step);

      // Inform modules of checkout completion if the next page is Completed.
      if ($next_step == 'complete') {
        commerce_checkout_complete($order);
      }
      drupal_goto('checkout/' . $order->order_id . '/' . $next_step);
    }
  }
}

/**
 * Implements hook_commerce_checkout_page_info().
 */
function sagepay_3d_secure_commerce_checkout_page_info() {
  $checkout_pages = array();

  // Define an additional Checkout page for 3D Secure authentication
  // Add at a default weight of 15 so it appears before the thank you page
  // which has a default weight of 20.
  $checkout_pages['3d_secure'] = array(
    'title' => t('3D Secure Authentication'),
    'help' => t('Your payment card provider has requested some additional security details.'),
    'status_cart' => FALSE,
    'locked' => TRUE,
    'buttons' => FALSE,
    'weight' => 22,
  );
  return $checkout_pages;
}

/**
 * Implements hook_commerce_checkout_pane_info().
 */
function sagepay_3d_secure_commerce_checkout_pane_info() {
  $checkout_panes = array();
  $checkout_panes['3d_secure'] = array(
    'title' => t('3D Secure Authentication'),
    'module' => 'commerce_3d_secure',
    'page' => '3d_secure',
    'collapsible' => FALSE,
    'collapsed' => FALSE,
    'weight' => 0,
    'enabled' => TRUE,
    'review' => FALSE,
    'base' => 'sagepay_3d_secure_pane',
    'callbacks' => array(
      'checkout_form',
    ),
  );
  return $checkout_panes;
}

/**
 * Implements hook_checkout_form().
 */
function sagepay_3d_secure_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {

  // Add the fields required for 3D Secure transaction.
  drupal_add_js("window.onload = function() { document.forms['commerce-checkout-form-3d-secure'].submit(); }", 'inline');
  $form['PaReq'] = array(
    '#type' => 'hidden',
    '#default_value' => $order->data['extra_authorisation']['PAReq'],
  );
  $form['TermUrl'] = array(
    '#type' => 'hidden',
    '#default_value' => $order->data['extra_authorisation']['TermUrl'],
  );
  $form['MD'] = array(
    '#type' => 'hidden',
    '#default_value' => $order->data['extra_authorisation']['MD'],
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Proceed with 3D Secure Authentication',
  );
  $form['hidden_action'] = array(
    '#type' => 'hidden',
    '#value' => $order->data['extra_authorisation']['ACSURL'],
  );
  $form['iframe_3d_secure'] = array(
    '#markup' => '<iframe width="550" height="440" scrolling="auto" frameborder="0" name ="iframe_3d_secure" id ="iframe_3d_secure" src="' . url('sagepay_3d_secure/3d_secure_waiting_page') . '" ></iframe>',
  );
  return $form;
}

/**
 * Implements hook_form_alter().
 */
function sagepay_3d_secure_form_alter(&$form, &$form_state, $form_id) {

  // Modify the checkout form so we can change the action to redirect offsite.
  // Also rewrite the fields so they are at the top level of the form array.
  if ($form_id == 'commerce_checkout_form_3d_secure') {
    $form['#action'] = $form['3d_secure']['hidden_action']['#value'];
    $form['#attributes']['target'] = 'iframe_3d_secure';

    // Move the fields to the top level.
    $form['PaReq'] = $form['3d_secure']['PaReq'];
    $form['TermUrl'] = $form['3d_secure']['TermUrl'];
    $form['MD'] = $form['3d_secure']['MD'];
    $form['submit'] = $form['3d_secure']['submit'];
    $form['iframe_3d_secure'] = $form['3d_secure']['iframe_3d_secure'];
    unset($form['submit']);

    // Remove redundant form elements.
    unset($form['3d_secure']);
  }
  elseif ($form_id == 'commerce_sagepay_settings_form') {

    // Override security settings with our options
    $form['security'][SAGEPAY_SETTING_APPLY_3D_SECURE] = array(
      '#type' => 'radios',
      '#title' => t('3D Secure Mode'),
      '#description' => t('3D Secure mode used by default on all transactions.'),
      '#options' => array(
        '0' => t('If 3D-Secure checks are possible and rules allow, perform the checks and apply the authorisation rules. (default)'),
        '1' => t('Force 3D-Secure checks for this transaction if possible and apply rules for authorisation.'),
        '2' => t('Do not perform 3D-Secure checks for this transaction and always authorise.'),
        '3' => t('Force 3D-Secure checks for this transaction if possible but ALWAYS obtain an auth code, irrespective of rule base.'),
      ),
      '#default_value' => variable_get(SAGEPAY_SETTING_APPLY_3D_SECURE, 0),
    );
  }
}

/**
 * A temporary page that is displayed until the 3d secure form is submitted.
 *
 * Since the form is submitted via a javascript call, this page is only visible
 * to those that have JS turned off.
 */
function sagepay_3d_secure_waiting_page() {
  print '<html><head><title></title></head><body><p>';
  print t('Please wait to be redirected to your card provider for authorisation.');
  print '</p></body></html>';
}

/**
 * Process callback response from merchant server.
 *
 * @param int $order_id
 *  The order number being processed.
 */
function sagepay_3d_secure_callback($order_id) {

  // If there's no data in the POST, return a page not found.
  if (empty($_POST)) {
    drupal_not_found();
  }

  // Attempt to reload the order.
  $order = commerce_order_load($order_id);

  // If the order doesn't exist, return a page not found.
  if (!isset($order)) {
    drupal_not_found();
  }

  // Check the order status - if it's already complete return a page not found.
  $order_status = commerce_order_status_load($order->status);
  if ($order_status['name'] != 'checkout_3d_secure') {
    drupal_not_found();
  }

  // Check for 3d secure response field.
  if (!isset($_POST['MD']) || !isset($_POST['PaRes'])) {
    watchdog('commerce_sagepay_direct', 'Invalid data received in 3D Secure response', array(), WATCHDOG_ERROR);
    drupal_not_found();
  }
  $md = check_plain($_POST['MD']);
  $pares = check_plain($_POST['PaRes']);

  // Assemble post request to send to SagePay.
  $post = 'MD=' . $md;
  $post .= '&PaRes=' . $pares;
  $payment_method_instance = commerce_payment_method_instance_load($order->data['payment_method']);

  // Check the transaction mode we are in to determine which server to send
  // the 3D Secure callback.
  switch (variable_get(SAGEPAY_SETTING_TRANSACTION_MODE)) {
    case SAGEPAY_TXN_MODE_LIVE:
      $server_url = SAGEPAY_DIRECT_SERVER_3D_SECURE_CALLBACK_LIVE;
      break;
    case SAGEPAY_TXN_MODE_TEST:
      $server_url = SAGEPAY_DIRECT_SERVER_3D_SECURE_CALLBACK_TEST;
      break;
    case SAGEPAY_TXN_MODE_SIMULATION:
      $server_url = SAGEPAY_DIRECT_SERVER_SIMULATION;
      break;
    default:
      $server_url = SAGEPAY_TXN_MODE_SIMULATION;
  }

  // Process the callback.
  $response = _commerce_sagepay_request_post($server_url, $post);

  // Process response.
  $success = commerce_sagepay_process_response($payment_method_instance, $order, $response);
  if ($success) {
    $checkout_pages = commerce_checkout_pages();
    $next_step = $checkout_pages['3d_secure']['next_page'];
    $order = commerce_order_status_update($order, 'checkout_' . $next_step);

    // Inform modules of checkout completion if the next page is Completed.
    if ($next_step == 'complete') {
      commerce_checkout_complete($order);
    }
    $redirect = 'checkout/' . $order->order_id . '/' . $next_step;
  }
  else {
    $redirect = 'checkout/' . $order->order_id . '/payment';
  }
  sagepay_3d_secure_clear_iframe($redirect);
}

/**
 * Reset the page target to the top document and not the iframe.
 *
 * @param string $redirect_url
 *  The url to redirect to after clearing the iframe.
 */
function sagepay_3d_secure_clear_iframe($redirect_url) {
  print '<html><head><title></title></head><body onload="document.forms[0].submit();">';
  print '<form name="3d_secure_callback" method="post" action="' . url($redirect_url) . '" target="_top">';
  print '<noscript>';
  print '<input type="submit" value="';
  print t('Please click here to continue.');
  print '" />';
  print '</noscript>';
  print '</form>';
  print '</body></html>';
}

/**
 * Check if an order has a partial 3D secure transaction and return it.
 *
 * @param array $tokens
 *  An array of available tokens.
 * @param commerce_order $order
 *  The Commerce Order.
 */
function sagepay_3d_secure_load_transaction($tokens, $order) {

  // There are only two possible status codes - OK or NOTAUTHED.
  $status_3d_secure = isset($tokens['3DSecureStatus']) ? $tokens['3DSecureStatus'] : '';

  // If there is no 3D Secure status, we don't need to do anything else.
  if ($status_3d_secure == '') {
    return NULL;
  }

  // A successful 3D Secure transaction will also have a CAVV value.
  $cavv_code = isset($tokens['CAVV']) ? $tokens['CAVV'] : '';

  // Search for exisiting transaction.
  $conditions = array(
    'order_id' => $order->order_id,
    'remote_status' => SAGEPAY_REMOTE_STATUS_3D_SECURE,
  );
  $transactions = commerce_payment_transaction_load_multiple(array(), $conditions);
  if (empty($transactions)) {
    return NULL;
  }
  $transaction = array_pop($transactions);
  return $transaction;
}

Functions

Namesort descending Description
sagepay_3d_secure_callback Process callback response from merchant server.
sagepay_3d_secure_clear_iframe Reset the page target to the top document and not the iframe.
sagepay_3d_secure_commerce_checkout_page_info Implements hook_commerce_checkout_page_info().
sagepay_3d_secure_commerce_checkout_pane_info Implements hook_commerce_checkout_pane_info().
sagepay_3d_secure_commerce_checkout_router Implements hook_commerce_checkout_router().
sagepay_3d_secure_form_alter Implements hook_form_alter().
sagepay_3d_secure_load_transaction Check if an order has a partial 3D secure transaction and return it.
sagepay_3d_secure_menu Implements hook_menu().
sagepay_3d_secure_pane_checkout_form Implements hook_checkout_form().
sagepay_3d_secure_waiting_page A temporary page that is displayed until the 3d secure form is submitted.