From 2ceafd06b7d2080cbafc9b99ce2f2669770154a2 Mon Sep 17 00:00:00 2001
From: Lauri Eskola <lauri.eskola@acquia.com>
Date: Sat, 28 Oct 2023 17:01:40 +0300
Subject: [PATCH] Issue #3052663 by joaopauloc.dev, smustgrave, quietone, Balu
 Ertl, xjm, scottsperry: Validate the min, max and default values for Numeric
 fields

---
 .../Field/FieldType/NumericItemBase.php       | 29 ++++++++
 .../src/Functional/Number/NumberFieldTest.php | 11 ++++
 .../src/Kernel/Number/NumberItemTest.php      | 66 +++++++++++++++++++
 3 files changed, 106 insertions(+)

diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.php
index d1f3d644dbcb..1e4fbbb2b6da 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.php
@@ -33,6 +33,7 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
       '#type' => 'number',
       '#title' => $this->t('Minimum'),
       '#default_value' => $settings['min'],
+      '#element_validate' => [[static::class, 'validateMinAndMaxConfig']],
       '#description' => $this->t('The minimum value that should be allowed in this field. Leave blank for no minimum.'),
     ];
     $element['max'] = [
@@ -124,4 +125,32 @@ protected static function truncateDecimal($decimal, $num) {
     return floor($decimal * pow(10, $num)) / pow(10, $num);
   }
 
+  /**
+   * Validates that the minimum value is less than the maximum.
+   *
+   * @param array[] $element
+   *   The numeric element to be validated.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array[] $complete_form
+   *   The complete form structure.
+   */
+  public static function validateMinAndMaxConfig(array &$element, FormStateInterface &$form_state, array &$complete_form): void {
+    $settingsValue = $form_state->getValue('settings');
+
+    // Ensure that the minimum and maximum are numeric.
+    $minValue = is_numeric($settingsValue['min']) ? (float) $settingsValue['min'] : NULL;
+    $maxValue = is_numeric($settingsValue['max']) ? (float) $settingsValue['max'] : NULL;
+
+    // Only proceed with validation if both values are numeric.
+    if ($minValue === NULL || $maxValue === NULL) {
+      return;
+    }
+
+    if ($minValue > $maxValue) {
+      $form_state->setError($element, t('The minimum value must be less than or equal to %max.', ['%max' => $maxValue]));
+      return;
+    }
+  }
+
 }
diff --git a/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php b/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php
index 9450f5f8c3cf..93030b0f9290 100644
--- a/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php
+++ b/core/modules/field/tests/src/Functional/Number/NumberFieldTest.php
@@ -284,6 +284,17 @@ public function testNumberIntegerField() {
     // Verify that the "content" attribute has been set to the value of the
     // field, and the prefix is being displayed.
     $this->assertSession()->elementTextContains('xpath', '//div[@content="' . $integer_value . '"]', 'ThePrefix' . $integer_value);
+
+    $field_configuration_url = 'entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name;
+    $this->drupalGet($field_configuration_url);
+
+    // Tests Number validation messages.
+    $edit = [
+      'settings[min]' => 10,
+      'settings[max]' => 8,
+    ];
+    $this->submitForm($edit, 'Save settings');
+    $this->assertSession()->pageTextContains("The minimum value must be less than or equal to {$edit['settings[max]']}.");
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php b/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
index 37030b4925c3..6d5f6b4a58a0 100644
--- a/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
+++ b/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Field\FieldItemInterface;
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\NumericItemBase;
+use Drupal\Core\Form\FormState;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\Tests\field\Kernel\FieldKernelTestBase;
@@ -188,4 +190,68 @@ public function dataNumberFieldSettingsProvider() {
     yield ['decimal', 1, 2, 1.5, FALSE];
   }
 
+  /**
+   * Tests the validation of minimum and maximum values.
+   *
+   * @param int|float|string $min
+   *   Min value to be tested.
+   * @param int|float|string $max
+   *   Max value to be tested.
+   * @param int|float|string $value
+   *   Value to be tested with min and max values.
+   * @param bool $hasError
+   *   Expected validation result.
+   * @param string $message
+   *   (optional) Error message result.
+   *
+   * @dataProvider dataTestMinMaxValue
+   */
+  public function testFormFieldMinMaxValue(int|float|string $min, int|float|string $max, int|float|string $value, bool $hasError, string $message = ''): void {
+    $element = [
+      '#type' => 'number',
+      '#title' => 'min',
+      '#default_value' => $min,
+      '#element_validate' => [[NumericItemBase::class, 'validateMinAndMaxConfig']],
+      '#description' => 'The minimum value that should be allowed in this field. Leave blank for no minimum.',
+      '#parents' => [],
+      '#name' => 'min',
+    ];
+
+    $form_state = new FormState();
+    $form_state->setValue('min', $value);
+    $form_state->setValue('settings', [
+      'min' => $min,
+      'max' => $max,
+      'prefix' => '',
+      'suffix' => '',
+      'precision' => 10,
+      'scale' => 2,
+    ]);
+    $completed_form = [];
+    NumericItemBase::validateMinAndMaxConfig($element, $form_state, $completed_form);
+    $errors = $form_state->getErrors();
+    $this->assertEquals($hasError, count($errors) > 0);
+    if ($errors) {
+      $error = current($errors);
+      $this->assertEquals($error, $message);
+    }
+  }
+
+  /**
+   * Data provider for testFormFieldMinMaxValue().
+   *
+   * @return \Generator
+   *   The test data.
+   */
+  public function dataTestMinMaxValue() {
+    yield [1, 10, 5, FALSE, ''];
+    yield [10, 5, 6, TRUE, 'The minimum value must be less than or equal to 5.'];
+    yield [1, 0, 6, TRUE, 'The minimum value must be less than or equal to 0.'];
+    yield [0, -2, 0.5, TRUE, 'The minimum value must be less than or equal to -2.'];
+    yield [-10, -20, -5, TRUE, 'The minimum value must be less than or equal to -20.'];
+    yield [1, '', -5, FALSE, ''];
+    yield ['', '', '', FALSE, ''];
+    yield ['2', '1', '', TRUE, 'The minimum value must be less than or equal to 1.'];
+  }
+
 }
-- 
GitLab