You are here

uuid_redirect.module in UUID Redirect 7

Redirects entity URLs to the correct place on this or other sites, based on UUID.

File

uuid_redirect.module
View source
<?php

/**
 * @file
 * Redirects entity URLs to the correct place on this or other sites, based on UUID.
 */

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

/**
 * Implements hook_menu().
 */
function uuid_redirect_menu() {
  $items['admin/config/search/uuid_redirect'] = array(
    'title' => 'URL redirects using universally unique identifiers',
    'description' => 'Redirect users from URLs on this site to corresponding URLs on another site, using universally unique identifiers to match them.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uuid_redirect_settings_form',
    ),
    'access arguments' => array(
      'administer uuid redirects',
    ),
    'file' => 'uuid_redirect.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_module_implements_alter().
 */
function uuid_redirect_module_implements_alter(&$implementations, $hook) {

  // Our hook_menu_alter() implementation needs to run after any others, since
  // we are trying to completely take over certain URLs and redirect them to
  // another site.
  if ($hook == 'menu_alter') {
    $group = $implementations['uuid_redirect'];
    unset($implementations['uuid_redirect']);
    $implementations['uuid_redirect'] = $group;
  }
}

/**
 * Implements hook_menu_alter().
 *
 * Alter any menu callbacks that need to be redirected to an external URL.
 */
function uuid_redirect_menu_alter(&$items) {
  if (variable_get('uuid_redirect_external_base_url') && ($paths = variable_get('uuid_redirect_menu_paths'))) {
    $altered_paths = array();
    foreach (preg_split('/\\s/', $paths) as $path) {

      // Go through each component of the path and look for patterns like
      // "%node" or "%node--article". Extract the information we need, and
      // replace the path components like "%node--article" with the actual
      // component we expect to find in the menu item (e.g., "%node").
      $args = arg(NULL, $path);
      $wildcard_info = array();
      foreach ($args as $index => &$arg) {
        if (preg_match('/^(%(.*?))(?:--(.+))?$/', $arg, $matches)) {
          $arg = $matches[1];

          // We need to cheat a little bit to support node revisions. Those use
          // a named wildcard ("%node") followed by an unnamed one ("%") in
          // which the revision is stored. So we modify the wildcard info for
          // the latter accordingly in that case.
          $wildcard_name = $matches[2];
          if (empty($wildcard_name) && ($previous_wildcard_info = end($wildcard_info))) {
            $load_function = $previous_wildcard_info['load function'];
            $revision = TRUE;
          }
          else {
            $load_function = $wildcard_name . '_load';
            $revision = FALSE;
          }
          $wildcard_info[$index] = array(
            'load function' => $load_function,
            'bundle restrictions' => isset($matches[3]) ? array(
              $matches[3],
            ) : array(),
            'revision' => $revision,
          );
        }
      }
      $path = implode('/', $args);

      // Now modify the corresponding menu item (if there is one) to replace
      // its page callback with one that will perform the redirect. (This is
      // done in a page callback so that only users with access to the URL
      // actually experience the redirect.)
      if (isset($items[$path]) && isset($items[$path]['page callback'])) {

        // If it's the first time we're altering this path, we append two items
        // to the end of the 'page arguments' array. The first stores the
        // original page callback (which we use as a fallback in case the
        // redirect shouldn't happen), and the second stores our wildcard info
        // for this path. (Note that above we skipped modifying the menu item
        // altogether if it did not have an existing page callback; i.e., if it
        // was a default local task. We do not need to support redirecting
        // default local tasks anyway, since the main path can be configured to
        // redirect instead and the default local task will inherit it.)
        if (empty($altered_paths[$path])) {
          $items[$path]['page arguments'][] = $items[$path]['page callback'];
          $items[$path]['page arguments'][] = $wildcard_info;
        }
        else {
          $page_argument_keys = array_keys($items[$path]['page arguments']);
          $wildcard_info_key = array_pop($page_argument_keys);
          foreach ($wildcard_info as $index => $info) {
            $current_bundle_info =& $items[$path]['page arguments'][$wildcard_info_key][$index]['bundle restrictions'];

            // If at any point the path is declared to work for all bundles
            // (e.g., with "%node"), that trumps all bundle restrictions.
            $current_bundle_info = empty($info['bundle restrictions']) || empty($current_bundle_info) ? array() : array_merge($current_bundle_info, $info['bundle restrictions']);
          }
        }

        // Define the custom page callback in which the redirect will occur.
        $items[$path]['page callback'] = 'uuid_redirect_to_external_site';
        $altered_paths[$path] = TRUE;
      }
    }
  }
}

/**
 * Page callback: Redirects to an external URL, after replacing IDs with UUIDs.
 */
function uuid_redirect_to_external_site() {
  $page_arguments = func_get_args();
  $wildcard_info = array_pop($page_arguments);
  $original_page_callback = array_pop($page_arguments);

  // Replace any entity IDs in the current path with UUIDs, and redirect to the
  // external URL.
  $args = arg();
  $item = menu_get_item();
  $skip_redirect = FALSE;
  foreach ($wildcard_info as $index => $info) {
    if (isset($item['map'][$index])) {

      // We need to cheat a little bit to support node revisions. If an entity
      // was not loaded from the wildcard (for example, if the wildcard is "%"
      // rather than a named wildcard), we just keep using the previous loaded
      // entity from earlier in the path.
      if (is_object($item['map'][$index])) {
        $entity = $item['map'][$index];
      }
      $entity_types_by_load_function = _uuid_redirect_entity_types_by_load_function();
      if (isset($entity_types_by_load_function[$info['load function']])) {

        // Allow modules to skip the redirect for this entity; see, for
        // example, uuid_redirect_uuid_redirect_skip_redirect(). For
        // consistency we always invoke the hook for all entities in the path,
        // but if any module indicates that the redirect should be skipped, we
        // honor that regardless of what happens later.
        $entity_type = $entity_types_by_load_function[$info['load function']];
        $responses = module_invoke_all('uuid_redirect_skip_redirect', $entity_type, $entity, $info);
        $skip_redirect = $skip_redirect || in_array(TRUE, $responses, TRUE);

        // Perform the appropriate UUID replacement.
        $uuid_key_type = $info['revision'] ? 'revision uuid' : 'uuid';
        $entity_info = entity_get_info($entity_type);
        if (isset($entity_info['entity keys'][$uuid_key_type])) {
          $uuid_key = $entity_info['entity keys'][$uuid_key_type];
          if (isset($entity->{$uuid_key})) {
            $args[$index] = $entity->{$uuid_key};
          }
        }
      }
    }
  }

  // Only redirect if we have a URL, and if we didn't already decide to skip
  // redirecting above.
  if (!$skip_redirect && ($base_url = variable_get('uuid_redirect_external_base_url'))) {
    $new_path = implode('/', $args);
    $new_url = $base_url . '/' . $new_path;
    $redirect_function = module_exists('overlay') && overlay_get_mode() == 'child' ? 'overlay_close_dialog' : 'drupal_goto';
    unset($_GET['destination']);
    $redirect_function($new_url, array(
      'query' => drupal_get_query_parameters(),
      'external' => TRUE,
    ));
  }
  else {
    return call_user_func_array($original_page_callback, $page_arguments);
  }
}

/**
 * Implements hook_uuid_redirect_skip_redirect().
 */
function uuid_redirect_uuid_redirect_skip_redirect($entity_type, $entity, $menu_info) {

  // Do not redirect if we are only redirecting certain bundles and this isn't
  // one of them.
  if (!empty($menu_info['bundle restrictions'])) {
    $entity_info = entity_get_info($entity_type);
    if (isset($entity_info['entity keys']['bundle'])) {
      $bundle_key = $entity_info['entity keys']['bundle'];
      if (isset($entity->{$bundle_key}) && !in_array($entity->{$bundle_key}, $menu_info['bundle restrictions'])) {
        return TRUE;
      }
    }
  }
}

/**
 * Implements hook_menu_get_item_alter().
 *
 * Replace any UUIDs in the current path with entity IDs, and, if any
 * replacements were made, redirect to the new path on this site.
 *
 * We need to perform the redirects from inside this hook implementation
 * because it is the only place where we always have access to the menu item
 * corresponding to the current requested URL (even in cases where the request
 * is for a path that would normally give a 404 error).
 */
function uuid_redirect_menu_get_item_alter(&$router_item, $path, $original_map) {

  // If the current path's menu item has wildcards in it which are used to load
  // an entity by its ID (e.g., node/%node/edit), check if a UUID was provided
  // for the wildcard instead, and if so, substitute in the ID and redirect to
  // the new URL.
  if ($path == $_GET['q'] && !empty($router_item['load_functions'])) {

    // We can tell which type of entity (if any) the wildcard is supposed to
    // correspond to by looking at the menu item's load functions (e.g.,
    // 'node_load') and seeing if it matches one defined in hook_entity_info().
    $load_functions = unserialize($router_item['load_functions']);
    $entity_types_by_load_function = _uuid_redirect_entity_types_by_load_function();
    $replacements = array();
    foreach ($load_functions as $arg => $load_function) {
      $candidate_args = array(
        $arg,
      );

      // If there were load arguments passed (e.g., in the case of node
      // revisions) we should check those for UUIDs also.
      if (is_array($load_function)) {
        $load_arguments = reset($load_function);
        $candidate_args = array_merge($candidate_args, $load_arguments);
        $load_function = key($load_function);
      }
      if (isset($entity_types_by_load_function[$load_function])) {
        $entity_type = $entity_types_by_load_function[$load_function];
        foreach ($candidate_args as $candidate_arg) {

          // Only perform the replacement if a valid UUID was provided for the
          // wildcard in the URL. Since there's no easy way to distinguish
          // revisions from non-revisions, we just check both each time by
          // calling entity_get_id_by_uuid() twice.
          $candidate_uuid = arg($candidate_arg, $path);
          if (uuid_is_valid($candidate_uuid) && (($ids = entity_get_id_by_uuid($entity_type, array(
            $candidate_uuid,
          ), FALSE)) || ($ids = entity_get_id_by_uuid($entity_type, array(
            $candidate_uuid,
          ), TRUE)))) {
            $replacements[$candidate_arg] = reset($ids);
          }
        }
      }
    }
    if (!empty($replacements)) {
      $args = arg(NULL, $path);
      foreach ($replacements as $arg => $id) {
        $args[$arg] = $id;
      }
      $new_path = implode('/', $args);
      unset($_GET['destination']);
      drupal_goto($new_path, array(
        'query' => drupal_get_query_parameters(),
      ));
    }
  }
}

/**
 * Returns an array of entity types keyed by their load functions.
 */
function _uuid_redirect_entity_types_by_load_function() {
  $entity_types = array();
  foreach (entity_get_info() as $type => $info) {
    $entity_types[$info['load hook']] = $type;
  }

  // Special case for taxonomy terms, since they have an additional menu load
  // function which is not found in hook_entity_info().
  if (isset($entity_types['taxonomy_vocabulary_load']) && !isset($entity_types['taxonomy_vocabulary_machine_name_load'])) {
    $entity_types['taxonomy_vocabulary_machine_name_load'] = $entity_types['taxonomy_vocabulary_load'];
  }
  return $entity_types;
}

Functions

Namesort descending Description
uuid_redirect_menu Implements hook_menu().
uuid_redirect_menu_alter Implements hook_menu_alter().
uuid_redirect_menu_get_item_alter Implements hook_menu_get_item_alter().
uuid_redirect_module_implements_alter Implements hook_module_implements_alter().
uuid_redirect_permission Implements hook_permission().
uuid_redirect_to_external_site Page callback: Redirects to an external URL, after replacing IDs with UUIDs.
uuid_redirect_uuid_redirect_skip_redirect Implements hook_uuid_redirect_skip_redirect().
_uuid_redirect_entity_types_by_load_function Returns an array of entity types keyed by their load functions.