=== modified file 'includes/form.inc' --- includes/form.inc 2009-12-29 20:16:09 +0000 +++ includes/form.inc 2009-12-30 19:48:23 +0000 @@ -879,15 +879,34 @@ 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); } + // Check whether validation needs to be skipped for this element according to + // the #validate_values of the clicked button. We have to recurse into all + // form elements, because submitted form values can vary based on #parents and + // #parents do not necessarily follow the elements in the form structure. + // After the complete form has been validated, the button-level #validate + // handlers are executed via form_execute_handlers(), so we must not skip + // validation on the top-level. + if (!isset($form_id) && _form_validate_skip($elements, $form_state)) { + // @TODO: obliterate this hack in D8. This should be a simple return. + // Instead, we save the form errors and error messages in the beginning and + // restore at the end. This code violates encapsulation and should not be + // in core. + $skip = TRUE; + $form_errors = drupal_static('form_set_error', array()); + $error_messages = isset($_SESSION['messages']['error']) ? $_SESSION['messages']['error'] : NULL; + } + else { + $skip = FALSE; + } + + + // 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'])) { @@ -943,7 +962,74 @@ function _form_validate(&$elements, &$fo } } $elements['#validated'] = TRUE; + // @TODO: obliterate this hack in D8. This should be a simple return. + // Instead, we save the form errors in the beginning and restore at the + // end. + if ($skip) { + $current_form_errors = &drupal_static('form_set_error', array()); + $current_form_errors = $form_errors; + $_SESSION['messages']['error'] = $error_messages; + } + } +} + +/** + * Check if the validation of an element needs to be skipped. + * + * Multistep forms not wanting to validate the whole form can set the + * #validate_values property on buttons to avoid the validation of some + * elements. 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 form-level #validate and + * #submit handlers won't be executed. Only button-level #validate and #submit + * handlers will be executed and extreme care should be taken, since + * $form_state['values'] will contain not validated values. However, that is + * typically not a problem as these buttons (like "Previous" or "Add more") 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 validate $form_state['values']['step1']['choice'], since the first + * key is 'step1'. Anything under $form_state['values']['step2'] won't be + * validated. + * $form_state['values']['foo'] won't be validated, but everything under + * $form_state['values']['foo']['bar'] will be. + * + * @see _form_validate() + */ +function _form_validate_skip($element, $form_state) { + // Validation of submitted form values can only be skipped, if the clicked + // button defines the #validate_values property. Otherwise, the entire form + // needs to be validated. + if (isset($form_state['clicked_button']['#validate_values'])) { + // Certain form buttons may want to skip form validation entirely, e.g. a + // "Previous" button in a multi-step form. + if (!$form_state['clicked_button']['#validate_values']) { + return TRUE; + } + // Compare all defined keys in #validate_values with the #parents of the + // passed $element. Since multiple form "sections" can be specified in + // #validate_values, we need to walk through all and only return FALSE if + // we find a direct match. + foreach ($form_state['clicked_button']['#validate_values'] as $section) { + if (array_slice($element['#parents'], 0, count($section)) === $section) { + return FALSE; + } + } + return TRUE; } + return FALSE; } /** @@ -967,8 +1053,9 @@ function form_execute_handlers($type, &$ if (isset($form_state[$type . '_handlers'])) { $handlers = $form_state[$type . '_handlers']; } - // Otherwise, check for a form-level handler. - elseif (isset($form['#' . $type])) { + // Otherwise, check for a form-level handler, but only if the complete form + // was validated. + elseif (isset($form['#' . $type]) && !isset($form_state['clicked_button']['#validate_values'])) { $handlers = $form['#' . $type]; } else { @@ -1006,7 +1093,7 @@ 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 = '') { @@ -3261,7 +3348,7 @@ function batch_process($redirect = NULL, $batch =& batch_get(); drupal_theme_initialize(); - + if (isset($batch)) { // Add process information $process_info = array( @@ -3276,7 +3363,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); === modified file 'modules/field/field.form.inc' --- modules/field/field.form.inc 2009-12-21 13:47:31 +0000 +++ modules/field/field.form.inc 2009-12-30 19:31:59 +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( === modified file 'modules/poll/poll.module' --- modules/poll/poll.module 2009-12-26 16:50:08 +0000 +++ modules/poll/poll.module 2009-12-30 19:32:39 +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', @@ -901,4 +902,3 @@ function poll_user_cancel($edit, $accoun break; } } - === modified file 'modules/simpletest/tests/form.test' --- modules/simpletest/tests/form.test 2009-12-17 17:18:02 +0000 +++ modules/simpletest/tests/form.test 2009-12-30 19:31:59 +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' => 'pass'); + $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 is validated'); + + // 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 is validated'); + } } /** === modified file 'modules/simpletest/tests/form_test.module' --- modules/simpletest/tests/form_test.module 2009-12-17 17:18:02 +0000 +++ modules/simpletest/tests/form_test.module 2009-12-30 19:31:59 +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,40 @@ 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_form_validate_test'), + ); + $form['actions']['partial'] = array( + '#type' => 'submit', + '#validate_values' => array(array('test')), + '#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_form_validate_test($form, &$form_state) { + if ($form_state['values']['test'] == 'pass') { + drupal_set_message('Test is validated'); + } +} + +/** * Create a header and options array. Helper function for callbacks. */ function _form_test_tableselect_get_data() { @@ -895,7 +936,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 +946,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) {