<?php

/**
 * @file
 * Enables custom 404 (not found) and 403 (access denied) pages in Drupal.
 *
 * There is no need for creating real nodes under taxonomies.
 *
 * Copyright 2005 Khalid Baheyeldin http://2bits.com
 */

/**
 * Helper function to set up array of error coedes.
 *
 * This is where the error codes and their default descriptions are
 * stored. Add here as necessary.
 */
function _customerror_enum_errors() {
  $errors = array(
    404 => t('requested page not found'),
    403 => t('access denied'),
  );

  // Sorting array here by keys so they are logically sorted on form.
  ksort($errors);

  return $errors;
}

/**
 * Fetch error.
 */
function _customerror_fetch_error($error_code) {
  $errors = _customerror_enum_errors();

  $default_desc = t('unknown error: @error_code', array('@error_code' => $error_code));

  $r = $default_desc;

  foreach ($errors as $code => $desc) {
    if ($error_code == $code) {
      $r = $desc;
    }
  }
  return $r;
}

/**
 * Implements hook_help().
 */
function customerror_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/modules#description':
    case 'admin/settings/customerr':
      $output = t('Enables the creation of custom error pages for 404 and 403 errors.');
      break;
  }
  return $output;
}

/**
 * Callback, page arguments.
 */
function customerror_admin_settings() {
  $form = array(
    'customerror_form_description' => array(
      '#type' => 'markup',
      '#value' => t('Enter the error pages that will be seen by your visitors when they get the respective errors. You can enter any HTML text. You can point the users to the FAQ, inform them that you reorganized the site, ask them to report the error, login or register, ...etc.'),
    ),
  );

  $themes = system_theme_data();
  ksort($themes);
  $theme_options[0] = t('System default');
  foreach ($themes as $theme) {
    if ($theme->status) {
      $theme_options[$theme->name] = $theme->name;
    }
  }

  $errors = _customerror_enum_errors();
  foreach ($errors as $code => $desc) {
    if (variable_get('site_' . $code, '') != 'customerror/' . $code) {
      drupal_set_message(t('Error reporting is not set for error !error. Please ensure that the default !error page is set to be customerror/!error on the !link.',
        array(
          '!error' => $code,
          '!link' => l(t('Error reporting settings page'), 'admin/settings/error-reporting'),
        )),
        'error');
    }
  }

  foreach ($errors as $code => $desc) {
    $group = 'customerror_' . $code . '_group';
    $form[$group] = array(
      '#type'          => 'fieldset',
      '#title'         => t('!code Error Settings', array('!code' => $code)),
      '#collapsed'     => TRUE,
      '#collapsible'   => TRUE,
    );
    $form[$group]['customerror_' . $code . '_title'] = array(
      '#type'          => 'textfield',
      '#title'         => t('Title for @code', array('@code' => $code)),
      '#default_value' => variable_get('customerror_' . $code . '_title', $desc),
      '#size'          => 70,
      '#maxlength'     => 70,
      '#description'   => t('Title of @code error page', array('@code' => $code)),
    );
    $form[$group]['customerror_' . $code] = array(
      '#type'          => 'textarea',
      '#title'         => t('Description for @code', array('@code' => $code)),
      '#default_value' => variable_get('customerror_' . $code, $desc),
      '#rows'          => 10,
      '#description'   => t('This text will be displayed if a @code (@desc) error occurs.', array('@code' => $code, '@desc' => $desc)),
    );
    $form[$group]['customerror_' . $code . '_theme'] = array(
      '#type' => 'select',
      '#options' => $theme_options,
      '#title' => t('Theme'),
      '#description' => t('Theme to be used on the error page below the admin path.'),
      '#default_value' => variable_get('customerror_' . $code . '_theme', 0),
    );
    $form[$group]['customerror_' . $code . '_php'] = array(
      '#type'          => 'checkbox',
      '#title'         => t('Allow PHP code to be executed for @code', array('@code' => $code)),
      '#default_value' => variable_get('customerror_' . $code . '_php', FALSE),
      '#description'   => t('This allows you to include PHP code (enclosed in &lt;?php ?&gt; tags) for the @code (@desc) message. Note that this can be dangerous in some situations. Make sure that you are aware of the implications.', array('@code' => $code, '@desc' => $desc)),
    );
  }

  $form['redirects'] = array(
    '#type'            => 'fieldset',
    '#title'           => t('404 Redirects'),
    '#collapsed'       => TRUE,
    '#collapsible'     => TRUE,
  );
  $form['redirects']['customerror_redirect'] = array(
    '#type'            => 'textarea',
    '#title'           => t('Redirect list'),
    '#default_value'   => variable_get('customerror_redirect', ''),
    '#rows'            => 10,
    '#description'     => t('These are custom redirect pairs, one per line. Each pair requires a path to match (which is a regular expression) and a destination separated by a space. The keyword <em>&lt;front></em> is allowed as a destination. If you are unfamilar with regular expressions, a simple search string will work, but will match any part of the URl. For example <em>index.html &lt;front></em> will match both <em>http://example.com/index.html &amp; http://example.com/archive/index.html</em>.'),
  );

  return system_settings_form($form);
}

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

  $items['admin/settings/customerror'] = array(
    'title'            => 'Custom error',
    'description'      => 'Administer custom error.',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('customerror_admin_settings'),
    'access arguments' => array('administer site configuration'),
    'type'             => MENU_NORMAL_ITEM,
  );

  $items['customerror'] = array(
    'title'           => 'customerror',
    'access callback' => TRUE,
    'page callback'   => 'customerror_page',
    'type'            => MENU_CALLBACK,
  );

  return $items;
}

/**
 * Implements hook_simpletest().
 */
function customerror_simpletest() {
  return array_keys(file_scan_directory(
    drupal_get_path('module', 'customerror') . '/tests', '\.test$'));
}

/**
 * Displays the 403 or 404 error page.
 */
function customerror_page() {
  $code = arg(1);

  // Set custom theme for error_page if any has been selected, 0 => default.
  $theme = customerror_get_theme($code);
  if (!empty($theme)) {
    global $custom_theme;
    $custom_theme = $theme;
  }

  switch ($code) {
    case 403:
      $internal_path = substr(request_uri(), strlen(base_path()));
      if ($internal_path) {
        $_REQUEST['destination'] = substr(request_uri(), 1);
      }
      else {
        $_REQUEST['destination'] = variable_get('site_frontpage', 'node');
      }
      $_SESSION['destination'] = $_REQUEST['destination'];

    case 404:
    default:
      // Treat an unknown method as a 404.
      // Check if we should redirect.
      customerror_check_redirect();

      // Make sure that we sent an appropriate header.
      customerror_header($code);

      module_invoke_all('customerror_pre_render', $code);

      drupal_set_title(variable_get('customerror_' . $code . '_title', _customerror_fetch_error($code)));
      $output = theme('customerror', $code, variable_get('customerror_' . $code, _customerror_fetch_error($code)));
      $output = (variable_get('customerror_' . $code . '_php', FALSE)) ? drupal_eval($output) : $output;
      break;
  }
  return $output;
}

/**
 * Make sure that we sent an appropriate header.
 */
function customerror_header($code) {
  switch ($code) {
    case 403:
      drupal_set_header('HTTP/1.1 403 Forbidden');
      break;

    case 404:
      drupal_set_header('HTTP/1.1 404 Not Found');
      break;
  }
}

/**
 * Implements hook_theme().
 */
function customerror_theme() {
  return array(
    'customerror' => array(
      'arguments' => array('code', 'content'),
    ),
  );
}

/**
 * Themeable function.
 */
function theme_customerror($code, $content) {
  return $content;
}

/**
 * Implements hook_user().
 */
function customerror_user($op, $edit, $user) {
  switch ($op) {
    case 'login':
      // Check if we have a destination saved in the session.
      if (!empty($_SESSION['destination'])) {
        // If there is one, then set the REQUEST destination to it.
        $_REQUEST['destination'] = $_SESSION['destination'];
        // And clear the one in the session.
        unset($_SESSION['destination']);
        // User module then does a drupal_goto() after we return from here.
      }
  }
}

/**
 * Check list of redirects.
 */
function customerror_check_redirect() {
  $destination = $_REQUEST['destination'];
  if (empty($destination)) {
    return;
  }

  $redirects = trim(variable_get('customerror_redirect', ''));
  if (empty($redirects)) {
    return;
  }
  $redirect_list = explode("\n", $redirects);

  foreach ($redirect_list as $item) {
    list($src, $dst) = explode(' ', $item);
    if (isset($src) && isset($dst)) {
      $src = str_replace("/", "\\/", $src);
      $dst = str_replace("\r", "", $dst);
      // In case there are spaces in the URL, we escape them.
      $orig_dst = str_replace(" ", "%20", $destination);
      if (preg_match('/' . $src . '/', $orig_dst)) {
        $_REQUEST['destination'] = $dst;
        drupal_goto($dst);
      }
    }
  }
}

/**
 * Implements hook_form_alter().
 *
 * We need this to be able to search from the error pages, otherwise
 * the form POST's to the error page, bringing no results page.
 */
function customerror_form_alter(&$form, &$form_state, $form_id) {
  switch ($form_id) {
    case 'search_theme_form':
      if (arg(0) == 'customerror') {
        $form['#action'] = url('search/node');
      }
      break;
  }
}

/**
 * Gets the configured display theme for the given error code.
 */
function customerror_get_theme($code = 404) {
  $theme = 0;

  $admin_theme = variable_get('admin_theme', 0);
  if ($admin_theme) {
    $theme = variable_get('customerror_' . $code . '_theme', $admin_theme);
  }

  return $theme;
}
