summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAngie Byron <webchick@24967.no-reply.drupal.org>2010-01-02 23:30:53 +0000
committerAngie Byron <webchick@24967.no-reply.drupal.org>2010-01-02 23:30:53 +0000
commit70e53b33c1074655f4ee917c0c4f4b1219bb109d (patch)
tree7033541b9bc09dfc85aa69be2b57a1f8c8fa9b85
parentd4f4d3c32e2b7028527b13fc3d63d84576562590 (diff)
downloadbrdo-70e53b33c1074655f4ee917c0c4f4b1219bb109d.tar.gz
brdo-70e53b33c1074655f4ee917c0c4f4b1219bb109d.tar.bz2
#370537 by chx, sun, effulgentsia, quicksketch, eaton, Heine, and yched: Allow buttons to only validate sections of forms, e.g. More buttons. (with tests)
-rw-r--r--includes/form.inc142
-rw-r--r--modules/field/field.form.inc19
-rw-r--r--modules/poll/poll.module20
-rw-r--r--modules/simpletest/tests/form.test21
-rw-r--r--modules/simpletest/tests/form_test.module46
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) {