Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.422
diff -u -p -r1.422 form.inc
--- includes/form.inc	29 Dec 2009 20:16:09 -0000	1.422
+++ includes/form.inc	31 Dec 2009 00:26:53 -0000
@@ -786,7 +786,11 @@ function drupal_validate_form($form_id, 
     }
   }
 
+  // Provide the internal function _form_set_error_suppress() with the form
+  // state to use while _form_validate() is running.
+  _form_set_error_suppress(NULL, $form_state, 1);
   _form_validate($form, $form_state, $form_id);
+  _form_set_error_suppress(NULL, $form_state, -1);
   $validated_forms[$form_id] = TRUE;
 }
 
@@ -879,15 +883,14 @@ function drupal_redirect_form($form_stat
  *   theming, and hook_form_alter functions.
  */
 function _form_validate(&$elements, &$form_state, $form_id = NULL) {
-  // Also used in the installer, pre-database setup.
-  $t = get_t();
-
   // Recurse through all children.
   foreach (element_children($elements) as $key) {
-    if (isset($elements[$key]) && $elements[$key]) {
-      _form_validate($elements[$key], $form_state);
-    }
+    _form_validate($elements[$key], $form_state);
   }
+
+  // Also used in the installer, pre-database setup.
+  $t = get_t();
+
   // Validate the current input.
   if (!isset($elements['#validated']) || !$elements['#validated']) {
     if (isset($elements['#needs_validation'])) {
@@ -947,6 +950,95 @@ function _form_validate(&$elements, &$fo
 }
 
 /**
+ * Check if the validation errors of an element need to be suppressed.
+ *
+ * Multistep forms not wanting to validate the whole form can set the
+ * #validate_values property on buttons to avoid validation errors of some
+ * elements preventing the button's submit handlers from running. For example,
+ * pressing the "Previous" button should not fire validation errors just because
+ * the current step has invalid values. AJAX is another typical example.
+ *
+ * If this property is set on the clicked button, the button-level #submit
+ * handlers will be executed even if there is invalid input, so extreme care
+ * should be taken with respect to what is performed by those handlers. This is
+ * typically not a problem with buttons like "Previous" or "Add more" that do
+ * not invoke persistent storage of the submitted form values.
+ *
+ * Do not use this property on buttons that trigger saving of form values to the
+ * database.
+ *
+ * The #validate_values property is a list of $form_state['values'] keys. For
+ * example:
+ * @code
+ *   $form['actions']['previous']['#validate_values'] = array(
+ *     array('foo', 'bar'),
+ *     array('step1'),
+ *   );
+ * @endcode
+ * This will require $form_state['values']['step1']['choice'] to be valid, since
+ * the first key is 'step1'. Validation errors will be suppressed for
+ * $form_state['values']['step2'] and everything under it.
+ * Errors for $form_state['values']['foo'] will be suppressed, but errors will
+ * not be suppressed for $form_state['values']['foo']['bar'] and everything
+ * under it.
+ *
+ * @see form_set_error()
+ */
+function _form_set_error_suppress($name, &$form_state = NULL, $mode = 0) {
+  static $form_states = array();
+
+  // Because form_set_error() is called without a $form_state parameter, we
+  // support the ability for this function to be called with $mode=1 to add a
+  // form state to the stack and $mode=-1 to pop the stack. Then, if called with
+  // the default mode and without a form state, we use the one most recently
+  // added to the stack. We store the form states in the stack by reference so
+  // that we're always working with up-to-date information about the state. We
+  // maintain a stack instead of a single variable to support the edge case of a
+  // validate handler calling drupal_build_form() of another form.
+  if ($mode == 1) {
+    $form_states[] = &$form_state;
+    return;
+  }
+  if ($mode == -1) {
+    array_pop($form_states);
+    return;
+  }
+  if (!isset($form_state) && ($n = count($form_states))) {
+    // This code is primarily for when the $form_state parameter isn't
+    // passed at all, but if it is passed as an explicit parameter with a
+    // value of NULL, we don't want to change that function's variable, so
+    // we first break the reference.
+    unset($form_state);
+    $form_state = $form_states[$n-1];
+  }
+
+  // Validation errors should only be suppressed if the clicked button defines
+  // the #validate_values property. Otherwise, the entire form needs to be
+  // fully valid. We prevent insecure execution of form-level submit handlers
+  // by not supporting error suppression unless the button also defines its own
+  // submit handlers.
+  if (isset($form_state['clicked_button']['#validate_values']) && isset($form_state['clicked_button']['#submit'])) {
+    // #validate_values is an array of "sections" for which to not suppress
+    // errors. If the element is within any of the sections, we must return
+    // FALSE. #validate_values can be an empty array, in which case all errors
+    // are suppressed. For example, a "Previous" button might want its submit
+    // action triggered even if none of the submitted values are valid.
+    foreach ($form_state['clicked_button']['#validate_values'] as $section) {
+      // Exploding by '][' reconstructs the element's #parents. If this
+      // reconstructed #parents array begins with all the same entries as the
+      // specified section, then the element's values exist within the part
+      // of $form_state['values'] that the clicked button requires to be valid,
+      // so errors for this name must not be suppressed.
+      if (array_slice(explode('][', $name), 0, count($section)) === $section) {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
  * A helper function used to execute custom validation and submission
  * handlers for a given form. Button-specific handlers are checked
  * first. If none exist, the function falls back to form-level handlers.
@@ -1006,12 +1098,12 @@ function form_execute_handlers($type, &$
  * @param $message
  *   The error message to present to the user.
  * @return
- *   Return value is for internal use only. To get a list of errors, use 
+ *   Return value is for internal use only. To get a list of errors, use
  *   form_get_errors() or form_get_error().
  */
 function form_set_error($name = NULL, $message = '') {
   $form = &drupal_static(__FUNCTION__, array());
-  if (isset($name) && !isset($form[$name])) {
+  if (isset($name) && !isset($form[$name]) && !_form_set_error_suppress($name)) {
     $form[$name] = $message;
     if ($message) {
       drupal_set_message($message, 'error');
@@ -3261,7 +3353,7 @@ function batch_process($redirect = NULL,
   $batch =& batch_get();
 
   drupal_theme_initialize();
-  
+
   if (isset($batch)) {
     // Add process information
     $process_info = array(
@@ -3276,7 +3368,7 @@ function batch_process($redirect = NULL,
     );
     $batch += $process_info;
 
-    // The batch is now completely built. Allow other modules to make changes to the 
+    // The batch is now completely built. Allow other modules to make changes to the
     // batch so that it is easier to reuse batch processes in other enviroments.
     drupal_alter('batch', $batch);
 
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.38
diff -u -p -r1.38 field.form.inc
--- modules/field/field.form.inc	21 Dec 2009 13:47:32 -0000	1.38
+++ modules/field/field.form.inc	31 Dec 2009 00:26:54 -0000
@@ -214,6 +214,7 @@ function field_multiple_value_form($fiel
           '#name' => $field_name . '_add_more',
           '#value' => t('Add another item'),
           '#attributes' => array('class' => array('field-add-more-submit')),
+          '#validate_values' => array(array($field_name, $langcode)),
           // Submit callback for disabled JavaScript.
           '#submit' => array('field_add_more_submit'),
           '#ajax' => array(
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.330
diff -u -p -r1.330 poll.module
--- modules/poll/poll.module	26 Dec 2009 16:50:09 -0000	1.330
+++ modules/poll/poll.module	31 Dec 2009 00:26:54 -0000
@@ -273,6 +273,7 @@ function poll_form($node, &$form_state) 
     '#value' => t('More choices'),
     '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
     '#weight' => 1,
+    '#validate_values' => array(array('choice')),
     '#submit' => array('poll_more_choices_submit'), // If no javascript action.
     '#ajax' => array(
       'callback' => 'poll_choice_js',
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.31
diff -u -p -r1.31 form.test
--- modules/simpletest/tests/form.test	17 Dec 2009 17:18:03 -0000	1.31
+++ modules/simpletest/tests/form.test	31 Dec 2009 00:26:54 -0000
@@ -231,6 +231,27 @@ class FormValidationTestCase extends Dru
     $this->assertNoFieldByName('name', t('Form element was hidden.'));
     $this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.'));
   }
+
+  /**
+   * Tests partial form validation through #validate_values.
+   */
+  function testValidateValues() {
+    $edit = array('test' => 'invalid');
+    $path = 'form-test/validate-values';
+
+    // Submit the form by pressing the button with #validate_values and ensure
+    // that the title field is not validated, but the #element_validate handler
+    // for the 'test' field is triggered.
+    $this->drupalPost($path, $edit, t('Partial validate'));
+    $this->assertNoText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Test element is invalid');
+
+    // Now test full form validation and ensure that the #element_validate
+    // handler is still triggered.
+    $this->drupalPost($path, $edit, t('Full validate'));
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Test element is invalid');
+  }
 }
 
 /**
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.24
diff -u -p -r1.24 form_test.module
--- modules/simpletest/tests/form_test.module	17 Dec 2009 17:18:03 -0000	1.24
+++ modules/simpletest/tests/form_test.module	31 Dec 2009 00:26:54 -0000
@@ -17,6 +17,13 @@ function form_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/validate-values'] = array(
+    'title' => 'Form validation values test_form',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_validate_values_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -204,6 +211,41 @@ function form_test_validate_form_validat
 }
 
 /**
+ * Builds a simple form with a button triggering partial validation.
+ */
+function form_test_validate_values_form($form, &$form_state) {
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+  );
+  $form['test'] = array(
+    '#type' => 'textfield',
+    '#element_validate' => array('form_test_validate_values_element_validate_test'),
+  );
+  $form['actions']['partial'] = array(
+    '#type' => 'submit',
+    '#validate_values' => array(array('test')),
+    '#submit' => array(),
+    '#value' => t('Partial validate'),
+  );
+  $form['actions']['full'] = array(
+    '#type' => 'submit',
+    '#value' => t('Full validate'),
+  );
+  return $form;
+}
+
+/**
+ * Form element validation handler for the 'test' element.
+ */
+function form_test_validate_values_element_validate_test(&$element, &$form_state) {
+  if ($element['#value'] == 'invalid') {
+    form_error($element, 'Test element is invalid');
+  }
+}
+
+/**
  * Create a header and options array. Helper function for callbacks.
  */
 function _form_test_tableselect_get_data() {
@@ -895,7 +937,7 @@ function form_test_state_persist($form, 
 
 /**
  * Submit handler.
- * 
+ *
  * @see form_test_state_persist()
  */
 function form_test_state_persist_submit($form, &$form_state) {
@@ -905,7 +947,7 @@ function form_test_state_persist_submit(
 
 /**
  * Implements hook_form_FORM_ID_alter().
- * 
+ *
  * @see form_test_state_persist()
  */
 function form_test_form_form_test_state_persist_alter(&$form, &$form_state) {
