9 * @defgroup redirect_api Redirection API
11 * Functions related to URL redirects.
13 * @} End of "defgroup redirect_api".
15 use Drupal\Component\Utility\UrlHelper;
16 use Drupal\Core\Cache\Cache;
17 use Drupal\Core\Entity\EntityInterface;
18 use Drupal\Core\Field\FieldItemList;
19 use Drupal\Core\Form\FormStateInterface;
20 use Drupal\Core\Language\Language;
21 use Drupal\Core\Routing\RouteMatchInterface;
23 use Drupal\Core\Site\Settings;
24 use Drupal\redirect\Entity\Redirect;
25 use Symfony\Component\Routing\Exception\RouteNotFoundException;
28 * Implements hook_hook_info().
30 function redirect_hook_info() {
33 'redirect_load_by_source_alter',
44 return array_fill_keys($hooks, array('group' => 'redirect'));
48 * Implements hook_help().
50 function redirect_help($route_name, RouteMatchInterface $route_match) {
52 switch ($route_name) {
53 case 'help.page.redirect':
54 $output = '<h3>' . t('About') . '</h3>';
55 $output .= '<p>' . t('The Redirect module allows users to redirect from old URLs to new URLs. For more information, see the <a href=":online">online documentation for Redirect</a>.', [':online' => 'https://www.drupal.org/documentation/modules/path-redirect']) . '</p>';
57 $output .= '<h3>' . t('Uses') . '</h3>';
58 $output .= '<dd>' . t('Redirect is accessed from three tabs that help you manage <a href=":list">URL Redirects</a>.', [':list' => Url::fromRoute('redirect.list')->toString()]) . '</dd>';
59 $output .= '<dt>' . t('Manage URL Redirects') . '</dt>';
60 $output .= '<dd>' . t('The <a href=":redirect">"URL Redirects"</a> page is used to setup and manage URL Redirects. New redirects are created here using the <a href=":add_form">Add redirect</a> button which presents a form to simplify the creation of redirects . The URL redirects page provides a list of all redirects on the site and allows you to edit them.', [':redirect' => Url::fromRoute('redirect.list')->toString(), ':add_form' => Url::fromRoute('redirect.add')->toString()]) . '</dd>';
61 if (\Drupal::moduleHandler()->moduleExists('redirect_404')) {
62 $output .= '<dt>' . t('Fix 404 pages') . '</dt>';
63 $output .= '<dd>' . t('<a href=":fix_404">"Fix 404 pages"</a> lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them. This 404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested.', [':fix_404' => Url::fromRoute('redirect_404.fix_404')->toString()]) . '</dd>';
65 elseif (!\Drupal::moduleHandler()->moduleExists('redirect_404') && \Drupal::currentUser()->hasPermission('administer modules')) {
66 $output .= '<dt>' . t('Fix 404 pages') . '</dt>';
67 $output .= '<dd>' . t('404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested. Please install the <a href=":extend">Redirect 404</a> submodule to be able to log all paths that have resulted in 404 errors.', [':extend' => Url::fromRoute('system.modules_list')->toString()]) . '</dd>';
69 $output .= '<dt>' . t('Configure Global Redirects') . '</dt>';
70 $output .= '<dd>' . t('The <a href=":settings">"Settings"</a> page presents you with a number of means to adjust redirect settings.', [':settings' => Url::fromRoute('redirect.settings')->toString()]) . '</dd>';
78 * Implements hook_entity_delete().
80 * Will delete redirects based on the entity URL.
82 function redirect_entity_delete(EntityInterface $entity) {
84 if ($entity->getEntityType()->hasLinkTemplate('canonical') && $entity->toUrl('canonical')->isRouted()) {
85 redirect_delete_by_path('internal:/' . $entity->toUrl('canonical')->getInternalPath());
86 redirect_delete_by_path('entity:' . $entity->getEntityTypeId() . '/' . $entity->id());
89 catch (RouteNotFoundException $e) {
90 // This can happen if a module incorrectly defines a link template, ignore
96 * Implements hook_path_update().
98 * Will create redirect from the old path alias to the new one.
100 function redirect_path_update(array $path) {
101 if (!\Drupal::config('redirect.settings')->get('auto_redirect')) {
104 $original_path = $path['original'];
106 // Delete all redirects having the same source as this alias.
107 redirect_delete_by_path($path['alias'], $path['langcode'], FALSE);
108 if ($original_path['alias'] != $path['alias']) {
109 if (!redirect_repository()->findMatchingRedirect($original_path['alias'], array(), $original_path['langcode'])) {
110 $redirect = Redirect::create();
111 $redirect->setSource($original_path['alias']);
112 $redirect->setRedirect($path['source']);
113 $redirect->setLanguage($original_path['langcode']);
114 $redirect->setStatusCode(\Drupal::config('redirect.settings')->get('default_status_code'));
121 * Implements hook_path_insert().
123 function redirect_path_insert(array $path) {
124 // Delete all redirects having the same source as this alias.
125 redirect_delete_by_path($path['alias'], $path['langcode'], FALSE);
129 * Implements hook_path_delete().
131 function redirect_path_delete($path) {
132 if (!\Drupal::config('redirect.settings')->get('auto_redirect')) {
135 elseif (isset($path['redirect']) && !$path['redirect']) {
138 elseif (empty($path)) {
139 // @todo Remove this condition and allow $path to use an array type hint
140 // when http://drupal.org/node/1025904 is fixed.
144 // Redirect from a deleted alias to the system path.
145 //if (!redirect_load_by_source($path['alias'], $path['language'])) {
146 // $redirect = new stdClass();
147 // redirect_create($redirect);
148 // $redirect->source = $path['alias'];
149 // $redirect->redirect = $path['source'];
150 // $redirect->language = $path['language'];
151 // redirect_save($redirect);
156 * Implements hook_page_build().
158 * Adds an action on 404 pages to create a redirect.
160 * @todo hook_page_build() can no longer be used for this. Find a different way.
162 function redirect_page_build(&$page) {
163 if (redirect_is_current_page_404() && \Drupal::currentUser()->hasPermission('administer redirects')) {
164 if (!isset($page['content']['system_main']['actions'])) {
165 $page['content']['system_main']['actions'] = array(
168 '#attributes' => array('class' => array('action-links')),
172 // We cannot simply use current_path() because if a 404 path is set, then
173 // that value overrides whatever is in $_GET['q']. The
174 // drupal_deliver_html_page() function thankfully puts the original current
175 // path into $_GET['destination'].
176 $destination = drupal_get_destination();
177 $page['content']['system_main']['actions']['#links']['add_redirect'] = array(
178 'title' => t('Add URL redirect from this page to another location'),
179 'href' => 'admin/config/search/redirect/add',
180 'query' => array('source' => $destination['destination']) + drupal_get_destination(),
186 * Gets the redirect repository service.
188 * @return \Drupal\redirect\RedirectRepository
189 * The repository service.
191 function redirect_repository() {
192 return \Drupal::service('redirect.repository');
196 * Delete any redirects associated with a path or any of its sub-paths.
198 * Given a source like 'node/1' this function will delete any redirects that
199 * have that specific source or any sources that match 'node/1/%'.
201 * @param string $path
202 * An string with an internal Drupal path.
203 * @param string $langcode
204 * (optional) If specified, limits deletion to redirects for the given
205 * language. Defaults to all languages.
206 * @param bool $match_subpaths_and_redirect
207 * (optional) Whether redirects with a destination to the given path and
208 * sub-paths should also be deleted.
210 * @ingroup redirect_api
212 function redirect_delete_by_path($path, $langcode = NULL, $match_subpaths_and_redirect = TRUE) {
213 $path = ltrim($path, '/');
214 $database = \Drupal::database();
215 $query = $database->select('redirect');
216 $query->addField('redirect', 'rid');
218 $query_or->condition('redirect_source__path', $database->escapeLike($path), 'LIKE');
219 if ($match_subpaths_and_redirect) {
220 $query_or->condition('redirect_source__path', $database->escapeLike($path . '/') . '%', 'LIKE');
221 $query_or->condition('redirect_redirect__uri', $database->escapeLike($path), 'LIKE');
222 $query_or->condition('redirect_redirect__uri', $database->escapeLike($path . '/') . '%', 'LIKE');
226 $query->condition('language', $langcode);
229 $query->condition($query_or);
230 $rids = $query->execute()->fetchCol();
233 foreach (redirect_repository()->loadMultiple($rids) as $redirect) {
240 * Sort an array recusively.
243 * The array to sort, by reference.
245 * The sorting callback to use (e.g. 'sort', 'ksort', 'asort').
248 * TRUE on success or FALSE on failure.
250 function redirect_sort_recursive(&$array, $callback = 'sort') {
251 $result = $callback($array);
252 foreach ($array as $key => $value) {
253 if (is_array($value)) {
254 $result &= redirect_sort_recursive($array[$key], $callback);
261 * Build the URL of a redirect for display purposes only.
263 function redirect_url($path, array $options = array(), $clean_url = NULL) {
264 // @todo - deal with removal of clean_url config. See
265 // https://drupal.org/node/1659580
266 if (!isset($clean_url)) {
267 //$clean_url = variable_get('clean_url', 0);
274 if (!isset($options['alter']) || !empty($options['alter'])) {
275 \Drupal::moduleHandler()->alter('redirect_url', $path, $options);
278 // The base_url might be rewritten from the language rewrite in domain mode.
279 if (!isset($options['base_url'])) {
280 // @todo - is this correct? See https://drupal.org/node/1798832.
281 if (isset($options['https']) && Settings::get('mixed_mode_sessions', FALSE)) {
282 if ($options['https'] === TRUE) {
283 $options['base_url'] = $GLOBALS['base_secure_url'];
284 $options['absolute'] = TRUE;
286 elseif ($options['https'] === FALSE) {
287 $options['base_url'] = $GLOBALS['base_insecure_url'];
288 $options['absolute'] = TRUE;
292 $options['base_url'] = $GLOBALS['base_url'];
296 if (empty($options['absolute']) || url_is_external($path)) {
300 $url = $options['base_url'] . base_path() . $path;
303 if (isset($options['query'])) {
304 $url .= $clean_url ? '?' : '&';
305 $url .= UrlHelper::buildQuery($options['query']);
307 if (isset($options['fragment'])) {
308 $url .= '#' . $options['fragment'];
314 function redirect_status_code_options($code = NULL) {
316 300 => t('300 Multiple Choices'),
317 301 => t('301 Moved Permanently'),
318 302 => t('302 Found'),
319 303 => t('303 See Other'),
320 304 => t('304 Not Modified'),
321 305 => t('305 Use Proxy'),
322 307 => t('307 Temporary Redirect'),
324 return isset($codes[$code]) ? $codes[$code] : $codes;
328 * Returns if the current page request is a page not found (404 status error).
330 * Why the fuck do we have to do this? Why is there not an easier way???
333 * TRUE if the current page is a 404, or FALSE otherwise.
335 function redirect_is_current_page_404() {
336 return drupal_get_http_header('Status') == '404 Not Found';
340 * uasort callback; Compare redirects based on language neutrality and rids.
342 function _redirect_uasort($a, $b) {
343 $a_weight = isset($a->weight) ? $a->weight : 0;
344 $b_weight = isset($b->weight) ? $b->weight : 0;
345 if ($a_weight != $b_weight) {
346 // First sort by weight (case sensitivity).
347 return $a_weight > $b_weight;
349 elseif ($a->language != $b->language) {
350 // Then sort by language specific over language neutral.
351 return $a->language == Language::LANGCODE_NOT_SPECIFIED;
353 elseif (!empty($a->source_options['query']) != !empty($b->source_options['query'])) {
354 // Then sort by redirects that do not have query strings over ones that do.
355 return empty($a->source_options['query']);
358 // Lastly sort by the highest redirect ID.
359 return $a->rid < $b->rid;
364 * Implements hook_form_FORM_ID_alter() on behalf of locale.module.
366 function locale_form_redirect_edit_form_alter(array &$form, FormStateInterface $form_state) {
367 $form['language'] = array(
369 '#title' => t('Language'),
370 '#options' => array(Language::LANGCODE_NOT_SPECIFIED => t('All languages')) + \Drupal::languageManager()->getLanguages(),
371 '#default_value' => $form['language']['#value'],
372 '#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>.'),
377 * Fetch an array of redirect bulk operations.
379 * @see hook_redirect_operations()
380 * @see hook_redirect_operations_alter()
382 function redirect_get_redirect_operations() {
383 $operations = &drupal_static(__FUNCTION__);
385 if (!isset($operations)) {
386 $operations = \Drupal::moduleHandler()->invokeAll('redirect_operations');
387 \Drupal::moduleHandler()->alter('redirect_operations', $operations);
394 * Implements hook_redirect_operations().
396 function redirect_redirect_operations() {
397 $operations['delete'] = array(
398 'action' => t('Delete'),
399 'action_past' => t('Deleted'),
400 'callback' => 'redirect_delete_multiple',
407 * Ajax callback for the redirect link widget.
409 function redirect_source_link_get_status_messages(array $form, FormStateInterface $form_state) {
410 return $form['redirect_source']['widget'][0]['status_box'];
414 * Implements hook_entity_extra_field_info().
416 function redirect_entity_extra_field_info() {
419 if (\Drupal::service('module_handler')->moduleExists('node')) {
420 $node_types = \Drupal::entityTypeManager()
421 ->getStorage('node_type')
424 foreach ($node_types as $node_type) {
425 $extra['node'][$node_type->id()]['form']['url_redirects'] = [
426 'label' => t('URL redirects'),
427 'description' => t('Redirect module form elements'),
437 * Implements hook_form_node_form_alter().
439 function redirect_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
440 /** @var \Drupal\node\NodeInterface $node */
441 $node = $form_state->getFormObject()->getEntity();
442 if (!$node->isNew() && \Drupal::currentUser()->hasPermission('administer redirects')) {
446 // Find redirects to this node.
447 $redirects = \Drupal::service('redirect.repository')
448 ->findByDestinationUri(["internal:/node/$nid", "entity:node/$nid"]);
450 // Assemble the rows for the table.
452 /** @var \Drupal\Core\Entity\EntityListBuilder $list_builder */
453 $list_builder = \Drupal::service('entity.manager')->getListBuilder('redirect');
454 /** @var \Drupal\redirect\Entity\Redirect[] $redirects */
455 foreach ($redirects as $redirect) {
457 $path = $redirect->getSourcePathWithQuery();
459 'class' => ['redirect-table__path'],
460 'data' => ['#plain_text' => $path],
463 $row['operations'] = [
465 '#type' => 'operations',
466 '#links' => $list_builder->getOperations($redirect),
472 // Add the list to the vertical tabs section of the form.
474 ['class' => ['redirect-table__path'], 'data' => t('From')],
475 ['class' => ['redirect-table__operations'], 'data' => t('Operations')],
477 $form['url_redirects'] = [
478 '#type' => 'details',
479 '#title' => t('URL redirects'),
480 '#group' => 'advanced',
484 '#header' => $header,
486 '#empty' => t('No URL redirects available.'),
487 '#attributes' => ['class' => ['redirect-table']],
491 'redirect/drupal.redirect.admin',
497 $form['url_redirects']['warning'] = [
498 '#markup' => t('Note: links open in the current window.'),