Problem/Motivation
In #2767857: Add destination to edit, delete, enable, disable links in entity list builders we added destination query parameters to config entity lists and operations in Views. This unearthed the fact that some of our forms are not destination query compatible - we broke the image style user interface unexpectedly. See #2940165: [regression] Cannot add effects to image style via the UI.
The problem is that the destination param is set to win regardless of what a user does in the form. This means that any Drupal form can be redirected to somewhere using it.
As discussed in #2950759: Revert 2767857, right now, it is necessary to alter the global request object to disable a destination query arg based redirect override. That's complicated to understand and we shouldn't be doing something like that.
Older related issues are:
#579366: How many ways are there to redirect after form submission?
#1627654: drupal_redirect_form() should state that it is overridden by a destination in the request
#2325463: Destination URL breaks preview
Proposed resolution
We need a way to set a higher priority redirect on a form. So the developer can say actually regardless of the destination parameter go here because obeying the redirect makes no sense. The fix however shouldn't be tied to the forms system so the patch at the moment adds a new IgnoreDestinationRedirectReponse and the RedirectResponseSubscriber checks and doesn't apply the ?destination override if it is.
However since 90% of the redirects we are interested are from Forms the patch also adds helpers to FormState to make redirects behave like this.
FormStateInterface::getIgnoreDestination()
FormStateInterface::ignoreDestination($status)
Remaining tasks
Add more tests. One is currently provided by core/modules/image/src/Tests/ImageAdminStylesTest.php which proves this fixes #2940165: [regression] Cannot add effects to image style via the UI whilst bringing back the nice consistent UI behaviour of #2767857: Add destination to edit, delete, enable, disable links in entity list builders for Image Styles.
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #80 | interdiff-2950883-75-80.txt | 1.23 KB | acbramley |
| #80 | 2950883-80.patch | 15.32 KB | acbramley |
Issue fork drupal-2950883
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 2950883-allow-form-redirects
changes, plain diff MR !2678
Comments
Comment #2
berdirAh, I created #2950885: Add an API to disable to automatic destination-based redirect override, didn't see this.
I don't think it is or should be restricted to the form system, if anything, it could provide an accessor that would make it easier to use but that will also result in more dependencies in FormState.
Comment #3
alexpottThe patch attached reverts the fix from #2940165: [regression] Cannot add effects to image style via the UI and implements the ability for forms to say ignore the destination and trust my redirect using a IgnoreDestinationRedirectResponse class.
Comment #4
alexpott@Berdir it's not tied to the forms system it's just in other cases you return the IgnoreDestinationRedirectResponse directly.
Comment #5
alexpottMerged in #2950885: Add an API to disable to automatic destination-based redirect override and marked that as a dupe.
Comment #6
alexpottComment #7
andypostThere's another issue which using different way to override destination #2640672: Make redirect.destination service update request
Comment #8
alexpott@andypost yes but that issue is expressly to change the current behaviour ie. destination winning to redirect winning - whereas this issue is about let destination win most of the time apart from where it doesn't make sense.
Comment #10
mlncn commentedWorkaround:
Comment #11
trevorbradley commentedRe: @mlncn's workaround:
What if you want the page you're redirecting to to have the destination intact in the query?
I came at this issue from #2932050: The "Save & Edit" button redirects to admin/content using destination query. There, you want the destination to be temporarily overridden, but for the form submission to take you back to a form where the old destination is intact.
Comment #12
kristen polThanks for the patch. Although there was discussion about the patch, it didn't look like the code was actually reviewed, so I did that below. This still needs testing.
Nitpick: I know this is the same as the original code but I find empty catch clauses confusing. Consider adding a comment here.
Nitpick: "Determines" makes me think of a getter, not a setter. Consider changing the wording. Sets? Defines?
Nitpick: Comment is over 80 chars.
Nitpick: Might be good to explain that the class is empty intentionally and why.
It would be good to explain why this is being set for this form.
Comment #13
kristen polComment #15
m.lebedev commentedComment #16
panchoI'm coming from contrib issue #3046178: Temporarily override the original 'destination' query parameter and am wondering whether the
IgnoreDestinationRedirectReponsewould help with our case (and similar ones, such as Core's node entity) or if that's a separate issue.In the node case, the user coming from
admin/contentgoes tonode/1/edit(by hitting the edit button). Now onnode/1/edit, the user clicks 'Delete' but then decides to 'Cancel'.Now the user is sent all the way back to
admin/contentinstead of just going one step back tonode/1/edit.This is because the overview page appended itself as
?destination, while the node edit page preserved the?destinationrather than overriding it.While using a
IgnoreDestinationRedirectReponsein the confirm form to always send the user to the edit form would cover this particular case, it doesn't seem the right thing to do. Finally, we do rely on?destinationbeing properly set, as otherwise we don't know where the user comes from. They might have used the 'Delete' button on the overview form, so shouldn't always be sent to the edit form.Instead I would expect
node/1/edit:?destinationit inherited fromadmin/content?destinationcould be an array).Comment #17
tim.plunkettComment #19
bsztreha commentedJust did an update with 2950883-2.patch file to able to apply 8.8.x branch (also tested on 8.9.x too), attached new patch file
Comment #20
extexan commentedJust wanted to add this case where the patch didn't work...
I have a views bulk action that includes what is essentially a confirm form on steroids. The action has evolved into a rather complicated process, so it is now a 3-page multi-step form. At various stages, it needs to take the user either back to the original page from where they initiated the action, or redirect them to a node created in the submit for form step 1 (of 3). The views bulk action process automatically puts the "destination" arg in the URL, which I want to ignore in favor of what I am returning in the response.
It's failing to redirect the user to the newly created node, returning them, instead, to the original page (which is specified in the "destination=/path/to/original/page").
Via debug, I've tried to narrow it down to where it's failing. In the dispatch() method of ContainerAwareEventDispatcher, it goes through a foreach loop several times. The value of $event->response->response is correct (/sales/releases/release-12345) for several iterations through the loop. But at some point, that gets changed to the original page (/shipping/shipment-3456). I believe it is happening here:
...which is at line 111 in my Drupal 8.8.5 version, with the #19 patch above applied.
The value of the $definition array is as follows (this may be a partial rendering, and I may not have gotten the indentation right):
$definition = {array} [2]
service = {array} [2]
0 = "redirect_response_subscriber"
1 = "checkRedirectUrl"
callable = {array} [2]
0 = {Drupal\Core\EventSubscriber\RedirectResponseSubscriber} [3]
unroutedUrlAssembler = {Drupal\Core\Utility\UnroutedUrlAssembler} [3]
requestContext = {Drupal\Core\Routing\RequestContext} [11]
completeBaseUrl = "http://localhost"
*Symfony\Component\Routing\RequestContext*baseUrl = ""
*Symfony\Component\Routing\RequestContext*pathInfo = "/actions/release-containers"
*Symfony\Component\Routing\RequestContext*method = "POST"
*Symfony\Component\Routing\RequestContext*host = "localhost"
*Symfony\Component\Routing\RequestContext*scheme = "http"
*Symfony\Component\Routing\RequestContext*httpPort = {int} 80
*Symfony\Component\Routing\RequestContext*httpsPort = {int} 443
*Symfony\Component\Routing\RequestContext*queryString = "destination=/shipping/shipment-9194"
*Symfony\Component\Routing\RequestContext*parameters = {array} [0]
_serviceId = "router.request_context"
The important part of that is the queryString entry. That's the wrong destination.
I wasn't able to copy/paste the stack from my debugger, so I've attached a couple of screenshots instead.
I'm going to try @mincn's work-around (post #10), but hopefully this will help point someone toward a fix.
Comment #22
phenaproximaAdding #3033960: After adding media, return the user to the media listing they started from as a related issue, since it is also affected by this bug.
Comment #23
phenaproximaRegarding additional test coverage, we can easily adapt some of the coverage from #3033960-52: After adding media, return the user to the media listing they started from. It proves that EntityController::addPage() is also broken by the
destinationparameter when there is only one bundle of the entity being added.Comment #24
phenaproximaFixing a typo in the issue title.
Comment #25
phenaproximaFixing the title again to clarify the actual scope of this patch.
Comment #26
phenaproximaHere's a very basic test of this functionality, with a fail patch to prove the test is effective.
Comment #27
alexpottOne thing I'm wondering is what happens when we want the same functionality for any of the other sub classes of RedirectResponse. There are quite a few in core and probably more in contrib. I'm wondering if instead of implementing a class to do this we implement a switch on redirect_response_subscriber to disable destination overriding. And then inject this service in the form_submitter service. It's a little less elegant but we already have a tonne of state going on with FormState and it makes this fix generalisable to all redirect responses.
Comment #29
phenaproximaIMHO it makes sense to do this, but I think we should expose the switch on the redirect.destination service. My reasoning is that RedirectDestinationTrait is already widely used, so consuming classes wouldn't need to adjust their dependencies to inject an obscure event subscriber.
I wonder if something like this might make sense:
$this->getRedirectDestination()->set(FALSE), as an explicit way of saying "completely disable the redirect destination". Internally, the RedirectDestination service would then call a ignoreQueryParameter() method on RedirectResponseSubscriber. This approach has two main benefits I can see -- very little code needs to be adjusted to benefit from this, and backwards compatibility is maintained.Comment #31
douggreen commentedreroll of #26
Comment #32
raman.b commentedComment #34
acbramley commentedI've taken #26 and implemented #27 - no interdiff since it's a different approach and needed a re-roll.
I kind of agree with #29 although if RedirectDestination is calling something in RedirectResponseSubscriber anyway then we're really just putting an intermediate step inbetween for semantics because it feels weird to inject an event subscriber (fwiw I agree it feels kinda weird). That would then mean consumers could inject either service, making another "more than 1 ways to do the same thing" XD
Hiding patch #32 as it includes .orig files.
Comment #35
acbramley commentedFixed phpcs issues.
Comment #37
acbramley commentedFixed FormSubmitterTest and FormBuilderTest
Comment #38
acbramley commentedAlso adding a BC layer for the constructor changes, will wait for confirmation on this approach before creating the CR and updating the link.
Comment #39
idebr commentedReroll after #3229012: Fix copy-paste mistake in code comment was committed.
Comment #40
idebr commentedAnother reroll after #3130606: MockBuilder::setMethods is deprecated in PHPUnit8 and removed from PHPUnit10 was committed.
Comment #41
joachim commentedThis needs documentation.
This creates a 3-level hierarchy, AFAICT, from low to high priority
- the form's redirect
- the URL destination query parameter
- the form's redirect if the form really means it
At the very least, FormSubmitterInterface::redirectForm() needs a rewrite to explain this. It would be nice to see some overview docs about this too, say on RedirectDestination?
Comment #43
suresh prabhu parkala commentedJust a re-roll against the latest 9.4.x. Please review.
Comment #44
bceyssensIgnore destination will remove the destination completely, in some cases it would be better if it is passed on to the next page.
This could be useful in flows, like password reset. The user would complete all steps of the password reset flow and then be redirect to the destination he/she had on the login page.
Comment #45
jonathanshaw+1 for #11 / #44
Comment #49
akalam commentedCreated a new MR to reroll the patch #43 to the 9.5 branch
Comment #50
akalam commentedAdded a new commit trying to move the redirection to the target url, as suggested on #11 and #44.
Comment #52
emircanerkul commented#10 works well. But we can use this in EntityForm while waiting for a real solution accepted
$this->requestStack->getCurrentRequest()->query->remove('destination');Comment #54
smustgrave commented#3325167: Revisit the redirect to 'add block' form in the 'add block content' form appears to be blocked by this as well.
Comment #55
smustgrave commentedTried starting a 11.x branch but no luck so patch it is
I started from MR 2678
Updated the RedirectTest to check the path not destination (since it was ignored)
Updated typehinting for D10
Updated trigger_error message, still needs a change record
I undid the change for core/modules/image/src/ImageStyleListBuilder.php as I wasn't sure it was needed? If it is then the solution needs to be looked at because currently when adding an image style
1. The url is messed up with the destination parameter now appearing.
2. When saving an image style instead of going back to the individual style you get redirected to the whole styles page.
Had issues with the interdiff so just made a diff.
Comment #56
smustgrave commentedOr this approach where I add the changes back to ImageStyleListBuilder but in RedirectResponseSubscriber I don't add the destination paramter for item 55.1
Which also solved 55.2
Comment #57
lauriiiWhy do we need this 🤔
We can use property type declaration 😇
We could use property type declaration here too 😇
Wondering if this should be
setIgnoreDestinationgiven that this can be used for disabling this behavior too?Comment #58
smustgrave commented#57.1 = I'm trying to remember but lets see what happens without it.
#57.2 = added type
#57.3 = added type
#57.4 = changed all function calls to setIgnoreDestination
Comment #59
smustgrave commentedAdded back that snippet from 57.1
Seems to grab the URL minus the destination.
Also started the CR https://www.drupal.org/node/3375113
Comment #60
larowlanUpdated issue credits
Can we get a comment explaining what this is doing? It seems to parse the query, and then un-parse it and re-set it on the same url?
shouldn't this be ?RedirectResponseSubscriber
Comment #61
smustgrave commentedWill work on this tomorrow. If I don’t get to in 2 days anyone can have at it.
Assigning to myself as a submaintainer
Comment #62
smustgrave commentedLets try this.
Comment #63
smustgrave commentedAddress #60
Comment #64
acbramley commentedUpdated our patch for this issue during a 10.1 upgrade and something has broken with a mix of
$form_state->setRedirect()andsetIgnoreDestination()- now the form doesn't redirect at all, instead the page refreshes with an empty query string.Comment #65
acbramley commentedDebugging through it, somehow $target_url is becoming literally a ? after this line:
$target_url = substr($target_url, 0, strpos($target_url, '?')) . '?' . http_build_query($query);Where before the line, target_url is
http://127.0.0.1:8080/admin/content/service-status-messages/560/scheduled-transitionsand $query is empty.
I guess we need test coverage for this and only do this if $target_url contains a ?
Comment #66
acbramley commentedWeird cause this test case should be testing exactly thatActually no that's incorrect, it should be
$this->assertSession()->addressEquals($edit['destination']);EDIT 2: Just so it's clear (because calling something "destination" when it's actually the redirect URL in the context of this test is extremely confusing), we do this in the test form:
$form_state->setRedirectUrl(Url::fromUserInput('/' . $form_state->getValue('destination')));Comment #67
acbramley commentedAddressing this bug, looks like it was first introduced in #55
Comment #68
smustgrave commentedThe snippet was added in the MR to retain the destination in the url
Comment #69
acbramley commentedFixed, also added coverage for when the redirect url itself contains a query param.
Comment #70
acbramley commented@smustgrave no, the MR still had the correct check in the test https://git.drupalcode.org/project/drupal/-/merge_requests/2678/diffs#62...
Comment #72
smustgrave commentedWas referring to $target_url = substr($target_url, 0, strpos($target_url, '?')) . '?' . http_build_query($query);
But #69 is all green and still working.
Would be nice to get this in as it's a blocker for a block_content redirect.
Comment #74
larowlanSorry if I'm being daft here, but I don't understand what this code is doing.
It seems to
a) get the query
b) if it contains a ? parse the query
c) recreate the same query
https://3v4l.org/XYAX5 - the body of queryManip is copy pasted from here
Comment #75
acbramley commentedConfirming #57, #60 and #74 - this code is redundant.
What it looks to be trying to achieve is what's described in #11 and #44 which while it might be nice, is IMO out of scope of this issue. If people want this functionality (retaining the destination query param on the redirected URL) then please file a follow up. That way we can discuss specific use cases and have a more targeted discussion.
Comment #76
smustgrave commentedAwesome!
Comment #78
WebbehFollow-up issue created: #3376832: Pass on desgination redirect when ignoring form redirects' ?destination query parameter
Comment #79
larowlanWe can typehint this
This needs an actual change record URL
Comment #80
acbramley commentedCR added https://www.drupal.org/node/3377297
Type and comment updated.
Comment #81
smustgrave commentedFeedback addressed
Comment #83
larowlanCommitted 92abbb5 and pushed to 11.x. Thanks!
Thanks all, we got there in the end.
Publishing both change records.
Comment #84
lauriiiOpened a minor follow-up for this: #3377882: \Drupal\Core\Form\FormSubmitter::__construct $redirect_response_subscriber missing default value.