Commit 6642fbc7 authored by David_Rothstein's avatar David_Rothstein

Drupal 7.27

parent dc791ec5
Drupal 7.27, 2014-04-16
----------------------
- Fixed security issues (information disclosure). See SA-CORE-2014-002.
Drupal 7.26, 2014-01-15 Drupal 7.26, 2014-01-15
---------------------- ----------------------
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001. - Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001.
......
...@@ -308,10 +308,11 @@ function ajax_render($commands = array()) { ...@@ -308,10 +308,11 @@ function ajax_render($commands = array()) {
* pulls the form info from $_POST. * pulls the form info from $_POST.
* *
* @return * @return
* An array containing the $form and $form_state. Use the list() function * An array containing the $form, $form_state, $form_id, $form_build_id and an
* to break these apart: * initial list of Ajax $commands. Use the list() function to break these
* apart:
* @code * @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 * @endcode
*/ */
function ajax_get_form() { function ajax_get_form() {
...@@ -331,6 +332,17 @@ function ajax_get_form() { ...@@ -331,6 +332,17 @@ function ajax_get_form() {
drupal_exit(); 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. // Since some of the submit handlers are run, redirects need to be disabled.
$form_state['no_redirect'] = TRUE; $form_state['no_redirect'] = TRUE;
...@@ -345,7 +357,7 @@ function ajax_get_form() { ...@@ -345,7 +357,7 @@ function ajax_get_form() {
$form_state['input'] = $_POST; $form_state['input'] = $_POST;
$form_id = $form['#form_id']; $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() { ...@@ -366,7 +378,7 @@ function ajax_get_form() {
* @see system_menu() * @see system_menu()
*/ */
function ajax_form_callback() { 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); drupal_process_form($form['#form_id'], $form, $form_state);
// We need to return the part of the form (or some other content) that needs // We need to return the part of the form (or some other content) that needs
...@@ -379,7 +391,19 @@ function ajax_form_callback() { ...@@ -379,7 +391,19 @@ function ajax_form_callback() {
$callback = $form_state['triggering_element']['#ajax']['callback']; $callback = $form_state['triggering_element']['#ajax']['callback'];
} }
if (!empty($callback) && function_exists($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) { ...@@ -1210,3 +1234,26 @@ function ajax_command_restripe($selector) {
'selector' => $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'],
);
}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
/** /**
* The current system version. * The current system version.
*/ */
define('VERSION', '7.26'); define('VERSION', '7.27');
/** /**
* Core API compatibility. * Core API compatibility.
......
...@@ -168,6 +168,12 @@ function drupal_get_form($form_id) { ...@@ -168,6 +168,12 @@ function drupal_get_form($form_id) {
* processed. * processed.
* - base_form_id: Identification for a base form, as declared in a * - base_form_id: Identification for a base form, as declared in a
* hook_forms() implementation. * 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 * - rebuild_info: Internal. Similar to 'build_info', but pertaining to
* drupal_rebuild_form(). * drupal_rebuild_form().
* - rebuild: Normally, after the entire form processing is completed and * - 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) { ...@@ -459,16 +465,24 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
$form = drupal_retrieve_form($form_id, $form_state); $form = drupal_retrieve_form($form_id, $form_state);
// If only parts of the form will be returned to the browser (e.g., Ajax or // 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 // RIA clients), or if the form already had a new build ID regenerated when it
// manually update the hidden 'build_id' input element. // was retrieved from the form cache, reuse the existing #build_id.
// Otherwise, a new #build_id is generated, to not clobber the previous // 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 // build's data in the form cache; also allowing the user to go back to an
// earlier build, make changes, and re-submit. // earlier build, make changes, and re-submit.
// @see drupal_prepare_form() // @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']; $form['#build_id'] = $old_form['#build_id'];
if ($old_form_is_mutable_copy) {
$form['#build_id_old'] = $old_form['#build_id_old'];
}
} }
else { else {
if (isset($old_form['#build_id'])) {
$form['#build_id_old'] = $old_form['#build_id'];
}
$form['#build_id'] = 'form-' . drupal_random_key(); $form['#build_id'] = 'form-' . drupal_random_key();
} }
...@@ -523,6 +537,15 @@ function form_get_cache($form_build_id, &$form_state) { ...@@ -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; return $form;
} }
} }
...@@ -535,15 +558,28 @@ function form_set_cache($form_build_id, $form, $form_state) { ...@@ -535,15 +558,28 @@ function form_set_cache($form_build_id, $form, $form_state) {
// 6 hours cache life time for forms should be plenty. // 6 hours cache life time for forms should be plenty.
$expire = 21600; $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. // Cache form structure.
if (isset($form)) { if (isset($form)) {
if ($GLOBALS['user']->uid) { if ($GLOBALS['user']->uid) {
$form['#cache_token'] = drupal_get_token(); $form['#cache_token'] = drupal_get_token();
} }
unset($form['#build_id_old']);
cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire); cache_set('form_' . $form_build_id, $form, 'cache_form', REQUEST_TIME + $expire);
} }
// Cache form state. // 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()))) { 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); cache_set('form_state_' . $form_build_id, $data, 'cache_form', REQUEST_TIME + $expire);
} }
......
...@@ -616,6 +616,13 @@ Drupal.ajax.prototype.commands = { ...@@ -616,6 +616,13 @@ Drupal.ajax.prototype.commands = {
.removeClass('odd even') .removeClass('odd even')
.filter(':even').addClass('odd').end() .filter(':even').addClass('odd').end()
.filter(':odd').addClass('even'); .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);
} }
}; };
......
...@@ -246,7 +246,7 @@ function file_ajax_upload() { ...@@ -246,7 +246,7 @@ function file_ajax_upload() {
return array('#type' => 'ajax', '#commands' => $commands); 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) { if (!$form) {
// Invalid form_build_id. // Invalid form_build_id.
...@@ -284,7 +284,6 @@ function file_ajax_upload() { ...@@ -284,7 +284,6 @@ function file_ajax_upload() {
$js = drupal_add_js(); $js = drupal_add_js();
$settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']);
$commands = array();
$commands[] = ajax_command_replace(NULL, $output, $settings); $commands[] = ajax_command_replace(NULL, $output, $settings);
return array('#type' => 'ajax', '#commands' => $commands); return array('#type' => 'ajax', '#commands' => $commands);
} }
......
...@@ -2269,6 +2269,13 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path ...@@ -2269,6 +2269,13 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path
} }
break; 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 // @todo Add suitable implementations for these commands in order to
// have full test coverage of what ajax.js can do. // have full test coverage of what ajax.js can do.
case 'remove': case 'remove':
......
...@@ -497,6 +497,85 @@ class AJAXMultiFormTestCase extends AJAXTestCase { ...@@ -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. * Miscellaneous Ajax tests using ajax_test module.
*/ */
......
...@@ -1156,6 +1156,182 @@ class FormsFormStorageTestCase extends DrupalWebTestCase { ...@@ -1156,6 +1156,182 @@ class FormsFormStorageTestCase extends DrupalWebTestCase {
$this->assertText('State persisted.'); $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.');
}
} }
/** /**
......
...@@ -90,6 +90,21 @@ function form_test_menu() { ...@@ -90,6 +90,21 @@ function form_test_menu() {
'type' => MENU_CALLBACK, '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( $items['form_test/wrapper-callback'] = array(
'title' => 'Form wrapper callback test', 'title' => 'Form wrapper callback test',
'page callback' => 'form_test_wrapper_callback', 'page callback' => 'form_test_wrapper_callback',
...@@ -746,9 +761,36 @@ function form_test_storage_form($form, &$form_state) { ...@@ -746,9 +761,36 @@ function form_test_storage_form($form, &$form_state) {
$form_state['cache'] = TRUE; $form_state['cache'] = TRUE;
} }
if (isset($_REQUEST['immutable'])) {
$form_state['build_info']['immutable'] = TRUE;
}
return $form; 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(). * 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) { ...@@ -785,6 +827,56 @@ function form_test_storage_form_submit($form, &$form_state) {
$form_state['redirect'] = 'node'; $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) {