Unverified Commit d5babbbe authored by Alex Pott's avatar Alex Pott
Browse files

fix: #1797438 HTML5 validation is preventing form submit and not fully accessible

By: Bojhan
By: xjm
By: nod_
By: mgifford
By: webchick
By: sun
By: ParisLiakos
By: catch
By: dcam
By: jessebeach
By: cosmicdreams
By: vprocessor
By: skaught
By: idebr
By: jefuri
By: dmsmidt
By: lauriii
By: GrandmaGlassesRopeMan
By: andrewmacpherson
By: fenstrat
By: acbramley
By: sja112
By: ankiitsinghh
By: pameeela
By: tanubansal
By: himanshu_jhaloya
By: smustgrave
By: Everett Zufelt
By: tim.plunkett
By: gaurav.kapoor
By: geek-merlin
By: kieran.cott
By: suresh prabhu parkala
By: prudloff
By: kentr
By: shubham_pareek_19
By: jcandan
parent a2ee2493
Loading
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -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.
 */
+6 −0
Original line number Diff line number Diff line
@@ -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'];
  }
+1 −1
Original line number Diff line number Diff line
@@ -71,7 +71,7 @@ public function testRenderLayout($layout_id, array $config, array $regions, arra

    // Add in the wrapping form elements and prefix/suffix.
    array_unshift($html, 'Test prefix');
    array_unshift($html, '<form data-drupal-selector="the-form-id" action="/" method="post" id="the-form-id" accept-charset="UTF-8">');
    array_unshift($html, '<form data-drupal-selector="the-form-id" action="/" method="post" id="the-form-id" accept-charset="UTF-8" novalidate="">');
    // 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 = '<input data-drupal-selector="edit-the-form-id" type="hidden" name="form_id" value="the_form_id"/>';
+10 −0
Original line number Diff line number Diff line
@@ -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 <a href=":url">the change record</a> for more information.', [':url' => 'https://www.drupal.org/node/3537128']),
        ];
      }
    }

    // Check if the SameSite cookie attribute is set to a valid value. Since
+0 −179
Original line number Diff line number Diff line
@@ -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.');
      }
    }
  }

}
Loading