Problem/Motivation
When the machine name is the same as an existing one and ajax is used a fatal error happens.
This occurs because the default value is changed after the AJAX submission to have the value the user entered. The user entered value becomes the default value because the form's entity is created from values in form state when the form is rebuilt. See \Drupal\Core\Entity\EntityForm::afterBuild()
- which eventually calls \Drupal\Core\Entity\EntityForm::copyFormValuesToEntity()
. And then in \Drupal\Core\Render\Element\MachineName::validateMachineName() we do if ($element['#default_value'] !== $element['#value']) {
to detect if the exists check needs to be triggered. Because the $element['#default_value']
is now the new value value we don't do the exists check.
Steps to reproduce:
- Go to /admin/config/content/formats/add
- Set name with basic_html
- Change text editor to CKEditor
- Save.
- See WSOD with
Drupal\Core\Entity\EntityStorageException: 'filter_format' entity with ID 'basic_html' already exists
Affected forms:
- \Drupal\filter\FilterFormatFormBase::form
- \Drupal\media\MediaTypeForm::form
- \Drupal\responsive_image\ResponsiveImageStyleForm::form
Potentially affected forms:
- \Drupal\action\ActionFormBase::form
- \Drupal\block_content\BlockContentTypeForm::form
- \Drupal\comment\CommentTypeForm::form
- \Drupal\contact\ContactFormEditForm::form
- \Drupal\field_ui\Form\EntityDisplayModeFormBase::form
- \Drupal\image\Form\ImageStyleFormBase::form
- \Drupal\menu_ui\MenuForm::form
- \Drupal\node\NodeTypeForm::form
- \Drupal\search\Form\SearchPageFormBase::form
- \Drupal\shortcut\ShortcutSetForm::form
- \Drupal\system\Form\DateFormatFormBase::form
- \Drupal\taxonomy\VocabularyForm::form
- \Drupal\user\RoleForm::form
- \Drupal\workflows\Form\WorkflowAddForm::form
- \Drupal\workspace\Form\WorkspaceForm::form
Proposed resolution
Set an explicit property on form state to revalidate when an error is set. (#86)
OR
Store the initial value in form state (#99)
Remaining tasks
Review
User interface changes
None
API changes
None. Both solutions use a form state property to more reliably do the exists check.
Data model changes
None
Comment | File | Size | Author |
---|---|---|---|
#118 | 2557299-118.patch | 17.36 KB | alexpott |
#118 | 116-118-interdiff.txt | 639 bytes | alexpott |
#116 | 2557299-116.patch | 17.37 KB | alexpott |
#116 | 110-116-interdiff.txt | 1.93 KB | alexpott |
#110 | 2557299-110.patch | 17.47 KB | alexpott |
Comments
Comment #2
larowlanComment #3
segi CreditAttribution: segi at Cheppers commentedI could reproduce the problem. I think I found a bug in the validation function, when the function try to check that there is similar format name or not.
The machine name was checked with not equal. I think we have to check the test as well.
This bug independents from ajax or it is a new bug?
Comment #4
segi CreditAttribution: segi at Cheppers commentedComment #6
larowlanComment #7
larowlanFailing test
Comment #9
larowlanSo the issue is the limit validation errors added by editor module means the machine name validation is discarded, but the default value is set, which means the next time around the machine name doesn't see the value as changed and doesn't run validation.
The fix is to also include the format in the validation.
Comment #10
larowlanComment #11
larowlanComment #13
LKS90 CreditAttribution: LKS90 commentedI fixed the test fails which suddenly appeared with the patch in EditorAdminTest.
The problem were missing required fields in the $edit array when using the selectUnicornEditor() function, I fixed it by adding a parameter to this helper function, the $edit array passed by reference. So an existing $edit array is modified by the function and used in it's drupalPostForm() call.
Comment #14
giancarlosotelo CreditAttribution: giancarlosotelo at MD Systems GmbH commentedWell the patch solved the problem with the editor but this problem is not only about that.
Every machine name verification that has an ajax call after is broken and also affects some modules e.g flag, monitoring.
The problem is bigger but I don't know if we should fix each module with a similar solution or try to fix this in core and probably in the
MachineName
class.Comment #15
segi CreditAttribution: segi at Cheppers commentedI think we have to fix it somewhere deeper maybe in MachineName class.
Comment #16
swentel CreditAttribution: swentel commentedYep, this is a general problem - I'd say this almost is critical worthy.
Comment #17
swentel CreditAttribution: swentel commentedMoving to forms component - I agree with #13 and #14 to look into forms system / MachineName class somewhere as this affects a lot of contrib and custom modules.
Comment #18
swentel CreditAttribution: swentel commentedChanging title as well
Comment #19
swentel CreditAttribution: swentel commentedWow, machine names and ajax requests are really fun. The validation is actually happening when the ajax request is triggered, but the error is not shown. Then usually during an ajax request, a form rebuild is triggered and '#value' in form_state is now populated and then the actual submission doesn't validate the name anymore since default_value and value are equal.
Another example how broken this behavior is: edit some existing entity, edit the machine name, trigger an ajax request, then change the machine name to the original, then hit submit. You'll get the "The machine-readable name is already in use. It must be unique." message. Granted, this is a situation that won't happen that much, but it's possible.
Been looking into a patch, but have been running against form api stuff. Will look further tomorrow.
Comment #20
xjmThanks for tagging this for rc target triage! To have committers consider it for inclusion in RC, we should add a statement to the issue summary of why we need to make this change during RC, including what happens if we do not make the change and what any disruptions from it are. We can add a section
<h3>Why this should be an RC target</h3>
to the summary. Sounds like it has a significant impact on contrib?Comment #21
swentel CreditAttribution: swentel commentedComment #22
swentel CreditAttribution: swentel commentedComment #23
swentel CreditAttribution: swentel commentedUpdate the issue summary.
Patch attached with proof of concept - it at least now works for facetapi & search api - hopefully core is fine and no other things break ..
Attaching fail only from larowlan + fix.
Comment #24
swentel CreditAttribution: swentel commentedwill fix when the test is back
Comment #27
swentel CreditAttribution: swentel commentedComment #29
swentel CreditAttribution: swentel commentedSigh
Comment #30
tim.plunkettHaven't looked at it manually yet, just the patch itself.
Can we use get/setTemporaryValue here?
Might as well use static::
Comment #31
borisson_I can't get it to work in the temporary values.
I changed this to
static::
overself::
though.Comment #32
borisson_This can't work with the temporary value store, because that is not accessible between requests.
Because this is currently being set in an ajax request, this will not work.
So I think this is ready.
Comment #33
Wim LeersI think @tim.plunkett needs to the final review, or another Form API maintainer.
Nit: extraneous newline.
Nit: s/Verify/Verifies/
Missing typehint:
array
.Needs FQCN.
Eh, not an interface methinks :)
Missing typehint:
bool
.If this is actually a Form API bug, then I think this should also have explicit test coverage in Form API tests. Otherwise it could easily regress in the future.
Comment #34
borisson_This fixes #33 1 - 6
We should still add a test form api test as well.
Comment #35
borisson_Added a new test. test-only also serves as an interdiff.
Comment #36
swentel CreditAttribution: swentel commenteds/name still/name is still
facet :)
Comment #37
borisson_Fixes remarks made in #36.
Comment #38
borisson_The test-only patch in #35 didn't fail, so the test isn't good enough. Back to NW.
Comment #39
borisson_I looked at this again and I can't figure out how to write a failing test-only patch.
Comment #40
xjmComment #43
mbovan CreditAttribution: mbovan at MD Systems GmbH commentedRerolled and small changes.
Tried to debug test-only case and it seems that
$form_state->getValue('id')
doesn't have a value when the form is rebuilt on an ajax request.Core usually uses
#machine_name
with an entity, so maybe we can think about extending the form test and replacing the form state value with a test entity ID, since the state can be preserved in that case.Comment #44
Ginovski CreditAttribution: Ginovski at MD Systems GmbH commentedTested with inmail and collect modules, works with both. +1 from me.
Comment #45
BerdirOk, this is a good one. @mbovan was on the right track.
The ajax callback uses limit validation errors, but that is interestingly not active during validation and in #after_build callbacks, which is where config entity forms update $this->entity. So I changed the test a bit to simulate behavior, with a validate callback that sets the value and then submit that rebuilds the form. I'm actually wondering if that isn't a flaw/bug in the form system but I have no idea how to prevent that. And after build actually even happens *before* validation, so we're technically building and keeping the entity which is based on unvalidated input. Weird but I think not actually a real issue since we do then run validation and don't do anything if there are validation errors
That makes it fail/pass as expected. It's not exactly like an entity form but I think that's OK since we also have test that are actually using entity forms and this is more understandable/reproducible than those. Real use case tests and a simplified, documented test that doesn't require an unhealthy amount of knowledge about the form *and* entity systems to know what's going on ;)
Comment #47
mbovan CreditAttribution: mbovan at MD Systems GmbH commentedSince #45 addresses #33.7 (Test fail in \Drupal\system\Tests\Form\FormTest), this look good to me.
Comment #48
tim.plunkettRight now this is the same as
if ($needs_revalidation || $element['#default_value'] !== $element['#value']) {
Where are the calls to isUnique where $set_needs_revalidation would be FALSE?
Missing .
The (optional) part goes in the description
Comment #49
BerdirComment #51
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commented#45 was not applying any more, rerolled
Comment #52
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commentedI've just tested this patch to see if it solves the issue with the ckeditor configuration page described in #2701393: Switching between editors on the format configuration causes errors upon save, but the bug persists.
Comment #55
Dave ReidFYI this can be worked around for now by forcing the #default_value to be NULL for any new entities:
Comment #56
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commentedRerolling again. Also switched to short array syntax the code we're adding to
CKEditorAdminTest.php
.Comment #58
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commentedOops!
Comment #60
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commentedAttempting to fix the failing tests...
Comment #61
darvanenI think #48 still needs to be addressed.
Comment #62
xjm#2934154: [PP-1] Remove workaround to enforce machine name validation on MediaType forms is a followup for media once the general fix lands.
Comment #64
Krilo_89 CreditAttribution: Krilo_89 at Synetic commentedThe patch from #60 fixes #2934154: [PP-1] Remove workaround to enforce machine name validation on MediaType forms when the workaround from #2932226: Media Type entities don't validate machine name properly is removed.
I added a test in #2934154: [PP-1] Remove workaround to enforce machine name validation on MediaType forms which fails the current workaround from #2932226: Media Type entities don't validate machine name properly. In the second patch I added the patch from #60 and removed the old workaround, to show a passing test.
And in the third patch I only included the test and removed the old workaround, so when this issue is committed, #2934154: [PP-1] Remove workaround to enforce machine name validation on MediaType forms can be committed too.
Comment #65
phenaproximaAddressed the feedback in #48 and re-rolled for 8.6.x (hence, no interdiff).
Let's get this in, so we can finally land #2934154: [PP-1] Remove workaround to enforce machine name validation on MediaType forms too.
Comment #66
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commentedComment #67
alexpottWhy add more public API? I don't think we should add this method because it's not necessary. It should just be part of the validateMachineName() as before.
Comment #68
phenaproximaFixed! Plus a little more minor cleanup. Let's see if this flies.
Comment #69
phenaproximaI have some questions myself, directed at those who understand this patch more than I do:
Comment #70
tim.plunkettWhat if there are multiple machine name elements per form? Wouldn't needs_revalidation be overwritten incorrectly?
I think the answer to both questions in #69 is "yes"
Comment #71
phenaproximaThis problem manifests very visibly in the Media module (specifically in MediaTypeForm), so I have adapted Media's existing functional JS tests to prove the problem, and its fix. I've also reverted the temporary workaround Media had implemented to hack past this problem in #2932226: Media Type entities don't validate machine name properly.
Given that, I'm not sure we need additional dedicated JS tests.
Comment #72
phenaproximaTagging as part of the Media Initiative.
Comment #73
phenaproximaSwitching to 8.6.x, since both patches in #71 were rolled against that.
Comment #75
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation for Pfizer, Inc. commentedThis should fix the failing test (
drupal_set_message
was deprecated https://www.drupal.org/node/2774931).Comment #78
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation for Pfizer, Inc. commentedHuh?
Not sure why it fails,
FormTestMachineNameValidationForm
extendsDrupal\Core\Form\FormBase
which uses theMessengerTrait
.. so it should work no?Comment #79
phenaproximaYou need to use $this->messenger() — the method — not $this->messenger, the property, since it is not explicitly set until $this->messenger() is called.
Comment #80
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation for Pfizer, Inc. commentedcrap, of course - thanks @phenaproxima!
Comment #82
alexpottThis issue is critical for Media and I would argue should be targeted for 8.6.x as now the oEmbed patch has landed there's going to be more media type creation.
Comment #83
alexpottThis change is odd. The interface does not do the validation. We should file a follow up to fix this. It is unrelated to the problem.
I wonder what happens if a form has multiple machine name fields. It looks like this won't work.
I think this test needs to be Javascript test to properly test AJAX.
Comment #84
tim.plunkettNice markdown :)
Do we usually link to the bug we're fixing when we fix it?
Stepped through this with @phenaproxima. This code goes back to the origin of machine name validation as a standalone concept.
However, I don't understand why we're checking #default_value at all.
My suggestion would be to switch the condition to
if (isset($element['#value'])) {
and always run the validation.When this test is rewritten as a Functional JS test, this extra button shouldn't be necessary
🤔 Can't see why we need this for the test.
Comment #85
phenaproximaOkay, this should address all of the feedback. Let's see if this flies.
Comment #86
alexpottThis won't work. Forms that are editing something with a machine name will always fail the existing check with this change. That's why the default value check is there.
Comment #87
alexpott@tim.plunkett asked for an interdiff back to #80
Comment #89
tim.plunkettI wish this were solvable in a more holistic way (aka rewriting how
exists
callbacks work) but that is out of scope.Thanks for the updated approach and test, @alexpott
Comment #90
mahtab_alam CreditAttribution: mahtab_alam at Valuebound commentedComment #91
alexpott@mahtab_alam what's the purpose of #90? It's missing the new files (all the tests).
Comment #92
phenaproximaI can't "officially" RTBC this since I worked on it recently, but +1 for RTBC of #86.
Comment #93
alexpottSo #90 is just #86 without the new files so I'm removing @mahtab_alam from the issue credit as #90 doesn't seem to change anything and there's no comment explaining the purpose.
Comment #94
alexpottI wonder if there's a better approach available. The problem is being caused by the fact that the default value is assumed to the initial value the form element was created with. This is not true for Ajax requests where the value before the submission becomes the new default value. So we can store the initial values and check that instead. That makes the whole thing a bit less opaque at least to me.
If @tim.plunkett as Forms API maintainer feels this is a bad path to pursue then we can revert to #86.
Comment #95
alexpottComment #97
alexpottFixing the tests. Can't use $element['#id'] because that changes on AJAX submission doh.
Comment #99
alexpottWhoops.
Comment #100
alexpott#2981332: Media type form and Filter format form set machine name #default_value incorrectly for new entities shows yet another solution that doesn't rely on state at all but fixes the default value setting.
Comment #101
alexpottI've re-written the issue summary to better explain what the problem is and its scale. I think due to the scale in core and probably contrib this issue is critical. People shouldn't be able WSOD their Drupal install via regular operation.
Comment #102
alexpottComment #103
alexpottThe steps to reproduce in the issue summary can be adapted for the responsive image case:
The website encountered an unexpected error. Please try again later.</br></br><em class="placeholder">Drupal\Core\Entity\EntityStorageException</em>: 'responsive_image_style' entity with ID 'narrow' already exists.
Comment #104
alexpottI don't think
$element['#name']
is a good key. It's possible that if multiple forms for the same type of entity are on the same page this might cause a problem. Therefore changing this to use$element['#parents']
.Comment #105
alexpottImproved the documentation.
Comment #106
alexpottSpent a little bit more time thinking about this. For me the real issue is that
\Drupal\Core\Entity\EntityForm::afterBuild()
results in an entity being set with invalid values because it occurs before form validation. I'm not sure this is the best behaviour but changing it feels very very difficult because the whole purpose is so that AJAX submissions get a updated$this->entity
to work with. Anyhow, I still think #105 is the best solution.Comment #107
martin107 CreditAttribution: martin107 as a volunteer commentedYep, I think that is correct.... at least #105 is a well through out solution.
So while looking over the code I saw just one a minor issue.
For the new test code I have removed a few calls to t().
From https://www.drupal.org/docs/8/phpunit/phpunit-browser-test-tutorial
Comment #108
phenaproximaOne thing that worries me about using $element['#parents'] as a key is that additional process callbacks later on might change it, which if I understand things correctly could trigger this bug again. I'm not sure how much of an edge case that is, but it's worth mentioning here. Perhaps this should be set in an after_build callback, or maybe #array_parents should be used instead of #parents? Or maybe we should generate an arbitrary hash to identify the element in form state? Or should we not even worry about this?
Comment #109
alexpottYeah I had a similar fear with using #name too. At least #parents allows fields with the same #name in the same form with different parents. But yeah maybe #array_parents would be better because that should not change.
Comment #110
alexpottMoved to using
#array_parents
and also merged the new JS test with an existing one that is fit for the same purpose.Comment #111
seanBManually tried to break it but it seemed to work as expected for several forms (node types/media types/text formats).
The tests look good, obsolete media code is removed and all feedback is addressed. I think it's done!
Comment #112
tim.plunkettWhat's wrong with #name? I see that discussed in #104, but I was fairly certain we now have separate form_states per form, so it wouldn't matter how many forms are on a page...
Comment #113
alexpott@tim.plunkett I think using #array_parents would allow someone to have two machine names with the same #name on the same form where they are using #tree to so they don't overwrite each others form values.
Comment #114
alexpottI've tried testing #113 but there are problems with forms like
$form['tree']['name'] = [
'#type' => 'textfield',
'#default_value' => 'blah',
];
That seem to be well beyond the scope of this issue. The default value from
$form['tree']['name']
is copied to$form['name']
?!?!So #name is probably okay as forms don't seem to work very well when there are duplicates but I'm not sure that we should rely on the fact that FAPI struggles with multiple elements with the same #name as that feels like a bug to me.
Comment #115
phenaproximaThe element #name, as I understand is, is the value that will appear in the HTML as <input name="...">. I'm pretty sure those can't be duplicates anyway, because otherwise the values submitted to the server would be fubar'ed. So #name should, in theory, work fine (I'd like a Form API expert to validate that statement, though). I also feel like #array_parents is also pretty safe Drupal-specific unique identifier, though, I don't feel terribly strongly about changing it to #name.
Comment #116
alexpott@tim.plunkett nothing is wrong with #name! I've overcomplicated it. I've just learnt when you #tree the parents get added to the name automatically so it's perfect for our use.
Comment #118
alexpottForgot about the unit test.
Comment #119
phenaproximaGlad we got that nailed down. Interdiffs look good to me, so back to RTBC once all backends are green.
Comment #120
tim.plunkettLooks great, thanks for that change. Signing off as a FAPI maintainer ✅
Comment #122
catchI think it might be worth a follow-up for EntityForm::afterBuild() - although whether it's actually fixable is a different matter. A couple of the comments in the test coverage are hard to read, but I could not think of a way to make them more readable, just a difficult thing to describe.
Comment #123
Wim LeersYay, this not only helps Media, but also the CKEditor module :)