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 1 Jan 2010 19:01:44 -0000 @@ -888,19 +888,11 @@ function _form_validate(&$elements, &$fo _form_validate($elements[$key], $form_state); } } + // Validate the current input. if (!isset($elements['#validated']) || !$elements['#validated']) { + // The following errors are always showed. if (isset($elements['#needs_validation'])) { - // Make sure a value is passed when the field is required. - // A simple call to empty() will not cut it here as some fields, like - // checkboxes, can return a valid value of '0'. Instead, check the - // length if it's a string, and the item count if it's an array. - // An unchecked checkbox has a #value of numeric 0, different than string - // '0', which could be a valid value. - if ($elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) { - form_error($elements, $t('!name field is required.', array('!name' => $elements['#title']))); - } - // Verify that the value is not longer than #maxlength. if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) { form_error($elements, $t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => drupal_strlen($elements['#value'])))); @@ -929,6 +921,36 @@ function _form_validate(&$elements, &$fo } } + // While this element is being validated, it may be desired that some calls + // to form_set_error() be suppressed and not result in a form error, so + // that a button that implements low-risk functionality (such as "Previous" + // or "Add more") that doesn't require all user input to be valid can still + // have its submit handlers triggered. The clicked button's #show_errors + // property contains the information for which errors are needed, and all + // other errors are to be suppressed. The #show_errors property is ignored + // if the button doesn't define its own submit handlers, because it's too + // large a security risk to allow any error suppression if form-level + // submit handlers will be executed. + if (isset($form_state['clicked_button']['#show_errors']) && isset($form_state['clicked_button']['#submit'])) { + form_set_error(NULL, '', $form_state['clicked_button']['#show_errors']); + } + else { + // As an extra security measure, explicitly turn off error suppression. + // Since we also do this at the end of this function, this only matters + // in a rare edge case where a validate handler invokes form processing + // of another form. + drupal_static_reset('form_set_error:show_errors'); + } + // Make sure a value is passed when the field is required. + // A simple call to empty() will not cut it here as some fields, like + // checkboxes, can return a valid value of '0'. Instead, check the + // length if it's a string, and the item count if it's an array. + // An unchecked checkbox has a #value of numeric 0, different than string + // '0', which could be a valid value. + if (isset($elements['#needs_validation']) && $elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) { + form_error($elements, $t('!name field is required.', array('!name' => $elements['#title']))); + } + // Call user-defined form level validators. if (isset($form_id)) { form_execute_handlers('validate', $elements, $form_state); @@ -944,6 +966,11 @@ function _form_validate(&$elements, &$fo } $elements['#validated'] = TRUE; } + + // Done validating this element, so turn off error suppression. + // _form_validate() turns it on again when starting on the next element, if + // it's still appropriate to do so. + drupal_static_reset('form_set_error:show_errors'); } /** @@ -1005,18 +1032,75 @@ function form_execute_handlers($type, &$ * element where the #parents array starts with 'foo'. * @param $message * The error message to present to the user. + * @param $show_errors + * Internal use only. The #show_errors property of the clicked button if it + * exists. Multistep forms not wanting to validate the whole form can set the + * #show_errors 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 #show_errors property is a list of $form_state['values'] keys. For + * example: + * @code + * $form['actions']['previous']['#show_errors'] = 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. + * * @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 = '') { +function form_set_error($name = NULL, $message = '', $show_errors = NULL) { $form = &drupal_static(__FUNCTION__, array()); + $show_sections = &drupal_static(__FUNCTION__ . ':show_errors'); + if (isset($show_errors)) { + $show_sections = $show_errors; + } + if (isset($name) && !isset($form[$name])) { - $form[$name] = $message; - if ($message) { - drupal_set_message($message, 'error'); + $record = TRUE; + if (isset($show_sections)) { + // #show_errors is an array of "sections" for which to display errors. If + // the element is within one of the sections, no errors are recorded. + // #show_errors can be an empty array, in which case all errors are + // suppressed. For example, a "Previous" button might want its submit + // action to be triggered even if none of the submitted values are valid. + $record = FALSE; + foreach ($show_sections as $section) { + // Exploding by '][' reconstructs the element's #parents. If the + // reconstructed #parents begin with the same keys as the specified + // section, then the element's values are within the part of + // $form_state['values'] that the clicked button requires to be valid, + // so errors for this element must be recorded. + if (array_slice(explode('][', $name), 0, count($section)) === $section) { + $record = TRUE; + break; + } + } + } + if ($record) { + $form[$name] = $message; + if ($message) { + drupal_set_message($message, 'error'); + } } } + return $form; } @@ -3261,7 +3345,7 @@ function batch_process($redirect = NULL, $batch =& batch_get(); drupal_theme_initialize(); - + if (isset($batch)) { // Add process information $process_info = array( @@ -3276,7 +3360,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 1 Jan 2010 18:55:51 -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')), + '#show_errors' => 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 1 Jan 2010 18:55:51 -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, + '#show_errors' => 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 1 Jan 2010 18:55:51 -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 #show_errors. + */ + function testValidateValues() { + $edit = array('test' => 'invalid'); + $path = 'form-test/validate-values'; + + // Submit the form by pressing the button with #show_errors 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 1 Jan 2010 18:55:51 -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_show_errors_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_show_errors_form($form, &$form_state) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => 'Title', + '#required' => TRUE, + ); + $form['test'] = array( + '#type' => 'textfield', + '#element_validate' => array('form_test_show_errors_element_validate_test'), + ); + $form['actions']['partial'] = array( + '#type' => 'submit', + '#show_errors' => 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_show_errors_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) {