Problem/Motivation
While using a Windows OS adding a new line in a textarea element that has a #maxlength property will cause a mismatch between the JavaScript maxlength validation and the server-side form validation. This is due to Windows using CR+LF (\r\n) as new line characters. JavaScript truncates this to only LF (\n) for the purposes of determining the length of the input. But the raw character data gets sent to the server for PHP to validate, which can subsequently cause it to fail. You may fill a textarea to its maximum allowed length with a string that includes new lines and everything appears to be fine before submission. But then after submitting the form you're presented with a form validation error that says:
Textarea with maxlength cannot be longer than X characters but is currently X+N characters long.
This is a frequently reported problem between JavaScript and server-side validation across many libraries and frameworks for decades, not just Drupal.
Steps to reproduce
- Use an OS that uses CRLF for new lines.
- Create a form with a textarea that has a
#maxlengthproperty in its render array. - View the form.
- Enter as many characters as the textarea will allow. One or more of the characters should be new lines.
- Optional: You can verify the number of characters that the browser thinks is in the textarea value by inspecting it in the dev tools, then entering
$0.value.length
in the console.
- Submit the form
Expected result
The form should be submitted without validation errors.
Actual result
The form reloads with a validation error stating that the maxlength was exceeded by the #maxlength + (number of new lines in the text).
Proposed resolution
From #19 and the HTML5 spec:
...newline normalization converts "\r\n" and "\r" to "\n". So in PHP, the maxlength should be validated against the submitted value with the same newline normalization.
Remaining tasks
Decide on a fix.- Create an MR.
- Write a test.
- Review.
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|
Issue fork drupal-3226410
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:
- 3226410-textarea-maxlength-validation
changes, plain diff MR !11677
Comments
Comment #2
sourabhjainI am working on this issue.
Comment #3
sourabhjainI have fixed the issue. Please review it.
Comment #4
cilefen commentedCan you please prove you fixed it by means of an automated test?
Comment #5
longwaveAlso, how does this fit in with where the data might be used later? If we are specifying a maxlength of 100 and then technically allowing more than that by removing newlines from the count, but someone stores that data in a column that is 100 characters wide, what happens?
Comment #6
tobiberlin@longwave - I think in table (Mysql at least) the newline is only counted once as well. We do not remove newlines from the counting - we only remove the double counting for a new line. For one newline the counting now results to one count instead of two.
Comment #8
vikashsoni commented@sourabhjain Patch applied successfully in drupal-9.3.x-dev
thanks for the patch
Comment #11
smustgrave commentedThis issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.
For the tests requested in #4
Comment #13
dcam commentedI verified the problem on my Windows laptop. This is still an issue with the latest 11.x-dev.
The issue summary has been updated with information about the actual problem. This isn't the result of PHP "counting new line characters twice." The problem is that in Windows new lines are literally two characters, a carriage return and line feed (
\r\n). JavaScript and browsers ignore the\rwhen calculating string length. PHP counts them both.Thus, the former proposed fix and patch in #3 are unacceptable solutions. They would cause strings to pass form validation, but would eventually result Drupal trying to save strings that are too long into database fields whose sizes are set to the given maxlengths. That would result in even worse errors, probably WSoDs.
I don't know what the best solution is to the problem. A framework manager will have to decide.
Comment #15
dcam commentedI wrote a very basic FunctionalJavascript test that demonstrates the issue. The test fails because it finds the server-side validation error message after submitting the form. It will have to be modified appropriately after a solution for the bug has been decided, but it demonstrates that there's a problem for now. I wanted to make it verify the contents of the textarea field before submitting the form, but I don't know how. This is literally the first FunctionalJavascript test that I've written.
Anyway, I'm leaving the Needs Tests tag since it will have to be improved or modified. But I'm changing the status to Needs Review to get the attention of a maintainer for an opinion on a fix.
Comment #16
dcam commentedComment #17
dcam commentedForm validation issues are described as having Major status.
Comment #18
smustgrave commentedNot 100% sure putting into review will grab the attention.
Though it's good to have the hard part done in a test.
Will post in slack though to try and kick start it.
Comment #19
godotislateLooking at the HTML5 Standard, the maxlength attribute for a form control is validated by its "API Value".
And the newline normalization converts "\r\n" and "\r" to "\n". So in PHP, the maxlength should be validated against the submitted value with the same newline normalization.
This likely affects submissions regardless of the user's brower/OS. Also per the HTML5 standard, the submitted form values should have "\n" and "\r" normalized to "\r\n". So the form values in PHP should have "\r\n" exclusively. Testing on Chrome in MacOS, I can validate that this is what I've observed. Putting a breakpoint on these lines in
Drupal\Core\Form\FormValidator::performRequiredValidation():If I only hit "return" 20 times in a textarea with
#maxlength20 and submit the form, then the value of$elements['#value']is "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n", andmb_strlen($elements['#value'])is 40.I think best way forward is to do the same newline normalization in
Drupal\Core\Render\Element\Textarea::valueCallback(), so that the maxlength validation is equivalent on the frontend and backend. I think it also makes sense to store this normalized value, so that there isn't an error if the same character limit is applied on the database column, but it needs to be validated that when the value is retrieved and presented back to browsers, that this doesn't create any issues across browsers/OSes.Comment #20
dcam commentedUpdated the issue summary with the appropriate solution.
Comment #21
dcam commentedI implemented the prescribed fix per the HTML5 standard. The FunctionalJavascript test was reverted in favor of Unit test cases in the existing TextareaTest.
Comment #22
godotislateBecause of the browser behavior described in #19, I think it makes sense to include a Browser test after all. I think it'd also make sense to test that the saved value(s) do not exceed DB column sizes, so I think the test could look like this:
textfield (Drupal\text\Plugin\Field\FieldType\TextItem) and set the maxlength storage setting to something lowComment #23
dcam commented@godotislate this issue is about textarea elements. It looks like core's fields with textarea elements don't have settings for maxlength attributes. Am I missing something?
Comment #24
godotislateAh right, I'd forgotten that the textarea widget isn't used on
textfield items. Maybe the unit test is enough, so putting to RTBC.Comment #25
nod_There is suggestion in the MR
Comment #26
godotislateApplied the MR suggestion and rebased. Since the change is minor and there were no other suggestions, going to push this back to RTBC.
Comment #27
alexpottSome thoughts while reviewing this issue.
return str_replace(["\r", "\n"], '', $input);.I'm not sure what all these thoughts mean at this point but I just wanted to note them down.
Comment #28
godotislateThe reason to do that here is that it's possible that database storage has the same character limit, so without converting "\r\n" to "\n", it's possible there could be a database error if it passes JS and PHP validation. I don't think core has any instance of character-limited storage for a field that has textareas used for entry, but it seems possible for something in contrib or custom code.
Also, the browser is already converting "\n" or "\r" to "\r\n" on submit, so technically for users on non-Windows clients, the input they entered has already been changed.
I think I did try awhile ago to see if other web projects might be handling "\r\n" for similar concerns, but didn't find anything conclusive.
Comment #29
alexpottReally? Yes it does.... I've confirmed that with a debugger. This is quite messed up right? What a PITA. FWIW though how real is it to enforce max length on the database level for a field using a text area? And is doing that even correct. I don't think it is. Sure you can enforce max length on the field level but backing a text area field with anything other than big text field feels like you are opening a can of worms.
Also I think that point 4 from #27 is important.
Comment #30
godotislateYes, this was surprising to me as well, but apparently it is HTML spec. See #19.
I don't think I necessarily disagree, esp for sites with content translation, but is it obvious? #5 and #13 brought up the storage concerns.
Also, it's not uncommon for stakeholders to request to have title (replacement) fields that support formatted text and new lines. This could be implemented as a TextItem field, with an alter to allow a textarea widget to be used. But maybe it's too edge-casey?
Agreed, not sure about what to do about that and open to ideas. Is maxlength checked at all for JSON:API requests, since the validation is done in FormValidator?
Comment #31
larowlanIn
\Drupal\Core\Render\Element\Textfield::valueCallbackwe havereturn str_replace(["\r", "\n"], '', $input);which unless I'm mistaken is also problematic here because in the case of windows,\r\nbecomes\n\nand would result in a different max-length validation to that of the browser - should we be handling that (And any other text-like inputs like telephone, machine name etc) here as well - the issue title doesn't specifically mention text areas.Comment #32
godotislateThat
str_replace()strips out every instance of\ror\nfrom the input, so\r\nwould be stripped out as well. Though technically, textfields and other<input>elements don't allow new lines in input: https://stackoverflow.com/a/42744064. That's a stackoverflow answer, but from a quick Google, official resources seem to say the same without being as declarative.Doing some git archaeology, I found that that
str_replacecomes from way back in #53480: required checkbox not stylized like a required field, and it was for form input hardening (see #6):If we need to add similar hardening for non-form submissions to telephone, machine_name, etc., I think that'd be a separate issue?
Back to textareas, a couple thoughts:
I don't think there are any field widgets in core that set the
#maxlengthproperty on aTextareaform element object. So if we gate replacing"/\r\n?/"with"\n"based on whether#maxlengthis set, something like this:return isset($element['#maxlength']) ? preg_replace("/\r\n?/", "\n", (string) $input) : (string) $input;then values submitted via JSON:API requests will be the same as content entity form submissions, except for contrib/custom field widgets that set
#maxlengthon the Textarea. Arguably it would be on the devs implementing the field widgets, and possibly their respective field types, to add the appropriate Constraints and normalize the JSON:API data.An alternative idea is to add a kernel request event subscriber that goes through all properties in
$request->queryand$request->request, and request content as well for requests with JSON bodies, and replace all\r\nwith\n. But doing this for request content is tricky, and I think this approach overall might be overly aggressive.Comment #33
dcam commentedThere aren't, unless something changed recently. I checked before writing #23.
Comment #34
larowlanGating it behind fields with a maxlength is a great idea - nice one
Comment #36
ironnuts commentedSome work needed on the test case. Add newlines to the random textarea value and test how that affects the length?
Comment #37
godotislateAdded this to the MR and additional tests. Ready for review again.
Comment #38
ironnuts commented@godotislate I have looked at your new kernel test coverage. Can you please advise me if you think I should refactor my test coverage 'borrowing' from your code to take account of new lines?
Comment #39
godotislateI don't think it's easily accomplishable in a unit test. As for the rest, I would just leave it as is.
Comment #40
ironnuts commented#29 +1. I realised I had come across this before with Symfony textarea field. I wanted to allow the user to enter a list, actually Black Cab runs which students of the Knowledge study and memorise, for example,
Leave on Left Green Lanes
Right Highbury New Park
etc
etc
etc
Forward into Gibson Square
All the runs follow a similar pattern, but they vary a lot in length (how many lines in total).
You have a choice to either present one textbox per line or a textarea for all the lines. Thinking it through I realised that the newlines are easily handled by explode() on the TEXT (string) value. Then custom validation can be applied to the array. The flexibility of the textarea field type, including the length of the field value, is what seems to make it so useful.
I think there might be edge cases where a maxlength value is desirable. But IMO it is best to leave the core textarea field as it is. Developers can create a custom field or use custom validation. But such cases will be relatively infrequent.
Comment #41
dcam commentedI'm pretty sure there is duplication between this issue and #3202631: Add Textarea option to normalize newlines to \n.
Comment #42
tim.plunkettThe other issue fixes the generic \r\n problem for all forms, with an opt-out.
This issue fixes the \r\n issue only for forms with a #maxlength set.
I would recommend closing this issue and proceeding with the approach in #3202631: Add Textarea option to normalize newlines to \n
Comment #43
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #45
prudloff commentedShould this be postponed until #3202631: Add Textarea option to normalize newlines to \n is committed and then we can see if it is still a problem?
Comment #46
godotislate#41 and #42 suggest this should be closed as dupe, which I think makes sense.
Comment #47
godotislateClosing as dupe of #3202631: Add Textarea option to normalize newlines to \n. We can reopen if not everything ends up being addressed there.
Comment #49
godotislate