diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9f1bcf129ffc24b42f2ae48502e517d11545836e..3a63aea19f26ae402c62c0e6a2217ff4d113f832 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,8 @@ +Drupal 7.27, 2014-04-16 +---------------------- +- Fixed security issues (information disclosure). See SA-CORE-2014-002. + Drupal 7.26, 2014-01-15 ---------------------- - Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001. diff --git a/includes/ajax.inc b/includes/ajax.inc index ab0111cedec8ed375f61768ecf8906ac08777ae4..8446bf8914e5107cbdb8802904ad78f88bb0be76 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -308,10 +308,11 @@ function ajax_render($commands = array()) { * pulls the form info from $_POST. * * @return - * An array containing the $form and $form_state. Use the list() function - * to break these apart: + * An array containing the $form, $form_state, $form_id, $form_build_id and an + * initial list of Ajax $commands. Use the list() function to break these + * apart: * @code - * list($form, $form_state, $form_id, $form_build_id) = ajax_get_form(); + * list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); * @endcode */ function ajax_get_form() { @@ -331,6 +332,17 @@ function ajax_get_form() { drupal_exit(); } + // When a page level cache is enabled, the form-build id might have been + // replaced from within form_get_cache. If this is the case, it is also + // necessary to update it in the browser by issuing an appropriate Ajax + // command. + $commands = array(); + if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) { + // If the form build ID has changed, issue an Ajax command to update it. + $commands[] = ajax_command_update_build_id($form); + $form_build_id = $form['#build_id']; + } + // Since some of the submit handlers are run, redirects need to be disabled. $form_state['no_redirect'] = TRUE; @@ -345,7 +357,7 @@ function ajax_get_form() { $form_state['input'] = $_POST; $form_id = $form['#form_id']; - return array($form, $form_state, $form_id, $form_build_id); + return array($form, $form_state, $form_id, $form_build_id, $commands); } /** @@ -366,7 +378,7 @@ function ajax_get_form() { * @see system_menu() */ function ajax_form_callback() { - list($form, $form_state) = ajax_get_form(); + list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); drupal_process_form($form['#form_id'], $form, $form_state); // We need to return the part of the form (or some other content) that needs @@ -379,7 +391,19 @@ function ajax_form_callback() { $callback = $form_state['triggering_element']['#ajax']['callback']; } if (!empty($callback) && function_exists($callback)) { - return $callback($form, $form_state); + $result = $callback($form, $form_state); + + if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) { + // Turn the response into a #type=ajax array if it isn't one already. + $result = array( + '#type' => 'ajax', + '#commands' => ajax_prepare_response($result), + ); + } + + $result['#commands'] = array_merge($commands, $result['#commands']); + + return $result; } } @@ -1210,3 +1234,26 @@ function ajax_command_restripe($selector) { 'selector' => $selector, ); } + +/** + * Creates a Drupal Ajax 'update_build_id' command. + * + * This command updates the value of a hidden form_build_id input element on a + * form. It requires the form passed in to have keys for both the old build ID + * in #build_id_old and the new build ID in #build_id. + * + * The primary use case for this Ajax command is to serve a new build ID to a + * form served from the cache to an anonymous user, preventing one anonymous + * user from accessing the form state of another anonymous users on Ajax enabled + * forms. + * + * @param $form + * The form array representing the form whose build ID should be updated. + */ +function ajax_command_update_build_id($form) { + return array( + 'command' => 'updateBuildId', + 'old' => $form['#build_id_old'], + 'new' => $form['#build_id'], + ); +} diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index e5de2d1806023b1161ab88945c5b69ad48a08144..4cc39142eec29a53a50a160c04aa83e43db21d76 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.26'); +define('VERSION', '7.27'); /** * Core API compatibility. diff --git a/includes/form.inc b/includes/form.inc index 4e467bab3af09d87a53fdab69843e32ee11a0f88..fd80e09bb3b36e026f3b65eef7ad0272dbd2dc63 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -168,6 +168,12 @@ function drupal_get_form($form_id) { * processed. * - base_form_id: Identification for a base form, as declared in a * hook_forms() implementation. + * - immutable: If this flag is set to TRUE, a new form build id is + * generated when the form is loaded from the cache. If it is subsequently + * saved to the cache again, it will have another cache id and therefore + * the original form and form-state will remain unaltered. This is + * important when page caching is enabled in order to prevent form state + * from leaking between anonymous users. * - rebuild_info: Internal. Similar to 'build_info', but pertaining to * drupal_rebuild_form(). * - rebuild: Normally, after the entire form processing is completed and @@ -459,16 +465,24 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { $form = drupal_retrieve_form($form_id, $form_state); // If only parts of the form will be returned to the browser (e.g., Ajax or - // RIA clients), re-use the old #build_id to not require client-side code to - // manually update the hidden 'build_id' input element. + // RIA clients), or if the form already had a new build ID regenerated when it + // was retrieved from the form cache, reuse the existing #build_id. // Otherwise, a new #build_id is generated, to not clobber the previous // build's data in the form cache; also allowing the user to go back to an // earlier build, make changes, and re-submit. // @see drupal_prepare_form() - if (isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id'])) { + $enforce_old_build_id = isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id']); + $old_form_is_mutable_copy = isset($old_form['#build_id_old']); + if ($enforce_old_build_id || $old_form_is_mutable_copy) { $form['#build_id'] = $old_form['#build_id']; + if ($old_form_is_mutable_copy) { + $form['#build_id_old'] = $old_form['#build_id_old']; + } } else { + if (isset($old_form['#build_id'])) { + $form['#build_id_old'] = $old_form['#build_id']; + } $form['#build_id'] = 'form-' . drupal_random_key(); } @@ -523,6 +537,15 @@ function form_get_cache($form_build_id, &$form_state) { } } } + // Generate a new #build_id if the cached form was rendered on a cacheable + // page. + if (!empty($form_state['build_info']['immutable'])) { + $form['#build_id_old'] = $form['#build_id']; + $form['#build_id'] = 'form-' . drupal_random_key(); + $form['form_build_id']['#value'] = $form['#build_id']; + $form['form_build_id']['#id'] = $form['#build_id']; + unset($form_state['build_info']['immutable']); + } return $form; } } @@ -535,15 +558,28 @@ function form_set_cache($form_build_id, $form, $form_state) { // 6 hours cache life time for forms should be plenty. $expire = 21600; + // Ensure that the form build_id embedded in the form structure is the same as + // the one passed in as a parameter. This is an additional safety measure to + // prevent legacy code operating directly with form_get_cache and + // form_set_cache from accidentally overwriting immutable form state. + if ($form['#build_id'] != $form_build_id) { + watchdog('form', 'Form build-id mismatch detected while attempting to store a form in the cache.', array(), WATCHDOG_ERROR); + return; + } + // Cache form structure. if (isset($form)) { if ($GLOBALS['user']->uid) { $form['#cache_token'] = drupal_get_token(); } + unset($form['#build_id_old']); cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire); } // Cache form state. + if (variable_get('cache', 0) && drupal_page_is_cacheable()) { + $form_state['build_info']['immutable'] = TRUE; + } if ($data = array_diff_key($form_state, array_flip(form_state_keys_no_cache()))) { cache_set('form_state_' . $form_build_id, $data, 'cache_form', REQUEST_TIME + $expire); } diff --git a/misc/ajax.js b/misc/ajax.js index 90c3bb8bbef3e306cfa2b176920a9eff5adf18b0..63dd65fdde10d970ac73be02719755655751892e 100644 --- a/misc/ajax.js +++ b/misc/ajax.js @@ -616,6 +616,13 @@ Drupal.ajax.prototype.commands = { .removeClass('odd even') .filter(':even').addClass('odd').end() .filter(':odd').addClass('even'); + }, + + /** + * Command to update a form's build ID. + */ + updateBuildId: function(ajax, response, status) { + $('input[name="form_build_id"][value="' + response.old + '"]').val(response.new); } }; diff --git a/modules/file/file.module b/modules/file/file.module index 3d351fa2c73936783fbd046f250e9b956d359485..5a635fd7521b3c60335c4bd4c45b536f76f40d0b 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -246,7 +246,7 @@ function file_ajax_upload() { return array('#type' => 'ajax', '#commands' => $commands); } - list($form, $form_state) = ajax_get_form(); + list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); if (!$form) { // Invalid form_build_id. @@ -284,7 +284,6 @@ function file_ajax_upload() { $js = drupal_add_js(); $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); - $commands = array(); $commands[] = ajax_command_replace(NULL, $output, $settings); return array('#type' => 'ajax', '#commands' => $commands); } diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 6d0e59a4a917cfff9b694f144a160e204c98cd85..d71b1e1e6d158a80c1870d7fcb68e93f50446f85 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -2269,6 +2269,13 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path } break; + case 'updateBuildId': + $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0); + if ($buildId) { + $buildId->setAttribute('value', $command['new']); + } + break; + // @todo Add suitable implementations for these commands in order to // have full test coverage of what ajax.js can do. case 'remove': diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index 664d52042af6dc51095aaf2e332f9bb766bd47e2..a0c7be8a2472ea2871d2ff049f9e94dada15a663 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -497,6 +497,85 @@ class AJAXMultiFormTestCase extends AJAXTestCase { } } +/** + * Test Ajax forms when page caching for anonymous users is turned on. + */ +class AJAXFormPageCacheTestCase extends AJAXTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'AJAX forms on cached pages', + 'description' => 'Tests that AJAX forms work properly for anonymous users on cached pages.', + 'group' => 'AJAX', + ); + } + + public function setUp() { + parent::setUp(); + + variable_set('cache', TRUE); + } + + /** + * Return the build id of the current form. + */ + protected function getFormBuildId() { + $build_id_fields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page'); + return (string) $build_id_fields[0]['value']; + } + + /** + * Create a simple form, then POST to system/ajax to change to it. + */ + public function testSimpleAJAXFormValue() { + $this->drupalGet('ajax_forms_test_get_form'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $build_id_initial = $this->getFormBuildId(); + + $edit = array('select' => 'green'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_first_ajax = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission'); + $expected = array( + 'command' => 'updateBuildId', + 'old' => $build_id_initial, + 'new' => $build_id_first_ajax, + ); + $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission'); + + $edit = array('select' => 'red'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_second_ajax = $this->getFormBuildId(); + $this->assertEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id remains the same on subsequent AJAX submissions'); + + // Repeat the test sequence but this time with a page loaded from the cache. + $this->drupalGet('ajax_forms_test_get_form'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $build_id_from_cache_initial = $this->getFormBuildId(); + $this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request'); + + $edit = array('select' => 'green'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_from_cache_first_ajax = $this->getFormBuildId(); + $this->assertNotEqual($build_id_from_cache_initial, $build_id_from_cache_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission'); + $this->assertNotEqual($build_id_first_ajax, $build_id_from_cache_first_ajax, 'Build id from first user is not reused'); + $expected = array( + 'command' => 'updateBuildId', + 'old' => $build_id_from_cache_initial, + 'new' => $build_id_from_cache_first_ajax, + ); + $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission'); + + $edit = array('select' => 'red'); + $commands = $this->drupalPostAJAX(NULL, $edit, 'select'); + $build_id_from_cache_second_ajax = $this->getFormBuildId(); + $this->assertEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id remains the same on subsequent AJAX submissions'); + } +} + + /** * Miscellaneous Ajax tests using ajax_test module. */ diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index 1d430b582495944675cb7869a0f47ac1877248e8..f90b854c7d7c4a9a2ca94c3ef234c638a7e01641 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -1156,6 +1156,182 @@ class FormsFormStorageTestCase extends DrupalWebTestCase { $this->assertText('State persisted.'); } } + + /** + * Verify that the form build-id remains the same when validation errors + * occur on a mutable form. + */ + function testMutableForm() { + // Request the form with 'cache' query parameter to enable form caching. + $this->drupalGet('form_test/form-storage', array('query' => array('cache' => 1))); + $buildIdFields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page'); + $buildId = (string) $buildIdFields[0]['value']; + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Continue submit'); + + // Verify that the build-id did not change. + $this->assertFieldByName('form_build_id', $buildId, 'Build id remains the same when form validation fails'); + } + + /** + * Verifies that form build-id is regenerated when loading an immutable form + * from the cache. + */ + function testImmutableForm() { + // Request the form with 'cache' query parameter to enable form caching. + $this->drupalGet('form_test/form-storage', array('query' => array('cache' => 1, 'immutable' => 1))); + $buildIdFields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page'); + $buildId = (string) $buildIdFields[0]['value']; + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Continue submit'); + + // Verify that the build-id did change. + $this->assertNoFieldByName('form_build_id', $buildId, 'Build id changes when form validation fails'); + + // Retrieve the new build-id. + $buildIdFields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($buildIdFields), 1, 'One form build id field on the page'); + $buildId = (string) $buildIdFields[0]['value']; + + // Trigger validation error by again submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Continue submit'); + + // Verify that the build-id does not change the second time. + $this->assertFieldByName('form_build_id', $buildId, 'Build id remains the same when form validation fails subsequently'); + } + + /** + * Verify that existing contrib code cannot overwrite immutable form state. + */ + public function testImmutableFormLegacyProtection() { + $this->drupalGet('form_test/form-storage', array('query' => array('cache' => 1, 'immutable' => 1))); + $build_id_fields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page'); + $build_id = (string) $build_id_fields[0]['value']; + + // Try to poison the form cache. + $original = $this->drupalGetAJAX('form_test/form-storage-legacy/' . $build_id); + $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded'); + $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated'); + + // Assert that a watchdog message was logged by form_set_cache. + $status = (bool) db_query_range('SELECT 1 FROM {watchdog} WHERE message = :message', 0, 1, array(':message' => 'Form build-id mismatch detected while attempting to store a form in the cache.')); + $this->assert($status, 'A watchdog message was logged by form_set_cache'); + + // Ensure that the form state was not poisoned by the preceeding call. + $original = $this->drupalGetAJAX('form_test/form-storage-legacy/' . $build_id); + $this->assertEqual($original['form']['#build_id_old'], $build_id, 'Original build_id was recorded'); + $this->assertNotEqual($original['form']['#build_id'], $build_id, 'New build_id was generated'); + $this->assert(empty($original['form']['#poisoned']), 'Original form structure was preserved'); + $this->assert(empty($original['form_state']['poisoned']), 'Original form state was preserved'); + } +} + +/** + * Test the form storage when page caching for anonymous users is turned on. + */ +class FormsFormStoragePageCacheTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Forms using form storage on cached pages', + 'description' => 'Tests a form using form storage and makes sure validation and caching works when page caching for anonymous users is turned on.', + 'group' => 'Form API', + ); + } + + public function setUp() { + parent::setUp('form_test'); + + variable_set('cache', TRUE); + } + + /** + * Return the build id of the current form. + */ + protected function getFormBuildId() { + $build_id_fields = $this->xpath('//input[@name="form_build_id"]'); + $this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page'); + return (string) $build_id_fields[0]['value']; + } + + /** + * Build-id is regenerated when validating cached form. + */ + public function testValidateFormStorageOnCachedPage() { + $this->drupalGet('form_test/form-storage-page-cache'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_initial = $this->getFormBuildId(); + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText($build_id_initial, 'Old build id on the page'); + $build_id_first_validation = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_first_validation, 'Build id changes when form validation fails'); + + // Trigger validation error by again submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_second_validation = $this->getFormBuildId(); + $this->assertEqual($build_id_first_validation, $build_id_second_validation, 'Build id remains the same when form validation fails subsequently'); + + // Repeat the test sequence but this time with a page loaded from the cache. + $this->drupalGet('form_test/form-storage-page-cache'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_from_cache_initial = $this->getFormBuildId(); + $this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request'); + + // Trigger validation error by submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText($build_id_initial, 'Old build id is initial build id'); + $build_id_from_cache_first_validation = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_from_cache_first_validation, 'Build id changes when form validation fails'); + $this->assertNotEqual($build_id_first_validation, $build_id_from_cache_first_validation, 'Build id from first user is not reused'); + + // Trigger validation error by again submitting an empty title. + $edit = array('title' => ''); + $this->drupalPost(NULL, $edit, 'Save'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_from_cache_second_validation = $this->getFormBuildId(); + $this->assertEqual($build_id_from_cache_first_validation, $build_id_from_cache_second_validation, 'Build id remains the same when form validation fails subsequently'); + } + + /** + * Build-id is regenerated when rebuilding cached form. + */ + public function testRebuildFormStorageOnCachedPage() { + $this->drupalGet('form_test/form-storage-page-cache'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $this->assertText('No old build id', 'No old build id on the page'); + $build_id_initial = $this->getFormBuildId(); + + // Trigger rebuild, should regenerate build id. + $edit = array('title' => 'something'); + $this->drupalPost(NULL, $edit, 'Rebuild'); + $this->assertText($build_id_initial, 'Initial build id as old build id on the page'); + $build_id_first_rebuild = $this->getFormBuildId(); + $this->assertNotEqual($build_id_initial, $build_id_first_rebuild, 'Build id changes on first rebuild.'); + + // Trigger subsequent rebuild, should regenerate the build id again. + $edit = array('title' => 'something'); + $this->drupalPost(NULL, $edit, 'Rebuild'); + $this->assertText($build_id_first_rebuild, 'First build id as old build id on the page'); + $build_id_second_rebuild = $this->getFormBuildId(); + $this->assertNotEqual($build_id_first_rebuild, $build_id_second_rebuild, 'Build id changes on second rebuild.'); + } } /** diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index c7885d7a0f99ec643ef0ccfc5e1d65f08322b04c..602b4090dd7c6100eb495a600b69b8029e191b94 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -90,6 +90,21 @@ function form_test_menu() { 'type' => MENU_CALLBACK, ); + $items['form_test/form-storage-legacy'] = array( + 'title' => 'Emulate legacy AHAH-style ajax callback', + 'page callback' => 'form_test_storage_legacy_handler', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + $items['form_test/form-storage-page-cache'] = array( + 'title' => 'Form storage with page cache test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_storage_page_cache_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + $items['form_test/wrapper-callback'] = array( 'title' => 'Form wrapper callback test', 'page callback' => 'form_test_wrapper_callback', @@ -746,9 +761,36 @@ function form_test_storage_form($form, &$form_state) { $form_state['cache'] = TRUE; } + if (isset($_REQUEST['immutable'])) { + $form_state['build_info']['immutable'] = TRUE; + } + return $form; } +/** + * Emulate legacy AHAH-style ajax callback. + * + * Drupal 6 AHAH callbacks used to operate directly on forms retrieved using + * form_get_cache and stored using form_set_cache after manipulation. This + * callback helps testing whether form_set_cache prevents resaving of immutable + * forms. + */ +function form_test_storage_legacy_handler($form_build_id) { + $form_state = array(); + $form = form_get_cache($form_build_id, $form_state); + + drupal_json_output(array( + 'form' => $form, + 'form_state' => $form_state, + )); + + $form['#poisoned'] = TRUE; + $form_state['poisoned'] = TRUE; + + form_set_cache($form_build_id, $form, $form_state); +} + /** * Form element validation handler for 'value' element in form_test_storage_form(). * @@ -785,6 +827,56 @@ function form_test_storage_form_submit($form, &$form_state) { $form_state['redirect'] = 'node'; } +/** + * A simple form for testing form storage when page caching is enabled. + */ +function form_test_storage_page_cache_form($form, &$form_state) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => 'Title', + '#required' => TRUE, + ); + + $form['test_build_id_old'] = array( + '#type' => 'item', + '#title' => 'Old build id', + '#markup' => 'No old build id', + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + + $form['rebuild'] = array( + '#type' => 'submit', + '#value' => 'Rebuild', + '#submit' => array('form_test_storage_page_cache_rebuild'), + ); + + $form['#after_build'] = array('form_test_storage_page_cache_old_build_id'); + $form_state['cache'] = TRUE; + + return $form; +} + +/** + * Form element #after_build callback: output the old form build-id. + */ +function form_test_storage_page_cache_old_build_id($form) { + if (isset($form['#build_id_old'])) { + $form['test_build_id_old']['#markup'] = check_plain($form['#build_id_old']); + } + return $form; +} + +/** + * Form submit callback: Rebuild the form and continue. + */ +function form_test_storage_page_cache_rebuild($form, &$form_state) { + $form_state['rebuild'] = TRUE; +} + /** * A form for testing form labels and required marks. */