<?php

/**
 * @defgroup redirect_api Redirection API
 * @{
 * Functions related to URL redirects.
 *
 * @} End of "defgroup redirect_api".
 */

/**
 * Modules should return this value from hook_redirect_access() to allow access
 * to a redirect.
 */
define('REDIRECT_ACCESS_ALLOW', 'allow');

/**
 * Modules should return this value from hook_redirect_access() to deny access
 * to a redirect.
 */
define('REDIRECT_ACCESS_DENY', 'deny');

/**
 * Modules should return this value from hook_redirect_access() to not affect
 * redirect access.
 */
define('REDIRECT_ACCESS_IGNORE', NULL);

/**
 * Implements hook_entity_info().
 */
function redirect_entity_info() {
  $info['redirect'] = array(
    'label' => t('Redirect'),
    'base table' => 'redirect',
    'controller class' => 'RedirectController',
    'entity keys' => array(
      'id' => 'rid',
      'bundle' => 'type',
    ),
    'fieldable' => FALSE,
    'uuid' => FALSE,
    'redirect' => FALSE,
  );

  return $info;
}

/**
 * Controller class for redirects.
 *
 * This extends the DrupalDefaultEntityController class, adding required
 * special handling for redirect objects.
 */
class RedirectController extends DrupalDefaultEntityController {

  protected function attachLoad(&$redirects, $revision_id = FALSE) {
    // Unserialize the URL option fields.
    foreach ($redirects as $key => $redirect) {
      $redirects[$key]->source_options = unserialize($redirect->source_options);
      $redirects[$key]->redirect_options = unserialize($redirect->redirect_options);
    }
    parent::attachLoad($redirects, $revision_id);
  }
}

/**
 * Implements hook_hook_info().
 */
function redirect_hook_info() {
  $hooks = array(
    'redirect_load',
    'redirect_load_by_source_alter',
    'redirect_access',
    'redirect_prepare',
    'redirect_validate',
    'redirect_presave',
    'redirect_insert',
    'redirect_update',
    'redirect_delete',
    'redirect_alter',
  );

  return array_fill_keys($hooks, array('group' => 'redirect'));
}

/**
 * Implements hook_permission().
 */
function redirect_permission() {
  $permissions['administer redirects'] = array(
    'title' => t('Administer URL redirections'),
  );
  return $permissions;
}

/**
 * Implements hook_help().
 */
function redirect_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/config/search/redirect/404':
      $output = '<p>' . t('This page lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them.') . '</p>';
      break;
    case 'admin/reports/page-not-found':
      break;
  }
  return $output;
}

/**
 * Implements hook_menu().
 */
function redirect_menu() {
  $items['admin/config/search/redirect'] = array(
    'title' => 'URL redirects',
    'description' => 'Redirect users from one URL to another.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('redirect_list_form'),
    'access arguments' => array('administer redirects'),
    'file' => 'redirect.admin.inc',
  );
  $items['admin/config/search/redirect/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/config/search/redirect/add'] = array(
    'title' => 'Add redirect',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('redirect_edit_form'),
    'access callback' => 'redirect_access',
    'access arguments' => array('create', 'redirect'),
    'file' => 'redirect.admin.inc',
    'type' => MENU_LOCAL_ACTION,
  );
  $items['admin/config/search/redirect/edit/%redirect'] = array(
    'title' => 'Edit redirect',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('redirect_edit_form', 5),
    'access callback' => 'redirect_access',
    'access arguments' => array('update', 5),
    'file' => 'redirect.admin.inc',
  );
  $items['admin/config/search/redirect/delete/%redirect'] = array(
    'title' => 'Delete redirect',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('redirect_delete_form', 5),
    'access callback' => 'redirect_access',
    'access arguments' => array('delete', 5),
    'file' => 'redirect.admin.inc',
  );
  $items['admin/config/search/redirect/settings'] = array(
    'title' => 'Settings',
    'description' => 'Configure behavior for URL redirects.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('redirect_settings_form'),
    'access arguments' => array('administer redirects'),
    'file' => 'redirect.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 50,
  );

  // If the database logging module is enabled, add special 404 listing pages.
  if (module_exists('dblog')) {
    $items['admin/config/search/redirect/404'] = array(
      'title' => 'Fix 404 pages',
      'description' => 'Add redirects for 404 pages.',
      'page callback' => 'redirect_404_list',
      'access arguments' => array('administer redirects'),
      'file' => 'redirect.admin.inc',
      'type' => MENU_LOCAL_TASK,
      'weight' => 20,
    );
    $items['admin/reports/page-not-found/redirect'] = array(
      'title' => 'Fix 404 pages with URL redirects',
      'page callback' => 'drupal_goto',
      'page arguments' => array('admin/config/search/redirect/404'),
      'access arguments' => array('administer redirects'),
      'type' => MENU_LOCAL_ACTION,
    );
  }

  // Devel generate integration.
  if (module_exists('devel_generate')) {
    $items['admin/config/development/generate/redirects'] = array(
      'title' => 'Generate redirects',
      'description' => 'Generate a given number of redirects. Optionally delete current redirects.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('redirect_generate_form'),
      'access arguments' => array('administer redirects'),
      'file' => 'redirect.generate.inc',
    );
    $items['admin/config/search/redirect/generate'] = $items['admin/config/development/generate/redirects'];
    $items['admin/config/search/redirect/generate']['type'] = MENU_LOCAL_ACTION;
  }

  return $items;
}

function redirect_set_current_redirect($redirect) {
  $static = &drupal_static(__FUNCTION__);
  $static = $redirect;
}

function redirect_get_current_redirect() {
  $redirect = drupal_static('redirect_set_current_redirect', NULL);

  // If a redirect has not been set with redirect_set_current_redirect(), then
  // attempt to find a redirect matching the current path, query string, and
  // language code.
  if (!isset($redirect)) {
    $redirect = redirect_load_by_source(current_path(), $GLOBALS['language']->language, drupal_get_query_parameters());
  }

  // @todo Add an alter hook here?
  return $redirect;
}

/**
 * Implements hook_url_inbound_alter().
 */
function redirect_url_inbound_alter(&$path, $original_path, $path_language) {
  // If the current path global does not exist, then drupal_get_path_alias()
  // will fail. This condition only happens when $path is the front page.
  // @todo Remove when http://drupal.org/node/1329914 is fixed in core.
  if (empty($_GET['q'])) {
    $_GET['q'] = variable_get('site_frontpage', 'node');
    return;
  }

  // If the inbound path has been changed, then attempt to find a redirect
  // matching the original path and save it for processing later in
  // redirect_init(). For example, if the Sub-pathauto module changes the path
  // 'foo/redirect' to 'node/1/redirect', and there is a redirect enabled for
  // the path 'foo/redirect', then redirect_init() would normally fail since it
  // would not find a match.
  if ($path != $original_path && $original_path == current_path()) {
    $current_langcode = !empty($path_language) ? $path_language : $GLOBALS['language']->language;
    $current_query = drupal_get_query_parameters();
    if ($redirect = redirect_load_by_source($original_path, $current_langcode, $current_query)) {
      redirect_set_current_redirect($redirect);
    }
  }

  // Redirect to canonical URLs.
  if ($path && variable_get('redirect_canonical', 1)) {
    $alias = drupal_get_path_alias($path, $path_language);
    if ($alias != $path && $alias != $original_path) {
      //return redirect_redirect(array('redirect' => $alias, 'type' => 'global'));
    }

    // Redirect from default entity paths to the proper entity path.
    //if ($path_entity = redirect_load_entity_from_path($path)) {
    //  if ($uri = entity_uri($path_entity['entity_type'], $path_entity['entity'])) {
    //    if ($path != $uri['path']) {
    //      return redirect_redirect(array('redirect' => $uri['path'], 'redirect_options' => $uri['options'], 'type' => 'global'));
    //    }
    //  }
    //}
  }
}

/**
 * Implements hook_entity_info_alter().
 */
function redirect_entity_info_alter(&$info) {
  $default_paths = array(
    'node' => 'node/%node',
    'user' => 'user/%user',
    'taxonomy_term' => 'taxonomy/term/%taxonomy_term',
  );

  foreach ($default_paths as $entity_type => $default_path) {
    if (isset($info[$entity_type]) && !isset($info[$entity_type]['default path'])) {
      $info[$entity_type]['default path'] = $default_path;
    }
  }

  // Disable support for some entity types that cause problems.
  $unsupported_entity_types = array(
    'comment',
    'media',
  );
  foreach ($unsupported_entity_types as $unsupported_entity_type) {
    if (isset($info[$unsupported_entity_type])) {
      $info[$unsupported_entity_type]['redirect'] = FALSE;
    }
  }
}

/**
 * Check if an entity type supports redirects.
 *
 * @param $entity_type
 *   An entity type.
 *
 * @return
 *   TRUE if the entity type has an uri callback and supports redirects, or
 *   FALSE otherwise.
 */
function redirect_entity_type_supports_redirects($entity_type) {
  $types = &drupal_static(__FUNCTION__);

  if (!isset($types)) {
    $types = array();
    foreach (entity_get_info() as $type => $entity_info) {
      $types[$type] = !empty($entity_info['uri callback']) && (!isset($entity_info['redirect']) || !empty($entity_info['redirect']));
    }
  }

  return isset($types[$entity_type]) ? $types[$entity_type] : FALSE;
}

/**
 * Implements hook_init().
 */
function redirect_init() {
  if (!redirect_can_redirect()) {
    return;
  }

  // Fetch the current redirect.
  if ($redirect = redirect_get_current_redirect()) {
    redirect_redirect($redirect);
  }

  $redirect_global = FALSE;
  $request_uri = $original_uri = ltrim(request_uri(), '/');

  // Redirect from non-clean URLs to clean URLs.
  if (variable_get('redirect_global_clean', 1) && variable_get('clean_url', 0) && strpos($request_uri, '?q=') !== FALSE) {
    //$redirect_global = TRUE;
    //$request_uri = str_replace('?q=', '', $request_uri);
  }

  if (strpos($request_uri, 'index.php') !== FALSE) {
    //$redirect_global = TRUE;
    //$request_uri = str_replace('index.php', '', $request_uri);
  }

  //$request_uri = ltrim($request_uri, '/');
  //$parsed = parse_url($request_uri);

  if ($redirect_global && $request_uri != $original_uri) {
    redirect_redirect(array(/*'redirect' => $request_uri,*/ 'type' => 'global'));
  }
}

/**
 * Implements hook_cron().
 */
function redirect_cron() {
  // Purge inactive self-managed redirects from the database.
  redirect_purge_inactive_redirects();
}

/**
 * Implements hook_exit().
 */
function redirect_exit($destination = NULL) {
  // If the current page is being cached, track it.
  if (drupal_get_http_header('Location') && $rid = drupal_get_http_header('X-Redirect-ID')) {
    // Ensure the database is loaded. This is only the next bootstrap step
    // after DRUPAL_BOOTSTRAP_DATABASE
    drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
    db_update('redirect')
      ->fields(array('access' => REQUEST_TIME))
      ->expression('count', 'count + 1')
      ->condition('rid', $rid)
      ->execute();
  }
}

/**
 * Implements hook_entity_delete().
 */
function redirect_entity_delete($entity, $entity_type) {
  if (redirect_entity_type_supports_redirects($entity_type)) {
    redirect_delete_by_entity_path($entity_type, $entity);
  }
}

/**
 * Implements hook_path_update().
 */
function redirect_path_update(array $path) {
  if (!variable_get('redirect_auto_redirect', TRUE)) {
    return;
  }
  elseif (isset($path['redirect']) && !$path['redirect']) {
    return;
  }

  if (!empty($path['original']['pid']) && $path['original']['pid'] == $path['pid'] && $path['original']['alias'] != $path['alias']) {
    $redirect = new stdClass();
    redirect_object_prepare($redirect);
    $redirect->source = $path['original']['alias'];
    $redirect->redirect = $path['source'];
    $redirect->language = $path['original']['language'];
    // Check if the redirect exists before saving.
    $hash = redirect_hash($redirect);
    if (!redirect_load_by_hash($hash)) {
      redirect_save($redirect);
    }
  }
}

/**
 * Implements hook_path_delete().
 */
function redirect_path_delete($path) {
  if (!variable_get('redirect_auto_redirect', TRUE)) {
    return;
  }
  elseif (isset($path['redirect']) && !$path['redirect']) {
    return;
  }
  elseif (empty($path)) {
    // @todo Remove this condition and allow $path to use an array type hint
    // when http://drupal.org/node/1025904 is fixed.
    return;
  }

  // Redirect from a deleted alias to the system path.
  //if (!redirect_load_by_source($path['alias'], $path['language'])) {
  //  $redirect = new stdClass();
  //  redirect_object_prepare($redirect);
  //  $redirect->source = $path['alias'];
  //  $redirect->redirect = $path['source'];
  //  $redirect->language = $path['language'];
  //  redirect_save($redirect);
  //}
}

/**
 * Implements hook_views_api().
 */
function redirect_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'redirect') . '/views',
  );
}

/**
 * Implements hook_page_build().
 *
 * Adds an action on 404 pages to create a redirect.
 */
function redirect_page_build(&$page) {
  if (redirect_is_current_page_404() && user_access('administer redirects')) {
    if (!isset($page['content']['system_main']['actions'])) {
      $page['content']['system_main']['actions'] = array(
        '#theme' => 'links',
        '#links' => array(),
        '#attributes' => array('class' => array('action-links')),
        '#weight' => -100,
      );
    }
    // We cannot simply use current_path() because if a 404 path is set, then
    // that value overrides whatever is in $_GET['q']. The
    // drupal_deliver_html_page() function thankfully puts the original current
    // path into $_GET['destination'].
    $destination = drupal_get_destination();
    $page['content']['system_main']['actions']['#links']['add_redirect'] = array(
      'title' => t('Add URL redirect from this page to another location'),
      'href' => 'admin/config/search/redirect/add',
      'query' => array('source' => $destination['destination']) + drupal_get_destination(),
    );
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds support for creating redirects if a node URL alias is changed.
 */
function redirect_form_node_form_alter(&$form, $form_state) {
  if (!empty($form['path']['pid']['#value']) && !isset($form['path']['original'])) {
    $form['path']['original'] = array('#type' => 'value', '#value' => path_load($form['path']['pid']['#value']));
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds support for creating redirects if a taxonomy term URL alias is changed.
 */
function redirect_form_taxonomy_form_term_alter(&$form, $form_state) {
  if (!empty($form['path']['pid']['#value']) && !isset($form['path']['original'])) {
    $form['path']['original'] = array('#type' => 'value', '#value' => path_load($form['path']['pid']['#value']));
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds support for creating redirects if an URL alias is changed.
 */
function redirect_form_path_admin_form_alter(&$form, $form_state) {
  if (!empty($form['pid']['#value']) && !isset($form['original'])) {
    $form['original'] = array('#type' => 'value', '#value' => path_load($form['pid']['#value']));
  }
}

/**
 * Load an URL redirect from the database.
 *
 * @param $rid
 *   The URL redirect ID.
 * @param $reset
 *   Whether to reset the redirect_load_multiple cache.
 *
 * @return
 *   An URL redirect object, or FALSE if loading failed.
 *
 * @ingroup redirect_api
 */
function redirect_load($rid, $reset = FALSE) {
  $redirects = entity_load('redirect', array($rid), array(), $reset);
  return !empty($redirects) ? reset($redirects) : FALSE;
}

/**
 * Load an URL redirect from the database by {redirect}.hash.
 *
 * @param $hash
 *   The hash of the URL redirect.
 * @param $reset
 *   Whether to reset the redirect_load_multiple cache.
 *
 * @return
 *   An URL redirect object, or FALSE if loading failed.
 *
 * @ingroup redirect_api
 */
function redirect_load_by_hash($hash, $reset = FALSE) {
  $redirects = entity_load('redirect', FALSE, array('hash' => $hash), $reset);
  return !empty($redirects) ? reset($redirects) : FALSE;
}

/**
 * Load multiple URL redirects from the database by {redirect}.source.
 *
 * @param $source
 *   The source of the URL redirect.
 *
 * @return
 *   An array of URL redirect objects indexed by redirect IDs.
 *
 * @see redirect_load_multiple()
 * @see _redirect_uasort()
 * @see redirect_compare_array_recursive()
 *
 * @ingroup redirect_api
 */
function redirect_load_by_source($source, $language = LANGUAGE_NONE, array $query = array()) {
  // Run a case-insensitive query for matching RIDs first.
  $rid_query = db_select('redirect');
  $rid_query->addField('redirect', 'rid');
  if ($source != variable_get('site_frontpage', 'node')) {
    $rid_query->condition('source', db_like($source), 'LIKE');
  }
  else {
    $source_condition = db_or();
    $source_condition->condition('source', db_like($source), 'LIKE');
    $source_condition->condition('source', '');
    $rid_query->condition($source_condition);
  }
  $rid_query->condition('language', array($language, LANGUAGE_NONE));
  $rids = $rid_query->execute()->fetchCol();

  if ($rids && $redirects = redirect_load_multiple($rids)) {
    // Narrow down the list of candidates.
    foreach ($redirects as $rid => $redirect) {
      if (!empty($redirect->source_options['query'])) {
        if (empty($query) || !redirect_compare_array_recursive($redirect->source_options['query'], $query)) {
          unset($redirects[$rid]);
          continue;
        }
      }

      // Add a case sensitive matches condition to be used in sorting.
      if ($source !== $redirect->source) {
        $redirects[$rid]->weight = 1;
      }
    }

    if (!empty($redirects)) {
      // Sort the redirects in the proper order.
      uasort($redirects, '_redirect_uasort');

      // Allow other modules to alter the redirect candidates before selecting the top one.
      $context = array('language' => $language, 'query' => $query);
      drupal_alter('redirect_load_by_source', $redirects, $source, $context);

      return !empty($redirects) ? reset($redirects) : FALSE;
    }
  }

  return FALSE;
}

/**
 * Load multiple URL redirects from the database.
 *
 * @param $rids
 *   An array of redirect IDs.
 * @param $conditions
 *   An array of conditions on the {redirect} table in the form 'field' =>
 *   $value.
 * @param $reset
 *   Whether to reset the redirect_load_multiple cache.
 *
 * @return
 *   An array of URL redirect objects indexed by redirect IDs.
 *
 * @ingroup redirect_api
 */
function redirect_load_multiple($rids = array(), array $conditions = array(), $reset = FALSE) {
  return entity_load('redirect', $rids, $conditions, $reset);
}

/**
 * Determine whether the current user may perform the given operation on the
 * specified redirect.
 *
 * @param $op
 *   The operation to be performed on the redirect. Possible values are:
 *   - "create"
 *   - "update"
 *   - "delete"
 * @param $redirect
 *   The redirect object on which the operation is to be performed, or redirect
 *   type (e.g. 'feedburner') for the "create" operation.
 * @param $account
 *   Optional, a user object representing the user for whom the operation is to
 *   be performed. Determines access for a user other than the current user.
 *
 * @return
 *   TRUE if the operation may be performed, FALSE otherwise.
 */
function redirect_access($op, $redirect, $account = NULL) {
  global $user;

  $rights = &drupal_static(__FUNCTION__, array());

  if (!$redirect || !in_array($op, array('create', 'update', 'delete'), TRUE)) {
    // If there was no redirect to check against, or the $op was not one of the
    // supported ones, we return access denied.
    return FALSE;
  }
  // If no user object is supplied, the access check is for the current user.
  if (empty($account)) {
    $account = $user;
  }

  $cid = isset($redirect->rid) ? $redirect->rid : $redirect;

  // Return cached value if access already checked for this redirect, user and op.
  if (isset($rights[$account->uid][$cid][$op])) {
    return $rights[$account->uid][$cid][$op];
  }

  // Administrators can access all redirects.
  if (user_access('administer redirects', $account)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;
  }

  // We grant access to the redirect if both of the following conditions are met:
  // - No modules say to deny access.
  // - At least one module says to grant access.
  $access = module_invoke_all('redirect_access', $op, $redirect, $account);
  if (in_array(REDIRECT_ACCESS_DENY, $access, TRUE)) {
    $rights[$account->uid][$cid][$op] = FALSE;
    return FALSE;
  }
  elseif (in_array(REDIRECT_ACCESS_ALLOW, $access, TRUE)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;
  }

  return FALSE;
}

/**
 * Validate a redirect.
 */
function redirect_validate($redirect, $form, &$form_state) {
  $redirect = (object) $redirect;

  // check that there there are no redirect loops
  if (url($redirect->source) == url($redirect->redirect)) {
    form_set_error('redirect', t('You are attempting to redirect the page to itself. This will result in an infinite loop.'));
  }

  redirect_hash($redirect);
  if ($existing = redirect_load_by_hash($redirect->hash)) {
    if ($redirect->rid != $existing->rid) {
      form_set_error('source', t('The source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?', array('%source' => redirect_url($redirect->source, $redirect->source_options), '@edit-page' => url('admin/config/search/redirect/edit/'. $existing->rid))));
    }
  }

  // Allow other modules to validate the SSH public key.
  foreach (module_implements('redirect_validate') as $module) {
    $function = $module . '_redirect_validate';
    $function($redirect, $form, $form_state);
  }
}

function redirect_object_prepare($redirect, $defaults = array()) {
  $defaults += array(
    'rid' => NULL,
    'type' => 'redirect',
    'uid' => $GLOBALS['user']->uid,
    'source_options' => array(),
    'redirect_options' => array(),
    'language' => LANGUAGE_NONE,
    'status_code' => 0,
    'count' => 0,
    'access' => 0,
    'hash' => '',
  );

  foreach ($defaults as $key => $default) {
    if (!isset($redirect->{$key})) {
      $redirect->{$key} = $default;
    }
  }

  module_invoke_all('redirect_prepare', $redirect);
}

/**
 * Save an URL redirect.
 *
 * @param $redirect
 *   The URL redirect object to be saved. If $redirect->rid is omitted (or
 *   $redirect->is_new is TRUE), a new redirect will be added.
 *
 * @ingroup redirect_api
 */
function redirect_save($redirect) {
  $transaction = db_transaction();

  try {
    if (!empty($redirect->rid) && !isset($redirect->original)) {
      $redirect->original = entity_load_unchanged('redirect', $redirect->rid);
    }

    // Determine if we will be inserting a new node.
    if (!isset($redirect->is_new)) {
      $redirect->is_new = empty($redirect->rid);
    }

    // The changed timestamp is always updated for bookkeeping purposes.
    //$redirect->changed = time();

    redirect_hash($redirect);
    if ($redirect->is_new || $redirect->hash != $redirect->original->hash) {
      // Only new or changed redirects reset the last used value.
      $redirect->count = 0;
      $redirect->access = 0;
    }

    // Allow other modules to alter the redirect before saving.
    module_invoke_all('redirect_presave', $redirect);
    module_invoke_all('entity_presave', $redirect, 'redirect');

    // Save the redirect to the database and invoke the post-save hooks.
    if ($redirect->is_new) {
      drupal_write_record('redirect', $redirect);
      module_invoke_all('redirect_insert', $redirect);
      module_invoke_all('entity_insert', $redirect, 'redirect');
    }
    else {
      drupal_write_record('redirect', $redirect, array('rid'));
      module_invoke_all('redirect_update', $redirect);
      module_invoke_all('entity_update', $redirect, 'redirect');
    }

    // Clear internal properties.
    unset($redirect->is_new);
    unset($redirect->original);

    // Clear the static loading cache.
    entity_get_controller('redirect')->resetCache(array($redirect->rid));

    // Ignore slave server temporarily to give time for the
    // saved node to be propagated to the slave.
    db_ignore_slave();
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('redirect', $e);
    throw $e;
  }
}

/**
 * Implements hook_redirect_insert().
 */
function redirect_redirect_insert($redirect) {
  redirect_page_cache_clear($redirect);
}

/**
 * Implements hook_redirect_update().
 */
function redirect_redirect_update($redirect) {
  redirect_page_cache_clear($redirect);

  // Clear the page cache for the original redirect as well.
  if (!empty($redirect->original) && $redirect->original->source != $redirect->source) {
    redirect_page_cache_clear($redirect->original);
  }
}

/**
 * Implements hook_redirect_delete().
 */
function redirect_redirect_delete($redirect) {
  redirect_page_cache_clear($redirect);
}

/**
 * Delete a single URL redirect.
 *
 * @param $rid
 *   The ID of the redirect to delete.
 *
 * @ingroup redirect_api
 */
function redirect_delete($rid) {
  return redirect_delete_multiple(array($rid));
}

/**
 * Delete any redirects associated with a path or any of its sub-paths.
 *
 * Given a source like 'node/1' this function will delete any redirects that
 * have that specific source or any sources that match 'node/1/%'.
 *
 * @param $path
 *   An string with an internal Drupal path.
 *
 * @ingroup redirect_api
 */
function redirect_delete_by_path($path) {
  $query = db_select('redirect');
  $query->addField('redirect', 'rid');
  $query_or = db_or();
  $query_or->condition('source', db_like($path), 'LIKE');
  $query_or->condition('source', db_like($path . '/') . '%', 'LIKE');
  $query_or->condition('redirect', db_like($path), 'LIKE');
  $query_or->condition('redirect', db_like($path . '/') . '%', 'LIKE');
  $query->condition($query_or);
  $rids = $query->execute()->fetchCol();

  if ($rids) {
    return redirect_delete_multiple($rids);
  }
}

/**
 * Delete an entity URL alias and any of its sub-paths.
 *
 * This function also checks to see if the default entity URI is different from
 * the current entity URI and will delete any of the default aliases.
 *
 * @param $entity_type
 *   A string with the entity type.
 * @param $entity
 *   An entity object.
 *
 * @ingroup redirect_api
 */
function redirect_delete_by_entity_path($entity_type, $entity) {
  if ($uri = entity_uri($entity_type, $entity)) {
    redirect_delete_by_path($uri['path']);
  }

  //$info = entity_get_info($entity_type);
  //if (isset($info['default path'])) {
  //  list($id) = entity_extract_ids($entity_type, $entity);
  //  $default_path = str_replace('[id]', $id, $info['default path']);
  //  if ($uri['path'] !== $default_path) {
  //    redirect_delete_by_path($default_path);
  //  }
  //}
}

/**
 * Delete multiple URL redirects.
 *
 * @param $rids
 *   An array of redirect IDs to delete.
 *
 * @ingroup redirect_api
 */
function redirect_delete_multiple(array $rids) {
  $transaction = db_transaction();
  if (!empty($rids)) {
    $redirects = redirect_load_multiple($rids);

    try {
      // Let modules react to the individual redirects being deleted.
      foreach ($redirects as $rid => $redirect) {
        module_invoke_all('redirect_delete', $redirect);
        module_invoke_all('entity_delete', $redirect, 'redirect');
      }

      db_delete('redirect')
        ->condition('rid', $rids, 'IN')
        ->execute();
    }
    catch (Exception $e) {
      $transaction->rollback();
      watchdog_exception('redirect', $e);
      throw $e;
    }

    // Clear the redirect_load_multiple cache.
    entity_get_controller('redirect')->resetCache();
  }
}

/**
 * Purge inactive redirects from the database.
 *
 * @param $types
 *   An array of redirect types to remove. Default is only the self-managed
 *   'redirect'. If not provided all redirect types will be eligible for
 *   removal.
 * @param $interval
 *   The number of seconds to subtract from the current time and used to
 *   find the inactive redirects.
 *
 * @return
 *   An array of redirect IDs that were deleted or FALSE if none were.
 */
function redirect_purge_inactive_redirects(array $types = array('redirect'), $interval = NULL) {
  if (!isset($interval)) {
    $interval = variable_get('redirect_purge_inactive', 0);
  }

  if (!$interval || !variable_get('redirect_page_cache', 0) || !variable_get('page_cache_invoke_hooks', TRUE)) {
    // If serving redirects from the page cache is enabled and hooks are not
    // executed during page caching, then we cannot track when a redirect is
    // used. Therefore, we cannot remove unused redirects.
    return FALSE;
  }

  $query = db_select('redirect');
  $query->addField('redirect', 'rid');
  if (!empty($types)) {
    $query->condition('type', $types);
  }
  $query->condition('access', REQUEST_TIME - $interval, '<');
  $query->addTag('redirect_purge');
  $rids = $query->execute()->fetchCol();

  if (count($rids)) {
    redirect_delete_multiple($rids);
    watchdog('redirect', format_plural(count($rids), 'Removed 1 inactive redirect from the database.', 'Removed @count inactive redirects from the database.'));
    return $rids;
  }
}

/**
 * Perform an URL redirect.
 *
 * @param $redirect
 *   An optional URL redirect array.
 *
 * @ingroup redirect_api
 */
function redirect_redirect($redirect = NULL) {
  // First check if we're in an infinite loop.
  $session_id = session_id();
  if (flood_is_allowed('redirection', 5, 15, $session_id ? $session_id : NULL)) {
    flood_register_event('redirection', 60, $session_id ? $session_id : NULL);
  }
  else {
    watchdog('redirect', 'Infinite loop stopped.');
    drupal_set_message('Oops, looks like this request tried to create an infinite loop. We do not allow such things here. We are a professional website!');
    return FALSE;
  }

  if (!isset($redirect)) {
    $redirect = new stdClass();
  }
  redirect_object_prepare($redirect, array('redirect' => current_path(), 'type' => 'manual', 'callback' => 'redirect_goto', 'cache' => TRUE));

  if (empty($redirect->status_code)) {
    $redirect->status_code = variable_get('redirect_default_status_code', 301);
  }

  if (variable_get('redirect_passthrough_querystring', 1)) {
    // Preserve the current query parameters in the redirect.
    $redirect->redirect_options += array('query' => array());
    $redirect->redirect_options['query'] += drupal_get_query_parameters();
  }

  // Prevent the destination query parameter from overriding this redirect.
  //if (isset($_GET['destination'])) {
    // Simply unset the parameter since it has already been passed into
    // $options['query'] in the previous code.
  //  unset($_GET['destination']);
  //}

  // Allow other modules to alter the redirect before passing to drupal_goto().
  drupal_alter('redirect', $redirect);

  // Continue if the redirect has not been disabled by hook_redirect_alter().
  if (isset($redirect->redirect) && isset($redirect->callback) && $redirect->redirect !== FALSE && function_exists($redirect->callback)) {
    // Perform the actual redirect.
    $callback = $redirect->callback;
    $callback($redirect);
  }
}

/**
 * Redirect callback; perform an URL redirect.
 */
function redirect_goto($redirect) {
  $redirect->redirect_options['absolute'] = TRUE;
  $url = url($redirect->redirect, $redirect->redirect_options);
  drupal_add_http_header('Location', $url);
  drupal_add_http_header('Status', redirect_status_code_options($redirect->status_code));

  if (!empty($redirect->rid)) {
    // Add a custom header for the redirect ID so when the redirect is served
    // from the page cache, we can track it.
    drupal_add_http_header('X-Redirect-ID', $redirect->rid);
  }

  if (!variable_get('redirect_page_cache', 0) || !variable_get('cache', 0) || !drupal_page_is_cacheable() || empty($redirect->cache)) {
    drupal_exit($url);
  }

  // @see drupal_exit()
  if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
    if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
      module_invoke_all('exit', $url);
    }
    drupal_session_commit();
    if (variable_get('cache', 0)) {
      // We must output something to allow the request to be cached.
      echo ' ';
      if ($cache = drupal_page_set_cache()) {
        // When caching this redirect for the first time we still need to ensure
        // that the correct cache headers are sent.
        // @see drupal_page_footer()
        drupal_serve_page_from_cache($cache);
      }
    }
  }

  exit;
}

function redirect_hash($redirect) {
  $hash = array(
    'source' => $redirect->source,
    'language' => $redirect->language,
  );
  if (!empty($redirect->source_options['query'])) {
    $hash['source_query'] = $redirect->source_options['query'];
  }
  drupal_alter('redirect_hash', $hash, $redirect);
  redirect_sort_recursive($hash, 'ksort');
  $redirect->hash = drupal_hash_base64(serialize($hash));
  return $redirect->hash;
}

/**
 * Clear a page from the page cache.
 */
function redirect_page_cache_clear($redirect = NULL) {
  if (!variable_get('redirect_page_cache', 0)) {
    return;
  }

  if (isset($redirect)) {
    $path = url($redirect->source, array('absolute' => TRUE));
    // Use a wildcard to catch paths with query strings.
    cache_clear_all($path, 'cache_page', TRUE);
  }
  else {
    // Clear the entire page cache.
    cache_clear_all('*', 'cache_page', TRUE);
  }
}

/**
 * Given a path determine if it is an entity default path.
 *
 * @param $path
 *   The internal path. The id of the entity should be in the string as '[id]'.
 * @return
 *   An array with the entity type and the loaded entity object.
 */
function redirect_load_entity_from_path($path) {
  $entity_paths = &drupal_static(__FUNCTION__);

  if (!isset($entity_paths)) {
    $entity_paths = array();
    foreach (entity_get_info() as $entity_type => $entity_info) {
      if (isset($entity_info['default path'])) {
        $default_path = $entity_info['default path'];
        $default_path = preg_quote($default_path, '/');
        $default_path = str_replace(preg_quote('%' . $entity_type, '/'), '(\d+)', $default_path);
        $entity_paths[$entity_type] = $default_path;
      }
    }
  }

  foreach ($entity_paths as $entity_type => $default_path) {
    if (preg_match("/^{$default_path}$/", $path, $matches)) {
      if ($entity = entity_load($entity_type, array($matches[1]))) {
        return array('entity_type' => $entity_type, 'entity' => reset($entity));
      }
      break;
    }
  }
}

/**
 * Check the ability to perform redirects with the current request context.
 *
 * This function checks the following conditions:
 * - If the PHP entry point is the root index.php file.
 * - If PHP is not running as CLI.
 * - If the site is not offline or in install/update mode.
 * - If the curerent page is not an admin page (check can be disabled).
 * - If the current request does not have any POST data since a redirect
 *   may interrupt form submission.
 *
 * @return
 *   TRUE if redirections can be performed, or FALSE otherwise.
 */
function redirect_can_redirect() {
  $can_redirect = &drupal_static(__FUNCTION__);

  if (!isset($can_redirect)) {
    $path = current_path();
    $can_redirect = TRUE;

    if ($_SERVER['SCRIPT_NAME'] != $GLOBALS['base_path'] . 'index.php') {
      // Do not redirect if the root script is not /index.php.
      $can_redirect = FALSE;
    }
    elseif (!empty($_POST)) {
      // Do not redirect if this is a post request with data.
      $can_redirect = FALSE;
    }
    elseif (drupal_is_cli()) {
      // If this is a command line request (Drush, etc), skip processing.
      $can_redirect = FALSE;
    }
    elseif (variable_get('maintenance_mode', 0) || defined('MAINTENANCE_MODE')) {
      // Do not redirect in offline or maintenance mode.
      $can_redirect = FALSE;
    }
    elseif (!variable_get('redirect_global_admin_paths', 0) && path_is_admin($path)) {
      // Do not redirect on admin paths.
      $can_redirect = FALSE;
    }
  }

  return $can_redirect;
}

/**
 * Compare tha all values and associations in one array match another array.
 *
 * We cannot use array_diff_assoc() here because we need to be recursive.
 *
 * @param $match
 *   The array that has the values.
 * @param $haystack
 *   The array that will be searched for values.
 * @return
 *   TRUE if all the elements of $match were found in $haystack, or FALSE
 *   otherwise.
 */
function redirect_compare_array_recursive($match, $haystack) {
  foreach ($match as $key => $value) {
    if (!array_key_exists($key, $haystack)) {
      return FALSE;
    }
    elseif (is_array($value)) {
      if (!is_array($haystack[$key])) {
        return FALSE;
      }
      elseif (!redirect_compare_array_recursive($value, $haystack[$key])) {
        return FALSE;
      }
    }
    elseif ($value != $haystack[$key]) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Sort an array recusively.
 *
 * @param $array
 *   The array to sort, by reference.
 * @param $callback
 *   The sorting callback to use (e.g. 'sort', 'ksort', 'asort').
 *
 * @return
 *   TRUE on success or FALSE on failure.
 */
function redirect_sort_recursive(&$array, $callback = 'sort') {
  $result = $callback($array);
  foreach ($array as $key => $value) {
    if (is_array($value)) {
      $result &= redirect_sort_recursive($array[$key], $callback);
    }
  }
  return $result;
}

/**
 * Load a language object by its language code.
 *
 * @todo Remove when http://drupal.org/node/660736 is fixed in Drupal core.
 *
 * @param $language
 *   A language code. If not provided the default language will be returned.
 * @return
 *   A language object.
 */
function redirect_language_load($language = LANGUAGE_NONE) {
  $languages = &drupal_static(__FUNCTION__);

  if (!isset($languages)) {
    $languages = language_list();
    $languages[LANGUAGE_NONE] = NULL;
  }

  return isset($languages[$language]) ? $languages[$language] : NULL;
}

/**
 * Build the URL of a redirect for display purposes only.
 */
function redirect_url($path, array $options = array(), $clean_url = NULL) {
  if (!isset($clean_url)) {
    $clean_url = variable_get('clean_url', 0);
  }

  if ($path == '') {
    $path = '<front>';
  }

  if (!isset($options['alter']) || !empty($options['alter'])) {
    drupal_alter('redirect_url', $path, $options);
  }

  // The base_url might be rewritten from the language rewrite in domain mode.
  if (!isset($options['base_url'])) {
    if (isset($options['https']) && variable_get('https', FALSE)) {
      if ($options['https'] === TRUE) {
        $options['base_url'] = $GLOBALS['base_secure_url'];
        $options['absolute'] = TRUE;
      }
      elseif ($options['https'] === FALSE) {
        $options['base_url'] = $GLOBALS['base_insecure_url'];
        $options['absolute'] = TRUE;
      }
    }
    else {
      $options['base_url'] = $GLOBALS['base_url'];
    }
  }

  if (empty($options['absolute']) || url_is_external($path)) {
    $url = $path;
  }
  else {
    $url = $options['base_url'] . base_path() . $path;
  }

  if (isset($options['query'])) {
    $url .= $clean_url ? '?' : '&';
    $url .= drupal_http_build_query($options['query']);
  }
  if (isset($options['fragment'])) {
    $url .= '#' . $options['fragment'];
  }

  return $url;
}

function redirect_variables() {
  return array(
    'redirect_default_status_code' => 301,
    'redirect_auto_redirect' => TRUE,
    'redirect_warning' => FALSE,
    'redirect_passthrough_querystring' => 1,
    'redirect_page_cache' => 0,
    'redirect_purge_inactive' => 0,
    'redirect_global_home' => 1,
    'redirect_global_clean' => 1,
    'redirect_global_canonical' => 1,
    'redirect_global_admin_paths' => 0,
  );
}

//function redirect_get_redirect_info() {
//  $info = &drupal_static(__FUNCTION__);
//
//  if (!isset($info)) {
//    if ($cache = cache_get('redirect:info')) {
//      $info = $cache->data;
//    }
//    else {
//      $info = module_invoke_all('redirect_info');
//      drupal_alter('redirect_info', $info);
//      cache_set('redirect:info', $info);
//    }
//  }
//
//  return $info;
//}

function redirect_parse_url($url) {
  $original_url = $url;
  $url = trim($url, " \t\n\r\0\x0B\/");
  $parsed = parse_url($url);

  if (isset($parsed['fragment'])) {
    $url = substr($url, 0, -strlen($parsed['fragment']));
    $url = trim($url, '#');
  }
  if (isset($parsed['query'])) {
    $url = substr($url, 0, -strlen($parsed['query']));
    $url = trim($url, '?&');
    $parsed['query'] = drupal_get_query_array($parsed['query']);
  }

  // Convert absolute to relative.
  if (isset($parsed['scheme']) && isset($parsed['host'])) {
    $base_secure_url = rtrim($GLOBALS['base_secure_url'] . base_path(), '/');
    $base_insecure_url = rtrim($GLOBALS['base_insecure_url'] . base_path(), '/');
    if (strpos($url, $base_secure_url) === 0) {
      $url = str_replace($base_secure_url, '', $url);
      $parsed['https'] = TRUE;
    }
    elseif (strpos($url, $base_insecure_url) === 0) {
      $url = str_replace($base_insecure_url, '', $url);
    }
  }

  $url = trim($url, '/');

  // Convert to frontpage paths.
  if ($url == '<front>') {
    $url = '';
  }

  //$parsed['url'] = http_build_query($url, HTTP_URL_STRIP_QUERY | HTTP_URL_STRIP_FRAGMENT);
  $parsed['url'] = $url;

  // Allow modules to alter the parsed URL.
  drupal_alter('redirect_parse_url', $parsed, $original_url);

  return $parsed;
}

function redirect_status_code_options($code = NULL) {
  $codes = array(
    300 => t('300 Multiple Choices'),
    301 => t('301 Moved Permanently'),
    302 => t('302 Found'),
    303 => t('303 See Other'),
    304 => t('304 Not Modified'),
    305 => t('305 Use Proxy'),
    307 => t('307 Temporary Redirect'),
  );
  return isset($codes[$code]) ? $codes[$code] : $codes;
}

/**
 * Returns if the current page request is a page not found (404 status error).
 *
 * Why the fuck do we have to do this? Why is there not an easier way???
 *
 * @return
 *   TRUE if the current page is a 404, or FALSE otherwise.
 */
function redirect_is_current_page_404() {
  return drupal_get_http_header('Status') == '404 Not Found';
}

/**
 * uasort callback; Compare redirects based on language neutrality and rids.
 */
function _redirect_uasort($a, $b) {
  $a_weight = isset($a->weight) ? $a->weight : 0;
  $b_weight = isset($b->weight) ? $b->weight : 0;
  if ($a_weight != $b_weight) {
    // First sort by weight (case sensitivity).
    return $a_weight > $b_weight;
  }
  elseif ($a->language != $b->language) {
    // Then sort by language specific over language neutral.
    return $a->language == LANGUAGE_NONE;
  }
  elseif (!empty($a->source_options['query']) != !empty($b->source_options['query'])) {
    // Then sort by redirects that do not have query strings over ones that do.
    return empty($a->source_options['query']);
  }
  else {
    // Lastly sort by the highest redirect ID.
    return $a->rid < $b->rid;
  }
}

/**
 * Implements hook_form_FORM_ID_alter() on behalf of locale.module.
 */
function locale_form_redirect_edit_form_alter(&$form, &$form_state) {
  $form['language'] = array(
    '#type' => 'select',
    '#title' => t('Language'),
    '#options' => array(LANGUAGE_NONE => t('All languages')) + locale_language_list('name'),
    '#default_value' => $form['language']['#value'],
    '#description' => t('A redirect set for a specific language will always be used when requesting this page in that language, and takes precedence over redirects set for <em>All languages</em>.'),
  );
}

/**
 * Implements hook_field_attach_form().
 *
 * @todo Investigate using hook_entity_load() to load all entity redirects.
 * @todo Figure out how to support entity URIs that contain query strings.
 */
function redirect_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
  list($id) = entity_extract_ids($entity_type, $entity);
  if (!empty($form['redirect']) || empty($id)) {
    return;
  }

  // Check if this entity type supports redirects.
  if (!redirect_entity_type_supports_redirects($entity_type)) {
    return;
  }

  $uri = entity_uri($entity_type, $entity);
  if (empty($uri)) {
    // If the entity has no source path, then we cannot lookup the existing
    // redirects.
    return;
  }

  $info = entity_get_info($entity_type);
  $form['redirect'] = array(
    '#type' => 'fieldset',
    '#title' => t('URL redirects'),
    '#description' => t('The following are a list of URL redirects that point to this @entitytype.', array('@entitytype' => drupal_strtolower($info['label']))),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#access' => user_access('administer redirects'),
    '#weight' => 30,
    '#attributes' => array('class' => array('redirect-list')),
  );

  // Only support vertical tabs if there is a vertical tab element.
  foreach (element_children($form) as $key) {
    if (isset($form[$key]['#type']) && $form[$key]['#type'] == 'vertical_tabs') {
      $form['redirect']['#group'] = $key;
      $form['redirect']['#attached']['js']['vertical-tabs'] = drupal_get_path('module', 'redirect') . '/redirect.js';
    }
  }

  // We don't have to put our include in $form_state['build_info']['files']
  // since the build array will already be cached.
  module_load_include('inc', 'redirect', 'redirect.admin');
  $redirects = redirect_load_multiple(FALSE, array('redirect' => $uri['path']));
  $header = array('source', 'status_code', 'language', 'count', 'access', 'operations');
  $form['redirect'] += redirect_list_table($redirects, $header);

  $redirect = array(
    'redirect' => $uri['path'],
    'redirect_options' => array_diff_key($uri['options'], array('entity_type' => '', 'entity' => '')),
    'language' => $langcode,
  );

  $form['redirect']['actions'] = array(
    '#theme' => 'links',
    '#links' => array(),
    '#attributes' => array('class' => array('action-links')),
  );
  if (redirect_access('create', 'redirect')) {
    $form['redirect']['actions']['#links']['add'] = array(
      'title' => t('Add URL redirect to this @entitytype', array('@entitytype' => drupal_strtolower($info['label']))),
      'href' => 'admin/config/search/redirect/add',
      'query' => array_filter($redirect) + drupal_get_destination(),
    );
  }
}

/**
 * Implements hook_field_extra_fields().
 */
function redirect_field_extra_fields() {
  $entity_info = entity_get_info();
  foreach (array_keys($entity_info) as $entity_type) {
    if ($entity_type == 'comment') {
      // The comment entity type supports URIs, but they're not real.
      continue;
    }
    foreach (array_keys($entity_info[$entity_type]['bundles']) as $bundle) {
      if (!isset($entity_info[$entity_type]['bundles'][$bundle]['uri callback']) && !isset($entity_info[$entity_type]['uri callback'])) {
        // The bundle or base entity must have an URI callback defined otherwise
        // we cannot use the entity_uri() function to lookup the entity's source
        // path.
        continue;
      }
      $info[$entity_type][$bundle]['form']['redirect'] = array(
        'label' => t('URL redirects'),
        'description' => t('Redirect module form elements'),
        'weight' => 30,
      );
    }
  }
  return $info;
}

/**
 * Fetch an array of redirect bulk operations.
 *
 * @see hook_redirect_operations()
 * @see hook_redirect_operations_alter()
 */
function redirect_get_redirect_operations() {
  $operations = &drupal_static(__FUNCTION__);

  if (!isset($operations)) {
    $operations = module_invoke_all('redirect_operations');
    drupal_alter('redirect_operations', $operations);
  }

  return $operations;
}

/**
 * Implements hook_redirect_operations().
 */
function redirect_redirect_operations() {
  $operations['delete'] = array(
    'action' => t('Delete'),
    'action_past' => t('Deleted'),
    'callback' => 'redirect_delete_multiple',
    'confirm' => TRUE,
  );
  return $operations;
}
