diff --git a/core/core.services.yml b/core/core.services.yml index e84dfbb29eacd1ee7c4d2faa0153cbbf918bcb77..32e39e055eaa01eeaa04ffd761a26c6b6af09305 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -446,7 +446,7 @@ services: Drupal\Core\Form\FormValidatorInterface: '@form_validator' form_submitter: class: Drupal\Core\Form\FormSubmitter - arguments: ['@request_stack', '@url_generator'] + arguments: ['@request_stack', '@url_generator', '@redirect_response_subscriber'] Drupal\Core\Form\FormSubmitterInterface: '@form_submitter' form_error_handler: class: Drupal\Core\Form\FormErrorHandler diff --git a/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php index e950eae1bf331e6019ff7bcd1272f8b81319d42b..87863ef728739572b42d32bf0593c4ff6cf86c0b 100644 --- a/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php @@ -25,6 +25,13 @@ class RedirectResponseSubscriber implements EventSubscriberInterface { */ protected $unroutedUrlAssembler; + /** + * Whether to ignore the destination query parameter when redirecting. + * + * @var bool + */ + protected bool $ignoreDestination = FALSE; + /** * The request context. */ @@ -58,7 +65,7 @@ public function checkRedirectUrl(ResponseEvent $event) { // If $response is already a SecuredRedirectResponse, it might reject the // new target as invalid, in which case proceed with the old target. $destination = $request->query->get('destination'); - if ($destination) { + if ($destination && !$this->ignoreDestination) { // The 'Location' HTTP header must always be absolute. $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost()); try { @@ -133,6 +140,17 @@ protected function getDestinationAsAbsoluteUrl($destination, $scheme_and_host) { return $destination; } + /** + * Set whether the redirect response will ignore the destination query param. + * + * @param bool $status + * (optional) TRUE if the destination query parameter should be ignored. + * FALSE if not. Defaults to TRUE. + */ + public function setIgnoreDestination($status = TRUE) { + $this->ignoreDestination = $status; + } + /** * Registers the methods in this class that should be listeners. * diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php index 125aedb7d1cac7e02a28e454a1f3b78f7a864dfa..9c94c79d2c534ce50ea65e270cb2dc278b1826a0 100644 --- a/core/lib/Drupal/Core/Form/FormState.php +++ b/core/lib/Drupal/Core/Form/FormState.php @@ -136,6 +136,13 @@ class FormState implements FormStateInterface { */ protected $response; + /** + * Used to ignore destination when redirecting. + * + * @var bool + */ + protected bool $ignoreDestination = FALSE; + /** * Used to redirect the form on submission. * @@ -1057,6 +1064,21 @@ public function getRedirect() { return $this->redirect; } + /** + * {@inheritdoc} + */ + public function setIgnoreDestination(bool $status = TRUE) { + $this->ignoreDestination = $status; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getIgnoreDestination(): bool { + return $this->ignoreDestination; + } + /** * Sets the global status of errors. * diff --git a/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php b/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php index 2668f59a41ec03e97c131c5298c737381c484b6e..69ce349bb1d3ed1e7c8e93ba549ac9330796566e 100644 --- a/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php +++ b/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php @@ -611,6 +611,20 @@ public function getRedirect() { return $this->decoratedFormState->getRedirect(); } + /** + * {@inheritdoc} + */ + public function setIgnoreDestination(bool $status = TRUE) { + return $this->decoratedFormState->setIgnoreDestination($status); + } + + /** + * {@inheritdoc} + */ + public function getIgnoreDestination(): bool { + return $this->decoratedFormState->getIgnoreDestination(); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php index c6dd7353d08a53c2fb2c2bc15f046b98a154dc26..f1b459729da2f68568cbfb705667bc98a7debb1d 100644 --- a/core/lib/Drupal/Core/Form/FormStateInterface.php +++ b/core/lib/Drupal/Core/Form/FormStateInterface.php @@ -158,6 +158,26 @@ public function setRedirectUrl(Url $url); */ public function getRedirect(); + /** + * Determines whether the redirect respects the destination query parameter. + * + * @param bool $status + * (optional) TRUE if the redirect should take precedence over the + * destination query parameter. FALSE if not. Defaults to TRUE. + * + * @return $this + */ + public function setIgnoreDestination(bool $status = TRUE); + + /** + * Gets whether the redirect respects the destination query parameter. + * + * @return bool + * TRUE if the redirect should take precedence over the destination query + * parameter. + */ + public function getIgnoreDestination(): bool; + /** * Sets the entire set of arbitrary data. * diff --git a/core/lib/Drupal/Core/Form/FormSubmitter.php b/core/lib/Drupal/Core/Form/FormSubmitter.php index b5638ce9578c82437d748bb5d88a0975f5af5d29..ad64b30e5e0e51d671352d9bca536fb7e08b1ce0 100644 --- a/core/lib/Drupal/Core/Form/FormSubmitter.php +++ b/core/lib/Drupal/Core/Form/FormSubmitter.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Form; +use Drupal\Core\EventSubscriber\RedirectResponseSubscriber; use Drupal\Core\Url; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RequestStack; @@ -27,6 +28,13 @@ class FormSubmitter implements FormSubmitterInterface { */ protected $requestStack; + /** + * The redirect response subscriber. + * + * @var \Drupal\Core\EventSubscriber\RedirectResponseSubscriber + */ + protected RedirectResponseSubscriber $redirectResponseSubscriber; + /** * Constructs a new FormSubmitter. * @@ -34,10 +42,17 @@ class FormSubmitter implements FormSubmitterInterface { * The request stack. * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator * The URL generator. + * @param \Drupal\Core\EventSubscriber\RedirectResponseSubscriber|null $redirect_response_subscriber + * The redirect response subscriber. */ - public function __construct(RequestStack $request_stack, UrlGeneratorInterface $url_generator) { + public function __construct(RequestStack $request_stack, UrlGeneratorInterface $url_generator, ?RedirectResponseSubscriber $redirect_response_subscriber) { $this->requestStack = $request_stack; $this->urlGenerator = $url_generator; + if (is_null($redirect_response_subscriber)) { + @trigger_error('Calling ' . __CLASS__ . '::__construct() without the $redirect_response_subscriber argument is deprecated in drupal:10.2.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/3377297', E_USER_DEPRECATED); + $redirect_response_subscriber = \Drupal::service('redirect_response_subscriber'); + } + $this->redirectResponseSubscriber = $redirect_response_subscriber; } /** @@ -122,6 +137,8 @@ public function executeSubmitHandlers(&$form, FormStateInterface &$form_state) { public function redirectForm(FormStateInterface $form_state) { $redirect = $form_state->getRedirect(); + $this->redirectResponseSubscriber->setIgnoreDestination($form_state->getIgnoreDestination()); + // Allow using redirect responses directly if needed. if ($redirect instanceof RedirectResponse) { return $redirect; diff --git a/core/modules/image/src/Form/ImageStyleEditForm.php b/core/modules/image/src/Form/ImageStyleEditForm.php index 5a7ad036f8ffea39b2209b3127d32196a2f1c627..7d3c3206961d170290f91ec8b7d1080957140262 100644 --- a/core/modules/image/src/Form/ImageStyleEditForm.php +++ b/core/modules/image/src/Form/ImageStyleEditForm.php @@ -220,6 +220,7 @@ public function effectSave($form, FormStateInterface $form_state) { ], ['query' => ['weight' => $form_state->getValue('weight')]] ); + $form_state->setIgnoreDestination(); } // If there's no form, immediately add the image effect. else { diff --git a/core/modules/image/src/ImageStyleListBuilder.php b/core/modules/image/src/ImageStyleListBuilder.php index 41d9220b899a951ef0647d22f4f2bd325ed2cd8e..00a0ec1970291b8a42b8271d59c18cefd9bae2ae 100644 --- a/core/modules/image/src/ImageStyleListBuilder.php +++ b/core/modules/image/src/ImageStyleListBuilder.php @@ -39,17 +39,9 @@ public function getDefaultOperations(EntityInterface $entity) { 'url' => $entity->toUrl('flush-form'), ]; - $operations = parent::getDefaultOperations($entity) + [ + return parent::getDefaultOperations($entity) + [ 'flush' => $flush, ]; - - // Remove destination URL from the edit link to allow editing image - // effects. - if (isset($operations['edit'])) { - $operations['edit']['url'] = $entity->toUrl('edit-form'); - } - - return $operations; } /** diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php index 3df14d4a0436bb5c988ace0ee1bf0d98e511a2f2..e032bb04fae042711c043f5cd7ba17c338a4dc0e 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php @@ -37,6 +37,15 @@ public function buildForm(array $form, FormStateInterface $form_state) { ], ], ]; + $form['ignore_destination'] = [ + '#type' => 'checkbox', + '#title' => t('Ignore destination query parameter'), + '#states' => [ + 'visible' => [ + ':input[name="redirection"]' => ['checked' => TRUE], + ], + ], + ]; $form['submit'] = [ '#type' => 'submit', '#value' => t('Submit'), @@ -55,6 +64,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // @todo Revisit this in https://www.drupal.org/node/2418219. $form_state->setRedirectUrl(Url::fromUserInput('/' . $form_state->getValue('destination'))); } + $form_state->setIgnoreDestination((bool) $form_state->getValue('ignore_destination')); } else { $form_state->disableRedirect(); diff --git a/core/modules/system/tests/src/Functional/Form/RedirectTest.php b/core/modules/system/tests/src/Functional/Form/RedirectTest.php index ab2cb865ded10056b61c5c48f24daea7293cf771..4dd2490514c9411ee56c56b1ea4093f4d94876dc 100644 --- a/core/modules/system/tests/src/Functional/Form/RedirectTest.php +++ b/core/modules/system/tests/src/Functional/Form/RedirectTest.php @@ -78,15 +78,34 @@ public function testRedirect() { $this->assertSession()->addressEquals($path); // Test redirection back to the original path with query parameters. - $edit = [ - 'redirection' => TRUE, - 'destination' => '', - ]; $this->drupalGet($path, $options); $this->submitForm($edit, 'Submit'); // When using an empty redirection string, there should be no redirection, // and the query parameters should be passed along. $this->assertSession()->addressEquals($path . '?foo=bar'); + + // Test basic redirection, ignoring the 'destination' query parameter. + $options['query']['destination'] = $this->randomMachineName(); + $edit = [ + 'redirection' => TRUE, + 'destination' => $this->randomMachineName(), + 'ignore_destination' => TRUE, + ]; + $this->drupalGet($path, $options); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->addressEquals($edit['destination']); + + // Test redirection with query param, ignoring the 'destination' query + // parameter. + $options['query']['destination'] = $this->randomMachineName(); + $edit = [ + 'redirection' => TRUE, + 'destination' => $this->randomMachineName() . '?foo=bar', + 'ignore_destination' => TRUE, + ]; + $this->drupalGet($path, $options); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->addressEquals($edit['destination']); } /** diff --git a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php index 6d98cd00a76e3bcb8ff572f0d2914d1d681b71f9..a7d8157ff80252ae849c78d436de1580eb5bdf00 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\Core\Form; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\EventSubscriber\RedirectResponseSubscriber; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; @@ -35,6 +36,11 @@ class FormSubmitterTest extends UnitTestCase { */ protected $unroutedUrlAssembler; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\Core\EventSubscriber\RedirectResponseSubscriber + */ + protected $redirectResponseSubscriber; + /** * {@inheritdoc} */ @@ -42,6 +48,7 @@ protected function setUp(): void { parent::setUp(); $this->urlGenerator = $this->createMock(UrlGeneratorInterface::class); $this->unroutedUrlAssembler = $this->createMock(UnroutedUrlAssemblerInterface::class); + $this->redirectResponseSubscriber = $this->createMock(RedirectResponseSubscriber::class); } /** @@ -258,7 +265,7 @@ protected function getFormSubmitter() { $request_stack = new RequestStack(); $request_stack->push(Request::create('/test-path')); return $this->getMockBuilder('Drupal\Core\Form\FormSubmitter') - ->setConstructorArgs([$request_stack, $this->urlGenerator]) + ->setConstructorArgs([$request_stack, $this->urlGenerator, $this->redirectResponseSubscriber]) ->onlyMethods(['batchGet']) ->getMock(); } diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php index f10f79be833cc362ac43d5c8eb5693e76693826f..a7d3c812200175163f3e0cc67a6c05ae6871cf55 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php +++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\Core\Form; use Drupal\Component\Utility\Html; +use Drupal\Core\EventSubscriber\RedirectResponseSubscriber; use Drupal\Core\Form\FormBuilder; use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormState; @@ -135,6 +136,11 @@ abstract class FormTestBase extends UnitTestCase { */ protected $logger; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\Core\EventSubscriber\RedirectResponseSubscriber + */ + protected $redirectResponseSubscriber; + /** * The mocked theme manager. * @@ -156,6 +162,7 @@ protected function setUp(): void { $this->formCache = $this->createMock('Drupal\Core\Form\FormCacheInterface'); $this->cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface'); $this->urlGenerator = $this->createMock('Drupal\Core\Routing\UrlGeneratorInterface'); + $this->redirectResponseSubscriber = $this->createMock(RedirectResponseSubscriber::class); $this->classResolver = $this->getClassResolverStub(); @@ -182,7 +189,7 @@ protected function setUp(): void { $form_error_handler = $this->createMock('Drupal\Core\Form\FormErrorHandlerInterface'); $this->formValidator = new FormValidator($this->requestStack, $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $form_error_handler); $this->formSubmitter = $this->getMockBuilder('Drupal\Core\Form\FormSubmitter') - ->setConstructorArgs([$this->requestStack, $this->urlGenerator]) + ->setConstructorArgs([$this->requestStack, $this->urlGenerator, $this->redirectResponseSubscriber]) ->onlyMethods(['batchGet']) ->getMock(); $this->root = dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)), 2);