diff options
-rw-r--r-- | includes/form.inc | 142 | ||||
-rw-r--r-- | modules/field/field.form.inc | 19 | ||||
-rw-r--r-- | modules/poll/poll.module | 20 | ||||
-rw-r--r-- | modules/simpletest/tests/form.test | 21 | ||||
-rw-r--r-- | modules/simpletest/tests/form_test.module | 46 |
5 files changed, 219 insertions, 29 deletions
diff --git a/includes/form.inc b/includes/form.inc index 0e5212444..13571a111 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -888,19 +888,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { _form_validate($elements[$key], $form_state); } } + // Validate the current input. if (!isset($elements['#validated']) || !$elements['#validated']) { + // The following errors are always shown. 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, &$form_state, $form_id = NULL) { } } + // 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 + // #limit_validation_errors property contains the information for which + // errors are needed, and all other errors are to be suppressed. The + // #limit_validation_errors property is ignored if the button doesn't also + // define its own submit handlers, because it's too large a security risk to + // have any invalid user input when executing form-level submit handlers. + if (isset($form_state['clicked_button']['#limit_validation_errors']) && isset($form_state['clicked_button']['#submit'])) { + form_set_error(NULL, '', $form_state['clicked_button']['#limit_validation_errors']); + } + else { + // As an extra security measure, explicitly turn off error suppression. + // Since this is also done at the end of this function, doing it here is + // only to handle the rare edge case where a validate handler invokes form + // processing of another form. + drupal_static_reset('form_set_error:limit_validation_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, &$form_state, $form_id = NULL) { } $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:limit_validation_errors'); } /** @@ -1005,18 +1032,99 @@ function form_execute_handlers($type, &$form, &$form_state) { * element where the #parents array starts with 'foo'. * @param $message * The error message to present to the user. + * @param $limit_validation_errors + * Internal use only. The #limit_validation_errors property of the clicked + * button if it exists. Multistep forms not wanting to validate the whole form + * can set the #limit_validation_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 must also define + * its #submit property and those handlers will be executed even if there is + * invalid input, so extreme care should be taken with respect to what is + * performed by them. 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 the #limit_validation_errors property on buttons that trigger + * saving of form values to the database. + * The #limit_validation_errors property is a list of "sections" within + * $form_state['values'] that must contain valid values. Each "section" is an + * array with the ordered set of keys needed to reach that part of + * $form_state['values'] (i.e., the #parents property of the element). + * For example: + * @code + * $form['actions']['previous']['#limit_validation_errors'] = array( + * array('step1'), + * array('foo', 'bar'), + * ); + * @endcode + * This will require $form_state['values']['step1'] and everything within it + * (for example, $form_state['values']['step1']['choice']) to be valid, so + * calls to form_set_error('step1', $message) or + * form_set_error('step1][choice', $message) will prevent the submit handlers + * from running, and result in the error message being displayed to the user. + * However, calls to form_set_error('step2', $message) and + * form_set_error('step2][groupX][choiceY', $message) will be suppressed, + * resulting in the message not being displayed to the user, and the submit + * handlers will run despite $form_state['values']['step2'] and + * $form_state['values']['step2']['groupX']['choiceY'] containing invalid + * values. Errors for an invalid $form_state['values']['foo'] will be + * suppressed, but errors for invalid values for + * $form_state['values']['foo']['bar'] and everything within it will be + * recorded. If the button doesn't need any user input to be valid, then the + * #limit_validation_errors can be set to an empty array, in which case, all + * calls to form_set_error() will be suppressed. + * Partial form validation is implemented by suppressing errors rather than by + * skipping the input processing and validation steps entirely, because some + * forms have button-level submit handlers that call Drupal API functions that + * assume that certain data exists within $form_state['values'], and while not + * doing anything with that data that requires it to be valid, PHP errors + * would be triggered if the input processing and validation steps were fully + * skipped. @see http://drupal.org/node/370537. + * * @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 = '', $limit_validation_errors = NULL) { $form = &drupal_static(__FUNCTION__, array()); + $sections = &drupal_static(__FUNCTION__ . ':limit_validation_errors'); + if (isset($limit_validation_errors)) { + $sections = $limit_validation_errors; + } + if (isset($name) && !isset($form[$name])) { - $form[$name] = $message; - if ($message) { - drupal_set_message($message, 'error'); + $record = TRUE; + if (isset($sections)) { + // #limit_validation_errors is an array of "sections" within which user + // input must be valid. If the element is within one of these sections, + // the error must be recorded. Otherwise, it can be suppressed. + // #limit_validation_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 ($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 +3369,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd $batch =& batch_get(); drupal_theme_initialize(); - + if (isset($batch)) { // Add process information $process_info = array( @@ -3276,7 +3384,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd ); $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); diff --git a/modules/field/field.form.inc b/modules/field/field.form.inc index 0970c1a6a..a2c03a5c1 100644 --- a/modules/field/field.form.inc +++ b/modules/field/field.form.inc @@ -214,7 +214,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, '#name' => $field_name . '_add_more', '#value' => t('Add another item'), '#attributes' => array('class' => array('field-add-more-submit')), - // Submit callback for disabled JavaScript. + '#limit_validation_errors' => array(array($field_name, $langcode)), '#submit' => array('field_add_more_submit'), '#ajax' => array( 'callback' => 'field_add_more_js', @@ -341,9 +341,13 @@ function field_default_form_errors($obj_type, $object, $field, $instance, $langc } /** - * Submit handler to add more choices to a field form. This handler is used when - * JavaScript is not available. It makes changes to the form state and the - * entire form is rebuilt during the page reload. + * Submit handler for the "Add another item" button of a field form. + * + * This handler is run regardless of whether JS is enabled or not. It makes + * changes to the form state. If the button was clicked with JS disabled, then + * the page is reloaded with the complete rebuilt form. If the button was + * clicked with JS enabled, then ajax_form_callback() calls field_add_more_js() + * to return just the changed part of the form. */ function field_add_more_submit($form, &$form_state) { // Set the form to rebuild and run submit handlers. @@ -360,7 +364,12 @@ function field_add_more_submit($form, &$form_state) { } /** - * Ajax callback for addition of new empty widgets. + * Ajax callback in response to a new empty widget being added to the form. + * + * This returns the new page content to replace the page content made obsolete + * by the form submission. + * + * @see field_add_more_submit() */ function field_add_more_js($form, $form_state) { // Retrieve field information. diff --git a/modules/poll/poll.module b/modules/poll/poll.module index 9c48de6b6..42d7afc68 100644 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -271,7 +271,8 @@ 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, - '#submit' => array('poll_more_choices_submit'), // If no javascript action. + '#limit_validation_errors' => array(array('choice')), + '#submit' => array('poll_more_choices_submit'), '#ajax' => array( 'callback' => 'poll_choice_js', 'wrapper' => 'poll-choices', @@ -322,9 +323,13 @@ function poll_form($node, &$form_state) { } /** - * Submit handler to add more choices to a poll form. This handler is used when - * javascript is not available. It makes changes to the form state and the - * entire form is rebuilt during the page reload. + * Submit handler to add more choices to a poll form. + * + * This handler is run regardless of whether JS is enabled or not. It makes + * changes to the form state. If the button was clicked with JS disabled, then + * the page is reloaded with the complete rebuilt form. If the button was + * clicked with JS enabled, then ajax_form_callback() calls poll_choice_js() to + * return just the changed part of the form. */ function poll_more_choices_submit($form, &$form_state) { include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc'; @@ -379,7 +384,12 @@ function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight } /** - * Menu callback for AHAH additions. Render the new poll choices. + * Ajax callback in response to new choices being added to the form. + * + * This returns the new page content to replace the page content made obsolete + * by the form submission. + * + * @see poll_more_choices_submit() */ function poll_choice_js($form, $form_state) { return $form['choice_wrapper']['choice']; diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index ba6a5d622..108391777 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -231,6 +231,27 @@ class FormValidationTestCase extends DrupalWebTestCase { $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 #limit_validation_errors. + */ + function testValidateLimitErrors() { + $edit = array('test' => 'invalid'); + $path = 'form-test/limit-validation-errors'; + + // Submit the form by pressing the button with #limit_validation_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'); + } } /** diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index 2036204ba..c8f565938 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -17,6 +17,13 @@ function form_test_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['form-test/limit-validation-errors'] = array( + 'title' => 'Form validation with some error suppression', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_limit_validation_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_validate(&$form, &$form_state) { } /** + * Builds a simple form with a button triggering partial validation. + */ +function form_test_limit_validation_errors_form($form, &$form_state) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => 'Title', + '#required' => TRUE, + ); + $form['test'] = array( + '#type' => 'textfield', + '#element_validate' => array('form_test_limit_validation_errors_element_validate_test'), + ); + $form['actions']['partial'] = array( + '#type' => 'submit', + '#limit_validation_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_limit_validation_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, &$form_state) { /** * 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($form, &$form_state) { /** * Implements hook_form_FORM_ID_alter(). - * + * * @see form_test_state_persist() */ function form_test_form_form_test_state_persist_alter(&$form, &$form_state) { |