If NestedArray::setValue() is called with an array structure that does not support adding the value at the given position, the page crashes with a
Warning: Illegal string offset 'id' in Drupal\Component\Utility\NestedArray::setValue()
and
Fatal error: Cannot create references to/from string offsets nor overloaded objects in /[..]/core/lib/Drupal/Component/Utility/NestedArray.php
The method does have a $force parameter which can prevent such situations.
But in some cases it is called without this parameter.
E.g. in FormBuilder::handleInputElement() we find this code:
NestedArray::setValue($form_state->getUserInput(), $element['#parents'], NULL);
For me this blew up when I changed a form structure in PHP, and then resubmitted the form.
So it could happen when a user resubmits a form at the moment of a deployment.
It's a bit rare maybe but still..
A way to artificially reproduce this with custom code:
$array = ['x' => 'X'];
$parents = ['x', 'y'];
$value = 5;
$force = false;
\Drupal\Component\Utility\NestedArray::setValue($array, $parents, $value, $force);
My main point here is that the NestedArray::setValue() method is not programmed in a robust way.
It has no well-defined behavior in a case like above.
My first solution would be to let it throw an exception instead.
E.g. like this:
public static function setValue(array &$array, array $parents, $value, $force = FALSE) {
$ref = &$array;
foreach ($parents as $i => $parent) {
// PHP auto-creates container arrays and NULL entries without error if $ref
// is NULL, but throws an error if $ref is set, but not an array.
if (NULL !== $ref && !is_array($ref)) {
if ($force) {
$ref = [];
}
else {
$conflict_parents = [];
foreach ($parents as $j => $theparent) {
if ($i === $j) {
break;
}
$conflict_parents[] = $theparent;
}
$parents_str = '$[' . implode('][', $parents) . ']';
$conflict_parents_str = '$[' . implode('][', $conflict_parents) . ']';
$type_at_ref = gettype($ref);
throw new \RuntimeException("Tried to set nested value at $parents_str, but found a $type_at_ref at $conflict_parents_str.");
}
}
$ref = &$ref[$parent];
}
$ref = $value;
}
This may be a bit overkill, but most of the added logic is only executed if the exception is going to be thrown.
The logic takes into account that $parents might not be cleanly indexed.
In the above artificial example, the output would be:
RuntimeException: Tried to set nested value at $[x][y], but found a string at $[x]. in Drupal\Component\Utility\NestedArray::setValue()
This is already better, isn't it?
| Comment | File | Size | Author |
|---|---|---|---|
| #33 | setValue_function_case_error-2901127-33.patch | 7.27 KB | ahmad khader |
| #27 | 2901127-26.patch | 9.29 KB | neclimdul |
| #27 | 2901127-26.interdiff.txt | 2.8 KB | neclimdul |
| #21 | 2901127-20.patch | 6.84 KB | neclimdul |
| #18 | 2901127-18.patch | 4.18 KB | neclimdul |
Comments
Comment #2
donquixote commentedFrom the doc comment:
So yeah this is kinda "works as advertised". But I think this is a flawed design, and simply letting PHP crash for a predictable error is not the correct way of doing things.
Comment #3
dawehnerThis is a tough issue, isn't it? At least for me PHP is a language where being strict is hard, as its not designed for that, on purpose.
What about providing something like "setValueForced" / "setForcedValue" or so, which enables you to use that parameter easier / in a nicer way?
Comment #4
donquixote commentedI think modern PHP is moving more and more to opt-in strictness. I read the term "gradually typed language".
And modern frameworks more and more choose to use it in the strict way where possible.
But I don't think this is a language problem, in this case.
E.g. the following example could be in any language:
Instead we would do this:
So, instead of just letting the language crash natively, we throw an exception. Ideally with some contextual information, which can be logged or reported somewhere.
You mean dedicated functions? It would look nicer, perhaps.
But the fact remains that
FormBuilder::handleInputElement()calls this with$force = FALSE, which can cause WSOD depending on user input.So the questions I see:
- Why does FormBuilder call it with $force = FALSE?
- Would $force = TRUE result in undesired behavior?
- Should we throw an exception instead of letting the error just happen?
- If we do so, where would we catch this exception?
- Would changing any of this be a BC break in FormBuilder?
I can think of two reasons why this error would occur:
1. Some form components on server side are misbehaving -> programmer error.
2. The form receives POST data that does not have the expected structure. This could be due to misbehaving client-side components, or due to a code change on server side between form build and form submission, or because a malicious user is sending manipulated POST data.
Both of these cases are "exceptional", so we should let the developer know that something is wrong.
Comment #8
donquixote commentedEquivalent D7 issue: #2682997: Manipulated POST data can cause fatal errors
Comment #9
donquixote commentedThis behavior changed from PHP 5.6 to PHP 7.
https://3v4l.org/dDDi0 - check "eol versions".
For non-empty strings this was always broken.
Empty strings were treated the same as NULL in PHP 5.
Comment #10
liam morlandComment #15
neclimdulI'm trying to come up with a simplified test case but using this process from #3160406: Exposed forms with checkboxes don't work correctly if the checkbox value is in the URL I did trigger this.
/test-page?statusThe form builder is populating from user input(which did honestly surprise me coming from the query) and the input structure doesn't match the form structure because of the nesting triggering a fatal error.
Because this is a fully fatal error, repeatable out of the box, and doesn't require any weird user interactions like POST manipulation I've bumped this to major. Still sort of an edge case but easily repeatable crash.
Additionally in case its helpful, my case the trace ended up looking like this:
Comment #16
neclimdulTurns out this is pretty easy to recreate inside out test suite. Just adding a query to one of the exposed filters tests for views triggers it right away.
Here's a patch with some tests that _mostly_ fixes the problem based on the solution in the d7 patch but throwing an exception instead of triggering an error. I figure the exception will allow forms to fail a bit more gracefully though I haven't started digging into the validation logic to confirm that. The case where the value is set will still WSOD a site so we should still have a failure on this patch.
Comment #17
neclimdulA bit of a self review, open question. This preserves the BC with old version of PHP which treated the empty string as null. But had this been fixed earlier, that would have likely been removed with 9 when the requirements where raised. Additionally because it fails, this has never been supported with D9 so... is it really BC?
So either this patch needs to properly deprecate the behavior, remove it, or document why its needed. Leaning toward just removing it.
Comment #18
neclimdulWoops...
Comment #19
neclimdulComment #21
neclimdulFound a good test for the Form component of this problem on #2932604: Fatal error when providing a string value to a form field that expects an array value by Lendude. Added it here and merged the issues since they're the same problem.
Comment #25
neclimdulGoodness, this bug/hole in NestedArray trickled out to a lot of things. The config tests are even asserting and working around the PHP 8 in the test... weird.
Comment #27
neclimdulPulling in the fix for config adjusted to use the exception.
Comment #28
karavkars commentedI was getting error when passing filter value in the url for multiselect exposed filter.
I could solve this with small alteration in the url parameter,
posting this if you find helpful.
Drupal Version: 9.2.x
Problem:
URL- http://localhost.com/vews-page?field_tag_target_id=261
Error - Error: Cannot create references to/from string offsets in Drupal\Component\Utility\NestedArray::setValue() (line 155 of core\lib\Drupal\Component\Utility\NestedArray.php).
Solution:
Changed in url - http://localhost.com/vews-page?field_tag_target_id[261]=261
Comment #30
bwoods commentedThank you for this patch. My use case was a Views Exposed Filter was not being set due to a malformed link. I kept seeing this error in watchdog, but didn't see anything wrong with the filter itself. Since the Exposed Filter existed in the query string, but the value was blank, NestedArray blew up with an empty string. I was temporarily able to get it working by adding an empty check as an OR in the if $ref check, but this patch looks a lot more comprehensive.
Comment #33
ahmad khader commentedReroll to 10.2.x
Comment #34
ahmad khader commentedComment #35
ahmad khader commented