From 80350c3a158c182564a69a8f0932891f3ce2a7f1 Mon Sep 17 00:00:00 2001
From: Tavish Armstrong <tavisharmstrong@gmail.com>
Date: Sun, 17 Jun 2012 14:03:26 -0400
Subject: [PATCH] Issue #742344 by neilnz, sun, pillarsdotnet, tarmstrong,
 wamilton: Allow forms to set custom validation error
 messages on required fields.

---
 core/includes/form.inc                             |   36 ++++++++++-----
 .../Drupal/system/Tests/Form/FileInclusionTest.php |    2 +-
 .../lib/Drupal/system/Tests/Form/FormTest.php      |   10 ++++-
 .../Drupal/system/Tests/Form/ValidationTest.php    |   46 ++++++++++++++++++++
 .../tests/modules/form_test/form_test.module       |   19 +++++++-
 5 files changed, 100 insertions(+), 13 deletions(-)

diff --git a/core/includes/form.inc b/core/includes/form.inc
index c94bc62..e4c4325 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -1387,16 +1387,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
       $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
       $is_empty_value = ($elements['#value'] === 0);
       if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
-        // Although discouraged, a #title is not mandatory for form elements. In
-        // case there is no #title, we cannot set a form error message.
-        // Instead of setting no #title, form constructors are encouraged to set
-        // #title_display to 'invisible' to improve accessibility.
-        if (isset($elements['#title'])) {
-          form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
-        }
-        else {
-          form_error($elements);
-        }
+        // Flag this element as #required_but_empty to allow #element_validate
+        // handlers to set a custom required error message, but without having
+        // to re-implement the complex logic to figure out whether the field
+        // value is empty.
+        $elements['#required_but_empty'] = TRUE;
       }
     }
 
@@ -1411,6 +1406,27 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
         $function($elements, $form_state, $form_state['complete_form']);
       }
     }
+
+    // Ensure that a #required form error is thrown, regardless of whether
+    // #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)) {
+      if (isset($elements['#required_error'])) {
+        form_error($elements, $elements['#required_error']);
+      }
+      // A #title is not mandatory for form elements, but without it we cannot
+      // set a form error message. So when a visible title is undesirable, form
+      // constructors are encouraged to set #title anyway, and then set
+      // #title_display to 'invisible'. This improves accessibility.
+      elseif (isset($elements['#title'])) {
+        form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
+      }
+      else {
+        form_error($elements);
+      }
+    }
+
     $elements['#validated'] = TRUE;
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php
index 588241a..acb5cab 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php
@@ -35,7 +35,7 @@ class FileInclusionTest extends WebTestBase {
   }
 
   /**
-   * Tests loading a custom specified inlcude.
+   * Tests loading a custom specified include.
    */
   function testLoadCustomInclude() {
     $this->drupalPost('form-test/load-include-custom', array(), t('Save'));
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
index 20d1f56..2640ac5 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
@@ -165,7 +165,15 @@ class FormTest extends WebTestBase {
     // messages for each field.
     $expected = array();
     foreach (array('textfield', 'checkboxes', 'select', 'radios') as $key) {
-      $expected[] = t('!name field is required.', array('!name' => $form[$key]['#title']));
+      if (isset($form[$key]['#required_error'])) {
+        $expected[] = $form[$key]['#required_error'];
+      }
+      elseif (isset($form[$key]['#form_test_required_error'])) {
+        $expected[] = $form[$key]['#form_test_required_error'];
+      }
+      else {
+        $expected[] = t('!name field is required.', array('!name' => $form[$key]['#title']));
+      }
     }
 
     // Check the page for error messages.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
index 2386a8e..7df272f 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
@@ -182,4 +182,50 @@ class ValidationTest extends WebTestBase {
     $this->drupalPost('form-test/pattern', $edit, 'Submit');
     $this->assertNoRaw(t('%name field is not in the right format.', array('%name' => 'Client side validation')));
   }
+
+   /**
+   * Tests #required with custom validation errors.
+   *
+   * @see form_test_validate_required_form()
+   */
+  function testCustomRequiredError() {
+    $form = $form_state = array();
+    $form = form_test_validate_required_form($form, $form_state);
+
+    // Verify that a custom #required error can be set.
+    $edit = array();
+    $this->drupalPost('form-test/validate-required', $edit, 'Submit');
+
+    foreach (element_children($form) as $key) {
+      if (isset($form[$key]['#required_error'])) {
+        $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+        $this->assertText($form[$key]['#required_error']);
+      }
+      elseif (isset($form[$key]['#form_test_required_error'])) {
+        $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+        $this->assertText($form[$key]['#form_test_required_error']);
+      }
+    }
+    $this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
+
+    // Verify that no custom validation error appears with valid values.
+    $edit = array(
+      'textfield' => $this->randomString(),
+      'checkboxes[foo]' => TRUE,
+      'select' => 'foo',
+    );
+    $this->drupalPost('form-test/validate-required', $edit, 'Submit');
+
+    foreach (element_children($form) as $key) {
+      if (isset($form[$key]['#required_error'])) {
+        $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+        $this->assertNoText($form[$key]['#required_error']);
+      }
+      elseif (isset($form[$key]['#form_test_required_error'])) {
+        $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+        $this->assertNoText($form[$key]['#form_test_required_error']);
+      }
+    }
+    $this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
+  }
 }
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 f681c75..5d58956 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -413,23 +413,30 @@ function form_test_validate_form_validate(&$form, &$form_state) {
  */
 function form_test_validate_required_form($form, &$form_state) {
   $options = drupal_map_assoc(array('foo', 'bar'));
+  $validate = array('form_test_validate_required_form_element_validate');
 
   $form['textfield'] = array(
     '#type' => 'textfield',
-    '#title' => 'Textfield',
+    '#title' => 'Name',
     '#required' => TRUE,
+    '#required_error' => t('Please enter a name.'),
+    '#element_validate' => $validate,
   );
   $form['checkboxes'] = array(
     '#type' => 'checkboxes',
     '#title' => 'Checkboxes',
     '#options' => $options,
     '#required' => TRUE,
+    '#form_test_required_error' => t('Please choose at least one option.'),
+    '#element_validate' => $validate,
   );
   $form['select'] = array(
     '#type' => 'select',
     '#title' => 'Select',
     '#options' => $options,
     '#required' => TRUE,
+    '#form_test_required_error' => t('Please select something.'),
+    '#element_validate' => $validate,
   );
   $form['radios'] = array(
     '#type' => 'radios',
@@ -448,6 +455,16 @@ function form_test_validate_required_form($form, &$form_state) {
 }
 
 /**
+ * Form element validation handler for 'Name' field in form_test_validate_required_form().
+ */
+function form_test_validate_required_form_element_validate($element, &$form_state) {
+  // Set a custom validation error on the #required element.
+  if (!empty($element['#required_but_empty']) && isset($element['#form_test_required_error'])) {
+    form_error($element, $element['#form_test_required_error']);
+  }
+}
+
+/**
  * Form submission handler for form_test_validate_required_form().
  */
 function form_test_validate_required_form_submit($form, &$form_state) {
-- 
1.7.9.5

