Commit 2416cc7b authored by Berdir's avatar Berdir Committed by Berdir
Browse files

Issue #3102143 by Berdir, phenaproxima, andralex: Remove Drupal 8.8...

Issue #3102143 by Berdir, phenaproxima, andralex: Remove Drupal 8.8 deprecations, compatibility with Drupal 9
parent 29131230
......@@ -3,8 +3,7 @@
"description": "Allows users to redirect from old URLs to new URLs.",
"type": "drupal-module",
"license": "GPL-2.0+",
"minimum-stability": "dev",
"require": {
"drupal/core": "^8.7.7 || ^9"
"drupal/core": "^8.8 || ^9"
}
}
......@@ -122,7 +122,7 @@ class DomainRedirectRequestSubscriberTest extends UnitTestCase {
$http_kernel = $this->getMockBuilder(HttpKernelInterface::class)
->getMock();
return new GetResponseEvent($http_kernel, $request, 'test');
return new GetResponseEvent($http_kernel, $request, HttpKernelInterface::MASTER_REQUEST);
}
/**
......
name: Redirect
type: module
description: Allows users to redirect from old URLs to new URLs.
core_version_requirement: ^8.7.7 || ^9
core_version_requirement: ^8.8 || ^9
configure: redirect.settings
dependencies:
- drupal:path_alias
- drupal:link
- drupal:views
......@@ -5,22 +5,12 @@
* The redirect module.
*/
/**
* @defgroup redirect_api Redirection API
* @{
* Functions related to URL redirects.
*
* @} End of "defgroup redirect_api".
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\Core\Site\Settings;
use Drupal\path_alias\PathAliasInterface;
use Drupal\redirect\Entity\Redirect;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Drupal\Core\Database\Query\Condition;
......@@ -94,63 +84,39 @@ function redirect_entity_delete(EntityInterface $entity) {
}
/**
* Implements hook_path_update().
*
* Will create redirect from the old path alias to the new one.
* Implements hook_ENTITY_TYPE_update() for path_alias.
*/
function redirect_path_update(array $path) {
if (!\Drupal::config('redirect.settings')->get('auto_redirect')) {
function redirect_path_alias_update(PathAliasInterface $path_alias) {
$config = \Drupal::config('redirect.settings');
if (!$config->get('auto_redirect')) {
return;
}
$original_path = $path['original'];
/** @var \Drupal\path_alias\PathAliasInterface $original_path_alias */
$original_path_alias = $path_alias->original;
// Delete all redirects having the same source as this alias.
redirect_delete_by_path($path['alias'], $path['langcode'], FALSE);
if ($original_path['alias'] != $path['alias']) {
if (!redirect_repository()->findMatchingRedirect($original_path['alias'], [], $original_path['langcode'])) {
redirect_delete_by_path($path_alias->getAlias(), $path_alias->language()->getId(), FALSE);
// Create redirect from the old path alias to the new one.
if ($original_path_alias->getAlias() != $path_alias->getAlias()) {
if (!redirect_repository()->findMatchingRedirect($original_path_alias->getAlias(), [], $original_path_alias->language()->getId())) {
$redirect = Redirect::create();
$redirect->setSource($original_path['alias']);
$redirect->setRedirect($path['source']);
$redirect->setLanguage($original_path['langcode']);
$redirect->setStatusCode(\Drupal::config('redirect.settings')->get('default_status_code'));
$redirect->setSource($original_path_alias->getAlias());
$redirect->setRedirect($path_alias->getPath());
$redirect->setLanguage($original_path_alias->language()->getId());
$redirect->setStatusCode($config->get('default_status_code'));
$redirect->save();
}
}
}
/**
* Implements hook_path_insert().
* Implements hook_ENTITY_TYPE_insert() for path_alias.
*/
function redirect_path_insert(array $path) {
function redirect_path_alias_insert(PathAliasInterface $path_alias) {
// Delete all redirects having the same source as this alias.
redirect_delete_by_path($path['alias'], $path['langcode'], FALSE);
}
/**
* Implements hook_path_delete().
*/
function redirect_path_delete($path) {
if (!\Drupal::config('redirect.settings')->get('auto_redirect')) {
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_create($redirect);
// $redirect->source = $path['alias'];
// $redirect->redirect = $path['source'];
// $redirect->language = $path['language'];
// redirect_save($redirect);
//}
redirect_delete_by_path($path_alias->getAlias(), $path_alias->language()->getId(), FALSE);
}
/**
......@@ -258,60 +224,6 @@ function redirect_sort_recursive(&$array, $callback = 'sort') {
return $result;
}
/**
* Build the URL of a redirect for display purposes only.
*/
function redirect_url($path, array $options = [], $clean_url = NULL) {
// @todo - deal with removal of clean_url config. See
// https://drupal.org/node/1659580
if (!isset($clean_url)) {
//$clean_url = variable_get('clean_url', 0);
}
if ($path == '') {
$path = '<front>';
}
if (!isset($options['alter']) || !empty($options['alter'])) {
\Drupal::moduleHandler()->alter('redirect_url', $path, $options);
}
// The base_url might be rewritten from the language rewrite in domain mode.
if (!isset($options['base_url'])) {
// @todo - is this correct? See https://drupal.org/node/1798832.
if (isset($options['https']) && Settings::get('mixed_mode_sessions', 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 .= UrlHelper::buildQuery($options['query']);
}
if (isset($options['fragment'])) {
$url .= '#' . $options['fragment'];
}
return $url;
}
function redirect_status_code_options($code = NULL) {
$codes = [
300 => t('300 Multiple Choices'),
......@@ -337,30 +249,6 @@ 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::LANGCODE_NOT_SPECIFIED;
}
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.
*/
......@@ -374,36 +262,6 @@ function locale_form_redirect_edit_form_alter(array &$form, FormStateInterface $
];
}
/**
* 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 = \Drupal::moduleHandler()->invokeAll('redirect_operations');
\Drupal::moduleHandler()->alter('redirect_operations', $operations);
}
return $operations;
}
/**
* Implements hook_redirect_operations().
*/
function redirect_redirect_operations() {
$operations['delete'] = [
'action' => t('Delete'),
'action_past' => t('Deleted'),
'callback' => 'redirect_delete_multiple',
'confirm' => TRUE,
];
return $operations;
}
/**
* Ajax callback for the redirect link widget.
*/
......
......@@ -11,7 +11,7 @@ services:
arguments: ['@config.factory', '@state', '@access_manager', '@current_user', '@router.route_provider']
redirect.request_subscriber:
class: Drupal\redirect\EventSubscriber\RedirectRequestSubscriber
arguments: ['@redirect.repository', '@language_manager', '@config.factory', '@path.alias_manager', '@module_handler', '@entity_type.manager', '@redirect.checker', '@router.request_context', '@path_processor_manager']
arguments: ['@redirect.repository', '@language_manager', '@config.factory', '@path_alias.manager', '@module_handler', '@entity_type.manager', '@redirect.checker', '@router.request_context', '@path_processor_manager']
tags:
- { name: event_subscriber }
redirect.settings_cache_tag:
......
......@@ -2,22 +2,19 @@
namespace Drupal\redirect\EventSubscriber;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\path_alias\AliasManagerInterface;
use Drupal\redirect\Exception\RedirectLoopException;
use Drupal\redirect\RedirectChecker;
use Drupal\redirect\RedirectRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -42,7 +39,7 @@ class RedirectRequestSubscriber implements EventSubscriberInterface {
protected $config;
/**
* @var \Drupal\Core\Path\AliasManager
* @var \Drupal\path_alias\AliasManagerInterface
*/
protected $aliasManager;
......@@ -82,7 +79,7 @@ class RedirectRequestSubscriber implements EventSubscriberInterface {
* The language manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config.
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
* The alias manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
......
......@@ -67,7 +67,7 @@ class RedirectSourceWidget extends WidgetBase {
try {
\Drupal::service('router')->match('/' . $form_state->getValue(['redirect_source', 0, 'path']));
$element['status_box'][]['#markup'] = '<div class="messages messages--warning">' . $this->t('The source path %path is likely a valid path. It is preferred to <a href="@url-alias">create URL aliases</a> for existing paths rather than redirects.',
['%path' => $source_path, '@url-alias' => Url::fromRoute('path.admin_add')->toString()]) . '</div>';
['%path' => $source_path, '@url-alias' => Url::fromRoute('entity.path_alias.add_form')->toString()]) . '</div>';
}
catch (ResourceNotFoundException $e) {
// Do nothing, expected behaviour.
......
<?php
namespace Drupal\redirect\Tests;
namespace Drupal\Tests\redirect\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Behat\Mink\Driver\GoutteDriver;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Global redirect test cases.
*
* @group redirect
*/
class GlobalRedirectTest extends WebTestBase {
class GlobalRedirectTest extends BrowserTestBase {
use PathAliasTestTrait;
/**
* Modules to enable.
......@@ -62,6 +66,31 @@ class GlobalRedirectTest extends WebTestBase {
*/
protected $node;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $minkDefaultDriverClass = GoutteDriver::class;
/**
* {@inheritdoc}
*/
protected function initMink() {
$session = parent::initMink();
/** @var \Behat\Mink\Driver\GoutteDriver $driver */
$driver = $session->getDriver();
// Since we are testing low-level redirect stuff, the HTTP client should
// NOT automatically follow redirects sent by the server.
$driver->getClient()->followRedirects(FALSE);
return $session;
}
/**
* {@inheritdoc}
*/
......@@ -101,7 +130,7 @@ class GlobalRedirectTest extends WebTestBase {
// Create an alias for the create story path - this is used in the
// "redirect with permissions testing" test.
\Drupal::service('path.alias_storage')->save('/admin/config/system/site-information', '/site-info');
$this->createPathAlias('/admin/config/system/site-information', '/site-info');
// Create a taxonomy term for the forum.
$term = Term::create([
......@@ -134,14 +163,12 @@ class GlobalRedirectTest extends WebTestBase {
* Will test the redirects.
*/
public function testRedirects() {
// First test that the good stuff can be switched off.
// First, test that redirects can be disabled.
$this->config->set('route_normalizer_enabled', FALSE)->save();
$this->assertRedirect('index.php/node/' . $this->node->id(), NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('index.php/test-node', NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('test-node/', NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('Test-node/', NULL, 'HTTP/1.1 200 OK');
$this->assertNoRedirect('index.php/node/' . $this->node->id());
$this->assertNoRedirect('index.php/test-node');
$this->assertNoRedirect('test-node/');
$this->assertNoRedirect('Test-node/');
$this->config->set('route_normalizer_enabled', TRUE)->save();
// Test alias normalization.
......@@ -157,20 +184,35 @@ class GlobalRedirectTest extends WebTestBase {
// Test front page redirects.
$this->config('system.site')->set('page.front', '/node')->save();
$this->assertRedirect('node', '<front>');
$this->assertRedirect('node', '/');
// Test front page redirects with an alias.
\Drupal::service('path.alias_storage')->save('/node', '/node-alias');
$this->assertRedirect('node-alias', '<front>');
// Test post request.
$this->drupalPost('Test-node', 'application/json', []);
$this->createPathAlias('/node', '/node-alias');
$this->assertRedirect('node-alias', '/');
// Test a POST request. It should stay on the same path and not try to
// redirect. Because Mink does not provide methods to do plain POSTs, we
// need to use the underlying Guzzle HTTP client directly.
/** @var \Behat\Mink\Driver\GoutteDriver $driver */
$driver = $this->getSession()->getDriver();
$response = $driver->getClient()
->getClient()
->post($this->getAbsoluteUrl('Test-node'), [
// Do not follow redirects. This way, we can assert that the server did
// not even _try_ to redirect us
'allow_redirects' => FALSE,
'headers' => [
'Accept' => 'application/json',
],
]);
// Does not do a redirect, stays in the same path.
$this->assertEqual(basename($this->getUrl()), 'Test-node');
$this->assertSame(200, $response->getStatusCode());
$this->assertEmpty($response->getHeader('Location'));
$this->assertNotContains('http-equiv="refresh', (string) $response->getBody());
// Test the access checking.
$this->config->set('access_check', TRUE)->save();
$this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 403 Forbidden');
$this->assertNoRedirect('admin/config/system/site-information', 403);
$this->config->set('access_check', FALSE)->save();
// @todo - here it seems that the access check runs prior to our redirecting
......@@ -181,11 +223,14 @@ class GlobalRedirectTest extends WebTestBase {
$this->assertRedirect('Test-node?&foo&.bar=baz', 'test-node?&foo&.bar=baz');
// Test alias normalization with trailing ?.
$this->assertRedirect('test-node?', 'test-node');
// @todo \GuzzleHttp\Psr7\Uri strips away the trailing ?, this should
// actually be a redirect but can't be tested with Guzzle. Improve in
// https://www.drupal.org/project/redirect/issues/3119503.
$this->assertNoRedirect('test-node?');
$this->assertRedirect('Test-node?', 'test-node');
// Test alias normalization still works without trailing ?.
$this->assertRedirect('test-node', NULL, 'HTTP/1.1 200 OK');
$this->assertNoRedirect('test-node');
$this->assertRedirect('Test-node', 'test-node');
// Login as user with admin privileges.
......@@ -199,7 +244,7 @@ class GlobalRedirectTest extends WebTestBase {
$this->assertRedirect('Test-node', 'test-node');
$this->config->set('ignore_admin_path', TRUE)->save();
$this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 200 OK');
$this->assertNoRedirect('admin/config/system/site-information');
// Test alias normalization again with ignore_admin_path true.
$this->assertRedirect('Test-node', 'test-node');
......@@ -225,7 +270,7 @@ class GlobalRedirectTest extends WebTestBase {
'language_configuration[content_translation]' => TRUE,
];
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', ['%type' => 'Page']), 'Basic page content type has been updated.');
$this->assertRaw(t('The content type %type has been updated.', ['%type' => 'Page']));
$spanish_node = $this->drupalCreateNode([
'type' => 'page',
......@@ -239,60 +284,54 @@ class GlobalRedirectTest extends WebTestBase {
}
/**
* Asserts the redirect from $path to the $expected_ending_url.
* Visits a path and asserts that it is a redirect.
*
* @param string $path
* The request path.
* @param $expected_ending_url
* @param string $expected_destination
* The path where we expect it to redirect. If NULL value provided, no
* redirect is expected.
* @param string $expected_ending_status
* @param int $status_code
* The status we expect to get with the first request.
*
* @throws \Behat\Mink\Exception\ExpectationException
*/
public function assertRedirect($path, $expected_ending_url, $expected_ending_status = 'HTTP/1.1 301 Moved Permanently') {
$this->drupalHead($GLOBALS['base_url'] . '/' . $path);
$headers = $this->drupalGetHeaders(TRUE);
$ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL;
$message = new FormattableMarkup('Testing redirect from %from to %to. Ending url: %url', [
'%from' => $path,
'%to' => $expected_ending_url,
'%url' => $ending_url,
]);
if ($expected_ending_url == '<front>') {
$expected_ending_url = $GLOBALS['base_url'] . '/';
}
elseif (!empty($expected_ending_url)) {
$expected_ending_url = $GLOBALS['base_url'] . '/' . $expected_ending_url;
}
else {
$expected_ending_url = NULL;
}
public function assertRedirect($path, $expected_destination, $status_code = 301) {
// Always just use getAbsolutePath() so that generating the link does not
// alter special requests.
$url = $this->getAbsoluteUrl($path);
$this->getSession()->visit($url);
$this->assertEqual($expected_ending_url, $ending_url);
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
$this->assertEqual($headers[0][':status'], $expected_ending_status);
$assert_session = $this->assertSession();
$assert_session->responseHeaderEquals('Location', $this->getAbsoluteUrl($expected_destination));
$assert_session->statusCodeEquals($status_code);
}
/**
* @inheritdoc}
* Visits a path and asserts that it is NOT a redirect.
*
* @param string $path
* The path to visit.
* @param int $status_code
* (optional) The expected HTTP status code. Defaults to 200.
*
* @throws \Behat\Mink\Exception\ExpectationException
*/
protected function drupalHead($path, array $options = [], array $headers = []) {
// Always just use getAbsolutePath() so that generating the link does not
// alter special requests.
protected function assertNoRedirect($path, $status_code = 200) {
$url = $this->getAbsoluteUrl($path);
$out = $this->curlExec([CURLOPT_NOBODY => TRUE, CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $headers]);
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
$this->getSession()->visit($url);
if ($this->dumpHeaders) {
$this->verbose('GET request to: ' . $path .
'<hr />Ending URL: ' . $this->getUrl() .
'<hr />Headers: <pre>' . Html::escape(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>');
}
$assert_session = $this->assertSession();
$assert_session->statusCodeEquals($status_code);
$assert_session->responseHeaderEquals('Location', NULL);
$assert_session->responseNotContains('http-equiv="refresh');
$assert_session->addressEquals($path);
return $out;
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
}
}
......@@ -94,7 +94,7 @@ class RedirectUITest extends BrowserTestBase {
$redirect = $this->repository->findMatchingRedirect('node_test_alias', [], Language::LANGCODE_NOT_SPECIFIED);
$this->assertTrue(empty($redirect));
\Drupal::service('path.alias_manager')->cacheClear();
\Drupal::service('path_alias.manager')->cacheClear();
$redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', [], Language::LANGCODE_NOT_SPECIFIED);
$this->drupalGet('node/' . $node->id() . '/edit');
......
......@@ -147,7 +147,7 @@ class RedirectJavascriptTest extends WebDriverTestBase {
$this->assertRaw(
t(
'The source path %path is likely a valid path. It is preferred to <a href="@url-alias">create URL aliases</a> for existing paths rather than redirects.',
['%path' => 'node', '@url-alias' => Url::fromRoute('path.admin_add')->toString()]
['%path' => 'node', '@url-alias' => Url::fromRoute('entity.path_alias.add_form')->toString()]
)
);
......
......@@ -16,7 +16,7 @@ class PathRedirectTest extends MigrateDrupalTestBase {