Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.29
diff -u -p -r1.29 ajax.inc
--- includes/ajax.inc	31 Mar 2010 19:34:56 -0000	1.29
+++ includes/ajax.inc	2 Apr 2010 00:16:19 -0000
@@ -209,42 +209,67 @@ function ajax_render($commands = array()
 /**
  * Get a form submitted via #ajax during an AJAX callback.
  *
- * This will load a form from the form cache used during AJAX operations. It
- * pulls the form info from $_POST.
+ * This retrieves the form identified by $_POST['form_build_id'] from the form
+ * cache and optionally processes and rebuilds it.
  *
+ * @param $process
+ *   If TRUE, drupal_build_form() is called to do the full
+ *   retrieve/process/rebuild sequence on $form and $form_state. If FALSE,
+ *   $form and $form_state are retrieved from cache but not processed or
+ *   rebuilt.
  * @return
  *   An array containing the $form and $form_state. 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) = ajax_get_form();
  *   @endcode
  */
-function ajax_get_form() {
-  $form_state = form_state_defaults();
+function ajax_get_form($process = FALSE) {
+  $form_state = array();
 
-  $form_build_id = $_POST['form_build_id'];
+  // Only a poorly written form pipeline would attempt a redirect for an AJAX
+  // submission, but in any case, Drupal's AJAX framework does not yet handle
+  // redirects, so we can be proactive in disabling them.
+  $form_state['no_redirect'] = TRUE;
 
-  // Get the form from the cache.
-  $form = form_get_cache($form_build_id, $form_state);
-  if (!$form) {
-    // If $form cannot be loaded from the cache, the form_build_id in $_POST
-    // must be invalid, which means that someone performed a POST request onto
-    // system/ajax without actually viewing the concerned form in the browser.
-    // This is likely a hacking attempt as it never happens under normal
-    // circumstances, so we just do nothing.
+  // The calling function may want a fully processed and rebuilt form or just
+  // what's in the form cache.
+  if ($process) {
+    $form = drupal_build_form(NULL, $form_state, TRUE);
+  }
+  elseif (!empty($_POST['form_build_id'])) {
+    // Even if the calling function wants an unprocessed form, it should be
+    // prepared for being passed to drupal_process_form(), so add defaults and
+    // input.
+    $form_state += form_state_defaults();
+    $form = form_get_cache($_POST['form_build_id'], $form_state);
+    $form_state['input'] = $_POST;
+  }
+
+  // If $form cannot be loaded from the cache, the form_build_id in $_POST must
+  // be invalid, which can happen if:
+  // - Someone performed a POST request to system/ajax without first viewing the
+  //   concerned form in the browser.
+  // - The size of the POST request exceeded PHP's post_max_size.
+  // - More time has elapsed between the user viewing the initial form and
+  //   performing the AJAX submission than the lifetime of entries in the form
+  //   cache (default: 6 hours).
+  // - The form cache was fully flushed, removing recent entries as well as
+  //   expired ones (see http://drupal.org/node/512026).
+  if (!isset($form)) {
     watchdog('ajax', 'Invalid form POST data.', array(), WATCHDOG_WARNING);
-    drupal_exit();
+    $message = t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.');
+    drupal_deliver_page(array('#type' => 'ajax', '#error' => $message));
+    // Not drupal_exit() because drupal_deliver_page() performs the necessary
+    // end-of-request tasks.
+    // @todo Add exit to drupal_deliver_page()?
+    exit;
   }
 
-  // Since some of the submit handlers are run, redirects need to be disabled.
-  $form_state['no_redirect'] = TRUE;
-
-  // The form needs to be processed; prepare for that by setting a few internal
-  // variables.
-  $form_state['input'] = $_POST;
-  $form_id = $form['#form_id'];
-
-  return array($form, $form_state, $form_id, $form_build_id);
+  // @todo It is pointless to return properties of $form if we're already
+  //   returning $form, but this is retained in Drupal 7 for legacy support. For
+  //   Drupal 8, change to just array($form, $form_state).
+  return array($form, $form_state, $form['#form_id'], $form['#build_id']);
 }
 
 /**
@@ -263,14 +288,7 @@ function ajax_get_form() {
  * enhanced function.
  */
 function ajax_form_callback() {
-  list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
-
-  // Build, validate and if possible, submit the form.
-  drupal_process_form($form_id, $form, $form_state);
-
-  // This call recreates the form relying solely on the $form_state that
-  // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form);
+  list($form, $form_state) = ajax_get_form(TRUE);
 
   // As part of drupal_process_form(), the element that triggered the form
   // submission is determined, and in the case of AJAX, it might not be a
@@ -292,10 +310,13 @@ function ajax_form_callback() {
  *   - An integer menu status constant: to indicate an error condition.
  *   - A string of HTML content.
  *   - A renderable array of content.
+ * @param $header
+ *   (optional) By default, this is TRUE, and a 'text/javascript' HTTP header
+ *   is added to the response. Can be set to FALSE to prevent this, necessary
+ *   when outputting to an IFRAME.
  */
-function ajax_deliver($page_callback_result) {
+function ajax_deliver($page_callback_result, $header = TRUE) {
   $commands = array();
-  $header = TRUE;
 
   // Normalize whatever was returned by the page callback to an AJAX commands
   // array.
@@ -323,7 +344,9 @@ function ajax_deliver($page_callback_res
     // Complex AJAX callbacks can return a result that contains an error message
     // or a specific set of commands to send to the browser.
     $page_callback_result += element_info('ajax');
-    $header = $page_callback_result['#header'];
+    if (isset($page_callback_result['#header'])) {
+      $header = $page_callback_result['#header'];
+    }
     $error = $page_callback_result['#error'];
     if (isset($error) && $error !== FALSE) {
       if ((empty($error) || $error === TRUE)) {
@@ -368,6 +391,13 @@ function ajax_deliver($page_callback_res
 }
 
 /**
+ * Package and send the result of a page callback to the browser as an AJAX response without the 'text/javascript' HTTP header.
+ */
+function ajax_deliver_without_header($page_callback_result) {
+  ajax_deliver($page_callback_result, FALSE);
+}
+
+/**
  * Perform end-of-AJAX-request tasks.
  *
  * This function is the equivalent of drupal_page_footer(), but for AJAX
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.447
diff -u -p -r1.447 form.inc
--- includes/form.inc	31 Mar 2010 19:34:56 -0000	1.447
+++ includes/form.inc	2 Apr 2010 00:16:20 -0000
@@ -79,11 +79,11 @@ function drupal_get_form($form_id) {
 }
 
 /**
- * Build and process a form based on a form id.
+ * Build and process a form based on a form id or from cache.
  *
- * The form may also be retrieved from the cache if the form was built in a
- * previous page-load. The form is then passed on for processing, validation
- * and submission if there is proper input.
+ * This function is most commonly called from drupal_get_form() for page
+ * requests that return complete pages or from ajax_get_form() for page requests
+ * that process an AJAX form submission and return a partial form.
  *
  * @param $form_id
  *   The unique string identifying the desired form. If a function with that
@@ -92,6 +92,9 @@ function drupal_get_form($form_id) {
  *   can implement hook_forms(), which maps different $form_id values to the
  *   proper form constructor function. Examples may be found in node_forms(),
  *   search_forms(), and user_forms().
+ *   NULL may be passed for this parameter, in which case, a form is processed
+ *   and returned only if it can be retrieved from cache, but if it can't be,
+ *   then no form constructor function is called.
  * @param &$form_state
  *   An array which stores information about the form. This is passed as a
  *   reference so that the caller can use it to examine what in the form changed
@@ -154,13 +157,24 @@ function drupal_get_form($form_id) {
  *     $form_state accordingly.
  *   Further $form_state properties controlling the redirection behavior after
  *   form submission may be found in drupal_redirect_form().
+ * @param $partial_rebuild
+ *   (optional) Whether only part of the built form will be returned to the
+ *   browser (e.g., AJAX). When this is TRUE and $form needs to be rebuilt,
+ *   drupal_rebuild_form() is passed the old $form so it can preserve properties
+ *   like #build_id and #action. For advanced use-cases, this parameter can be
+ *   an array of property names that are a subset of the ones that
+ *   drupal_rebuild_form() makes sure to preserve. For example, if this
+ *   parameter is array('#action'), then that property is preserved in the
+ *   rebuilt $form, but #build_id is not.
  *
  * @return
  *   The rendered form or NULL, depending upon the $form_state flags that were set.
  *
  * @see drupal_redirect_form()
+ * @see drupal_rebuild_form()
+ * @see ajax_get_form()
  */
-function drupal_build_form($form_id, &$form_state) {
+function drupal_build_form($form_id, &$form_state, $partial_rebuild = FALSE) {
   // Ensure some defaults; if already set they will not be overridden.
   $form_state += form_state_defaults();
 
@@ -181,9 +195,21 @@ function drupal_build_form($form_id, &$f
     // have to rebuild the form to proceed. In addition, if there is stored
     // form_state data from a previous step, we'll retrieve it so it can
     // be passed on to the form processing code.
-    if (isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id && !empty($form_state['input']['form_build_id'])) {
+    if (isset($form_state['input']['form_id']) && (!isset($form_id) || $form_state['input']['form_id'] == $form_id) && !empty($form_state['input']['form_build_id'])) {
       $form_build_id = $form_state['input']['form_build_id'];
       $form = form_get_cache($form_build_id, $form_state);
+      // If the calling function passed NULL as the form id, then it expected
+      // the form to be retrieved from cache using $form_build_id alone. If that
+      // was successful, $form_id can now be set. If it wasn't successful, we
+      // return NULL for the calling function to decide how to proceed.
+      if (!isset($form_id)) {
+        if ($form) {
+          $form_id = $form['#form_id'];
+        }
+        else {
+          return;
+        }
+      }
     }
 
     // If the previous bit of code didn't result in a populated $form
@@ -238,7 +264,22 @@ function drupal_build_form($form_id, &$f
   // passing in the latest $form_state in addition to any other variables passed
   // into drupal_get_form().
   if ($form_state['rebuild'] && $form_state['process_input'] && !form_get_errors()) {
-    $form = drupal_rebuild_form($form_id, $form_state);
+    // During AJAX and other partial rebuilds, some of the $form properties need
+    // to be preserved in the rebuilt $form.
+    if (!$partial_rebuild) {
+      $old_form = NULL;
+    }
+    elseif (!is_array($partial_rebuild)) {
+      $old_form = $form;
+    }
+    else {
+      // Usually, $partial_rebuild is FALSE or TRUE, but in advanced use-cases,
+      // it can be an explicit array of $form property names. For example, it
+      // can be array('#action') in order to have drupal_rebuild_form() preserve
+      // #action, but not other properties like #build_id.
+      $old_form = array_intersect_key($form, array_flip($partial_rebuild));
+    }
+    $form = drupal_rebuild_form($form_id, $form_state, $old_form);
   }
   // After processing the form, the form builder or a #process callback may
   // have set $form_state['cache'] to indicate that the original form and the
@@ -288,15 +329,6 @@ function form_state_defaults() {
 /**
  * Retrieves a form, caches it and processes it again.
  *
- * If your AJAX callback simulates the pressing of a button, then your AJAX
- * callback will need to do the same as what drupal_get_form() would do when the
- * button is pressed: get the form from the cache, run drupal_process_form over
- * it and then if it needs rebuild, run drupal_rebuild_form() over it. Then send
- * back a part of the returned form.
- * $form_state['triggering_element']['#array_parents'] will help you to find
- * which part.
- * @see ajax_form_callback() for an example.
- *
  * @param $form_id
  *   The unique string identifying the desired form. If a function
  *   with that name exists, it is called to build the form array.
@@ -316,11 +348,13 @@ function form_state_defaults() {
  *   The newly built form.
  */
 function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
-  // AJAX and other contexts may call drupal_rebuild_form() even when
-  // $form_state['rebuild'] isn't set, but _form_builder_handle_input_element()
-  // needs to distinguish a rebuild from an initial build in order to process
-  // user input correctly. Form constructors and form processing functions may
-  // also need to handle a rebuild differently than an initial build.
+  // _form_builder_handle_input_element() needs to distinguish a rebuild from an
+  // initial build in order to process user input correctly. Form constructors
+  // and form processing functions may also need to handle a rebuild differently
+  // than an initial build. There is no use-case within Drupal core for calling
+  // drupal_rebuild_form() without $form_state['rebuild'] already being TRUE.
+  // But to support legacy or edge-case modules, we make sure to set it here.
+  // @todo Remove this in Drupal 8.
   $form_state['rebuild'] = TRUE;
 
   $form = drupal_retrieve_form($form_id, $form_state);
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.24
diff -u -p -r1.24 file.module
--- modules/file/file.module	31 Mar 2010 19:34:56 -0000	1.24
+++ modules/file/file.module	2 Apr 2010 00:16:21 -0000
@@ -39,7 +39,7 @@ function file_menu() {
 
   $items['file/ajax'] = array(
     'page callback' => 'file_ajax_upload',
-    'delivery callback' => 'ajax_deliver',
+    'delivery callback' => 'ajax_deliver_without_header',
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
@@ -211,57 +211,41 @@ function file_ajax_upload() {
 
   if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) {
     // Invalid request.
-    drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
-    $commands = array();
-    $commands[] = ajax_command_replace(NULL, theme('status_messages'));
-    return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
+    $message = t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size())));
+    return array('#type' => 'ajax', '#error' => $message);
   }
 
-  list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
-
-  if (!$form) {
-    // Invalid form_build_id.
-    drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
-    $commands = array();
-    $commands[] = ajax_command_replace(NULL, theme('status_messages'));
-    return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
-  }
-
-  // Get the current element and count the number of files.
-  $current_element = $form;
-  foreach ($form_parents as $parent) {
-    $current_element = $current_element[$parent];
-  }
-  $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
-
-  // Build, validate and if possible, submit the form.
-  drupal_process_form($form_id, $form, $form_state);
-
-  // This call recreates the form relying solely on the form_state that the
-  // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form);
-
-  // Retrieve the element to be rendered.
+  // Get the $form prior to processing and rebuilding, then process and rebuild
+  // and get the new $form. Use that to get the old and new $element being
+  // updated and the old and new file count.
+  list($old_form) = ajax_get_form(FALSE);
+  list($new_form) = ajax_get_form(TRUE);
+  $old_element = $old_form;
+  $new_element = $new_form;
   foreach ($form_parents as $parent) {
-    $form = $form[$parent];
+    $old_element = $old_element[$parent];
+    $new_element = $new_element[$parent];
   }
+  $old_file_count = isset($old_element['#file_upload_delta']) ? $old_element['#file_upload_delta'] : 0;
+  $new_file_count = isset($new_element['#file_upload_delta']) ? $new_element['#file_upload_delta'] : 0;
 
   // Add the special AJAX class if a new file was added.
-  if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
-    $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
+  if ($new_file_count > $old_file_count) {
+    // @todo Why is $old_file_count used as the index here?
+    $new_element[$old_file_count]['#attributes']['class'][] = 'ajax-new-content';
   }
   // Otherwise just add the new content class on a placeholder.
   else {
-    $form['#suffix'] .= '<span class="ajax-new-content"></span>';
+    $new_element['#suffix'] .= '<span class="ajax-new-content"></span>';
   }
 
-  $output = theme('status_messages') . drupal_render($form);
+  $output = theme('status_messages') . drupal_render($new_element);
   $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, '#header' => FALSE);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.916
diff -u -p -r1.916 system.module
--- modules/system/system.module	1 Apr 2010 14:47:16 -0000	1.916
+++ modules/system/system.module	2 Apr 2010 00:16:23 -0000
@@ -289,7 +289,7 @@ function system_element_info() {
   // However, modules can set these properties (for example, to provide an HTML
   // debugging page that displays rather than executes AJAX commands).
   $types['ajax'] = array(
-    '#header' => TRUE,
+    '#header' => NULL,
     '#commands' => array(),
     '#error' => NULL,
   );
