<?php

/**
 * @file
 * Provide development features for Drupal Commerce development.
 */
function commerce_devel_enable() {
  drupal_set_message(t('The Commerce Devel module provides enhanced logging of errors and exceptions.'));
}

/**
 * Implements hook_menu().
 */
function commerce_devel_menu() {
  $items = array();

  // Check for existence of token module once.
  $token_exists = module_exists('token');

  if (module_exists('commerce_order_ui')) {
    $items['admin/commerce/orders/%commerce_order/devel'] = array(
      'title' => 'Devel',
      'page callback' => 'devel_load_object',
      'page arguments' => array('commerce_order', 3),
      'access arguments' => array('access devel information'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
    );
    $items['admin/commerce/orders/%commerce_order/devel/load'] = array(
      'title' => 'Load',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    if ($token_exists) {
      $items['admin/commerce/orders/%commerce_order/devel/token'] = array(
        'title' => 'Tokens',
        'page callback' => 'token_devel_token_object',
        'page arguments' => array('commerce_order', 3),
        'access arguments' => array('access devel information'),
        'type' => MENU_LOCAL_TASK,
        'file' => 'token.pages.inc',
        'file path' => drupal_get_path('module', 'token'),
        'weight' => 5,
      );
    }
  }

  if (module_exists('commerce_customer_ui')) {
    $items['admin/commerce/customer-profiles/%commerce_customer_profile/devel'] = array(
      'title' => 'Devel',
      'page callback' => 'devel_load_object',
      'page arguments' => array('commerce_customer_profile', 3),
      'access arguments' => array('access devel information'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
    );
    $items['admin/commerce/customer-profiles/%commerce_customer_profile/devel/load'] = array(
      'title' => 'Load',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    if ($token_exists) {
      $items['admin/commerce/customer-profiles/%commerce_customer_profile/devel/token'] = array(
        'title' => 'Tokens',
        'page callback' => 'token_devel_token_object',
        'page arguments' => array('commerce_customer_profile', 3),
        'access arguments' => array('access devel information'),
        'type' => MENU_LOCAL_TASK,
        'file' => 'token.pages.inc',
        'file path' => drupal_get_path('module', 'token'),
        'weight' => 5,
      );
    }
  }

  if (module_exists('commerce_payment_ui')) {
    $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/devel'] = array(
      'title' => 'Devel',
      'page callback' => 'devel_load_object',
      'page arguments' => array('commerce_payment_transaction', 5),
      'access arguments' => array('access devel information'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
    );
    $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/devel/load'] = array(
      'title' => 'Load',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    if ($token_exists) {
      $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/devel/token'] = array(
        'title' => 'Tokens',
        'page callback' => 'token_devel_token_object',
        'page arguments' => array('commerce_payment_transaction', 5),
        'access arguments' => array('access devel information'),
        'type' => MENU_LOCAL_TASK,
        'file' => 'token.pages.inc',
        'file path' => drupal_get_path('module', 'token'),
        'weight' => 5,
      );
    }
  }

  if (module_exists('commerce_product_ui')) {
    $items['admin/commerce/products/%commerce_product/devel'] = array(
      'title' => 'Devel',
      'page callback' => 'devel_load_object',
      'page arguments' => array('commerce_product', 3),
      'access arguments' => array('access devel information'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
    );
    $items['admin/commerce/products/%commerce_product/devel/load'] = array(
      'title' => 'Load',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    if ($token_exists) {
      $items['admin/commerce/products/%commerce_product/devel/token'] = array(
        'title' => 'Tokens',
        'page callback' => 'token_devel_token_object',
        'page arguments' => array('commerce_product', 3),
        'access arguments' => array('access devel information'),
        'type' => MENU_LOCAL_TASK,
        'file' => 'token.pages.inc',
        'file path' => drupal_get_path('module', 'token'),
        'weight' => 5,
      );
    }
  }

  if (module_exists('commerce_cart')) {
    $items['cart/line-items/%commerce_line_item/devel'] = array(
      'title' => 'Devel',
      'page callback' => 'devel_load_object',
      'page arguments' => array('commerce_line_item', 2),
      'access arguments' => array('access devel information'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
      'file' => 'devel.pages.inc',
      'file path' => drupal_get_path('module', 'devel'),
    );
    $items['cart/line-items/%commerce_line_item/devel/load'] = array(
      'title' => 'Load',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    if ($token_exists) {
      $items['cart/line-items/%commerce_line_item/devel/token'] = array(
        'title' => 'Tokens',
        'page callback' => 'token_devel_token_object',
        'page arguments' => array('commerce_line_item', 2),
        'access arguments' => array('access devel information'),
        'type' => MENU_LOCAL_TASK,
        'file' => 'token.pages.inc',
        'file path' => drupal_get_path('module', 'token'),
        'weight' => 5,
      );
    }
  }

  return $items;
}

/**
 * Implements hook_boot() to register error and exception handling functions.
 */
function commerce_devel_boot() {
  $old_error_handler = set_error_handler('_commerce_devel_error_handler');
  // If the error handler was not Drupal's, we will set it back and leave it
  // alone.
  if ($old_error_handler != '_drupal_error_handler') {
    set_error_handler($old_error_handler);
  }
  // Replace the exception handler..  unless we find out it wasn't Drupal's.
  $old_exception_handler = set_exception_handler('_commerce_devel_exception_handler');
  if ($old_exception_handler != '_drupal_exception_handler') {
    set_exception_handler($old_exception_handler);
  }
}

/**
 * PHP Error handler; uses PHP's standard args.
 * @param $error_level
 * @param $message
 * @param $filename
 * @param $line
 * @param $context
 */
function _commerce_devel_error_handler($error_level, $message, $filename, $line, $context) {
  require_once DRUPAL_ROOT . '/includes/errors.inc';
  if ($error_level & error_reporting()) {
    $types = drupal_error_levels();
    list($severity_msg, $severity_level) = $types[$error_level];
    $caller = _drupal_get_last_caller(debug_backtrace());

    if (!function_exists('filter_xss_admin')) {
      require_once DRUPAL_ROOT . '/includes/common.inc';
    }
    if ($error_level & error_reporting()) {
      _commerce_devel_log_error(array(
        '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
        // The standard PHP error handler considers that the error messages
        // are HTML. We mimick this behavior here.
        '!message' => filter_xss_admin($message),
        '%function' => $caller['function'],
        '%file' => $caller['file'],
        '%line' => $caller['line'],
        'severity_level' => $severity_level,
        '!backtrace' =>  commerce_devel_printable_backtrace(debug_backtrace()),
      ), $error_level == E_RECOVERABLE_ERROR);
    }
  }
}
/**
 * Create a printable backtrace.
 *
 * Using var_export() often runs into trouble with hitting recursion and
 * itself causing a fatal.
 *
 * @param $backtrace
 *   The backtrace array
 * @param $is_exception
 *   If true, do not remove the top item from the backtrace.
 */
function commerce_devel_printable_backtrace($backtrace, $is_exception = FALSE) {
  // From sun's http://drupal.org/node/1158322#comment-4476230

  // First trace is the error itself, already contained in the message.
  // While the second trace is the error source and also contained in the
  // message, the message doesn't contain argument values, so we output it
  // once more in the backtrace.
  if (!$is_exception) {
    array_shift($backtrace);
  }
  // Generate a backtrace containing only scalar argument values.
  $message = '<pre class="backtrace">';
  foreach ($backtrace as $trace) {
    $call = array('function' => '', 'args' => array());
    if (isset($trace['class'])) {
      $call['function'] = $trace['class'] . $trace['type'] . $trace['function'];
    }
    elseif (isset($trace['function'])) {
      $call['function'] = $trace['function'];
    }
    else {
      $call['function'] = 'main';
    }
    if (isset($trace['args'])) {
      foreach ($trace['args'] as $arg) {
        if (is_scalar($arg)) {
          $call['args'][] = is_string($arg) ? "'$arg'" : $arg;
        }
        else {
          $call['args'][] = ucfirst(gettype($arg));
        }
      }
    }

    $message .= $call['function'] . '(' . implode(', ', $call['args']) . ") ";
    if (!empty($trace['file']) && !empty($trace['line'])) {
      $message .= basename($trace['file']) . ':' . $trace['line'];
    }
    $message .= "\n";
  }
  $message .= '</pre>';
  return $message;
}

/**
 * Custom PHP exception handler.
 *
 * Uncaught exceptions are those not enclosed in a try/catch block. They are
 * always fatal: the execution of the script will stop as soon as the exception
 * handler exits.
 *
 * @param $exception
 *   The exception object that was thrown.
 */
function _commerce_devel_exception_handler($exception) {
  require_once DRUPAL_ROOT . '/includes/errors.inc';

  try {
    // Log the message to the watchdog and return an error page to the user.
    // Convert to an array.
    $exception_array = _drupal_decode_exception($exception);
    $exception_array['!backtrace'] = commerce_devel_printable_backtrace($exception->getTrace(), TRUE);
    _commerce_devel_log_error($exception_array, TRUE);
  }
  catch (Exception $exception2) {
    // Another uncaught exception was thrown while handling the first one.
    // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
    if (error_displayable()) {
      print '<h1>Additional uncaught exception thrown while handling exception.</h1>';
      print '<h2>Original</h2><p>' . _drupal_render_exception_safe($exception) . '</p>';
      print '<h2>Additional</h2><p>' . _drupal_render_exception_safe($exception2) . '</p><hr />';
    }
  }
}

/**
 * Log a PHP error or exception, display an error page in fatal cases.
 *
 * @param $error
 *   An array with the following keys: %type, !message, %function, %file, %line
 *   and severity_level. All the parameters are plain-text, with the exception of
 *   !message, which needs to be a safe HTML string.
 * @param $fatal
 *   TRUE if the error is fatal.
 */
function _commerce_devel_log_error($error, $fatal = FALSE) {
  // Initialize a maintenance theme if the boostrap was not complete.
  // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
  if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
    unset($GLOBALS['theme']);
    if (!defined('MAINTENANCE_MODE')) {
      define('MAINTENANCE_MODE', 'error');
    }
    drupal_maintenance_theme();
  }

  // When running inside the testing framework, we relay the errors
  // to the tested site by the way of HTTP headers.
  $test_info = &$GLOBALS['drupal_test_info'];
  if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
    // $number does not use drupal_static as it should not be reset
    // as it uniquely identifies each PHP error.
    static $number = 0;
    $assertion = array(
      $error['!message'],
      $error['%type'],
      array(
        'function' => $error['%function'],
        'file' => $error['%file'],
        'line' => $error['%line'],
      ),
    );
    header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
    $number++;
  }

  watchdog('php', '%type: !message in %function (line %line of %file). Backtrace:<br/>!backtrace', $error, $error['severity_level']);

  if ($fatal) {
    drupal_add_http_header('Status', '500 Service unavailable (with message)');
  }

  if (drupal_is_cli()) {
    if ($fatal) {
      // When called from CLI, simply output a plain text message.
      print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n";
      exit;
    }
  }

  if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
    if ($fatal) {
      // When called from JavaScript, simply output the error message.
      print t('%type: !message in %function (line %line of %file).', $error);
      exit;
    }
  }
  else {
    // Display the message if the current error reporting level allows this type
    // of message to be displayed, and unconditionally in update.php.
    if (error_displayable($error)) {
      $class = 'error';

      // If error type is 'User notice' then treat it as debug information
      // instead of an error message, see dd().
      if ($error['%type'] == 'User notice') {
        $error['%type'] = 'Debug';
        $class = 'status';
      }

      drupal_set_message(t('%type: !message in %function (line %line of %file). Backtrace:<br/>!backtrace', $error), $class);
    }

    if ($fatal) {
      drupal_set_title(t('Error'));
      // We fallback to a maintenance page at this point, because the page generation
      // itself can generate errors.
      print theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.')));
      exit;
    }
  }
}