diff --git a/includes/ajax.inc b/includes/ajax.inc index f059209bce..09b5fa0943 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -384,7 +384,16 @@ function ajax_get_form() { function ajax_form_callback() { list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); drupal_process_form($form['#form_id'], $form, $form_state); + return ajax_form_return_commands($form, $form_state, $commands); +} +/** + * Return a set of AJAX commands for updating a form. + * + * @see ajax_form_callback() + * @see drupal_get_form() + */ +function ajax_form_return_commands($form, $form_state, $commands = array()) { // We need to return the part of the form (or some other content) that needs // to be re-rendered so the browser can update the page with changed content. // Since this is the generic menu callback used by many Ajax elements, it is @@ -642,7 +651,7 @@ function ajax_footer() { */ function ajax_process_form($element, &$form_state) { $element = ajax_pre_render_element($element); - if (!empty($element['#ajax_processed'])) { + if (!empty($element['#ajax_processed']) && !isset($element['#ajax']['no_form_cache'])) { $form_state['cache'] = TRUE; } return $element; @@ -738,10 +747,23 @@ function ajax_pre_render_element($element) { $settings = $element['#ajax']; + $path = 'system/ajax'; + $options = array(); + + $enabled_callbacks = variable_get('drupal_ajax_no_form_cache_enabled_callbacks', array()); + if (isset($element['#ajax']['callback']) && isset($enabled_callbacks[$element['#ajax']['callback']]) && !isset($element['#ajax']['path'])) { + $query_parameters = drupal_get_query_parameters($_GET, array('q')); + + $path = $_GET['q']; + $options['query'] = $query_parameters; + + $element['#ajax']['no_form_cache'] = TRUE; + } + // Assign default settings. $settings += array( - 'path' => 'system/ajax', - 'options' => array(), + 'path' => $path, + 'options' => $options, ); // @todo Legacy support. Remove in Drupal 8. diff --git a/includes/form.inc b/includes/form.inc index 6c33de7f96..f64f133977 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -128,7 +128,36 @@ function drupal_get_form($form_id) { array_shift($args); $form_state['build_info']['args'] = $args; - return drupal_build_form($form_id, $form_state); + // Disable redirection if the request was made via AJAX. + $ajax_post = isset($_POST['ajax_page_state']) && $_POST['form_id'] === $form_id; + if ($ajax_post) { + $form_state['no_redirect'] = TRUE; + $form_state['rebuild_info']['copy']['#build_id'] = TRUE; + $form_state['rebuild_info']['copy']['#action'] = TRUE; + } + + $form = drupal_build_form($form_id, $form_state); + + // In the event that this form was submitted via AJAX, we need to return a + // set of AJAX commands to update the original form. Generate the JSON + // response and end the request. + if ($ajax_post && $form_state['process_input']) { + $commands = array(); + if (isset($form['#build_id_old']) && $form['#build_id_old'] !== $form['#build_id']) { + $commands[] = ajax_command_update_build_id($form); + } + $commands = ajax_form_return_commands($form, $form_state, $commands); + + $status = ob_get_status(); + if (!empty($status)) { + ob_clean(); + } + + ajax_deliver($commands); + exit(); + } + + return $form; } /** @@ -409,6 +438,7 @@ function form_state_defaults() { 'build_info' => array( 'args' => array(), 'files' => array(), + 'from_cache' => FALSE, ), 'temporary' => array(), 'submitted' => FALSE, @@ -464,6 +494,15 @@ function form_state_defaults() { function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { $form = drupal_retrieve_form($form_id, $form_state); + $copy_build_id = !empty($form_state['rebuild_info']['copy']['#build_id']); + + // If this form was not built from cache but needs to maintain the same build + // ID, copy the value from input onto the old form. + if (empty($form_state['build_info']['from_cache']) && isset($form_state['input']['form_build_id'])) { + $old_form['#build_id'] = $form_state['input']['form_build_id']; + $copy_build_id = FALSE; + } + // If only parts of the form will be returned to the browser (e.g., Ajax or // 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. @@ -471,7 +510,7 @@ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { // 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() - $enforce_old_build_id = isset($old_form['#build_id']) && !empty($form_state['rebuild_info']['copy']['#build_id']); + $enforce_old_build_id = isset($old_form['#build_id']) && $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']; @@ -524,6 +563,9 @@ function form_get_cache($form_build_id, &$form_state) { // Re-populate $form_state for subsequent rebuilds. $form_state = $cached->data + $form_state; + // Indicate this build was loaded from the cache, not a fresh build. + $form_state['build_info']['from_cache'] = TRUE; + // If the original form is contained in include files, load the files. // @see form_load_include() $form_state['build_info'] += array('files' => array()); diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index afe02306bd..81afed38f8 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -583,6 +583,71 @@ class AJAXFormPageCacheTestCase extends AJAXTestCase { } } +/** + * Test Ajax forms when form cache is disabled for the ajax form. + */ +class AJAXFormNoCacheTestCase extends AJAXTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'AJAX forms with form cache disabled', + 'description' => 'Tests that AJAX forms can have form cache disabled.', + 'group' => 'AJAX', + ); + } + + public function setUp() { + parent::setUp(); + + variable_set('drupal_ajax_no_form_cache_enabled_callbacks', array( + 'ajax_forms_test_simple_form_select_callback' => TRUE, + 'ajax_forms_test_simple_form_checkbox_callback' => 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']; + } + + /** + * Check for a given form build id in cache_form. + */ + protected function getFormFromCache($form_build_id) { + return (bool) cache_get('form_' . $form_build_id, 'cache_form'); + } + + /** + * Create a simple ajax form, then validate form cache is disabled. + */ + public function testSimpleAJAXFormValue() { + $this->drupalGet('ajax_forms_test_no_form_cache'); + $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'); + $this->assertFalse($this->getFormFromCache($build_id_initial), 'AJAX form was not retrieved from cache_form.'); + + // Now disable all callbacks, allowing the ajax form to be cached. + variable_set('drupal_ajax_no_form_cache_enabled_callbacks', array()); + $this->drupalGet('ajax_forms_test_no_form_cache'); + $build_id = $this->getFormBuildId(); + $this->assertTrue($this->getFormFromCache($build_id), 'AJAX form was successfully retrieved from cache_form.'); + } +} /** * Miscellaneous Ajax tests using ajax_test module. diff --git a/modules/simpletest/tests/ajax_forms_test.module b/modules/simpletest/tests/ajax_forms_test.module index de2fa0ba84..9e02556feb 100644 --- a/modules/simpletest/tests/ajax_forms_test.module +++ b/modules/simpletest/tests/ajax_forms_test.module @@ -16,6 +16,12 @@ function ajax_forms_test_menu() { 'page arguments' => array('ajax_forms_test_simple_form'), 'access callback' => TRUE, ); + $items['ajax_forms_test_no_form_cache'] = array( + 'title' => 'AJAX forms no form cache test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ajax_forms_test_no_form_cache'), + 'access callback' => TRUE, + ); $items['ajax_forms_test_ajax_commands_form'] = array( 'title' => 'AJAX forms AJAX commands test', 'page callback' => 'drupal_get_form', @@ -70,6 +76,37 @@ function ajax_forms_test_simple_form($form, &$form_state) { return $form; } +/** + * A basic form used to test form_state['values'] during callback. + */ +function ajax_forms_test_no_form_cache($form, &$form_state) { + $form = array(); + $form['select'] = array( + '#type' => 'select', + '#options' => array( + 'red' => 'red', + 'green' => 'green', + 'blue' => 'blue'), + '#ajax' => array( + 'callback' => 'ajax_forms_test_simple_form_select_callback', + ), + '#suffix' => '