Problem/Motivation
Currently FormBuilder::retrieveForm()
examines the result returned by a form builder function. If it is a response, it will send it to the browser and exit. As per #2230121: Remove exit() from FormBuilder this behavior is incompatible with the HTTP kernel because it gives any form whether it is rendered in the main content area or in a block the power to completely break the normal request/response flow.
Proposed resolution
Trigger a E_USER_DEPRECATED
when form builder methods return response objects and fix affected forms in core - instead having them throw a EnforcedResponseException.
Add a EnforcedResponseException::redirectToUrl factory method to simplify this process
Remaining tasks
Get tests green
Reviews
Change notice
Release notes snippet
User interface changes
API changes
Comment | File | Size | Author |
---|---|---|---|
#86 | 2363189-86.patch | 11.43 KB | shivam-kumar |
#84 | 2363189-83.patch | 11.27 KB | NivethaSubramaniyan |
#83 | interdiff_81-82.txt | 1.05 KB | NivethaSubramaniyan |
#81 | 2363189-81.patch | 11.3 KB | larowlan |
#81 | 2363189-81-interdiff.txt | 9.53 KB | larowlan |
Comments
Comment #1
znerol CreditAttribution: znerol commentedLet's see how this breaks.
Comment #3
tim.plunkettWe used to have more of these. LanguageDeleteForm::buildForm() seems to be the only usage left.
Comment #4
tim.plunkettNeeded to update the coverage for sendResponse to use the other call to it now that we're removing this one.
Comment #6
tim.plunkettI don't think this is actually possible to prevent. We should have a dozen more failures than we do, which means missing test coverage.
Look at ViewsFormBase::getForm() and UserMultipleCancelConfirm::buildForm() and explain how we're going to fix those.
Instead, we need to try again at allowing forms to *properly* redirect when they are functioning as controllers.
If that means that FormBuilder needs a separate
buildFormAsController
method that is called from Drupal\Core\Controller\FormController, then so be it.Comment #7
tim.plunkettHere are some more fixes and test coverage. This should fail.
And I haven't touched ViewsFormBase yet.
Comment #9
tim.plunkettMade a mistake with that, ViewsFormBase doesn't use _form, it's _content.
Comment #10
Crell CreditAttribution: Crell commentedIn IRC I suggested an extra interface with a dedicated method that forms can implement. If they do, then when used as a controller (only) that is called before buildForm() is, and it can either return a (redirect?) response (in which case buildForm is never called) or null, in which case buildForm() is called. That has the added advantage of separating the "should this form even happen" logic from the form definition itself.
While it may result in a little code duplication, I suspect factoring the code in the form class to separate methods will eliminate most of it, enough that I'm not worried about it.
Comment #12
tim.plunkettThis needs work and I don't have the time to work on it, but this might help.
Comment #13
dawehnerWell, we could convert many of those instances to use a custom controller which uses a form controller and set the redirect outside of it.
Comment #14
znerol CreditAttribution: znerol commentedThen we also should prevent that forms implementing that interface are rendered outside their controller. Because then the method will not be called and that may lead to weird issues.
I'd rather go for #13, there are only a couple of instances in core which are easy to convert I guess.
Comment #15
znerol CreditAttribution: znerol commentedA quick and dirty way to find all form builder functions which potentially attempt to redirect / return a result. Apply the script on a fresh clone (not your usual working directory),
sed
will remove anything from the source files outside the form builder functions.This list includes some false positives, obviously
FormBuilder
and alsoPerformanceForm
. The latter matches because of'#default_value' => $config->get('response.gzip'),
. I am unsure aboutNegotiationSessionForm
andNegotiationUrlForm
, they unconditionally call$form_state->setRedirect('language.negotiation');
.Comment #16
tim.plunkettHere it is without the additional interface.
Comment #18
tim.plunkettThat's important :)
Plus a 25K patch is much better than a 24.99K patch
Comment #19
Crell CreditAttribution: Crell commentedReplace "Array" with "Form array structure" to be clearer about the data "type" that is expected.
Using _content here is fine by me, too. Although, if we're going to put getForm() on the form class itself would it make sense to standardize that, much as we do create() on controllers? Which... becomes the interface suggestion all over again? :-)
Comment #20
tim.plunkettHere's a completely different approach. It doesn't require module authors to do anything differently for the 99.99% use case.
Comment #21
tim.plunkettComment #22
tim.plunkettThanks for the substantive reviews.
Comment #23
znerol CreditAttribution: znerol commentedAs per the @todo introduced in #2230121: Remove exit() from FormBuilder, it is still necessary to implement #18. We cannot continue to allow form builder methods to return responses.
Comment #24
znerol CreditAttribution: znerol commentedReroll #18.
Comment #25
znerol CreditAttribution: znerol commentedUse the identity operator (
===
).Not sure whether this is good UX. What about displaying a message that the selection is empty and also hide the submit button?
I like that approach much better than the one above.
Same concerns as above.
Same concerns as above.
It looks like this is only invoked if the form was retrieved from the non-js route. Do we have test-coverage for that case? It looks to me that this should fail with an
InvalidValueException
because this looks like a form builder function.FormBuilder::sendResponse()
does not exist anymore. I think this test is obsolete now.Comment #27
andypostSuppose access checker should do that!
That needs a code comment, also 403 is a strange way to break flow.
Maybe better to wrap that logic into controller?
good trick
I think it would be great to add some usable error message here and other similar places.
Comment #28
dawehnerLet's get some push behind this again. First let's someone reroll it.
Comment #29
martin107 CreditAttribution: martin107 commentedOne trivial conflict to resolve, lots of merging and auto-merging going on
That it installs is all I can guarentee ... let us see what testbot says
Comment #31
martin107 CreditAttribution: martin107 commentedI can see a way to resolve at least some of the broken tests
the function definition of
FormBuilderInterface::submitForm() has changed since this patch was last modified ...
I will fix this up.
Comment #32
tim.plunkettThis needs the "beta evaluation" criteria added.
I don't see how this should block release.
Comment #33
znerol CreditAttribution: znerol commentedThen we should at least document that returning responses from form builders is deprecated before release. Otherwise contrib will likely pick up that pattern and fixing post-release will be kind of an API change.
Comment #34
almaudoh CreditAttribution: almaudoh commentedHEAD now uses
_controller
instead of_content
Should this not have a message explaining why access is denied? Is that the right Exception to throw?
Comment #36
almaudoh CreditAttribution: almaudoh commentedCross-post
Comment #37
martin107 CreditAttribution: martin107 commentedLooking at FormBuilderTest
Stepping through the code, the mocks in testSendResponse() expect the dispatch and terminate events to fire. This is the reported error.
Actually what is happening in FormBubmitter::doSubmitForm() a response is set
and then FormBuilder::getForm() throws and error diverting the normal flow of code out of balance.
So this is interlinked with this issue...
#2367555: Deprecate EnforcedResponseException support
It may take sometime to find a common way forward.
Please ignore my comment from #31 .. I will file a separate issue.
Comment #38
martin107 CreditAttribution: martin107 commentedFrom #25
Yes.. wish I had seen that earlier :)
I also fixed the others from #25 all except 6.
This look a better way to control error paths, in the short term the error count should increase .. but not too much!
Comment #40
martin107 CreditAttribution: martin107 commentedI typed 'action' when I meant 'actions' so the submit buttons were not deleted, In these corner cases when a malformed/hacked form is submitted.
I have simplified the text I added in #38, and made the message text better. I am providing a sample screenshot to show the response
Comment #42
martin107 CreditAttribution: martin107 commented1) #34.1 as per https://www.drupal.org/node/2378809 now using _controller
2) SimpleResultsForm::getForm - should be on an interface but is not. Just providing an initial annotation,
Something bugs me about it that I cannot articulate - documenting it oftens helps.... the return complex type seems inelegant
[ This is just a note to myself to revisit it. :) ]
Comment #44
martin107 CreditAttribution: martin107 commentedUninstallTest now passes ... so this should be back to green.
I thought this logic, from \Drupal\system\Form\ModulesUninstallConfirmForm was being exercised...
turns out this has the last word... this is fine by me. ( \Drupal\system\Form\ModulesUninstallForm )
Comment #45
martin107 CreditAttribution: martin107 commentedAdded test to cover the new code in \Drupal\system\Form\ModulesUninstallConfirmForm()
Comment #46
martin107 CreditAttribution: martin107 commentedAdded tests to cover the new code in \Drupal\system\Form\ModulesListConfirmForm.
plus a couple of trivial tweaks.
Comment #49
martin107 CreditAttribution: martin107 commentedComment #50
rpayanmComment #51
Mile23Comment #52
Mile23Comment #53
neetu morwani CreditAttribution: neetu morwani as a volunteer commentedComment #55
lokapujyaThe point of this issue is that from builders should not return response objects. So, I am adding back the conversions that were removed in the reroll.
Comment #57
tim.plunkettNow that 8.0.x is out, we can't add an exception there anymore.
Also I understood the major-ness of removing exit, but now with \Drupal\Core\Form\EnforcedResponse and \Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber, is this really that major anymore?
Comment #67
geek-merlinHere's the simplest possible stab to deprecate this. Let's see what breaks.
Comment #68
geek-merlinComment #70
geek-merlinVerbosified message.
Comment #72
geek-merlinGot em all?
Comment #79
larowlanWe'll need to update these to 10.1 and 11.0 now
All the test fails are because we're passing a Url instead of a Response, which I think means all of the Url objects need to wrapped in a RedirectResponse - so maybe its worth adding a new factory method on
EnforcedResponseException
e.gthrow new EnforcedResponseException::redirectToUrl($url)
Comment #80
larowlanUpdated issue summary
Comment #81
larowlanAdded the factory method, hopefully this helps get tests greener
Comment #82
larowlanComment #83
NivethaSubramaniyan CreditAttribution: NivethaSubramaniyan as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedFixing CCF in #81
Comment #84
NivethaSubramaniyan CreditAttribution: NivethaSubramaniyan as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedFixing CCF
Comment #86
shivam-kumar CreditAttribution: shivam-kumar as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedFixed errors generated in #84. Have changed reference of formRoutes. It may help in error to be solved.
Comment #87
ayush.khare CreditAttribution: ayush.khare at OpenSense Labs for DrupalFit commentedComment #88
larowlanTests still failing