From aa0961ea915c8956202352d2945071df50eee758 Mon Sep 17 00:00:00 2001
From: Bob Vincent <bobvin@pillars.net>
Date: Fri, 12 Aug 2011 18:40:23 -0400
Subject: [PATCH] Issue #742344: Allow forms to set custom validation error messages on required fields.

---
 includes/form.inc                         |   36 +++++++++++++++------
 modules/simpletest/tests/form.test        |   46 +++++++++++++++++++++++++++
 modules/simpletest/tests/form_test.module |   48 +++++++++++++++++++++++++++++
 3 files changed, 120 insertions(+), 10 deletions(-)

diff --git a/includes/form.inc b/includes/form.inc
index 40363d6a738e9b0f373f4f8179898a45a91de928..fcaaa3fedc976772a67be6b149c540033ebd1bd1 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -1337,16 +1337,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_is_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_is_empty'] = TRUE;
       }
     }
 
@@ -1361,6 +1356,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']);
+      }
+      // 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.
+      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/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test
index 71187b5763e75127273132ac23b4913b8071d877..0f9d5e71d19feeda71c80308bb391a4c67b56856 100644
--- a/modules/simpletest/tests/form.test
+++ b/modules/simpletest/tests/form.test
@@ -122,6 +122,52 @@ class FormsTestCase extends DrupalWebTestCase {
   }
 
   /**
+   * 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.'));
+  }
+
+  /**
    * Test default value handling for checkboxes.
    *
    * @see _form_test_checkbox()
diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module
index a9348161a22a0e362571a117821aff6f46d97241..cc040647409544688ed2ecd06ac12a8f0bc5dc63 100644
--- a/modules/simpletest/tests/form_test.module
+++ b/modules/simpletest/tests/form_test.module
@@ -23,6 +23,12 @@ function form_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/validate-required'] = array(
+    'title' => 'Form #required validation',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_validate_required_form'),
+    'access callback' => TRUE,
+  );
   $items['form-test/limit-validation-errors'] = array(
     'title' => 'Form validation with some error suppression',
     'page callback' => 'drupal_get_form',
@@ -338,6 +344,48 @@ function form_test_validate_form_validate(&$form, &$form_state) {
 }
 
 /**
+ * Form constructor for simple #required tests.
+ */
+function form_test_validate_required_form($form, &$form_state) {
+  $form['textfield'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Name',
+    '#required' => TRUE,
+    '#required_error' => t('Please enter a name.'),
+    '#element_validate' => array('form_test_validate_required_form_element_validate'),
+  );
+  $form['checkboxes'] = array(
+    '#type' => 'checkboxes',
+    '#title' => 'Checkboxes',
+    '#options' => drupal_map_assoc(array('foo', 'bar')),
+    '#required' => TRUE,
+    '#form_test_required_error' => t('Please choose at least one option.'),
+    '#element_validate' => array('form_test_validate_required_form_element_validate'),
+  );
+  $form['select'] = array(
+    '#type' => 'select',
+    '#title' => 'Select',
+    '#options' => drupal_map_assoc(array('foo', 'bar')),
+    '#required' => TRUE,
+    '#form_test_required_error' => t('Please select something.'),
+    '#element_validate' => array('form_test_validate_required_form_element_validate'),
+  );
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Submit');
+  return $form;
+}
+
+/**
+ * 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_is_empty']) && isset($element['#form_test_required_error'])) {
+    form_error($element, $element['#form_test_required_error']);
+  }
+}
+
+/**
  * Builds a simple form with a button triggering partial validation.
  */
 function form_test_limit_validation_errors_form($form, &$form_state) {
-- 
1.7.4.1

