diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php index 286586288bdf47c7c1041a97b1cabfe42c30dcb2..52b18e1e882d7953496e3e940f533dfcdcb87993 100644 --- a/core/assets/scaffold/files/default.settings.php +++ b/core/assets/scaffold/files/default.settings.php @@ -707,6 +707,22 @@ # $config['system.site']['name'] = 'My Drupal site'; # $config['user.settings']['anonymous'] = 'Visitor'; +/** + * Enable HTML5 form validation. + * + * Drupal disables HTML5 form validation by default due to issues with + * usability and accessibility. Setting this to TRUE will allow user agents to + * perform client-side HTML5 validation. This prevents Drupal's Form API (FAPI) + * validation from executing, so FAPI validation error messages may not be + * displayed including those for required elements. + * + * This setting will be removed in Drupal 13. HTML form validation will always + * be disabled. + * + * @see https://www.drupal.org/node/3537128 + */ +# $settings['enable_html5_validation'] = TRUE; + /** * Load services definition file. */ diff --git a/core/lib/Drupal/Core/Form/FormPreprocess.php b/core/lib/Drupal/Core/Form/FormPreprocess.php index 3b3bad33ef4eac0615d43f7ebd7afdc3780852ba..342f92f1ff264895a3186652ce34ff0fd9a2bcc1 100644 --- a/core/lib/Drupal/Core/Form/FormPreprocess.php +++ b/core/lib/Drupal/Core/Form/FormPreprocess.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\RenderElementBase; +use Drupal\Core\Site\Settings; use Drupal\Core\Template\Attribute; /** @@ -33,6 +34,11 @@ public function preprocessForm(array &$variables): void { if (empty($element['#attributes']['accept-charset'])) { $element['#attributes']['accept-charset'] = "UTF-8"; } + if (!Settings::get('enable_html5_validation', FALSE)) { + // Prevent client-side HTML5 validation for usability and accessibility. + // @see https://www.drupal.org/node/3537128 + $element['#attributes']['novalidate'] = TRUE; + } $variables['attributes'] = $element['#attributes']; $variables['children'] = $element['#children']; } diff --git a/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php b/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php index df00763b565b787f548d35cd19037decb087a52a..224156b135e797fe6d8011400e9a261d6c999f6f 100644 --- a/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php +++ b/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php @@ -71,7 +71,7 @@ public function testRenderLayout($layout_id, $config, $regions, array $html): vo // Add in the wrapping form elements and prefix/suffix. array_unshift($html, 'Test prefix'); - array_unshift($html, '
'); + array_unshift($html, ''); // Retrieve the build ID from the rendered HTML since the string is random. $build_id_input = $this->cssSelect('input[name="form_build_id"]')[0]->asXML(); $form_id_input = ''; diff --git a/core/modules/system/src/Hook/SystemRequirementsHooks.php b/core/modules/system/src/Hook/SystemRequirementsHooks.php index 420cb794ff2fc2968a56d08bc1b0e483bd02a95a..bafd3ee849221dacfcc6f2e576c52ab99bb2285c 100644 --- a/core/modules/system/src/Hook/SystemRequirementsHooks.php +++ b/core/modules/system/src/Hook/SystemRequirementsHooks.php @@ -1232,6 +1232,16 @@ public function checkRequirements(string $phase): array { 'description' => $this->t('The rebuild_access setting is enabled in settings.php. It is recommended to have this setting disabled unless you are performing a rebuild.'), ]; } + + // Warn about HTML5 validation setting removal in Drupal 13. + if (Settings::get('enable_html5_validation') === TRUE) { + $requirements['enable_html5_validation'] = [ + 'title' => $this->t('HTML5 validation'), + 'value' => $this->t('Enabled'), + 'severity' => RequirementSeverity::Warning, + 'description' => $this->t('The enable_html5_validation setting will be removed in Drupal 13, and HTML5 validation will be disabled on all forms. Make sure your forms will still work the way you expect before upgrading by setting the value to FALSE in a test environment. See the change record for more information.', [':url' => 'https://www.drupal.org/node/3537128']), + ]; + } } // Check if the SameSite cookie attribute is set to a valid value. Since diff --git a/core/modules/system/tests/src/Functional/Form/ValidationTest.php b/core/modules/system/tests/src/Functional/Form/ValidationTest.php index 11946e4a2291306a70282863178510c7ad441c97..f4ba2259b8f87596de9b7c6d603ce7b1e2794134 100644 --- a/core/modules/system/tests/src/Functional/Form/ValidationTest.php +++ b/core/modules/system/tests/src/Functional/Form/ValidationTest.php @@ -4,7 +4,6 @@ namespace Drupal\Tests\system\Functional\Form; -use Drupal\Core\Render\Element; use Drupal\Tests\BrowserTestBase; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -87,182 +86,4 @@ public function testDisabledToken(): void { $this->assertSession()->pageTextContains('The form_test_validate_no_token form has been submitted successfully.'); } - /** - * Tests partial form validation through #limit_validation_errors. - */ - public function testValidateLimitErrors(): void { - $edit = [ - 'test' => 'invalid', - 'test_numeric_index[0]' => 'invalid', - 'test_substring[foo]' => 'invalid', - ]; - $path = 'form-test/limit-validation-errors'; - - // Render the form, and verify that the buttons with limited server-side - // validation have the proper 'formnovalidate' attribute (to prevent - // client-side validation by the browser). - $this->drupalGet($path); - $expected = 'formnovalidate'; - foreach (['partial', 'partial-numeric-index', 'substring'] as $type) { - // Verify the $type button has the proper formnovalidate attribute. - $this->assertSession()->elementExists('xpath', "//input[@id='edit-$type' and @formnovalidate='$expected']"); - } - // The button with full server-side validation should not have the - // 'formnovalidate' attribute. - $this->assertSession()->elementExists('xpath', "//input[@id='edit-full' and not(@formnovalidate)]"); - - // Submit the form by pressing the 'Partial validate' button (uses - // #limit_validation_errors) and ensure that the title field is not - // validated, but the #element_validate handler for the 'test' field - // is triggered. - $this->drupalGet($path); - $this->submitForm($edit, 'Partial validate'); - $this->assertSession()->pageTextNotContains('Title field is required.'); - $this->assertSession()->pageTextContains('Test element is invalid'); - - // Edge case of #limit_validation_errors containing numeric indexes: same - // thing with the 'Partial validate (numeric index)' button and the - // 'test_numeric_index' field. - $this->drupalGet($path); - $this->submitForm($edit, 'Partial validate (numeric index)'); - $this->assertSession()->pageTextNotContains('Title field is required.'); - $this->assertSession()->pageTextContains('Test (numeric index) element is invalid'); - - // Ensure something like 'foobar' isn't considered "inside" 'foo'. - $this->drupalGet($path); - $this->submitForm($edit, 'Partial validate (substring)'); - $this->assertSession()->pageTextNotContains('Title field is required.'); - $this->assertSession()->pageTextContains('Test (substring) foo element is invalid'); - - // Ensure not validated values are not available to submit handlers. - $this->drupalGet($path); - $this->submitForm([ - 'title' => '', - 'test' => 'valid', - ], 'Partial validate'); - $this->assertSession()->pageTextContains('Only validated values appear in the form values.'); - - // Now test full form validation and ensure that the #element_validate - // handler is still triggered. - $this->drupalGet($path); - $this->submitForm($edit, 'Full validate'); - $this->assertSession()->pageTextContains('Title field is required.'); - $this->assertSession()->pageTextContains('Test element is invalid'); - } - - /** - * Tests #pattern validation. - */ - public function testPatternValidation(): void { - $textfield_error = 'One digit followed by lowercase letters field is not in the right format.'; - $tel_error = 'Everything except numbers field is not in the right format.'; - $password_error = 'Password field is not in the right format.'; - - // Invalid textfield, valid tel. - $edit = [ - 'textfield' => 'invalid', - 'tel' => 'valid', - ]; - $this->drupalGet('form-test/pattern'); - $this->submitForm($edit, 'Submit'); - $this->assertSession()->pageTextContains($textfield_error); - $this->assertSession()->pageTextNotContains($tel_error); - $this->assertSession()->pageTextNotContains($password_error); - - // Valid textfield, invalid tel, valid password. - $edit = [ - 'textfield' => '7seven', - 'tel' => '818937', - 'password' => '0100110', - ]; - $this->drupalGet('form-test/pattern'); - $this->submitForm($edit, 'Submit'); - $this->assertSession()->pageTextNotContains($textfield_error); - $this->assertSession()->pageTextContains($tel_error); - $this->assertSession()->pageTextNotContains($password_error); - - // Non required fields are not validated if empty. - $edit = [ - 'textfield' => '', - 'tel' => '', - ]; - $this->drupalGet('form-test/pattern'); - $this->submitForm($edit, 'Submit'); - $this->assertSession()->pageTextNotContains($textfield_error); - $this->assertSession()->pageTextNotContains($tel_error); - $this->assertSession()->pageTextNotContains($password_error); - - // Invalid password. - $edit = [ - 'password' => $this->randomMachineName(), - ]; - $this->drupalGet('form-test/pattern'); - $this->submitForm($edit, 'Submit'); - $this->assertSession()->pageTextNotContains($textfield_error); - $this->assertSession()->pageTextNotContains($tel_error); - $this->assertSession()->pageTextContains($password_error); - - // The pattern attribute overrides #pattern and is not validated on the - // server side. - $edit = [ - 'textfield' => '', - 'tel' => '', - 'url' => 'http://www.example.com/', - ]; - $this->drupalGet('form-test/pattern'); - $this->submitForm($edit, 'Submit'); - $this->assertSession()->pageTextNotContains('Client side validation field is not in the right format.'); - } - - /** - * Tests #required with custom validation errors. - * - * @see \Drupal\form_test\Form\FormTestValidateRequiredForm - */ - public function testCustomRequiredError(): void { - $form = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestValidateRequiredForm'); - - // Verify that a custom #required error can be set. - $edit = []; - $this->drupalGet('form-test/validate-required'); - $this->submitForm($edit, 'Submit'); - - foreach (Element::children($form) as $key) { - if (isset($form[$key]['#required_error'])) { - $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); - $this->assertSession()->pageTextContains((string) $form[$key]['#required_error']); - } - elseif (isset($form[$key]['#form_test_required_error'])) { - $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); - $this->assertSession()->pageTextContains((string) $form[$key]['#form_test_required_error']); - } - if (isset($form[$key]['#title'])) { - $this->assertSession()->pageTextNotContains('The submitted value in the ' . $form[$key]['#title'] . ' element is not allowed.'); - } - } - - // Verify that no custom validation error appears with valid values. - $edit = [ - 'textfield' => $this->randomString(), - 'checkboxes[foo]' => TRUE, - 'select' => 'foo', - ]; - $this->drupalGet('form-test/validate-required'); - $this->submitForm($edit, 'Submit'); - - foreach (Element::children($form) as $key) { - if (isset($form[$key]['#required_error'])) { - $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); - $this->assertSession()->pageTextNotContains((string) $form[$key]['#required_error']); - } - elseif (isset($form[$key]['#form_test_required_error'])) { - $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); - $this->assertSession()->pageTextNotContains((string) $form[$key]['#form_test_required_error']); - } - if (isset($form[$key]['#title'])) { - $this->assertSession()->pageTextNotContains('The submitted value in the ' . $form[$key]['#title'] . ' element is not allowed.'); - } - } - } - } diff --git a/core/modules/system/tests/src/FunctionalJavascript/Form/ValidationTest.php b/core/modules/system/tests/src/FunctionalJavascript/Form/ValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d4eaf6927d1872b62d6391e7c1d13e8f46611894 --- /dev/null +++ b/core/modules/system/tests/src/FunctionalJavascript/Form/ValidationTest.php @@ -0,0 +1,210 @@ + 'invalid', + 'test_numeric_index[0]' => 'invalid', + 'test_substring[foo]' => 'invalid', + ]; + $path = 'form-test/limit-validation-errors'; + + // Render the form, and verify that the buttons with limited server-side + // validation have the proper 'formnovalidate' attribute (to prevent + // client-side validation by the browser). + $this->drupalGet($path); + $expected = 'formnovalidate'; + foreach (['partial', 'partial-numeric-index', 'substring'] as $type) { + // Verify the $type button has the proper formnovalidate attribute. + $this->assertSession()->elementExists('xpath', "//input[@id='edit-$type' and @formnovalidate='$expected']"); + } + // The button with full server-side validation should not have the + // 'formnovalidate' attribute. + $this->assertSession()->elementExists('xpath', "//input[@id='edit-full' and not(@formnovalidate)]"); + + // Submit the form by pressing the 'Partial validate' button (uses + // #limit_validation_errors) and ensure that the title field is not + // validated, but the #element_validate handler for the 'test' field + // is triggered. + $this->drupalGet($path); + $this->submitForm($edit, 'Partial validate'); + $this->assertSession()->pageTextNotContains('Title field is required.'); + $this->assertSession()->pageTextContains('Test element is invalid'); + + // Edge case of #limit_validation_errors containing numeric indexes: same + // thing with the 'Partial validate (numeric index)' button and the + // 'test_numeric_index' field. + $this->drupalGet($path); + $this->submitForm($edit, 'Partial validate (numeric index)'); + $this->assertSession()->pageTextNotContains('Title field is required.'); + $this->assertSession()->pageTextContains('Test (numeric index) element is invalid'); + + // Ensure something like 'foobar' isn't considered "inside" 'foo'. + $this->drupalGet($path); + $this->submitForm($edit, 'Partial validate (substring)'); + $this->assertSession()->pageTextNotContains('Title field is required.'); + $this->assertSession()->pageTextContains('Test (substring) foo element is invalid'); + + // Ensure not validated values are not available to submit handlers. + $this->drupalGet($path); + $this->submitForm([ + 'title' => '', + 'test' => 'valid', + ], 'Partial validate'); + $this->assertSession()->pageTextContains('Only validated values appear in the form values.'); + + // Now test full form validation and ensure that the #element_validate + // handler is still triggered. + $this->drupalGet($path); + $this->submitForm($edit, 'Full validate'); + $this->assertSession()->pageTextContains('Title field is required.'); + $this->assertSession()->pageTextContains('Test element is invalid'); + } + + /** + * Tests #pattern validation. + */ + public function testPatternValidation(): void { + $textfield_error = 'One digit followed by lowercase letters field is not in the right format.'; + $tel_error = 'Everything except numbers field is not in the right format.'; + $password_error = 'Password field is not in the right format.'; + + // Invalid textfield, valid tel. + $edit = [ + 'textfield' => 'invalid', + 'tel' => 'valid', + ]; + $this->drupalGet('form-test/pattern'); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->pageTextContains($textfield_error); + $this->assertSession()->pageTextNotContains($tel_error); + $this->assertSession()->pageTextNotContains($password_error); + + // Valid textfield, invalid tel, valid password. + $edit = [ + 'textfield' => '7seven', + 'tel' => '818937', + 'password' => '0100110', + ]; + $this->drupalGet('form-test/pattern'); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->pageTextNotContains($textfield_error); + $this->assertSession()->pageTextContains($tel_error); + $this->assertSession()->pageTextNotContains($password_error); + + // Non required fields are not validated if empty. + $edit = [ + 'textfield' => '', + 'tel' => '', + ]; + $this->drupalGet('form-test/pattern'); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->pageTextNotContains($textfield_error); + $this->assertSession()->pageTextNotContains($tel_error); + $this->assertSession()->pageTextNotContains($password_error); + + // Invalid password. + $edit = [ + 'password' => $this->randomMachineName(), + ]; + $this->drupalGet('form-test/pattern'); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->pageTextNotContains($textfield_error); + $this->assertSession()->pageTextNotContains($tel_error); + $this->assertSession()->pageTextContains($password_error); + + // The pattern attribute overrides #pattern and is not validated on the + // server side. + $edit = [ + 'textfield' => '', + 'tel' => '', + 'url' => 'http://www.example.com/', + ]; + $this->drupalGet('form-test/pattern'); + $this->submitForm($edit, 'Submit'); + $this->assertSession()->pageTextNotContains('Client side validation field is not in the right format.'); + } + + /** + * Tests #required with custom validation errors. + * + * @see \Drupal\form_test\Form\FormTestValidateRequiredForm + */ + public function testCustomRequiredError(): void { + $form = \Drupal::formBuilder()->getForm('\Drupal\form_test\Form\FormTestValidateRequiredForm'); + + // Verify that a custom #required error can be set. + $edit = []; + $this->drupalGet('form-test/validate-required'); + $this->submitForm($edit, 'Submit'); + + foreach (Element::children($form) as $key) { + if (isset($form[$key]['#required_error'])) { + $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); + $this->assertSession()->pageTextContains((string) $form[$key]['#required_error']); + } + elseif (isset($form[$key]['#form_test_required_error'])) { + $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); + $this->assertSession()->pageTextContains((string) $form[$key]['#form_test_required_error']); + } + if (isset($form[$key]['#title'])) { + $this->assertSession()->pageTextNotContains('The submitted value in the ' . $form[$key]['#title'] . ' element is not allowed.'); + } + } + + // Verify that no custom validation error appears with valid values. + $edit = [ + 'textfield' => $this->randomString(), + 'checkboxes[foo]' => TRUE, + 'select' => 'foo', + ]; + $this->drupalGet('form-test/validate-required'); + $this->submitForm($edit, 'Submit'); + + foreach (Element::children($form) as $key) { + if (isset($form[$key]['#required_error'])) { + $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); + $this->assertSession()->pageTextNotContains((string) $form[$key]['#required_error']); + } + elseif (isset($form[$key]['#form_test_required_error'])) { + $this->assertSession()->pageTextNotContains($form[$key]['#title'] . ' field is required.'); + $this->assertSession()->pageTextNotContains((string) $form[$key]['#form_test_required_error']); + } + if (isset($form[$key]['#title'])) { + $this->assertSession()->pageTextNotContains('The submitted value in the ' . $form[$key]['#title'] . ' element is not allowed.'); + } + } + } + +} diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 286586288bdf47c7c1041a97b1cabfe42c30dcb2..52b18e1e882d7953496e3e940f533dfcdcb87993 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -707,6 +707,22 @@ # $config['system.site']['name'] = 'My Drupal site'; # $config['user.settings']['anonymous'] = 'Visitor'; +/** + * Enable HTML5 form validation. + * + * Drupal disables HTML5 form validation by default due to issues with + * usability and accessibility. Setting this to TRUE will allow user agents to + * perform client-side HTML5 validation. This prevents Drupal's Form API (FAPI) + * validation from executing, so FAPI validation error messages may not be + * displayed including those for required elements. + * + * This setting will be removed in Drupal 13. HTML form validation will always + * be disabled. + * + * @see https://www.drupal.org/node/3537128 + */ +# $settings['enable_html5_validation'] = TRUE; + /** * Load services definition file. */