You are here

commerce_cart_expiration.module in Commerce Cart Expiration 7

Provides a time-based cart expiration feature.

File

commerce_cart_expiration.module
View source
<?php

/**
 * @file
 * Provides a time-based cart expiration feature.
 */

/**
 * Implements hook_cron().
 */
function commerce_cart_expiration_cron() {

  // Invoke our cron Rules event.
  rules_invoke_all('commerce_cart_expiration_cron');
}

/**
 * Implements hook_menu().
 */
function commerce_cart_expiration_menu() {
  $items = array();
  $items['commerce_cart_expiration/expire/%commerce_order'] = array(
    'page callback' => 'commerce_cart_expiration_expire_ajax',
    'page arguments' => array(
      2,
    ),
    'access callback' => 'commerce_cart_expiration_access',
    'access arguments' => array(
      'ajax_expire',
      2,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['cart/expired'] = array(
    'title' => 'Shopping cart expired',
    'page callback' => 'commerce_cart_expiration_page_explanation',
    'access arguments' => array(
      'ajax order expiration',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/commerce/config/commerce_cart_expiration'] = array(
    'title' => 'Cart expiration',
    'description' => 'Configure cart expiration settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'commerce_cart_expiration_admin_settings',
    ),
    'access arguments' => array(
      'configure store',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'commerce_cart_expiration.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function commerce_cart_expiration_permission() {
  $permissions = array(
    'ajax order expiration' => array(
      'title' => t('Ajax order expiration'),
      'description' => t('Enables deletion of an order by an ajax callback when the order has expired.'),
      'restrict access' => TRUE,
    ),
  );
  return $permissions;
}

/**
 * Access callback.
 */
function commerce_cart_expiration_access($op, $arg) {
  global $user;
  $access = FALSE;
  switch ($op) {
    case 'ajax_expire':

      // We compare the users current cart with the one that may get deleted.
      $order_user = commerce_cart_order_load($user->uid);
      $access = user_access('administer commerce_order entities') || $arg->order_id == $order_user->order_id && user_access('ajax order expiration');
      break;
  }
  return $access;
}

/**
 * Retrieves expired cart orders.
 *
 * @param int $interval
 *   Time span (in seconds) until shopping carts are considered expired.
 * @param int $limit
 *   Number of expired carts to get.
 * @param string $ignore_status
 *   (optional) Orders in this status will never be considered expired. Defaults
 *   to NULL.
 *
 * @return array
 *   An array of commerce order IDs. When no results are found, an empty array
 *   is returned.
 */
function commerce_cart_expiration_get_expired_carts($interval, $limit = 0, $ignore_status = NULL) {

  // If we're resetting order statuses (instead of deleting orders), ignore
  // orders already in the desired status, to make sure we process as many
  // orders that need resetting as possible.
  $statuses = array_keys(commerce_order_statuses(array(
    'cart' => TRUE,
  )));
  if ($ignore_status && ($key = array_search($ignore_status, $statuses)) !== FALSE) {
    unset($statuses[$key]);
  }
  $query = new EntityFieldQuery();
  $query
    ->entityCondition('entity_type', 'commerce_order', '=')
    ->propertyCondition('status', $statuses, 'IN')
    ->propertyCondition('changed', REQUEST_TIME - $interval, '<');
  $query
    ->addTag('skip_payment_transaction');
  if ($limit) {
    $query
      ->range(0, $limit);
  }
  $result = $query
    ->execute();
  return !empty($result) ? array_keys(reset($result)) : array();
}

/**
 * Implements hook_query_TAG_alter() for skip_payment_transaction.
 */
function commerce_cart_expiration_query_skip_payment_transaction_alter(QueryAlterableInterface $query) {
  if (module_exists('commerce_payment')) {
    $query
      ->leftJoin('commerce_payment_transaction', 'commerce_payment_transaction', 'commerce_order.order_id = commerce_payment_transaction.order_id');
    $query
      ->isNull('commerce_payment_transaction.transaction_id');
  }
}

/**
 * Implements hook_theme().
 */
function commerce_cart_expiration_theme() {
  return array(
    'commerce_cart_expiration_block' => array(
      'variables' => array(
        'order' => NULL,
        'contents_view' => NULL,
      ),
      'path' => drupal_get_path('module', 'commerce_cart_expiration') . '/theme',
      'template' => 'commerce-cart-expiration-block',
    ),
    'commerce_cart_expiration' => array(
      'variables' => array(
        'expires_in' => 0,
      ),
    ),
  );
}

/**
 * Implements hook_block_info().
 */
function commerce_cart_expiration_block_info() {
  $blocks = array();
  $blocks['cart_expiration'] = array(
    'info' => t('Cart expiration'),
    'cache' => DRUPAL_NO_CACHE,
    'visibility' => BLOCK_VISIBILITY_LISTED,
    'pages' => "checkout*\r\ncart",
  );
  return $blocks;
}

/**
 * Implements hook_block_configure().
 */
function commerce_cart_expiration_block_configure($delta = '') {
  $form = array();
  if ($delta == 'cart_expiration') {

    // Provide a field to format output.
    $form['cart_expiration_block_content_format'] = array(
      '#type' => 'text_format',
      '#title' => t('Content format'),
      '#default_value' => variable_get('commerce_cart_expiration_' . $delta . '_content', '<p>' . t('Your cart expires in [commerce-order:expiration-formatted].') . '<p>'),
      '#format' => variable_get('commerce_cart_expiration_' . $delta . '_content_format', NULL),
      '#description' => t('Use [commerce-order:expiration-formatted] to get a javascript compatible replacement.'),
    );
    $form['token_help'] = array(
      '#title' => t('Replacement patterns'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form['token_help']['help'] = array(
      '#theme' => 'token_tree',
      '#token_types' => array(
        'commerce-order',
        'user',
        'site',
      ),
    );
  }
  return $form;
}

/**
 * Implements hook_block_save().
 */
function commerce_cart_expiration_block_save($delta = '', $edit = array()) {
  variable_set('commerce_cart_expiration_' . $delta . '_content', $edit['cart_expiration_block_content_format']['value']);
  variable_set('commerce_cart_expiration_' . $delta . '_content_format', $edit['cart_expiration_block_content_format']['format']);
}

/**
 * Implements hook_block_view().
 */
function commerce_cart_expiration_block_view($delta) {
  global $user;
  if ($delta == 'cart_expiration') {
    $content = '';
    if ($order = commerce_cart_order_load($user->uid)) {
      $order_wrapper = entity_metadata_wrapper('commerce_order', $order);

      // Only show the block content if we found product line items.
      if (commerce_line_items_quantity($order_wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
        $interval = _commerce_cart_expiration_get_interval();

        // If an interval is set.
        if ($interval) {
          $statuses = commerce_order_statuses(array(
            'cart' => TRUE,
          ));
          if (isset($statuses[$order->status])) {

            // This order is in cart status.
            $expire_in = _commerce_cart_expiration_get_expiration($order, $interval);

            // Get text from block configuration.
            $text = variable_get('commerce_cart_expiration_' . $delta . '_content', '<p>' . t('Your cart expires in [commerce-order:expiration-formatted].') . '<p>');
            $text = check_markup($text, variable_get('commerce_cart_expiration_' . $delta . '_content_format', NULL));

            // Apply token values.
            $text = token_replace($text, array(
              'commerce-order' => $order,
              'site' => NULL,
              'user' => $user,
            ));
            $variables = array(
              'order' => $order,
              'expire_in' => $expire_in,
              'content' => $text,
            );
            $content = theme('commerce_cart_expiration_block', $variables);
          }
        }
      }
    }
    return array(
      'subject' => t('Cart expiration'),
      'content' => $content,
    );
  }
}

/**
 * Preprocess template for commerce_cart_expiration_block.
 */
function commerce_cart_expiration_preprocess_commerce_cart_expiration_block(&$variables) {
  if (!empty($variables['content'])) {

    // Add javascript file.
    drupal_add_js(drupal_get_path('module', 'commerce_cart_expiration') . '/js/commerce_cart_expiration.js');

    // Add javascript settings.
    $settings = array(
      'expire_in' => $variables['expire_in'],
      'url_ajax' => url('commerce_cart_expiration/expire/' . $variables['order']->order_id),
      'url_redirect' => url('cart/expired'),
    );
    drupal_add_js(array(
      'commerce_cart_expiration' => $settings,
    ), 'setting');
  }
}

/**
 * Ajax callback for commerce_cart_expiration/expire/%commerce_order.
 *
 * Triggers order deletion instantly when javascript recognizes its expiration.
 */
function commerce_cart_expiration_expire_ajax($order) {
  global $user;

  // Return status to calling ajax function.
  // 0: Order was not deleted as it was processed in the meantime.
  // 1: Order deleted; redirect to the explanation page.
  $status = 0;

  // Since the user may have processed the order in another browser tab we need
  // to check its status again.
  $statuses = commerce_order_statuses(array(
    'cart' => TRUE,
  ));
  if (isset($statuses[$order->status])) {

    // Replace explanation page tokens here because this is the last point
    // where we can use the order.
    $text = variable_get('commerce_cart_expiration_explanation_page', '<p>' . t('Sorry, you took too much time in the checkout process.') . '</p><p>' . t('<a href="[site:url]">Return to the front page.</a>') . '</p>');
    if (is_array($text)) {
      $text = check_markup($text['value'], $text['format']);
    }
    $text = token_replace($text, array(
      'commerce-order' => $order,
      'site' => NULL,
      'user' => $user,
    ));

    // Store this text in user session:
    $_SESSION['commerce_cart_expiration_text'] = $text;

    // Invoke a Rules event for deleting an expired cart order.
    rules_invoke_all('commerce_cart_expiration_delete_order', $order);

    // Delete the order.
    commerce_order_delete($order->order_id);
    $status = 1;
  }
  drupal_json_output(array(
    'status' => $status,
  ));
  exit;
}

/**
 * Callback for cart/expired.
 */
function commerce_cart_expiration_page_explanation() {
  global $user;

  // Return prerendered content if available.
  if (isset($_SESSION['commerce_cart_expiration_text'])) {
    $text = $_SESSION['commerce_cart_expiration_text'];
    unset($_SESSION['commerce_cart_expiration_text']);
    return $text;
  }

  // Otherwhise create content here without replacing commerce_order tokens.
  $text = variable_get('commerce_cart_expiration_explanation_page', '<p>' . t('Sorry, you took too much time in the checkout process.') . '</p><p>' . t('<a href="[site:url]">Return to the front page.</a>') . '</p>');
  if (is_array($text)) {
    $text = check_markup($text['value'], $text['format']);
  }
  return token_replace($text, array(
    'site' => NULL,
    'user' => $user,
  ));
}

/**
 * Get the interval configured in Rules, if any.
 *
 * @return number
 *   The expiration interval in seconds.
 */
function _commerce_cart_expiration_get_interval() {
  module_load_include('inc', 'commerce_cart_expiration', 'commerce_cart_expiration.rules');
  $interval = 0;
  $config = rules_config_load('commerce_cart_expiration_delete_expired_carts');
  foreach ($config
    ->actions() as $action) {
    if ($action
      ->getElementName() == 'commerce_cart_expiration_delete_orders') {
      $interval = $action->settings['interval'];
    }
  }
  return $interval;
}

/**
 * Returns formatted expiration time.
 *
 * Overwrite with caution! Ajax replaces the $('.time-left') selector with its
 * own calculation.
 *
 * @param $variables
 *   An associative array containing:
 *     - expires_in: Time left in seconds.
 */
function theme_commerce_cart_expiration($variables) {
  return '<span class="time-left">' . format_interval($variables['expires_in']) . '</span>';
}

/**
 * Get seconds left until an order expires.
 *
 * @param $order
 *   An commerce_order object.
 * @param $interval
 *   (optional) Seconds until a cart expires.
 *
 * @return number
 *   Seconds until the given order expires.
 */
function _commerce_cart_expiration_get_expiration($order, $interval = NULL) {
  if (!isset($interval)) {
    $interval = _commerce_cart_expiration_get_interval();
  }
  $expire_in = $order->changed + $interval - REQUEST_TIME;
  return $expire_in;
}

/**
 * Implements hook_token_info_alter().
 */
function commerce_cart_expiration_token_info_alter(&$data) {
  if (isset($data['tokens']['commerce-order'])) {
    $data['tokens']['commerce-order']['expiration-formatted'] = array(
      'name' => 'Expiration (formatted)',
      'description' => 'The time left until this order expires. Formatted by theme_commerce_cart_expiration().',
    );
    $data['tokens']['commerce-order']['expiration-raw'] = array(
      'name' => 'Expiration (raw)',
      'description' => 'The raw time left until this order expires in seconds.',
    );
  }
}

/**
 * Implements hook_tokens().
 */
function commerce_cart_expiration_tokens($type, $tokens, $data = array(), $options = array()) {
  $replacements = array();
  if ($type == 'commerce-order' && !empty($data['commerce-order'])) {
    $order = $data['commerce-order'];
    foreach ($tokens as $name => $original) {
      switch ($name) {
        case 'expiration-formatted':
          $interval = _commerce_cart_expiration_get_interval();
          $expires_in = _commerce_cart_expiration_get_expiration($order, $interval);
          $replacements[$original] = theme('commerce_cart_expiration', array(
            'expires_in' => $expires_in,
          ));
          break;
        case 'expiration-raw':
          $interval = _commerce_cart_expiration_get_interval();
          $replacements[$original] = _commerce_cart_expiration_get_expiration($order, $interval);
          break;
      }
    }
  }
  return $replacements;
}