diff --git a/includes/common.inc b/includes/common.inc index afb08eafd4b59cbeb3eb06641ba0cd358235ef61..3e9b9c7f9a81c5b9a8f6b7355787a1c7fb777847 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -5581,6 +5581,157 @@ function element_get_visible_children(array $elements) { return array_keys($visible_children); } +/** + * Sets a value in a nested array with variable depth. + * + * This helper function should be used when the depth of the array element you + * are changing may vary (that is, the number of parent keys is variable). It + * is primarily used for form structures and renderable arrays. + * + * Example: + * @code + * // Assume you have a 'signature' element somewhere in a form. It might be: + * $form['signature_settings']['signature'] = array( + * '#type' => 'text_format', + * '#title' => t('Signature'), + * ); + * // Or, it might be further nested: + * $form['signature_settings']['user']['signature'] = array( + * '#type' => 'text_format', + * '#title' => t('Signature'), + * ); + * @endcode + * + * To deal with the situation, the code needs to figure out the route to the + * element, given an array of parents that is either + * @code array('signature_settings', 'signature') @endcode in the first case or + * @code array('signature_settings', 'user', 'signature') @endcode in the second + * case. + * + * Without this helper function the only way to set the signature element in one + * line would be using eval(), which should be avoided: + * @code + * // Do not do this! Avoid eval(). + * eval('$form[\'' . implode("']['", $parents) . '\'] = $element;'); + * @endcode + * + * Instead, use this helper function: + * @code + * drupal_array_set_nested_value($form, $parents, $element); + * @endcode + * + * However if the number of array parent keys is static, the value should always + * be set directly rather than calling this function. For instance, for the + * first example we could just do: + * @code + * $form['signature_settings']['signature'] = $element; + * @endcode + * + * @param $array + * A reference to the array to modify. + * @param $parents + * An array of parent keys, starting with the outermost key. + * @param $value + * The value to set. + * + * @see drupal_array_get_nested_value() + */ +function drupal_array_set_nested_value(&$array, $parents, $value) { + $ref = &$array; + foreach ($parents as $parent) { + // Note that PHP is fine with referencing a not existing array key - in this + // case it just creates an entry with NULL as value. + $ref = &$ref[$parent]; + } + $ref = $value; +} + +/** + * Retrieves a value from a nested array with variable depth. + * + * This helper function should be used when the depth of the array element you + * are changing may vary (that is, the number of parent keys is variable). It is + * primarily used for form structures and renderable arrays. + * + * Without this helper function the only way to get a nested array value with + * variable depth in one line would be using eval(), which should be avoided: + * @code + * // Do not do this! Avoid eval(). + * // May also throw a PHP notice, if the variable array keys do not exist. + * eval('$value = $array[\'' . implode("']['", $parents) . "'];"); + * @endcode + * + * Instead, use this helper function: + * @code + * list($value, $value_exists) = drupal_array_get_nested_value($form, $parents); + * if ($value_exists) { + * // ... do something with $value ... + * } + * @endcode + * + * However if the number of array parent keys is static, the value should always + * be get directly rather than calling this function. For instance: + * @code + * $value = $form['signature_settings']['signature']; + * @endcode + * + * @param $array + * The array from which to get the value. + * @param $parents + * An array of parent keys of the value, starting with the outermost key. + * + * @return + * An indexed array containing: + * - The requested nested value, if it exists, or NULL if it does not. + * - TRUE if all the parent keys exist, FALSE otherwise. + * + * @see drupal_array_set_nested_value() + * @see drupal_array_value_exists() + */ +function drupal_array_get_nested_value($array, $parents) { + foreach ($parents as $parent) { + if (isset($array[$parent])) { + $array = $array[$parent]; + } + else { + return array(NULL, FALSE); + } + } + return array($array, TRUE); +} + +/** + * Determines whether a value in a nested array with variable depth exists. + * + * This helper function should be used when the depth of the array element to be + * checked may vary (that is, the number of parent keys is variable). See + * drupal_array_set_nested_value() for details. This helper is primarily used + * for form structures and renderable arrays. + * + * @param $array + * The array with the value to check for. + * @param $parents + * An array of parent keys of the value, starting with the outermost key. + * + * @return + * TRUE if all the parent keys exist, FALSE otherwise. + * + * @see drupal_array_set_nested_value() + * @see drupal_array_get_nested_value() + */ +function drupal_array_nested_value_exists($array, $parents) { + foreach ($parents as $parent) { + if (isset($array[$parent])) { + $array = $array[$parent]; + } + else { + return FALSE; + } + } + return TRUE; +} + + /** * Provide theme registration for themes across .inc files. */ diff --git a/includes/form.inc b/includes/form.inc index 6501941f2be9f20173414c7503e63261b2924cbf..3aaeec99a0c85f37517aa9a09141ea42d2271979 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -953,6 +953,24 @@ function drupal_validate_form($form_id, &$form, &$form_state) { _form_validate($form, $form_state, $form_id); $validated_forms[$form_id] = TRUE; + + // If validation errors are limited then remove any non validated form values, + // so that only values that passed validation are left for submit callbacks. + if (isset($form_state['triggering_element']['#limit_validation_errors']) && $form_state['triggering_element']['#limit_validation_errors'] !== FALSE) { + $values = array(); + foreach ($form_state['triggering_element']['#limit_validation_errors'] as $section) { + list($value, $value_exists) = drupal_array_get_nested_value($form_state['values'], $section); + if ($value_exists) { + drupal_array_set_nested_value($values, $section, $value); + } + } + // For convenience we always make the value of the pressed button available. + if (isset($form_state['triggering_element']['#button_type'])) { + $values[$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value']; + drupal_array_set_nested_value($values, $form_state['triggering_element']['#parents'], $form_state['triggering_element']['#value']); + } + $form_state['values'] = $values; + } } /** @@ -1727,11 +1745,9 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { // submit explicit NULL values when calling drupal_form_submit(), so we do // not modify $form_state['input'] for them. if (!$input_exists && !$form_state['rebuild'] && !$form_state['programmed']) { - // We leverage the internal logic of form_set_value() to change the - // input values by passing $form_state['input'] instead of the usual - // $form_state['values']. In effect, this adds the necessary parent keys - // to $form_state['input'] and sets the element's input value to NULL. - _form_set_value($form_state['input'], $element, $element['#parents'], NULL); + // Add the necessary parent keys to $form_state['input'] and sets the + // element's input value to NULL. + drupal_array_set_nested_value($form_state['input'], $element['#parents'], NULL); $input_exists = TRUE; } // If we have input for the current element, assign it to the #value @@ -1790,11 +1806,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { // Set the element's value in $form_state['values'], but only, if its key // does not exist yet (a #value_callback may have already populated it). - $values = $form_state['values']; - foreach ($element['#parents'] as $key) { - $values = (isset($values[$key]) ? $values[$key] : NULL); - } - if (!isset($values)) { + if (!drupal_array_nested_value_exists($form_state['values'], $element['#parents'])) { form_set_value($element, $element['#value'], $form_state); } } @@ -2140,26 +2152,7 @@ function form_type_token_value($element, $input = FALSE) { * Form state array where the value change should be recorded. */ function form_set_value($element, $value, &$form_state) { - _form_set_value($form_state['values'], $element, $element['#parents'], $value); -} - -/** - * Helper function for form_set_value() and _form_builder_handle_input_element(). - * - * We iterate over $parents and create nested arrays for them in $form_values if - * needed. Then we insert the value into the last parent key. - */ -function _form_set_value(&$form_values, $element, $parents, $value) { - $parent = array_shift($parents); - if (empty($parents)) { - $form_values[$parent] = $value; - } - else { - if (!isset($form_values[$parent])) { - $form_values[$parent] = array(); - } - _form_set_value($form_values[$parent], $element, $parents, $value); - } + drupal_array_set_nested_value($form_state['values'], $element['#parents'], $value); } /** diff --git a/modules/file/file.module b/modules/file/file.module index f5f642ef2fdd589e5dd415d43a0f71be6a4941c0..bc0e292cc24eaff48ec38da836ac92e59c263fcf 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -572,10 +572,7 @@ function file_managed_file_submit($form, &$form_state) { // and set $element to the managed_file element that contains that button. $parents = $form_state['triggering_element']['#array_parents']; $button_key = array_pop($parents); - $element = $form; - foreach ($parents as $parent) { - $element = $element[$parent]; - } + list($element) = drupal_array_get_nested_value($form, $parents); // No action is needed here for the upload button, because all file uploads on // the form are processed by file_managed_file_value() regardless of which @@ -593,13 +590,10 @@ function file_managed_file_submit($form, &$form_state) { // run, and for form building functions that run during the rebuild, such as // when the managed_file element is part of a field widget. // $form_state['input'] must be updated so that file_managed_file_value() - // has correct information during the rebuild. The Form API provides no - // equivalent of form_set_value() for updating $form_state['input'], so - // inline that implementation with the same logic that form_set_value() - // uses. + // has correct information during the rebuild. $values_element = $element['#extended'] ? $element['fid'] : $element; form_set_value($values_element, NULL, $form_state); - _form_set_value($form_state['input'], $values_element, $values_element['#parents'], NULL); + drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL); } // Set the form to rebuild so that $form is correctly updated in response to diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index 525a8a2910d57f2d556d3b3e53947cd83faaafe9..d628f488b281c1b1d57c0a47061a9bdd181a9c93 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -42,7 +42,10 @@ class FormsTestCase extends DrupalWebTestCase { $elements['password']['empty_values'] = $empty_strings; $elements['password_confirm']['element'] = array('#title' => $this->randomName(), '#type' => 'password_confirm'); - $elements['password_confirm']['empty_values'] = $empty_strings; + // Provide empty values for both password fields. + foreach ($empty_strings as $key => $value) { + $elements['password_confirm']['empty_values'][$key] = array('pass1' => $value, 'pass2' => $value); + } $elements['textarea']['element'] = array('#title' => $this->randomName(), '#type' => 'textarea'); $elements['textarea']['empty_values'] = $empty_strings; @@ -77,8 +80,7 @@ class FormsTestCase extends DrupalWebTestCase { $element = $data['element']['#title']; $form[$element] = $data['element']; $form[$element]['#required'] = $required; - $form_state['values'][$element] = $empty; - $form_state['input'] = $form_state['values']; + $form_state['input'][$element] = $empty; $form_state['input']['form_id'] = $form_id; $form_state['method'] = 'post'; drupal_prepare_form($form_id, $form, $form_state); @@ -405,6 +407,10 @@ class FormValidationTestCase extends DrupalWebTestCase { $this->assertNoText(t('!name field is required.', array('!name' => 'Title'))); $this->assertText('Test element is invalid'); + // Ensure not validated values are not available to submit handlers. + $this->drupalPost($path, array('title' => '', 'test' => 'valid'), t('Partial validate')); + $this->assertText('Only validated values appear in the form values.'); + // Now test full form validation and ensure that the #element_validate // handler is still triggered. $this->drupalPost($path, $edit, t('Full validate')); @@ -616,11 +622,11 @@ class FormsElementsTableSelectFunctionalTest extends DrupalWebTestCase { ); // Test with a valid value. - list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'row1')); + list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => array('row1' => 'row1'))); $this->assertFalse(isset($errors['tableselect']), t('Option checker allows valid values for checkboxes.')); // Test with an invalid value. - list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'non_existing_value')); + list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => array('non_existing_value' => 'non_existing_value'))); $this->assertTrue(isset($errors['tableselect']), t('Option checker disallows invalid values for checkboxes.')); } diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index ab0642fee81d0832a6ca4457cae22db716eb6561..17988cdaf26b19d317ef822ce95d2d318156abd2 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -313,7 +313,7 @@ function form_test_limit_validation_errors_form($form, &$form_state) { $form['actions']['partial'] = array( '#type' => 'submit', '#limit_validation_errors' => array(array('test')), - '#submit' => array(), + '#submit' => array('form_test_limit_validation_errors_form_partial_submit'), '#value' => t('Partial validate'), ); $form['actions']['full'] = array( @@ -332,6 +332,17 @@ function form_test_limit_validation_errors_element_validate_test(&$element, &$fo } } +/** + * Form submit handler for the partial validation submit button. + */ +function form_test_limit_validation_errors_form_partial_submit($form, $form_state) { + // The title has not been validated, thus its value - in case of the test case + // an empty string - may not be set. + if (!isset($form_state['values']['title']) && isset($form_state['values']['test'])) { + drupal_set_message('Only validated values appear in the form values.'); + } +} + /** * Create a header and options array. Helper function for callbacks. */