From 80cad41f1f29db1da88ce7cdfc4a99b7dbb571b8 Mon Sep 17 00:00:00 2001
From: Axel Rutz <axel.rutz@machbarmacher.net>
Date: Wed, 26 Jun 2019 19:56:17 +0200
Subject: [PATCH] Issue #2476569: Suppress validation of required fields on
 ajax calls

---
 core/lib/Drupal/Core/Field/WidgetBase.php     |  1 +
 .../Drupal/Core/Form/FormBuilderInterface.php |  5 +++
 core/lib/Drupal/Core/Form/FormValidator.php   |  8 +++-
 .../Core/Render/Element/RenderElement.php     |  5 +++
 .../tests/modules/form_test/form_test.module  | 37 +++++++++++++++
 .../tests/src/Functional/Form/RebuildTest.php | 45 ++++++++++++++++++-
 6 files changed, 99 insertions(+), 2 deletions(-)

diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php
index 63671576b2..93c318b99b 100644
--- a/core/lib/Drupal/Core/Field/WidgetBase.php
+++ b/core/lib/Drupal/Core/Field/WidgetBase.php
@@ -251,6 +251,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
             'callback' => [get_class($this), 'addMoreAjax'],
             'wrapper' => $wrapper_id,
             'effect' => 'fade',
+            'suppress_required_fields_validation' => TRUE,
           ],
         ];
       }
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index ca794379db..5d98504fb3 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -22,6 +22,11 @@
    */
   const AJAX_FORM_REQUEST = 'ajax_form';
 
+  /**
+   * Request key for AJAX forms to suppress validation of required fields.
+   */
+  const AJAX_SKIP_REQUIRED_VALIDATION = 'ajax_skip_required_validation';
+
   /**
    * Determines the ID of a form.
    *
diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.php
index 57d24cc21e..bbab3d6f4c 100644
--- a/core/lib/Drupal/Core/Form/FormValidator.php
+++ b/core/lib/Drupal/Core/Form/FormValidator.php
@@ -239,6 +239,12 @@ protected function doValidateForm(&$elements, FormStateInterface &$form_state, $
       }
     }
 
+    // The validation of required fields might be suppressed for ajax calls.
+    $request = $this->requestStack->getCurrentRequest();
+    $requires_validation = isset($elements['#required']) && $elements['#required'];
+    // Ensure there is an existing request with enforced validation suppression.
+    $suppress_required_fields_validation = $requires_validation && $request && $request->query->has(FormBuilderInterface::AJAX_FORM_REQUEST) && $request->query->has(FormBuilderInterface::AJAX_SKIP_REQUIRED_VALIDATION);
+
     // Validate the current input.
     if (!isset($elements['#validated']) || !$elements['#validated']) {
       // The following errors are always shown.
@@ -287,7 +293,7 @@ protected function doValidateForm(&$elements, FormStateInterface &$form_state, $
       // #element_validate handlers changed any properties. If $is_empty_value
       // is defined, then above #required validation code ran, so the other
       // variables are also known to be defined and we can test them again.
-      if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_null)) {
+      if (!$suppress_required_fields_validation && isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value)) {
         if (isset($elements['#required_error'])) {
           $form_state->setError($elements, $elements['#required_error']);
         }
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index 35814589b3..0d5f27ba45 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -348,6 +348,11 @@ public static function preRenderAjaxForm($element) {
         // to hide the password field dynamically.
         $settings['options']['query'] += \Drupal::request()->query->all();
         $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
+
+        // Set query option to enforce validation suppression of required fields.
+        if (isset($settings['suppress_required_fields_validation']) && $settings['suppress_required_fields_validation'] === TRUE) {
+          $settings['options']['query'][FormBuilderInterface::AJAX_SKIP_REQUIRED_VALIDATION] = TRUE;
+        }
       }
 
       // @todo Legacy support. Remove in Drupal 8.
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index 14e011877a..e78310a5ad 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -5,6 +5,8 @@
  * Helper module for the form API tests.
  */
 
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Field\WidgetBase;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
@@ -111,3 +113,38 @@ function form_test_form_form_test_vertical_tabs_access_form_alter(&$form, &$form
 function form_test_tableselect_ajax_callback($form, FormStateInterface $form_state) {
   return $form['tableselect'];
 }
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Used to add a form validate handler to the add more widget.
+ *
+ * @param $form
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * @param $form_id
+ *
+ * @see Drupal\system\Tests\Form\RebuildTest::testGeneratingMultipleRequiredFieldItemsPerAjax
+ */
+function form_test_form_node_form_edit_ajax_test_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  $form['field_ajax_test_required']['widget']['add_more']['#validate'][] = 'form_test_add_more_validate';
+}
+
+/**
+ * Validation handler for the "Add another item" button.
+ */
+function form_test_add_more_validate(array $form, FormStateInterface $form_state) {
+  $button = $form_state->getTriggeringElement();
+
+  if ($button['#name'] == 'field_ajax_test_required_add_more') {
+    // Go one level up in the form, to the widgets container.
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+    $field_name = $element['#field_name'];
+    $parents = $element['#field_parents'];
+
+    // Increment the items count.
+    $field_state = WidgetBase::getWidgetState($parents, $field_name, $form_state);
+    if ($field_state['items_count'] >= 1) {
+      $form_state->setError($element, 'Not more than three elements are allowed.');
+    }
+  }
+}
diff --git a/core/modules/system/tests/src/Functional/Form/RebuildTest.php b/core/modules/system/tests/src/Functional/Form/RebuildTest.php
index 3e951e2e80..877878d6b8 100644
--- a/core/modules/system/tests/src/Functional/Form/RebuildTest.php
+++ b/core/modules/system/tests/src/Functional/Form/RebuildTest.php
@@ -28,7 +28,7 @@ class RebuildTest extends BrowserTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+    $this->drupalCreateContentType(['type' => 'form_edit_ajax_test', 'name' => 'Ajax test page']);
 
     $this->webUser = $this->drupalCreateUser(['access content']);
     $this->drupalLogin($this->webUser);
@@ -58,4 +58,47 @@ public function testRebuildPreservesValues() {
     $assert_session->fieldValueEquals('edit-text-2', 'DEFAULT 2');
   }
 
+  /**
+   * Tests generating instances of required fields per ajax.
+   *
+   * The user should be able to generate more than one item at once of a field
+   * which is marked as required, without filling each of the field items out.
+   */
+  public function testGeneratingMultipleRequiredFieldItemsPerAjax() {
+    // Create a multi-valued field for 'page' nodes to use for Ajax testing.
+    $field_name = 'field_ajax_test_required';
+    entity_create('field_storage_config', [
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'type' => 'text',
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+    ])->save();
+    entity_create('field_config', [
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'bundle' => 'form_edit_ajax_test',
+      'required' => TRUE,
+    ])->save();
+    entity_get_form_display('node', 'form_edit_ajax_test', 'default')
+      ->setComponent($field_name, ['type' => 'text_textfield'])
+      ->save();
+
+    // Log in a user who can create 'form_edit_ajax_test' nodes.
+    $this->webUser = $this->drupalCreateUser(['create form_edit_ajax_test content']);
+    $this->drupalLogin($this->webUser);
+
+    // Get the form for adding a 'page' node. Submit an "add another item" Ajax
+    // submission and verify it worked by ensuring the updated page has two text
+    // field items in the field for which we just added an item.
+    $this->drupalGet('node/add/form_edit_ajax_test');
+    $this->drupalPostAjaxForm(NULL, [], ['field_ajax_test_required_add_more' => t('Add another item')], NULL, [], [], 'node-form-edit-ajax-test-form');
+    $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test-required")]//input[@type="text"]')) == 2, 'AJAX submission of required field succeeded.');
+
+    // Adding a third field per ajax. Here the validate function
+    // form_test_add_more_validate should set an error, because it limits the
+    // number of allowed fields up to two.
+    $this->drupalPostAjaxForm(NULL, [], ['field_ajax_test_required_add_more' => t('Add another item')], NULL, [], [], 'node-form-edit-ajax-test-form');
+    $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test-required")]//input[@type="text"]')) == 2, 'Correctly no third field item was added after an AJAX submission.');
+  }
+
 }
-- 
2.17.1