Commit 70e53b33 authored by webchick's avatar webchick
Browse files

#370537 by chx, sun, effulgentsia, quicksketch, eaton, Heine, and yched: Allow...

#370537 by chx, sun, effulgentsia, quicksketch, eaton, Heine, and yched: Allow buttons to only validate sections of forms, e.g. More buttons. (with tests)
parent d4f4d3c3
...@@ -888,19 +888,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { ...@@ -888,19 +888,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
_form_validate($elements[$key], $form_state); _form_validate($elements[$key], $form_state);
} }
} }
// Validate the current input. // Validate the current input.
if (!isset($elements['#validated']) || !$elements['#validated']) { if (!isset($elements['#validated']) || !$elements['#validated']) {
// The following errors are always shown.
if (isset($elements['#needs_validation'])) { 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. // Verify that the value is not longer than #maxlength.
if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#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'])))); 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) { ...@@ -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. // Call user-defined form level validators.
if (isset($form_id)) { if (isset($form_id)) {
form_execute_handlers('validate', $elements, $form_state); form_execute_handlers('validate', $elements, $form_state);
...@@ -944,6 +966,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { ...@@ -944,6 +966,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
} }
$elements['#validated'] = TRUE; $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) { ...@@ -1005,18 +1032,99 @@ function form_execute_handlers($type, &$form, &$form_state) {
* element where the #parents array starts with 'foo'. * element where the #parents array starts with 'foo'.
* @param $message * @param $message
* The error message to present to the user. * 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
* 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(). * 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()); $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])) { if (isset($name) && !isset($form[$name])) {
$form[$name] = $message; $record = TRUE;
if ($message) { if (isset($sections)) {
drupal_set_message($message, 'error'); // #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; return $form;
} }
...@@ -3261,7 +3369,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd ...@@ -3261,7 +3369,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd
$batch =& batch_get(); $batch =& batch_get();
drupal_theme_initialize(); drupal_theme_initialize();
if (isset($batch)) { if (isset($batch)) {
// Add process information // Add process information
$process_info = array( $process_info = array(
...@@ -3276,7 +3384,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd ...@@ -3276,7 +3384,7 @@ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = 'd
); );
$batch += $process_info; $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. // batch so that it is easier to reuse batch processes in other enviroments.
drupal_alter('batch', $batch); drupal_alter('batch', $batch);
......
...@@ -214,7 +214,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, ...@@ -214,7 +214,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form,
'#name' => $field_name . '_add_more', '#name' => $field_name . '_add_more',
'#value' => t('Add another item'), '#value' => t('Add another item'),
'#attributes' => array('class' => array('field-add-more-submit')), '#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'), '#submit' => array('field_add_more_submit'),
'#ajax' => array( '#ajax' => array(
'callback' => 'field_add_more_js', 'callback' => 'field_add_more_js',
...@@ -341,9 +341,13 @@ function field_default_form_errors($obj_type, $object, $field, $instance, $langc ...@@ -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 * Submit handler for the "Add another item" button of a field form.
* JavaScript is not available. It makes changes to the form state and the *
* entire form is rebuilt during the page reload. * 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) { function field_add_more_submit($form, &$form_state) {
// Set the form to rebuild and run submit handlers. // Set the form to rebuild and run submit handlers.
...@@ -360,7 +364,12 @@ function field_add_more_submit($form, &$form_state) { ...@@ -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) { function field_add_more_js($form, $form_state) {
// Retrieve field information. // Retrieve field information.
......
...@@ -271,7 +271,8 @@ function poll_form($node, &$form_state) { ...@@ -271,7 +271,8 @@ function poll_form($node, &$form_state) {
'#value' => t('More choices'), '#value' => t('More choices'),
'#description' => t("If the amount of boxes above isn't enough, click here to add more choices."), '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
'#weight' => 1, '#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( '#ajax' => array(
'callback' => 'poll_choice_js', 'callback' => 'poll_choice_js',
'wrapper' => 'poll-choices', 'wrapper' => 'poll-choices',
...@@ -322,9 +323,13 @@ function poll_form($node, &$form_state) { ...@@ -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 * Submit handler to add more choices to a poll form.
* javascript is not available. It makes changes to the form state and the *
* entire form is rebuilt during the page reload. * 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) { function poll_more_choices_submit($form, &$form_state) {
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc'; 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 ...@@ -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) { function poll_choice_js($form, $form_state) {
return $form['choice_wrapper']['choice']; return $form['choice_wrapper']['choice'];
......
...@@ -231,6 +231,27 @@ class FormValidationTestCase extends DrupalWebTestCase { ...@@ -231,6 +231,27 @@ class FormValidationTestCase extends DrupalWebTestCase {
$this->assertNoFieldByName('name', t('Form element was hidden.')); $this->assertNoFieldByName('name', t('Form element was hidden.'));
$this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.')); $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');
}
} }
/** /**
......
...@@ -17,6 +17,13 @@ function form_test_menu() { ...@@ -17,6 +17,13 @@ function form_test_menu() {
'access arguments' => array('access content'), 'access arguments' => array('access content'),
'type' => MENU_CALLBACK, '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( $items['form_test/tableselect/multiple-true'] = array(
'title' => 'Tableselect checkboxes test', 'title' => 'Tableselect checkboxes test',
...@@ -203,6 +210,41 @@ function form_test_validate_form_validate(&$form, &$form_state) { ...@@ -203,6 +210,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. * Create a header and options array. Helper function for callbacks.
*/ */
...@@ -895,7 +937,7 @@ function form_test_state_persist($form, &$form_state) { ...@@ -895,7 +937,7 @@ function form_test_state_persist($form, &$form_state) {
/** /**
* Submit handler. * Submit handler.
* *
* @see form_test_state_persist() * @see form_test_state_persist()
*/ */
function form_test_state_persist_submit($form, &$form_state) { function form_test_state_persist_submit($form, &$form_state) {
...@@ -905,7 +947,7 @@ 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(). * Implements hook_form_FORM_ID_alter().
* *
* @see form_test_state_persist() * @see form_test_state_persist()
*/ */
function form_test_form_form_test_state_persist_alter(&$form, &$form_state) { function form_test_form_form_test_state_persist_alter(&$form, &$form_state) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment