You are here

uc_stripe.module in Ubercart Stripe 6.2

A stripe.js PCI-compliant payment gateway Forked from Bitcookie's work (thanks!) which was posted at http://bitcookie.com/blog/pci-compliant-ubercart-and-stripe-js from discussion in the uc_stripe issue queue, https://www.drupal.org/node/1467886

File

uc_stripe.module
View source
<?php

/**
 * @file
 * A stripe.js PCI-compliant payment gateway
 * Forked from Bitcookie's work (thanks!) which was posted at
 * http://bitcookie.com/blog/pci-compliant-ubercart-and-stripe-js
 * from discussion in the uc_stripe issue queue,
 * https://www.drupal.org/node/1467886
 */

/**
 * Implements hook_payment_gateway to register this payment gateway
 * @return array
 */
function uc_stripe_payment_gateway() {
  $gateways = array();
  $gateways[] = array(
    'id' => 'uc_stripe',
    'title' => t('Stripe Gateway'),
    'description' => t('Process card payments using Stripe JS.'),
    'settings' => 'uc_stripe_settings_form',
    'credit' => 'uc_stripe_charge',
  );
  return $gateways;
}

/**
 * Implements hook_recurring_info() to integrate with uc_recurring
 *
 * @return mixed
 */
function uc_stripe_recurring_info() {
  $items['uc_stripe'] = array(
    'name' => t('Stripe'),
    'payment method' => 'credit',
    'module' => 'uc_recurring',
    'fee handler' => 'uc_stripe',
    'process callback' => 'uc_stripe_process',
    'renew callback' => 'uc_stripe_renew',
    'cancel callback' => 'uc_stripe_cancel',
    'own handler' => FALSE,
    'menu' => array(
      'charge' => UC_RECURRING_MENU_DEFAULT,
      'edit' => UC_RECURRING_MENU_DEFAULT,
      'cancel' => UC_RECURRING_MENU_DEFAULT,
    ),
  );
  return $items;
}

/**
 * Implements hook_form_FORMID_alter() to change the checkout form
 * All work as a result is done in JS, the ordinary post does not happen.
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) {

  // Prevent form submission if javascript has not enabled the button.
  // If JS happens to be disabled, we don't want user to be able to submit.
  $form['continue']['#disabled'] = TRUE;

  //Add stripe.js
  drupal_set_html_head('<script type="text/javascript" src="https://js.stripe.com/v2/"></script>');
  $apikey = variable_get('uc_stripe_testmode', TRUE) ? check_plain(variable_get('uc_stripe_api_key_test_publishable', '')) : check_plain(variable_get('uc_stripe_api_key_live_publishable', ''));
  drupal_add_js("Stripe.setPublishableKey('{$apikey}')", 'inline');

  // Add custom JS and CSS
  drupal_add_js(drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe.js');
  drupal_add_css(drupal_get_path('module', 'uc_stripe') . '/css/uc_stripe.css');

  // Add custom submit which will do saving away of token during submit.
  $form['#submit'][] = 'uc_stripe_checkout_form_customsubmit';

  // Add a section for stripe.js error messages (CC validation, etc.)
  $form['panes']['messages'] = array(
    '#type' => 'markup',
    '#value' => "<div id='uc_stripe_messages' class='messages error hidden'></div>",
  );

  // This image after loading triggers js to make sure the JS warning and disablings are removed
  $image_path = url(drupal_get_path('module', 'uc_stripe') . '/images/1px.png');
  $form['dummy_image_load'] = array(
    '#type' => 'markup',
    '#value' => "<img src='{$image_path}' style='display:none;' onload='uc_stripe_clean_cc_form()'>",
  );
  if (uc_credit_default_gateway() == 'uc_stripe') {
    if (variable_get('uc_stripe_testmode', TRUE)) {
      $form['panes']['testmode'] = array(
        '#prefix' => "<div class='messages' style='background-color:#BEEBBF'>",
        '#type' => 'markup',
        '#value' => t("Test mode is <strong>ON</strong> for the Stripe Payment Gateway. Your  card will not be charged. To change this setting, edit the !link", array(
          '!link' => l("Stripe settings", "admin/store/settings/payment/edit/gateways"),
        )),
        '#suffix' => "</div>",
      );
    }
  }
}

/**
 * Implements hook_order_pane to provide the stripe customer info
 *
 * @return array
 */
function uc_stripe_order_pane() {
  $panes[] = array(
    'id' => 'uc_stripe',
    'callback' => 'uc_stripe_order_pane_stripe',
    'title' => t('Stripe Customer Info'),
    'desc' => t("Stripe Information"),
    'class' => 'pos-left',
    'weight' => 3,
    'show' => array(
      'view',
      'edit',
    ),
  );
  return $panes;
}

/**
 * Implements hook_uc_checkout_complete()
 *
 * Saves stripe customer_id into the user object
 *
 * @param $order
 * @param $account
 */
function uc_stripe_uc_checkout_complete($order, $account) {

  // Pull the stripe customer ID from the session.
  // It got there in uc_stripe_checkout_form_customsubmit()
  $stripe_customer_id = $_SESSION['stripe']['customer_id'];
  $loaded_user = user_load(array(
    'uid' => $account->uid,
  ));
  user_save($loaded_user, array(
    "uc_stripe_customer_id" => $stripe_customer_id,
  ));
}

/**
 * Provide customer id for order pane.
 *
 * @param $op
 * @param $arg1
 * @return string
 */
function uc_stripe_order_pane_stripe($op, $arg1) {
  if ($op == 'view') {
    $stripe_customer_id = _uc_stripe_get_customer_id($arg1->uid);
    $output = t("Stripe Customer ID: ") . $stripe_customer_id;
    return $output;
  }
}

/**
 * Provide configuration form for uc_stripe
 *
 * @return mixed
 */
function uc_stripe_settings_form() {
  $form['uc_stripe_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Stripe settings'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_test_secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Test Secret Key'),
    '#default_value' => variable_get('uc_stripe_api_key_test_secret', ''),
    '#description' => t('Your Development Stripe API Key. Must be the "secret" key, not the "publishable" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_test_publishable'] = array(
    '#type' => 'textfield',
    '#title' => t('Test Publishable Key'),
    '#default_value' => variable_get('uc_stripe_api_key_test_publishable', ''),
    '#description' => t('Your Development Stripe API Key. Must be the "publishable" key, not the "secret" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_live_secret'] = array(
    '#type' => 'textfield',
    '#title' => t('Live Secret Key'),
    '#default_value' => variable_get('uc_stripe_api_key_live_secret', ''),
    '#description' => t('Your Live Stripe API Key. Must be the "secret" key, not the "publishable" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_api_key_live_publishable'] = array(
    '#type' => 'textfield',
    '#title' => t('Live Publishable Key'),
    '#default_value' => variable_get('uc_stripe_api_key_live_publishable', ''),
    '#description' => t('Your Live Stripe API Key. Must be the "publishable" key, not the "secret" one.'),
  );
  $form['uc_stripe_settings']['uc_stripe_testmode'] = array(
    '#type' => 'checkbox',
    '#title' => t('Test mode'),
    '#description' => 'Testing Mode: Stripe will use the development API key to process the transaction so the card will not actually be charged.',
    '#default_value' => variable_get('uc_stripe_testmode', TRUE),
  );
  $form['uc_stripe_settings']['uc_stripe_poweredby'] = array(
    '#type' => 'checkbox',
    '#title' => t('Powered by Stripe'),
    '#description' => 'Show "powered by Stripe" in shopping cart.',
    '#default_value' => variable_get('uc_stripe_poweredby', FALSE),
  );
  return $form;
}

/**
 * Implements hook_form_FORMID_alter()
 *
 * Add validation function for stripe settings
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_form_uc_payment_gateways_form_alter(&$form, &$form_state) {
  $form['#validate'][] = 'uc_stripe_settings_form_validate';
}

/**
 * Validation function and normalize keys (trim spaces)
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_settings_form_validate($form, &$form_state) {
  $elements = array(
    'uc_stripe_api_key_test_secret',
    'uc_stripe_api_key_test_publishable',
    'uc_stripe_api_key_live_secret',
    'uc_stripe_api_key_live_publishable',
  );
  foreach ($elements as $element_name) {
    $form_state['values'][$element_name] = _uc_stripe_sanitize_key($form_state['values'][$element_name]);
    if (!_uc_stripe_validate_key($form_state['values'][$element_name])) {
      form_set_error($element_name, t('@name does not appear to be a valid stripe key', array(
        '@name' => $element_name,
      )));
    }
  }
}

/**
 * Sanitize and strip whitespace from Stripe keys
 *
 * @param $key
 */
function _uc_stripe_sanitize_key($key) {
  $key = trim($key);
  $key = check_plain($key);
  return $key;
}

/**
 * Validate Stripe key
 *
 * @param $key
 * @return boolean
 */
function _uc_stripe_validate_key($key) {
  $valid = preg_match('/^[a-zA-Z0-9_]+$/', $key);
  return $valid;
}

/**
 * Custom submit function to store the stripe token
 *
 * Since we don't have a user account at this step, we're going to store the token
 * in the session. We'll grab the token in the charge callback and use it to charge
 *
 * @param $form
 * @param $form_state
 */
function uc_stripe_checkout_form_customsubmit($form, &$form_state) {

  // I do not understand why this is not in $form_state['values']
  // Reaching into $_POST is not "normal". but can't find it otherwise.
  $_SESSION['stripe']['token'] = $_POST['stripe_token'];
}

/**
 * Generic "charge" callback that runs on checkout and via the order's "card" terminal
 *
 * @param $order_id
 * @param $amount
 * @param $data
 * @return array
 */
function uc_stripe_charge($order_id, $amount, $data) {
  global $user;

  //  Load the stripe PHP API
  if (!_uc_stripe_prepare_api()) {
    $result = array(
      'success' => FALSE,
      'comment' => t('Stripe API not found.'),
      'message' => t('Stripe API not found. Contact the site administrator.'),
      'uid' => $user->uid,
      'order_id' => $order_id,
    );
    return $result;
  }
  $order = uc_order_load($order_id);
  $context = array(
    'revision' => 'formatted-original',
    'type' => 'amount',
  );
  $options = array(
    'sign' => FALSE,
    'thou' => FALSE,
    'dec' => FALSE,
    'prec' => 2,
  );
  $amount = uc_price($amount, $context, $options);
  $stripe_customer_id = FALSE;

  // If the user running the order is not the order's owner
  // (like if an admin is processing an order on someone's behalf)
  // then load the customer ID from the user object.
  // Otherwise, make a brand new customer each time a user checks out.
  if ($user->uid != $order->uid) {
    $stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
  }

  // Always Create a new customer in stripe for new orders
  if (!$stripe_customer_id) {
    try {

      // If the token is not in the user's session, we can't set up a new customer
      if (empty($_SESSION['stripe']['token'])) {
        throw new Exception('Token not found');
      }
      $stripe_token = $_SESSION['stripe']['token'];

      //Create the customer in stripe
      $customer = Stripe_Customer::create(array(
        "card" => $stripe_token,
        'description' => "OrderID: {$order->order_id}",
        'email' => "{$order->primary_email}",
      ));

      // Store the customer ID in the session,
      // We'll pick it up later to save it in the database since we might not have a $user object at this point anyway
      $stripe_customer_id = $_SESSION['stripe']['customer_id'] = $customer->id;
    } catch (Exception $e) {
      $result = array(
        'success' => FALSE,
        'comment' => $e
          ->getCode(),
        'message' => t("Stripe Customer Creation Failed for order !order: !message", array(
          "!order" => $order_id,
          "!message" => $e
            ->getMessage(),
        )),
        'uid' => $user->uid,
        'order_id' => $order_id,
      );
      uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
      watchdog('uc_stripe', 'Failed stripe customer creation: @message', array(
        '@message' => $result['message'],
      ));
      return $result;
    }
  }

  //  Charge the stripe customer the amount in the order

  //--Handle transactions for $0

  // Stripe can't handle transactions < $0.50, but $0 is a common value
  // so we will just return a positive result when the amount is $0.
  if ($amount == 0) {
    $result = array(
      'success' => TRUE,
      'message' => t('Payment of $0 approved'),
      'uid' => $user->uid,
      'trans_id' => md5(uniqid(rand())),
    );
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
    return $result;
  }

  // Charge the customer
  try {

    //Bail if there's no customer ID
    if (empty($stripe_customer_id)) {
      throw new Exception('No customer ID found');
    }

    // charge the Customer the amount in the order
    $charge = Stripe_Charge::create(array(
      "amount" => $amount,
      "currency" => "usd",
      "customer" => $stripe_customer_id,
    ));
    $formatted_amount = $amount / 100;
    $formatted_amount = number_format($formatted_amount, 2);
    $result = array(
      'success' => TRUE,
      'message' => t('Payment of @amount processed successfully, transaction id @transaction_id.', array(
        '@amount' => $formatted_amount,
        '@transaction_id' => $charge
          ->__get('id'),
      )),
      'uid' => $user->uid,
    );
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'order', 'completed', FALSE);
    return $result;
  } catch (Exception $e) {
    $result = array(
      'success' => FALSE,
      'comment' => $e
        ->getCode(),
      'message' => t("Stripe Charge Failed for order !order: !message", array(
        "!order" => $order_id,
        "!message" => $e
          ->getMessage(),
      )),
      'uid' => $user->uid,
      'order_id' => $order_id,
    );
    uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
    watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array(
      '@order' => $order_id,
      '@message' => $result['message'],
    ));
    return $result;
  }

  //  Default / Fallback procedure to fail if the above conditions aren't met
  $result = array(
    'success' => FALSE,
    'comment' => "Stripe Gateway Error",
    'message' => "Stripe Gateway Error",
    'uid' => $user->uid,
    'order_id' => $order_id,
  );
  uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin');
  watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array(
    'order_id' => $order_id,
  ));
  return $result;
}

/**
 * Handle renewing a recurring fee, called by uc_recurring
 *
 * Runs when the subscription interval is hit. So once a month or whatever.
 * This just charges the stripe customer whatever amount ubercart wants. It does
 * not use the Stripe subscription feature.
 *
 * @param $order
 * @param $fee
 * @return bool
 */
function uc_stripe_renew($order, &$fee) {
  try {

    //Load the API
    _uc_stripe_prepare_api();

    //Get the customer ID
    $stripe_customer_id = _uc_stripe_get_customer_id($order->uid);
    if (empty($stripe_customer_id)) {
      throw new Exception('No stripe customer ID found');
    }

    //Create the charge
    $amount = $fee->fee_amount;
    $amount = $amount * 100;
    $charge = Stripe_Charge::create(array(
      "amount" => $amount,
      "currency" => "usd",
      "customer" => $stripe_customer_id,
    ));
    uc_payment_enter($order->order_id, $order->payment_method, $order->order_total, $fee->uid, $charge, "Success");
    $formatted_amount = number_format($fee->fee_amount, 2);
    $message = t('Card renewal payment of @amount processed successfully.', array(
      '@amount' => $formatted_amount,
    ));
    uc_order_comment_save($fee->order_id, $order->uid, $message, 'order', 'completed', FALSE);
    return TRUE;
  } catch (Exception $e) {
    $result = array(
      'success' => FALSE,
      'comment' => $e
        ->getCode(),
      'message' => t("Renewal Failed for order !order: !message", array(
        "!order" => $order->order_id,
        "!message" => $e
          ->getMessage(),
      )),
    );
    uc_order_comment_save($order->order_id, $order->uid, $result['message'], 'admin');
    watchdog('uc_stripe', 'Renewal failed for order @order_id, code=@code, message: @message', array(
      '@order_id' => $order->order_id,
      '@code' => $result['comment'],
      '@message' => $result['message'],
    ));
    return FALSE;
  }
}

/**
 * UC Recurring: Process a new recurring fee.
 * This runs when subscriptions are "set up" for the first time.
 * There is no action to be taken here except returning TRUE because the customer
 * ID is already stored with the user, where it can be accessed when next charge
 * takes place.
 *
 * @param $order
 * @param $fee
 * @return bool
 */
function uc_stripe_process($order, &$fee) {
  return TRUE;
}

/**
 * UC Recurring: Cancel a recurring fee.
 * This runs when subscriptions are cancelled
 * Since we're handling charge intervals in ubercart, this doesn't need to do anything.
 *
 * @param $order
 * @param $op
 * @return bool
 */
function uc_stripe_cancel($order, $op) {
  $message = t("Subscription Canceled");
  uc_order_comment_save($order->order_id, $order->uid, $message, 'order', 'completed', FALSE);
  return TRUE;
}

/**
 * Load stripe API
 *
 * @return bool
 */
function _uc_stripe_prepare_api() {
  module_load_include('install', 'uc_stripe');
  if (!_uc_stripe_load_api()) {
    return FALSE;
  }
  if (!_uc_stripe_check_api_keys()) {
    watchdog('uc_stripe', 'Stripe API keys are not configured. Payments cannot be made without them.', array(), WATCHDOG_ERROR);
    return FALSE;
  }
  $secret_key = variable_get('uc_stripe_testmode', TRUE) ? variable_get('uc_stripe_api_key_test_secret', '') : variable_get('uc_stripe_api_key_live_secret', '');
  try {
    Stripe::setApiKey($secret_key);
  } catch (Exception $e) {
    watchdog('uc_stripe', 'Error setting the Stripe API Key. Payments will not be processed: %error', array(
      '%error' => $e
        ->getMessage(),
    ));
  }
  return TRUE;
}

/**
 * Check that all API keys are configured.
 *
 * @return bool
 *   TRUE if all 4 keys have a value.
 */
function _uc_stripe_check_api_keys() {
  return variable_get('uc_stripe_api_key_live_publishable', FALSE) && variable_get('uc_stripe_api_key_live_secret', FALSE) && variable_get('uc_stripe_api_key_test_publishable', FALSE) && variable_get('uc_stripe_api_key_test_secret', FALSE);
}

/**
 * Retrieve the Stripe customer id for a user
 *
 * @param $uid
 * @return bool
 */
function _uc_stripe_get_customer_id($uid) {
  $account = user_load(array(
    'uid' => $uid,
  ));

  // For Drupal 7 stripe data will have to be stored somewhere better than data,
  // which is gone in D7
  if (empty($account->uc_stripe_customer_id)) {
    return FALSE;
  }
  return $account->uc_stripe_customer_id;
}

/**
 * implements hook_form_alter() to make sure that we have control of the
 * credit form. It's critical that it have these elements; if we used
 * hook_form_FORMID_alter for this some other module might get an earlier
 * chance at it.
 *
 * @param $form
 * @param $form_state
 * @param $form_id
 */
function uc_stripe_form_alter(&$form, &$form_state, $form_id) {
  if ($form_id == 'uc_payment_method_credit_form') {

    // Make sure tp properly prepare the CC form. Because so much is done
    // by AJAX on this page, do this again on cc number change.
    $form['cc_number']['#attributes']['onchange'] = 'uc_stripe_clean_cc_form()';
    $form['stripe_nojs_warning'] = array(
      '#type' => 'item',
      '#value' => '<span id="stripe-nojs-warning" class="stripe-warning">' . t('Sorry, for security reasons your card cannot be processed because Javascript is disabled in your browser.') . '</span>',
      '#weight' => -1000,
    );
    $form['stripe_token'] = array(
      '#type' => 'hidden',
      '#maxlength' => 64,
    );
    if (!module_exists('uc_optional_checkout_review') || !variable_get('uc_checkout_skip_review', FALSE)) {
      unset($form['cc_number'], $form['cc_cvv']);
      $form['config_error'] = array(
        '#type' => 'markup',
        '#value' => t('<span class="stripe-warning">Misconfiguration of cart checkout, see status page</span>'),
      );
    }
  }
}

/**
 * Implements hook_theme_registry_alter() to make sure that we render
 * the entire credit form, including the key returned by JS.
 *
 * @param $theme_registry
 */
function uc_stripe_theme_registry_alter(&$theme_registry) {
  if (!empty($theme_registry['uc_payment_method_credit_form'])) {
    $theme_registry['uc_payment_method_credit_form']['function'] = 'uc_stripe_uc_payment_method_credit_form';
  }
}

/**
 * Replace uc_credit's form themeing with our own - adds stripe_token.
 * @param $form
 * @return string
 */
function uc_stripe_uc_payment_method_credit_form($form) {
  $output = drupal_render($form['stripe_nojs_warning']);
  $output = drupal_render($form['config_error']);
  $output .= theme_uc_payment_method_credit_form($form);
  $output .= drupal_render($form['stripe_token']);
  $output .= drupal_render($form['dummy_image_load']);
  return $output;
}

Functions

Namesort descending Description
uc_stripe_cancel UC Recurring: Cancel a recurring fee. This runs when subscriptions are cancelled Since we're handling charge intervals in ubercart, this doesn't need to do anything.
uc_stripe_charge Generic "charge" callback that runs on checkout and via the order's "card" terminal
uc_stripe_checkout_form_customsubmit Custom submit function to store the stripe token
uc_stripe_form_alter implements hook_form_alter() to make sure that we have control of the credit form. It's critical that it have these elements; if we used hook_form_FORMID_alter for this some other module might get an earlier chance at it.
uc_stripe_form_uc_cart_checkout_form_alter Implements hook_form_FORMID_alter() to change the checkout form All work as a result is done in JS, the ordinary post does not happen.
uc_stripe_form_uc_payment_gateways_form_alter Implements hook_form_FORMID_alter()
uc_stripe_order_pane Implements hook_order_pane to provide the stripe customer info
uc_stripe_order_pane_stripe Provide customer id for order pane.
uc_stripe_payment_gateway Implements hook_payment_gateway to register this payment gateway
uc_stripe_process UC Recurring: Process a new recurring fee. This runs when subscriptions are "set up" for the first time. There is no action to be taken here except returning TRUE because the customer ID is already stored with the user, where it can be…
uc_stripe_recurring_info Implements hook_recurring_info() to integrate with uc_recurring
uc_stripe_renew Handle renewing a recurring fee, called by uc_recurring
uc_stripe_settings_form Provide configuration form for uc_stripe
uc_stripe_settings_form_validate Validation function and normalize keys (trim spaces)
uc_stripe_theme_registry_alter Implements hook_theme_registry_alter() to make sure that we render the entire credit form, including the key returned by JS.
uc_stripe_uc_checkout_complete Implements hook_uc_checkout_complete()
uc_stripe_uc_payment_method_credit_form Replace uc_credit's form themeing with our own - adds stripe_token.
_uc_stripe_check_api_keys Check that all API keys are configured.
_uc_stripe_get_customer_id Retrieve the Stripe customer id for a user
_uc_stripe_prepare_api Load stripe API
_uc_stripe_sanitize_key Sanitize and strip whitespace from Stripe keys
_uc_stripe_validate_key Validate Stripe key