Skip to content

Commit

Permalink
Drupal 7.27
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidRothstein committed Apr 16, 2014
1 parent dc791ec commit 6642fbc
Show file tree
Hide file tree
Showing 10 changed files with 459 additions and 12 deletions.
4 changes: 4 additions & 0 deletions 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.
Expand Down
59 changes: 53 additions & 6 deletions includes/ajax.inc
Expand Up @@ -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() {
Expand All @@ -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;

Expand All @@ -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);
}

/**
Expand All @@ -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
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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'],
);
}
2 changes: 1 addition & 1 deletion includes/bootstrap.inc
Expand Up @@ -8,7 +8,7 @@
/**
* The current system version.
*/
define('VERSION', '7.26');
define('VERSION', '7.27');

/**
* Core API compatibility.
Expand Down
42 changes: 39 additions & 3 deletions includes/form.inc
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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);
}
Expand Down
7 changes: 7 additions & 0 deletions misc/ajax.js
Expand Up @@ -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);
}
};

Expand Down
3 changes: 1 addition & 2 deletions modules/file/file.module
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down
7 changes: 7 additions & 0 deletions modules/simpletest/drupal_web_test_case.php
Expand Up @@ -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':
Expand Down
79 changes: 79 additions & 0 deletions modules/simpletest/tests/ajax.test
Expand Up @@ -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.
*/
Expand Down

0 comments on commit 6642fbc

Please sign in to comment.