t('Redirect'), 'base table' => 'redirect', 'controller class' => 'RedirectController', 'entity keys' => array( 'id' => 'rid', 'bundle' => 'type', ), 'fieldable' => FALSE, 'uuid' => FALSE, 'redirect' => FALSE, ); return $info; } /** * 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 = '

' . t('This page lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them.') . '

'; 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. // Commented out per https://www.drupal.org/node/2048137. //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']) { // Disable all redirects having the same source as this alias. redirect_disable_by_path($path['alias'], $path['language']); $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); $existing = redirect_load_by_hash($hash); if (!$existing) { redirect_save($redirect); } // If the existing redirect is disabled, re-enable it. elseif (isset($existing->status) && $existing->status == 0) { $existing->status = 1; redirect_save($existing); } } } /** * Implements hook_path_insert(). */ function redirect_path_insert(array $path) { if (!empty($path['alias'])) { // Disable all redirects having the same source as this alias. redirect_disable_by_path($path['alias'], $path['language']); } } /** * 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; } /** * Fetches multiple URL redirect IDs from the database by {redirect}.source. * * @param $source * The source of the URL redirect. * @param $language * Language of the source URL. * @param $enabled_only * Boolean that indicates whether to only load enabled redirects. * * @return array * An indexed array of IDs, or an empty array if there is no result set. */ function redirect_fetch_rids_by_path($source, $language, $enabled_only = FALSE) { // Run a case-insensitive query for matching RIDs first. $rid_query = db_select('redirect'); $rid_query->addField('redirect', 'rid'); // Prevent errors if redirect_update_7101() has not yet been run. if ($enabled_only && db_field_exists('redirect', 'status')) { $rid_query->condition('status', 1); } 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(); return $rids; } /** * Load multiple URL redirects from the database by {redirect}.source. * * @param $source * The source of the URL redirect. * @param $language * Language of the source URL. * @param $query * Array of URL query parameters. * @param $enabled_only * Boolean that indicates whether to only load enabled redirects. * * @return * The first matched URL redirect object, or FALSE if there aren't any. * * @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(), $enabled_only = TRUE) { $rids = redirect_fetch_rids_by_path($source, $language, $enabled_only); 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('A redirect already exists for the source path %source. Do you want to edit the existing redirect?', 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 redirect. 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' => '', 'status' => 1, ); 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); } } /** * Disable any redirects associated with a path. * * Given a source like 'node/1' this function will delete any redirects that * have that specific source. * * @param $path * An string with an internal Drupal path. * * @param @langauge * The langcode of the path. * * @ingroup redirect_api */ function redirect_disable_by_path($path, $language) { $rids = redirect_fetch_rids_by_path($path, $language, FALSE); if ($rids) { return redirect_change_status_multiple($rids, 0); } } /** * 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) { $uri = entity_uri($entity_type, $entity); if (!empty($uri['path'])) { 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); // } //} } /** * Change the status of multiple URL redirects. * * @param array $rids * An array of redirect IDs to disable. * @param int|string $status * The status to set the redirect to: either disabled (0) or enabled (1). * * @ingroup redirect_api */ function redirect_change_status_multiple(array $rids, $status) { if ($status !== 0 && $status !== 1 && $status !== '0' && $status !== '1') { watchdog('Cannot change redirect status to %status', array('%status' => $status)); drupal_set_message(t('Cannot change redirect status to %status', array('%status' => $status))); return; } if (!empty($rids)) { $redirects = redirect_load_multiple($rids, array(), TRUE); foreach ($redirects as $rid => $redirect) { if (isset($redirect->status) && $redirect->status == $status) { // We no-op if the redirect is not actually changing status. // So if a disabled redirect is disabled, neither redirect_save() is // triggered, nor do we log any message. continue; } $redirect->status = $status; redirect_save($redirect); if ($status) { $redirect_link = l($redirect->redirect, $redirect->redirect) . '=> ' . l($redirect->source, $redirect->source); watchdog('redirect', 'Enabled redirect: !redirect_source_link', array('!redirect_link' => $redirect_link)); } else { $redirect_link = l($redirect->redirect, $redirect->redirect) . '=> ' . l($redirect->source, $redirect->source); watchdog('redirect', 'Disabled redirect: !redirect_source_link', array('!redirect_link' => $redirect_link)); } } } } /** * 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) { return FALSE; } if (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. watchdog('redirect', 'Due to existing settings, could not track when a redirect is used, so could not 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) { 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)) { // Prevent circular redirects. if ($GLOBALS['base_root'] . request_uri() == url($redirect->redirect, array('absolute' => TRUE) + $redirect->redirect_options)) { return FALSE; } // 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 (!preg_match('/index\.php$/', $_SERVER['SCRIPT_NAME'])) { // 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')) && !user_access('access site in 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 that 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 = ''; } 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 == '') { $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 All languages.'), ); } /** * 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['path'])) { // 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'), '#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'; } } $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(), ); } // 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', 'status_code', 'language', 'count', 'access', 'operations'); $form['redirect'] += redirect_list_table($redirects, $header); } /** * Implements hook_field_extra_fields(). */ function redirect_field_extra_fields() { $entity_info = entity_get_info(); foreach (array_keys($entity_info) as $entity_type) { if (!redirect_entity_type_supports_redirects($entity_type)) { // Redirect is explicitly disabled for this entity type. 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, ); $operations['disable'] = array( 'action' => t('Disable'), 'action_past' => t('Disabled'), 'callback' => 'redirect_change_status_multiple', 'callback arguments' => array(0), 'confirm' => TRUE, ); $operations['enable'] = array( 'action' => t('Enable'), 'action_past' => t('Enabled'), 'callback' => 'redirect_change_status_multiple', 'callback arguments' => array(1), 'confirm' => TRUE, ); return $operations; }