Problem/Motivation
Having a really long HTML tag (e.g. < img > with src:data/image...) makes preg_split fail and return false. On PHP 7.4 this throws a warning ("Warning: count(): Parameter must be an array or an object that implements Countable in _filter_url() (line 535 of core/modules/filter/filter.module).") and makes the field render empty, on PHP 8.0 this throws a fatal error.
Steps to reproduce
1) Have a text format that has Convert URLs into links enabled
2) Using that text format, add a node with content like this: https://gist.github.com/kporras07/618b3bf4cd77ff57fcd5034262220e99
3) Visit the node
4) You will get the warning and empty node or the fatal error depending on your PHP version
Proposed resolution
If $chunks is empty, keep $text.
Remaining tasks
1) Provide a patch
2) Review the patch
3) Commit :)
User interface changes
None
API changes
None
Data model changes
None
Release notes snippet
Probably not needed
| Comment | File | Size | Author |
|---|
Issue fork drupal-3239472
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:
Comments
Comment #2
kporras07 commentedComment #4
cilefen commentedI think we have been marking PHP 8 incompatibilities as critical.
Comment #5
longwaveLet's add a test that triggers this behaviour.
Comment #6
danflanagan8Here's a first pass a fail test. I took the approach of adding a ton of classes to a p tag. It's maybe not realistic, but I personally like the simplicity.
It may be interesting to note that the test passed for me locally with
$classes = str_repeat('dum-class ', 999999);. So the test is right at the threshold, at least for my local setup.Also, what's the best thing to do when adding a fail test when there's an issue branch with the fix already?
Comment #8
danflanagan8Sweet. Failed as hoped:
Is the next step to commit this to the issue fork? (Assuming it's deemed a sufficient test)
Comment #9
longwaveYes, add the failing test to the issue fork, which should continue to pass if the fix is correct. The test looks good enough to me, it is enough to trigger the error that was originally reported.
Comment #10
danflanagan8The new test passes on the issue fork.
Comment #11
borutpiletic commentedExperiencing the same issue with base64 inline images.
Investigating the issue further I had to increase the
pcre.backtract_limitin order to get it working.You can use
preg_last_error_msg()orpreg_last_error()to get more precise cause for yourpreg_splitfaliure: https://www.php.net/manual/en/function.preg-last-error.phpSubmitting a patch to fix the produced errors if
preg_splitfails.Comment #12
tancThe patch @borutpiletic provided is a simpler solution (changes one line of code). I suggest committing that along with the test from @danflanagan8. Attached is a patch with both.
Comment #14
danflanagan8As the failed test showed, the approach in #11 differs from the original one in the MR in more than just style. They both prevent the fatal error but the fix in #11 ends up setting the text to an empty string.
That happens in this line, which isn't in the patch. It's near line 570.
$text = implode($chunks);where $chunks has been set to an empty array. I think the if-block is the way to go.
Comment #16
stefanos.petrakis@danflanagan8 is right in #14, the patches provided in #11 and #12 contain an error.
Instead of a review on the PR, I did some refactoring, it can be used as review material.
Most importantly:
- The test is now part of
::testUrlFilterContent()since this seemed to me better than introducing a new method.- The test is now attempting to explicitly break PHP's
pcre.backtrack_limitsince this seemed more accurate than attempting to figure out the limit by setting it very high.- The actual solution does not use an
ifcondition in order to avoid nesting (readability).Comment #17
stefanos.petrakisTypo
Comment #18
stefanos.petrakisI will get there eventually
Comment #20
danflanagan8@stefanos.petrakis, I really like your new test. Definitely better than the one I added. There are a few lines though that strike me as something we don't need to commit:
That's not running any Drupal code. It helped me understand what's going on, but I think we'd want to remove it before committing.
And the fix looks correct and has a really small diff, which is cool. The following line, though, I had to read it a few times.
I think it would be easier to read if the condition were not negated. That is, I think it would be easier to read as:
$text = $chunks ? implode($chunks) : $text;I can't personally RTBC this though since I contributed code earlier, so I won't insist on either change. I'm going to leave this as NR to let someone with RTBC power to give feedback.
Comment #21
stefanos.petrakisHey @danflanagan8; thanks for the feedback, I really followed all the codes you placed in the PR so far, so credit's on you really :-)
Let's do it like this and stick to the process:
1. consider my patch-work as a review on the current PR, so you can update the PR accordingly. For the record, I totally agree with the second part about the negated condition; the first part I could argue some, but I leave it up to you, it's more informative than crucial.
2. re-request a review when you feel the PR is in good shape
3. then I or anyone else can re-review and RTBC if there are no further points to discuss
If you agree, you can hide the patches I submitted so that the issue stays clean.
Comment #22
danflanagan8@stefanos.petrakis,
I pushed some commits to the issue fork. They're not showing up in the MR for some reason. I didn't create theMR so there are some permissions I don't have. The MR system is tough when someone creates the MR and then leaves the issue.
Maybe I'll have to create a new MR. I don't like MRs though so I would just as happily post a new patch. This is giving me indigestion so I'm going to step away for a bit.
Comment #23
stefanos.petrakisHey there, saw the commits, I can help with this next week, if you wanna go the patch way also fine, anything to avoid indigestion. :-p
Comment #24
sjerdoInstead of ignoring the error result of the regex, shouldn't we change the regex to something that allows the filters to work?
This might work for example:
This gives the same results for simple HTML: https://3v4l.org/jHa5m
However, I'm not sure if this captures invalid HTML elements correctly.
Also, I don't know what the effect will be on performance.
Edit: maybe the pattern should be changed to
(<[^>]+?>)instead, to allow <> elementsComment #25
stefanos.petrakisThis issue is caused by deep backtracking as mentioned in #11
Changing the regex cannot solve the issue; it will still potentially fail when feeding the regex with a very long string (as is the case of base64 encoded inline images).
Comment #26
sjerdoPossibly those changes should be combined then. I tested the patterns
/(<[^>]+?>)/isand/(<[^>]+>)/iswith the attached html. Both seem to work fine, in contrast to/(<.+?>)/iswhich fails.If the way to go is to check if a regex fails when pcre.backtrack_limit is set to 1, all regex patterns in Drupal Core should be checked for this error. Or we should provide an advised minimum value for installations..
Comment #27
stefanos.petrakisIMHO revising the regex pattern and possibly checking/optimizing similar patterns across the whole core is another issue.
My understanding is that this issue focuses on the code's behavior when
preg_splitencounters some failure (and returnsFALSE); I would assume it could fail in other ways apart from reaching the backtracking limit, but I think the tests provided allow to cover at least that type of failure as well as proving that the code behaves correctly whenFALSEis returned.@sjerdo: I would suggest you open a related issue that focuses on modifying/tuning this and possibly other regexes; and we keep this issue focused on refactoring the affected codes, let me know what you think!
Comment #28
sjerdoWell, that depends on the regex being used. I don't see the suggested regex pattern exceed a backtrack limit of 1.
For example, with the regex pattern I provided, the following code states no error occurred (int(0) / PREG_NO_ERROR):
Test: https://3v4l.org/EWJM3
Unlike the original pattern, which does result in an error (int(2) / PREG_BACKTRACK_LIMIT_ERROR)
Test: https://3v4l.org/cVJP1
In conclusion, it seems like the backtrack limit error check is superfluous for this method if the regex pattern is changed.
Can someone come up with a a test case in which the suggested pattern isn't sufficient?
Comment #29
stefanos.petrakisThanks @sjerdo!
I insist that improving the regex is another issue, complementary to this one but different.
This issue is about making the code more defensive regarding
preg_split.Even if the regex is improved, there is no guarantee
preg_splitwould never returnFALSE; this is the case that the issue is about AFAICT and the expanded test with minor refactoring (discussion in comments #17-#23) tries to tackle these.Comment #30
danflanagan8I finally got the courage to come back and try to figure out how to get my commits to show up in the MR. (See #22 regarding my indigestion.) I clicked "Rebase" and then some magic happened! So I'm setting this back to NR.
I agree with @stefanos.petrakis regarding the scope of this issue (from #29):
That's all that the MR does. No changes to any regex patterns.
That said, I'm always happy to be overruled by the community! Cheers!
Comment #32
stefanos.petrakisComment #34
stefanos.petrakisRe-rolled against 9.5.x in a new PR !2842 (don't know how it's possible to now close the outdated PR !1247).
Comment #36
kristen polTesting on 9.4 Umami profile and you have to make sure that the "Convert URLs into links" is enabled on the text format so updating summary.
Comment #37
kristen polI have tested with and without the patch on 9.4 and it is working as expected. I still need to test on 9.5 and 10. If successful there, this can be moved to RTBC based on:
Comment #38
kristen polManual testing on 9.5 (php 8.0) and 10 (php 8.1) was successful, so moving to RTBC based on this and #37. Thanks, everyone!
Comment #39
alexpottI think we can slightly change the code to make things a bit more obvious - see comment on MR. Which is always nice in filters and loops. Nice test and I agree with the approach.
Comment #40
stefanos.petrakisTargeting 10.1.x and picking up after review in PR. Coming up soon.
Comment #42
alexpottI changed the test to:
And this fails because
$text = preg_replace_callback('`<!--(.*?)-->`s', '_filter_url_escape_comments', $text);fails! So I think we have more things to actually fix here. We need to do something like:And then at the end of the loop do:
This would be way easier to fix if we could break up _filter_url into methods on \Drupal\filter\Plugin\Filter\FilterUrl - my guess is we had an issue somewhere to move this functionality into that class.
I think the additions to the test case should be:
That way we'll have test coverage of errors during preg_replace_callback and preg_split
Comment #43
stefanos.petrakisRight you are, I saw preg_replace_callback() breaking too , it seems that any preg_* function is a candidate for breaking when playing with the backtrack limit.
I went for a ternary default value when using
preg_split()andpreg_replace_callback()instead of the$saved_textidea. It seemed more compact but not too cryptic. And I would have had to add a little more coding to avoid deprecation warnings when passing $text=NULL topreg_split()andpreg_replace_callback()Comment #44
alexpottI went for the saved text approach for several reasons - apart from the one noted in the MR I don't want to have to think about how this behaves when the $text becomes something PHP would consider to be falsey - ie. "0"...
Comment #46
stefanos.petrakisAll righty then, the main MR (Merge request !2862) is now green following Alex's suggestions (thanks).
Also opened a related Task #3315489: Introduce composer/pcre (or similar) in order to handle preg_* functions failures that focuses on preg_* functions lacking some loud failing as we witnessed here.
Setting this to NR and I mean Merge request !2862 by that.
Comment #47
benjifisherI reviewed MR !2862 and tested on Drupal 9.4.8. The testing went well.
I have one nit about the code changes: the original version has a blank line before this loop:
The blank line is lost in the MR. I like it better with the blank line, but not enough to hold up this issue.
The other thing that bothers me is that the outer loop escapes comments and then restores them for each iteration:
Why not escape once, before the loop, and then restore them once, after the loop? That is out of scope for this issue, except that it would be easier to review if the MR did not re-indent 40 lines of code. Instead of
it could be
Again, I do not want to hold up this issue for a change that is arguably out of scope. If you decide to make those changes, then I will be happy to re-review.
Comment #48
larowlanLeft some comments around complexity, otherwise this is looking good.
We've not had a 'tests only' failing patch here, but I think the committer can run it locally given its a Kernel test to confirm it fails as expected.
Comment #49
larowlanActually, we have had a failing test only patch, see #17, ignore me - mixed up my issues
Comment #50
benjifisherI discussed this issue with @larowlan on Slack. We agreed that the changes suggested in #47 and #48 are out of scope for this issue. I am setting the status back to RTBC, and we can make those simplifications in #3325466: Reduce complexity in _filter_url().
Comment #51
benjifisherI meant to explain, in #50, why the suggestion in #48 (and on the MR) is out of scope. The suggested changes are to the code that I described in #47 as
Comment #52
benjifisherComment #53
elneto commentedThanks. Can confirm that patch #26 worked for me! I was getting this error "TypeError: count(): Argument #1 ($value) must be of type Countable|array, bool given in _filter_url() (line 539 of /web/core/modules/filter/filter.module)." when running this cron job: "Updates indexable active search pages".
I am running Drupal 9.4.9 with PHP 8.1.12 & Maria DB 10.4.27.
I could not replicate this error in my local or in a DEV environment. This only happened in PROD.
Comment #54
stefanos.petrakis@benjifisher: Thanks for the review and follow-up! Rebased Merge request !2862
@elneto: The current coding work for this issue lives in Merge request !2862
The current patch can be obtained from https://git.drupalcode.org/project/drupal/-/merge_requests/2862.diff
Comment #55
stefanos.petrakisSetting this back to NR since there was an unresolved thread (that I tried to resolve today), regarding deprecation notices, see the previous comment here.
Comment #56
benjifisherFirst of all, it is distracting to have 4 MRs open for this issue. Can we close some of them?
I find the latest version of MR 2862 confusing:
The doc block does not have any
@paramannotations, but I think$textis supposed to start as astring. Why are we testing for NULL at the start of the loop? At what point can$textever become NULL?Looking at the code, I think the answer is that
preg_replace_callback()can return NULL if there is an error. So$textmight be set to NULL at the start or the end of the loop.This means we are expanding the scope of this issue. The current issue summary (IS) and title only mention failures in
preg_split(). If we also want to handle failures inpreg_replace_callback(), then we should update at least the IS, maybe also the title. I am also happy to go back to the version of the code that was RTBC and keep the current scope.If
preg_replace_callback()fails, is there any way to recover? I am pretty sure that the current code ends up setting$textto an empty string, then returns$saved_text. It would be clearer to return$saved_textright away:Then do something similar at the end of the loop.
Also, the end of the function can be simplified. Instead of
just make it
Or maybe just
return $text, if we are confident that we have returned early in all problem cases.We could do more. For example, we could set
$saved_textat the start of the loop. That way, if the replacement fails for one link type, then we could start over with the next type. But I think we should keep the changes in this issue simple. Just fail gracefully instead of getting PHP errors. We can do more in the follow-up issues #3315489: Introduce composer/pcre (or similar) in order to handle preg_* functions failures and #3325466: Reduce complexity in _filter_url().Comment #57
benjifisherTwo more thoughts:
First, if we expand the scope to include failures in
preg_replace_callback(), then we should test that. It should be good enough to add some HTML comments to the test fixture (filter.url-input.txt).Second, it might be worth setting
$saved_textat the start of the loop, because then you can avoid re-indenting 40-ish lines of code, as I mentioned in #47. Remove all references to$saved_textoutside the loop, and thenThat code comment is for explanation here. I do not think you want to keep it. Whatever you do, do not copy my code snippets: I had to add a space in
'/(<.+? >)/is'so that it would not be seen as a closing PHP tag.Comment #61
claudiu.cristeaWorks as expected.
Comment #62
alexpottCommitted and pushed e7ded38076 to 10.1.x and 687473d5fc to 10.0.x and bcd7a66c05 to 9.5.x. Thanks!
Comment #66
joseph.olstadThanks for the above fix,
Just hit this with PHP 8.1 / D9.5.5 and search_api, I added fields with phonetic and spellcheck type in the search index, indexed content, it got half way and on some weird content indexing an exception occured as described in this patch declaration
using this patch until a tagged release comes:
"3239472 - PHP 8.1 filter when using spellcheck search api field type and phonetic, indexing came up with this TypeError: count(): Argument #1 ($value) must be of type Countable|array, bool given in _filter_url() (line 539 of core/modules/filter/filter.module": "https://git.drupalcode.org/project/drupal/-/commit/bcd7a66c05d470271f49939cf5e6925892041da3.diff"Comment #68
nathan tsai commentedEncountered this issue today. Fixed by updating to Drupal 9.5.7.