Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.379 diff -u -p -r1.379 form.inc --- includes/form.inc 5 Oct 2009 01:18:25 -0000 1.379 +++ includes/form.inc 8 Oct 2009 01:32:01 -0000 @@ -1115,14 +1115,6 @@ function form_builder($form_id, $element // Internet Explorer button-click scenario. _form_builder_ie_cleanup($element, $form_state); - // We should keep the buttons array until the IE clean up function - // has recognized the submit button so the form has been marked - // as submitted. If we already know which button was submitted, - // we don't need the array. - if (!empty($form_state['submitted'])) { - unset($form_state['buttons']); - } - // If some callback set #cache, we need to flip a flag so later it // can be found. if (!empty($element['#cache'])) { @@ -1299,6 +1291,59 @@ function _form_builder_ie_cleanup($form, } /** + * Helper function to remove internal Form API elements and buttons from submitted form values. + * + * This function may be used when all submitted values of a form need to be + * processed for storage, so internal Form API values and button elements should + * not be contained. The following internal values are removed: + * - form_id + * - form_token + * - form_build_id + * - op + * + * @param &$form_state + * A keyed array containing the current state of the form, including + * submitted form values; altered by reference. + */ +function form_state_values_clean(&$form_state) { + // Remove internal Form API values. + unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']); + + // Remove button values. + // form_builder() collects all button elements in a form, keyed by button + // type. We remove the button value separately for each button element. + foreach ($form_state['buttons'] as $button_type => $buttons) { + foreach ($buttons as $button) { + // Find the value corresponding to this button. We iterate over the + // #parents of this button and move a reference to each parent in + // $form_state['values']. For example, if #parents is: + // array('foo', 'bar', 'baz') + // then the corresponding $form_state['values'] part will look like this: + // array( + // 'foo' => array( + // 'bar' => array( + // 'baz' => 'button_value', + // ), + // ), + // ) + // We start by (re)moving 'baz' to $last_parent, so we are able unset it + // at the end of the iteration. Initially, $values will contain a + // reference to $form_state['values'], but in the iteration we move the + // reference to $form_state['values']['foo'], and finally to + // $form_state['values']['foo']['bar'], which is the level where we can + // unset 'baz' (that is stored in $last_parent). + $parents = $button['#parents']; + $values = &$form_state['values']; + $last_parent = array_pop($parents); + foreach ($parents as $parent) { + $values = &$values[$parent]; + } + unset($values[$last_parent]); + } + } +} + +/** * Helper function to determine the value for an image button form element. * * @param $form Index: modules/simpletest/tests/form.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v retrieving revision 1.18 diff -u -p -r1.18 form.test --- modules/simpletest/tests/form.test 3 Oct 2009 19:16:04 -0000 1.18 +++ modules/simpletest/tests/form.test 8 Oct 2009 01:31:44 -0000 @@ -463,3 +463,69 @@ class FormsFormWrapperTestCase extends D } } +/** + * Test $form_state clearance. + */ +class FormStateValuesCleanTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Form state values clearance', + 'description' => 'Test proper removal of submitted form values using form_state_values_clean().', + 'group' => 'Form API', + ); + } + + /** + * Tests form_state_values_clean(). + */ + function testFormStateValuesClean() { + // Build an example form containing multiple submit and button elements; not + // only on the top-level. + $form_state = form_state_defaults(); + $form = array('#tree' => TRUE); + $form['foo'] = array('#type' => 'submit', '#value' => t('Submit')); + $form['bar'] = array('#type' => 'submit', '#value' => t('Submit')); + $form['beer'] = array('#type' => 'value', '#value' => 1000); + $form['baz']['foo'] = array('#type' => 'button', '#value' => t('Submit')); + $form['baz']['baz'] = array('#type' => 'submit', '#value' => t('Submit')); + $form['baz']['beer'] = array('#type' => 'value', '#value' => 2000); + // Populate internal Form API values. + drupal_prepare_form('', $form, $form_state); + // Invoke form_builder() once to generate element default values in + // $form_state['values']. + form_builder('', $form, $form_state); + // By copying the auto-generated 'values' into 'input', we let form + // processing believe that this form was properly submitted, which in turn + // will lead to populated $form_state['buttons']. + $form_state['input'] = $form_state['values']; + // Lastly, process the form. Since there are no submit handlers in the game, + // this will not redirect. + drupal_process_form('', $form, $form_state); + + // Setup the expected result. + $result = array( + 'beer' => 1000, + 'baz' => array('beer' => 2000), + ); + form_state_values_clean($form_state); + + // Verify that all internal Form API elements were removed. + $this->assertFalse(isset($form_state['values']['form_id']), t('%element was removed.', array('%element' => 'form_id'))); + $this->assertFalse(isset($form_state['values']['form_token']), t('%element was removed.', array('%element' => 'form_token'))); + $this->assertFalse(isset($form_state['values']['form_build_id']), t('%element was removed.', array('%element' => 'form_build_id'))); + $this->assertFalse(isset($form_state['values']['op']), t('%element was removed.', array('%element' => 'op'))); + + // Verify that all buttons were removed. + $this->assertFalse(isset($form_state['values']['foo']), t('%element was removed.', array('%element' => 'foo'))); + $this->assertFalse(isset($form_state['values']['bar']), t('%element was removed.', array('%element' => 'bar'))); + $this->assertFalse(isset($form_state['values']['baz']['foo']), t('%element was removed.', array('%element' => 'foo'))); + $this->assertFalse(isset($form_state['values']['baz']['baz']), t('%element was removed.', array('%element' => 'baz'))); + + // Verify that nested form value still exists. + $this->assertTrue(isset($form_state['values']['baz']['beer']), t('Nested form value still exists.')); + + // Verify that actual form values equal resulting form values. + $this->assertEqual($form_state['values'], $result, t('Expected form values equal actual form values.')); + } +} + Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.801 diff -u -p -r1.801 system.module --- modules/system/system.module 5 Oct 2009 02:43:01 -0000 1.801 +++ modules/system/system.module 8 Oct 2009 01:31:44 -0000 @@ -2225,7 +2225,7 @@ function system_settings_form($form, $au */ function system_settings_form_submit($form, &$form_state) { // Exclude unnecessary elements. - unset($form_state['values']['submit'], $form_state['values']['reset'], $form_state['values']['form_id'], $form_state['values']['op'], $form_state['values']['form_token'], $form_state['values']['form_build_id']); + form_state_values_clean($form_state); foreach ($form_state['values'] as $key => $value) { if (is_array($value) && isset($form_state['values']['array_filter'])) { Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.1055 diff -u -p -r1.1055 user.module --- modules/user/user.module 2 Oct 2009 14:49:10 -0000 1.1055 +++ modules/user/user.module 8 Oct 2009 01:31:44 -0000 @@ -3066,7 +3066,8 @@ function user_register_submit($form, &$f } // The unset below is needed to prevent these form values from being saved as // user data. - unset($form_state['values']['form_token'], $form_state['values']['submit'], $form_state['values']['op'], $form_state['values']['notify'], $form_state['values']['form_id'], $form_state['values']['affiliates'], $form_state['values']['destination'], $form_state['values']['form_build_id']); + form_state_values_clean($form_state); + unset($form_state['values']['notify']); $merge_data = array('pass' => $pass, 'init' => $mail, 'roles' => $roles); if (!$admin) { Index: modules/user/user.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.pages.inc,v retrieving revision 1.57 diff -u -p -r1.57 user.pages.inc --- modules/user/user.pages.inc 29 Sep 2009 15:31:17 -0000 1.57 +++ modules/user/user.pages.inc 8 Oct 2009 01:31:44 -0000 @@ -274,7 +274,8 @@ function user_profile_form_validate($for function user_profile_form_submit($form, &$form_state) { $account = $form['#user']; $category = $form['#user_category']; - unset($form_state['values']['op'], $form_state['values']['submit'], $form_state['values']['cancel'], $form_state['values']['form_token'], $form_state['values']['form_id'], $form_state['values']['form_build_id']); + // Remove unneeded values. + form_state_values_clean($form_state); $edit = (object)$form_state['values']; field_attach_submit('user', $edit, $form, $form_state);