You are here

globalredirect.module in Global Redirect 7

Same filename and directory in other branches
  1. 8 globalredirect.module
  2. 5 globalredirect.module
  3. 6 globalredirect.module

The Global Redirect module redirects for all paths which have aliases but are not using the aliases which reduces the risk of duplicate content.

File

globalredirect.module
View source
<?php

/**
 * @file
 * The Global Redirect module redirects for all paths which have aliases but
 * are not using the aliases which reduces the risk of duplicate content.
 */

/**
 * Implements hook_help().
 */
function globalredirect_help($path, $arg) {
  switch ($path) {
    case 'admin/help#globalredirect':
      return t('This module will do a 301 redirect for all nodes which have an alias but are not using that alias.');
  }
}

/**
 * Implements hook_init().
 */
function globalredirect_init() {
  global $language;
  require_once DRUPAL_ROOT . '/includes/locale.inc';

  /**
   * Get the settings.
   */
  $settings = _globalredirect_get_settings();

  // If GlobalRedirect is inactive (for any of the reasons listed in
  // _globalredirect_is_active()) then skip the rest of the function.
  if (!_globalredirect_is_active($settings)) {
    return FALSE;
  }

  // If menu checking is enabled, do the check. Note: Feature disabled by default.
  if ($settings['menu_check']) {

    // Check the access on the current path, return FALSE if access not
    // allowed. This stops redirection for paths without access permission.
    $item = menu_get_item();
    if (!$item['access']) {
      return FALSE;
    }
  }

  // Get the query string parameters. If none set, set to NULL.
  $query_string = drupal_get_query_parameters();
  if (empty($query_string)) {
    $query_string = NULL;
  }

  // Store the destination from the $_GET as, if we leave it in, drupal_goto()
  // will go to that instead.
  if (isset($_GET['destination'])) {
    $destination = $_GET['destination'];
    unset($_GET['destination']);
  }

  // Establish the language prefix that should be used, ie. the one that
  // drupal_goto() would use
  $options = array(
    'fragment' => '',
    'query' => $query_string,
    'absolute' => FALSE,
    'alias' => FALSE,
    'prefix' => '',
    'external' => FALSE,
    'globalredirect' => TRUE,
  );

  // Get the request path. We cannot use request_path() as it uses a completely trimmed path.
  // We need just left trimmed.
  $request_path = globalredirect_request_path();

  // Allow Drupal to rewrite the URL
  // Most commonly this will be used to invoke locale_language_url_rewrite_url().
  // Via the locale_url_outbound_alter() implementation of hook_url_outbound_alter().
  // NOTE: We use $request_path here as current_path() is $_GET['q'] which gets
  //       altered by core, whereas $request_path is untouched
  drupal_alter('url_outbound', $request_path, $options, $request_path);

  // Extract the prefix from the options.
  $prefix = rtrim($options['prefix'], '/');

  // Do a check if this is a front page
  if (drupal_is_front_page()) {

    // If frontpage redirection is disabled, halt here
    if (!$settings['frontpage_redirect']) {
      return;
    }

    // Redirect if the current request does not refer to the front page in the
    // configured fashion (with or without a prefix)
    if ($request_path != $prefix) {
      drupal_goto('', $options, 301);
    }
    elseif ($settings['nonclean_to_clean'] && (bool) variable_get('clean_url', 0) && (strpos(request_uri(), '?q=') || strpos(request_uri(), 'index.php'))) {
      drupal_goto('', $options, 301);
    }

    // If we've got to this point then we're on a front page with a VALID
    // request path (such as a language-prefix front page such as '/de')
    return;
  }

  // Get the current page - it's already "deslashed"
  $current_path = current_path();

  // Optional stripping of "/0". Disabled by default.
  switch ($settings['trailing_zero']) {
    case 2:

      // If 'taxonomy/term/*' only. If not, break out.
      if (drupal_substr($current_path, 0, 14) != 'taxonomy/term/') {
        break;
      }

    // If it is, fall through to general trailing zero method
    case 1:

      // If last 2 characters of URL are /0 then trim them off
      if (drupal_substr($current_path, -2) == '/0') {
        $current_path = drupal_substr($current_path, 0, -2);
      }
  }

  // If the feature is enabled, check and redirect taxonomy/term/* requests to their proper handler defined by hook_term_path().
  if ($settings['term_path_handler'] && module_exists('taxonomy') && preg_match('`^taxonomy/term/(\\d+)$`', $current_path, $matches)) {

    // So the feature is enabled, as is taxonomy module and the current request is a taxonomy term page.
    // NOTE: This will only match taxonomy term pages WITHOUT a depth modifier
    $term = taxonomy_term_load($matches[1]);

    // Get the term path for this term (handler is defined in the vocab table under module). If it differs from the request, then redirect.
    if (!empty($term) && ($term_path = entity_uri('taxonomy_term', $term)) && $term_path['path'] != $current_path) {
      $current_path = $term_path['path'];
      if (isset($options['query'])) {
        $options['query'] = array_merge($options['query'], $term_path['options']['query']);
      }
      elseif (isset($term_path['options']['query'])) {
        $options['query'] = $term_path['options']['query'];
      }
    }
  }

  // If Content Translation module is enabled then check the path is correct.
  if ($settings['language_redirect'] && module_exists('translation') && preg_match('/node\\/([0-9]+)(\\/view|)$/', $current_path, $matches)) {
    switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
      case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:

        // Check if there's a translation for the current language of the requested node...
        // $matches[0] is the entire matches path from above
        $node_translations = translation_path_get_translations($matches[0]);

        // If there is, go to the translation.
        if (!empty($node_translations[$language->language]) && $node_translations[$language->language] != $matches[0]) {
          globalredirect_goto($node_translations[$language->language], $options);
        }
        else {
          $node = node_load($matches[1]);

          // Check the node has a language set, that it isn't "NONE" (und) and that it dosn't match the site's current language
          if (isset($node->language) && $node->language != LANGUAGE_NONE && $node->language != $language->language) {
            $all_languages = language_list();

            // TODO: Is it possible to get here with a node in a language not in $all_languages?
            // Change the global $language's prefix, to make drupal_goto()
            // follow the proper prefix
            $options['language'] = $language = $all_languages[$node->language];
            globalredirect_goto('node/' . $node->nid . $matches[2], $options);
          }
        }
        break;
      case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:

        // Let's check is there other languages on site.
        $all_languages = language_list();
        if (count($all_languages) > 1) {
          foreach ($all_languages as $l => $lang) {

            // Only test for languages other than the current one.
            if ($lang->language != $language->language) {
              $alias = drupal_get_path_alias($current_path, $lang->language);

              // There is a matching language for this alias
              if ($alias != $current_path) {
                if (isset($lang->domain)) {
                  drupal_goto($lang->domain . '/' . $alias, $options, 301);
                }
                break;
              }
            }
          }
        }
        break;
    }
  }

  // Find an alias (if any) for the request
  $langcode = isset($options['language']->language) ? $options['language']->language : '';
  $alias = drupal_get_path_alias($current_path, $langcode);

  // Modules may alter outbound links by reference
  drupal_alter('url_outbound', $alias, $options, $current_path);

  // If we have a prefix AND an alias, we need to make sure the prefix end with
  // a slash
  if ($prefix && $alias) {
    $prefix .= '/';
  }

  // Alias case sensitivity check.
  // NOTE: This has changed. In D6 the $alias matched the request (in terms of
  // case). However in D7 $alias is already a true alias (accurate in case),
  // and therefore not the "true" request. So, if the alias and the request
  // path are case-insensitive the same then, if case sensitive URL's are
  // enabled, the alias SHOULD be the accurate $alias from above, otherwise it
  // should be the request_path().
  // TODO: Test if this breaks the language checking above!
  if (strcasecmp($alias, request_path()) == 0) {

    // The alias and the request are identical (case insensitive)... Therefore...
    $alias = $settings['case_sensitive_urls'] ? $alias : request_path();
  }

  // Compare the request to the alias. This also works as a 'deslashing'
  // agent. If we have a language prefix then prefix the alias
  if ($request_path != $prefix . $alias) {

    // If it's not just a slash or user has deslash on, redirect, but avoid
    // redirecting to a directory if we are only deslashing, as apache mod_dir
    // can cause an infinite loop.
    if (str_replace($prefix . $alias, '', $request_path) != '/' || $settings['deslash'] && !is_dir($alias)) {
      globalredirect_goto($alias, $options);
    }
  }

  // If no alias was returned, the final check is to direct non-clean to
  // clean - if clean is enabled
  if ($settings['nonclean_to_clean'] && (bool) variable_get('clean_url', 0) && strpos(request_uri(), '?q=')) {
    globalredirect_goto($current_path, $options);
  }

  // If on a comment path, try to redirect to the associate node
  if ($settings['comment_to_node'] && module_exists('comment') && preg_match('/comment\\/(?P<cid>[0-9]+)$/', $current_path, $matches)) {
    $cid = $matches['cid'];

    // Code "borrowed" from comment_permalink($cid)
    if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {

      // Find the current display page for this comment.
      $page = comment_get_display_page($comment->cid, $node->type);

      // If it's more than 0, set the query string parameter
      // TODO - maintain other params?!
      if ($page > 0) {
        $options['query']['page'] = $page;
      }

      // Add the fragment to the comment anchor
      $options['fragment'] = 'comment-' . $comment->cid;

      // Redirect to the node, with the comment querystring/fragment.
      globalredirect_goto('node/' . $node->nid, $options);
    }
  }

  // Restore the destination from earlier so its available in other places.
  if (isset($destination)) {
    $_GET['destination'] = $destination;
  }

  // Add the canonical link to the head of the document if desired.
  // TODO - The Canonical already gets set by Core for node page views... See http://api.drupal.org/api/function/node_page_view/7
  if ($settings['canonical']) {
    drupal_add_html_head_link(array(
      'rel' => 'canonical',
      'href' => url(drupal_is_front_page() ? '<front>' : $request_path, array(
        'absolute' => TRUE,
        'query' => $query_string,
      )),
    ));
  }

  // Add the Content-Location header to the page
  if ($settings['content_location_header']) {
    drupal_add_http_header('Content-Location', url(drupal_is_front_page() ? '<front>' : $request_path, array(
      'absolute' => TRUE,
      'query' => $query_string,
    )));
  }
}

/**
 * A wrapper for drupal_goto() that does not pass through if $path is an
 * external URL.
 */
function globalredirect_goto($path = '', array $options = array(), $http_response_code = 301) {
  if (!globalredirect_url_is_external($path)) {
    return drupal_goto($path, $options, $http_response_code);
  }
}

/**
 * Check if a path is external (e.g. http://example.com).
 *
 * This is a simple copy of url_is_external() which if given an URL with an
 * invalid protocal will return FALSE, an undesired value. This function will
 * return TRUE given an URL with an invalid protocol.
 */
function globalredirect_url_is_external($path) {
  $colonpos = strpos($path, ':');
  return $colonpos !== FALSE && !preg_match('![/?#]!', drupal_substr($path, 0, $colonpos));
}

/**
 * Implements hook_menu().
 */
function globalredirect_menu() {
  $items['admin/config/system/globalredirect'] = array(
    'title' => 'Global Redirect',
    'description' => 'Chose which features you would like enabled for Global Redirect',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'globalredirect_settings',
    ),
    'access arguments' => array(
      'administer global redirect',
    ),
    'file' => 'globalredirect.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function globalredirect_permission() {
  return array(
    'administer global redirect' => array(
      'title' => t('Administer Global Redirect'),
      'description' => t('Allow access to configure Global Redirect module.'),
    ),
  );
}

/**
 * Internal function to determine if GlobalRedirect is active.
 * Several rules have to be checked prior to execution, such as an empty post array,
 * the site must be online and we cannot be running in CLI mode (eg Drush).
 */
function _globalredirect_is_active($settings) {

  /**
   * We need to do a test to make sure we only clean up URL's for the main
   * request. This stops modules such as the Ad Module which had its own script
   * in its folder doing a bootstrap which invoked hook_init() and caused some
   * banners to get "cleaned up"
   *
   * @see http://drupal.org/node/205810
   * @see http://drupal.org/node/278615
   */
  if ($_SERVER['SCRIPT_NAME'] != $GLOBALS['base_path'] . 'index.php') {
    return FALSE;
  }

  /**
   * If this is a command line request (Drush, etc), skip processing.
   */
  if (drupal_is_cli()) {
    return FALSE;
  }

  /**
   * If the site is in offline mode there is little point doing any of this as
   * you might end up redirecting to a 503.
   */
  if (variable_get('maintenance_mode', 0) == 1) {
    return FALSE;
  }

  /**
   * If there is something posted, GlobalRedirect is not active
   */
  if (!empty($_POST)) {
    return FALSE;
  }

  /**
   * If drupal_get_path_alias isn't preset, GlobalRedirect is not active
   */
  if (!function_exists('drupal_get_path_alias')) {
    return FALSE;
  }

  /**
   * If menu_check is enabled AND the menu_get_item function is missing,
   * GlobalRedirect is disabled.
   */
  if ($settings['menu_check'] && !function_exists('menu_get_item')) {
    return FALSE;
  }

  /**
   * If we're going to ignore the admin path, return false.
   * This is added to allow people to continue to get to the 'admin*' paths,
   * even if the module is misbehaving.
   */
  if ($settings['ignore_admin_path'] && (arg(0) == 'admin' || arg(0) == 'batch')) {
    return FALSE;
  }

  /**
   * We seem to have passed all the tests - let say we're active
   */
  return TRUE;
}

/**
 * Return the settings with any defaults mapped over the top
 */
function _globalredirect_get_settings($default_only = FALSE) {
  $defaults = array(
    'deslash' => 1,
    'nonclean_to_clean' => 1,
    'trailing_zero' => 0,
    'menu_check' => 0,
    'case_sensitive_urls' => 1,
    'language_redirect' => 0,
    'canonical' => 0,
    'content_location_header' => 0,
    'term_path_handler' => 1,
    'frontpage_redirect' => 1,
    'ignore_admin_path' => 1,
    'comment_to_node' => 0,
  );
  if ($default_only) {
    return $defaults;
  }
  return variable_get('globalredirect_settings', array()) + $defaults;
}

/**
 * globalredirect_request_path() is borrowed from request_uri(), but it only ltrim's..
 */
function globalredirect_request_path() {
  if (request_uri()) {
    if (isset($_REQUEST['q'])) {
      $path = $_REQUEST['q'];
    }
    else {

      // This is a request using a clean URL. Extract the path from REQUEST_URI.
      $request_path = strtok(request_uri(), '?');
      $base_path_len = drupal_strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\\/'));

      // Unescape and strip $base_path prefix, leaving q without a leading slash.
      $path = drupal_substr(urldecode($request_path), $base_path_len + 1);
    }
  }
  else {

    // This is the front page.
    $path = '';
  }

  // Under certain conditions Apache's RewriteRule directive prepends the value
  // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing
  // slash in place, hence we need to normalize $_GET['q'].
  $path = ltrim($path, '/');
  return $path;
}

Functions

Namesort descending Description
globalredirect_goto A wrapper for drupal_goto() that does not pass through if $path is an external URL.
globalredirect_help Implements hook_help().
globalredirect_init Implements hook_init().
globalredirect_menu Implements hook_menu().
globalredirect_permission Implements hook_permission().
globalredirect_request_path globalredirect_request_path() is borrowed from request_uri(), but it only ltrim's..
globalredirect_url_is_external Check if a path is external (e.g. http://example.com).
_globalredirect_get_settings Return the settings with any defaults mapped over the top
_globalredirect_is_active Internal function to determine if GlobalRedirect is active. Several rules have to be checked prior to execution, such as an empty post array, the site must be online and we cannot be running in CLI mode (eg Drush).