diff --git a/includes/ajax.inc b/includes/ajax.inc index f059209bce..cdce76ea73 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 @@ -738,10 +747,21 @@ function ajax_pre_render_element($element) { $settings = $element['#ajax']; + $path = 'system/ajax'; + $options = array(); + + if (isset($element['#ajax']['callback']) && !isset($element['#ajax']['path'])) { + $query_parameters = drupal_get_query_parameters($_GET, array('q')); + $query_parameters['_drupal_ajax'] = 1; + + $path = $_GET['q']; + $options['query'] = $query_parameters; + } + // 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 e4ab8c8dac..5f24cd7d20 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -319,6 +319,14 @@ function drupal_build_form($form_id, &$form_state) { $form_state['input'] = $form_state['method'] == 'get' ? $_GET : $_POST; } + // 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; + } + if (isset($_SESSION['batch_form_state'])) { // We've been redirected here after a batch processing. The form has // already been processed, but needs to be rebuilt. See _batch_finished(). @@ -385,6 +393,25 @@ function drupal_build_form($form_id, &$form_state) { // can use it to know or update information about the state of the form. drupal_process_form($form_id, $form, $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(); + } + // If this was a successful submission of a single-step form or the last step // of a multi-step form, then drupal_process_form() issued a redirect to // another page, or back to this page, but as a new request. Therefore, if @@ -409,6 +436,7 @@ function form_state_defaults() { 'build_info' => array( 'args' => array(), 'files' => array(), + 'from_cache' => FALSE, ), 'temporary' => array(), 'submitted' => FALSE, @@ -464,6 +492,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 +508,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 +561,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/file/file.field.inc b/modules/file/file.field.inc index d592381bd5..0d5a138439 100644 --- a/modules/file/file.field.inc +++ b/modules/file/file.field.inc @@ -662,12 +662,10 @@ function file_field_widget_process($element, &$form_state, $form) { // file, the entire group of file fields is updated together. if ($field['cardinality'] != 1) { $parents = array_slice($element['#array_parents'], 0, -1); - $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value']; $field_element = drupal_array_get_nested_value($form, $parents); $new_wrapper = $field_element['#id'] . '-ajax-wrapper'; foreach (element_children($element) as $key) { if (isset($element[$key]['#ajax'])) { - $element[$key]['#ajax']['path'] = $new_path; $element[$key]['#ajax']['wrapper'] = $new_wrapper; } } diff --git a/modules/file/file.module b/modules/file/file.module index eea58470fa..94502b592d 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -234,6 +234,13 @@ function file_file_download($uri, $field_type = 'file') { * form processing is properly encapsulated in the widget element the form * should rebuild correctly using FAPI without the need for additional callbacks * or processing. + * + * This function is deprecated. This menu handler is no longer used by + * File module itself, but it exists for compatibility with contributed + * modules. Modules should now replace any use of $element['#ajax']['path'] + * with $element['#ajax']['callback'] = 'file_managed_file_ajax'. + * + * @see file_managed_file_ajax() */ function file_ajax_upload() { $form_parents = func_get_args(); @@ -374,7 +381,7 @@ function file_managed_file_process($element, &$form_state, $form) { $element['#tree'] = TRUE; $ajax_settings = array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], + 'callback' => 'file_managed_file_ajax', 'wrapper' => $original_id . '-ajax-wrapper', 'effect' => 'fade', 'progress' => array( @@ -704,6 +711,42 @@ function file_managed_file_save_upload($element) { return $file; } +/** + * AJAX callback for managed file elements. + */ +function file_managed_file_ajax($form, $form_state) { + $button = $form_state['triggering_element']; + + // Get the list of parents, minus the button pressed and field delta. + $parents = $button['#array_parents']; + $button_key = array_pop($parents); // Button pressed. + $delta_key = array_pop($parents); // The delta for the pressed button. + + // Drill down to the element being updated. + $element = $form; + foreach ($parents as $parent_key) { + $element = $element[$parent_key]; + } + + // Add the special Ajax class if a new file was added. + if (isset($element['#file_upload_delta']) && $delta_key < $element['#file_upload_delta']) { + $element[$delta_key]['#attributes']['class'][] = 'ajax-new-content'; + } + // Otherwise just add the new content class on a placeholder. + else { + $element[$delta_key][$button_key]['#suffix'] = ''; + } + + $output = drupal_render($element); + $commands = array(); + $commands[] = ajax_command_replace('#' . $button['#ajax']['wrapper'], $output); + $commands[] = ajax_command_prepend(NULL, theme('status_messages')); + return array( + '#type' => 'ajax', + '#commands' => $commands, + ); +} + /** * Returns HTML for a managed file element. * diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test index f764a90332..1d58a7f0c9 100644 --- a/modules/file/tests/file.test +++ b/modules/file/tests/file.test @@ -373,6 +373,8 @@ class FileTaxonomyTermTestCase extends DrupalWebTestCase { * that aren't related to fields into it. */ class FileManagedFileElementTestCase extends FileFieldTestCase { + protected $profile = 'minimal'; + public static function getInfo() { return array( 'name' => 'Managed file element test', diff --git a/modules/poll/poll.module b/modules/poll/poll.module index 336e44563d..a04e770057 100644 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -366,7 +366,7 @@ function poll_form($node, &$form_state) { function poll_more_choices_submit($form, &$form_state) { // If this is a Ajax POST, add 1, otherwise add 5 more choices to the form. if ($form_state['values']['poll_more']) { - $n = $_GET['q'] == 'system/ajax' ? 1 : 5; + $n = isset($_POST['ajax_page_state']) ? 1 : 5; $form_state['choice_count'] = count($form_state['values']['choice']) + $n; } // Renumber the choices. This invalidates the corresponding key/value diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 3124ffe82a..4b7ae4f8b0 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -2043,7 +2043,7 @@ class DrupalWebTestCase extends DrupalTestCase { * case, this value needs to be an array with the following keys: * - path: A path to submit the form values to for Ajax-specific processing, * which is likely different than the $path parameter used for retrieving - * the initial form. Defaults to 'system/ajax'. + * the initial form. Defaults to the current path. * - triggering_element: If the value for the 'path' key is 'system/ajax' or * another generic Ajax processing path, this needs to be set to the name * of the element. If the name doesn't identify the element uniquely, then @@ -2070,15 +2070,26 @@ class DrupalWebTestCase extends DrupalTestCase { * form, which is typically the same thing but with hyphens replacing the * underscores. * @param $extra_post - * (optional) A string of additional data to append to the POST submission. + * (optional) An array of additional data to append to the POST submission. * This can be used to add POST data for which there are no HTML fields, as - * is done by drupalPostAJAX(). This string is literally appended to the - * POST data, so it must already be urlencoded and contain a leading "&" - * (e.g., "&extra_var1=hello+world&extra_var2=you%26me"). + * is done by drupalPostAJAX(). */ - protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { + protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = array()) { $submit_matches = FALSE; $ajax = is_array($submit); + + // Backwards-compatibility, convert $extra_post string to an array. + if (is_string($extra_post)) { + $extra_post_array = explode('&', $extra_post); + $extra_post = array(); + foreach ($extra_post_array as $value) { + if ($value) { + list($key, $value) = explode('=', $value); + $extra_post[urldecode($key)] = urldecode($value); + } + } + } + if (isset($path)) { $this->drupalGet($path, $options); } @@ -2096,9 +2107,12 @@ class DrupalWebTestCase extends DrupalTestCase { $post = array(); $upload = array(); $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form); + $post += $extra_post; $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl(); if ($ajax) { - $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax'); + if ($submit['path']) { + $action = $this->getAbsoluteUrl($submit['path']); + } // Ajax callbacks verify the triggering element if necessary, so while // we may eventually want extra code that verifies it in the // handleForm() function, it's not currently a requirement. @@ -2108,7 +2122,6 @@ class DrupalWebTestCase extends DrupalTestCase { // We post only if we managed to handle every field in edit and the // submit button matches. if (!$edit && ($submit_matches || !isset($submit))) { - $post_array = $post; if ($upload) { // TODO: cURL handles file uploads for us, but the implementation // is broken. This is a less than elegant workaround. Alternatives @@ -2134,7 +2147,7 @@ class DrupalWebTestCase extends DrupalTestCase { // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 $post[$key] = urlencode($key) . '=' . urlencode($value); } - $post = implode('&', $post) . $extra_post; + $post = implode('&', $post); } $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers)); // Ensure that any changes to variables in the other thread are picked up. @@ -2146,7 +2159,7 @@ class DrupalWebTestCase extends DrupalTestCase { } $this->verbose('POST request to: ' . $path . '