Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.25
diff -u -p -r1.25 ajax.inc
--- includes/ajax.inc	27 Jan 2010 11:19:11 -0000	1.25
+++ includes/ajax.inc	19 Feb 2010 01:58:06 -0000
@@ -166,9 +166,10 @@
  *   $commands[] = ajax_command_replace('#object-1', 'some html here');
  *   // Add a visual "changed" marker to the '#object-1' element.
  *   $commands[] = ajax_command_changed('#object-1');
- *   // Output new markup to the browser and end the request.
- *   // Note: Only custom AJAX paths/page callbacks need to do this manually.
- *   ajax_render($commands);
+ *   // Menu 'page callback' and #ajax['callback'] functions are supposed to
+ *   // return render arrays. If returning an AJAX commands array, it must be
+ *   // encapsulated in a render array structure. 
+ *   return array('#type' => 'ajax', '#commands' => $commands);
  * @endcode
  *
  * When returning an AJAX command array, it is often useful to have
@@ -178,28 +179,20 @@
  *   $commands = array();
  *   $commands[] = ajax_command_replace(NULL, $output);
  *   $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
- *   return $commands;
+ *   return array('#type' => 'ajax', '#commands' => $commands);
  * @endcode
  *
  * See @link ajax_commands AJAX framework commands @endlink
  */
 
 /**
- * Render a commands array into JSON and exit.
- *
- * Commands are immediately handed back to the AJAX requester. This function
- * will render and immediately exit.
+ * Render a commands array into JSON.
  *
  * @param $commands
  *   A list of macro commands generated by the use of ajax_command_*()
  *   functions.
- * @param $header
- *   If set to FALSE the 'text/javascript' header used by drupal_json_output()
- *   will not be used, which is necessary when using an IFRAME. If set to
- *   'multipart' the output will be wrapped in a textarea, which can also be
- *   used as an alternative method when uploading files.
  */
-function ajax_render($commands = array(), $header = TRUE) {
+function ajax_render($commands = array()) {
   // Automatically extract any 'settings' added via drupal_add_js() and make
   // them the first command.
   $scripts = drupal_add_js(NULL, NULL);
@@ -210,36 +203,7 @@ function ajax_render($commands = array()
   // Allow modules to alter any AJAX response.
   drupal_alter('ajax_render', $commands);
 
-  // Use === here so that bool TRUE doesn't match 'multipart'.
-  if ($header === 'multipart') {
-    // We do not use drupal_json_output() here because the header is not true.
-    // We are not really returning JSON, strictly-speaking, but rather JSON
-    // content wrapped in a textarea as per the "file uploads" example here:
-    // http://malsup.com/jquery/form/#code-samples
-    print '<textarea>' . drupal_json_encode($commands) . '</textarea>';
-  }
-  elseif ($header) {
-    drupal_json_output($commands);
-  }
-  else {
-    print drupal_json_encode($commands);
-  }
-  drupal_exit();
-}
-
-/**
- * Send an error response back via AJAX and immediately exit.
- *
- * This function can be used to quickly create a command array with an error
- * string and send it, short-circuiting the error handling process.
- *
- * @param $error
- *   A string to display in an alert.
- */
-function ajax_render_error($error = '') {
-  $commands = array();
-  $commands[] = ajax_command_alert(empty($error) ? t('An error occurred while handling the request: The server received invalid input.') : $error);
-  ajax_render($commands);
+  return drupal_json_encode($commands);
 }
 
 /**
@@ -368,6 +332,10 @@ function ajax_form_callback() {
  */
 function ajax_deliver($page_callback_result) {
   $commands = array();
+  $header = TRUE;
+
+  // Normalize whatever was returned by the page callback to an AJAX commands
+  // array.
   if (!isset($page_callback_result)) {
     // Simply delivering an empty commands array is sufficient. This results
     // in the AJAX request being completed, but nothing being done to the page.
@@ -388,11 +356,20 @@ function ajax_deliver($page_callback_res
         break;
     }
   }
-  elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax_commands')) {
-    // Complex AJAX callbacks can return a result that contains a specific
-    // set of commands to send to the browser.
-    if (isset($page_callback_result['#ajax_commands'])) {
-      $commands = $page_callback_result['#ajax_commands'];
+  elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax')) {
+    // 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'];
+    $error = $page_callback_result['#error'];
+    if (isset($error) && $error !== FALSE) {
+      if ((empty($error) || $error === TRUE)) {
+        $error = t('An error occurred while handling the request: The server received invalid input.');
+      }
+      $commands[] = ajax_command_alert($error);
+    }
+    else {
+      $commands = $page_callback_result['#commands'];
     }
   }
   else {
@@ -405,7 +382,49 @@ function ajax_deliver($page_callback_res
     $commands[] = ajax_command_replace(NULL, $html);
     $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
   }
-  ajax_render($commands);
+
+  // This function needs to do the same thing that drupal_deliver_html_page()
+  // does: add any needed http headers, print rendered output, and perform
+  // end-of-request tasks. By default, $header=TRUE, and we add a
+  // 'text/javascript' header. The page callback can override $header by
+  // returning an 'ajax' element with a #header property. This can be set to
+  // FALSE to prevent the 'text/javascript' header from being output, necessary
+  // when outputting to an IFRAME. This can also be set to 'multipart', in which
+  // case, we don't output JSON, but JSON content wrapped in a textarea, making
+  // a 'text/javascript' header incorrect.
+  if ($header && $header !== 'multipart') {
+    drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8');
+  }
+  $output = ajax_render($commands);
+  if ($header === 'multipart') {
+    // jQuery file uploads: http://malsup.com/jquery/form/#code-samples
+    $output = '<textarea>' . $output . '</textarea>';
+  }
+  print $output;
+  ajax_footer();
+}
+
+/**
+ * Perform end-of-AJAX-request tasks.
+ *
+ * This function is the equivalent of drupal_page_footer(), but for AJAX
+ * requests.
+ *
+ * @see drupal_page_footer()
+ */
+function ajax_footer() {
+  // Even for AJAX requests, invoke hook_exit() implementations. There may be
+  // modules that need very fast AJAX responses, and therefore, run AJAX
+  // requests with an early bootstrap.
+  if (drupal_get_bootstrap_phase == DRUPAL_BOOTSTRAP_FULL && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) {
+    module_invoke_all('exit');
+  }
+
+  // Commit the user session. See above comment about the possibility of this
+  // function running without session.inc loaded.
+  if (function_exists('drupal_session_commit')) {
+    drupal_session_commit();
+  }
 }
 
 /**
Index: modules/book/book.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.pages.inc,v
retrieving revision 1.24
diff -u -p -r1.24 book.pages.inc
--- modules/book/book.pages.inc	9 Jan 2010 21:54:00 -0000	1.24
+++ modules/book/book.pages.inc	19 Feb 2010 01:58:07 -0000
@@ -237,7 +237,6 @@ function book_form_update() {
   // Load the form based upon the $_POST data sent via the ajax call.
   list($form, $form_state) = ajax_get_form();
 
-  $commands = array();
   $bid = $_POST['book']['bid'];
 
   // Validate the bid.
@@ -248,15 +247,9 @@ function book_form_update() {
     $form['book']['plid'] = _book_parent_select($book_link);
     form_set_cache($form['values']['form_build_id'], $form, $form_state);
 
-    // Build and render the new select element, then return it in JSON format.
+    // Build the new select element and return it.
     $form_state = array();
     $form = form_builder($form['form_id']['#value'], $form, $form_state);
-
-    $commands[] = ajax_command_replace(NULL, drupal_render($form['book']['plid']));
+    return $form['book']['plid'];
   }
-
-  // @todo: We could and should just return $form['book']['plid'] and skip the
-  // ajax_command_replace() above. But for now, this provides a test case of
-  // returning an AJAX commands array.
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
 }
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.42
diff -u -p -r1.42 field.form.inc
--- modules/field/field.form.inc	12 Feb 2010 05:38:09 -0000	1.42
+++ modules/field/field.form.inc	19 Feb 2010 01:58:07 -0000
@@ -397,7 +397,7 @@ function field_add_more_js($form, $form_
   $field = $field_info['field'];
 
   if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
-    ajax_render(array());
+    return;
   }
 
   // Navigate to the right element in the the form.
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.19
diff -u -p -r1.19 file.module
--- modules/file/file.module	11 Feb 2010 17:44:47 -0000	1.19
+++ modules/file/file.module	19 Feb 2010 01:58:07 -0000
@@ -214,7 +214,7 @@ function file_ajax_upload() {
     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'));
-    ajax_render($commands, FALSE);
+    return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
   }
 
   list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
@@ -224,7 +224,7 @@ function file_ajax_upload() {
     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'));
-    ajax_render($commands, FALSE);
+    return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
   }
 
   // Get the current element and count the number of files.
@@ -261,7 +261,7 @@ function file_ajax_upload() {
 
   $commands = array();
   $commands[] = ajax_command_replace(NULL, $output, $settings);
-  ajax_render($commands, FALSE);
+  return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
 }
 
 /**
Index: modules/simpletest/tests/ajax_forms_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax_forms_test.module,v
retrieving revision 1.3
diff -u -p -r1.3 ajax_forms_test.module
--- modules/simpletest/tests/ajax_forms_test.module	12 Dec 2009 23:36:28 -0000	1.3
+++ modules/simpletest/tests/ajax_forms_test.module	19 Feb 2010 01:58:08 -0000
@@ -67,7 +67,7 @@ function ajax_forms_test_simple_form_sel
   $commands = array();
   $commands[] = ajax_command_html('#ajax_selected_color', $form_state['values']['select']);
   $commands[] = ajax_command_data('#ajax_selected_color', 'form_state_value_select', $form_state['values']['select']);
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -77,7 +77,7 @@ function ajax_forms_test_simple_form_che
   $commands = array();
   $commands[] = ajax_command_html('#ajax_checkbox_value', (int)$form_state['values']['checkbox']);
   $commands[] = ajax_command_data('#ajax_checkbox_value', 'form_state_value_select', (int)$form_state['values']['checkbox']);
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 
@@ -233,7 +233,7 @@ function ajax_forms_test_advanced_comman
 
   $commands = array();
   $commands[] = ajax_command_after($selector, "This will be placed after");
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -242,7 +242,7 @@ function ajax_forms_test_advanced_comman
 function ajax_forms_test_advanced_commands_alert_callback($form, $form_state) {
   $commands = array();
   $commands[] = ajax_command_alert("Alert");
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -252,7 +252,7 @@ function ajax_forms_test_advanced_comman
   $selector = '#append_div';
   $commands = array();
   $commands[] = ajax_command_append($selector, "Appended text");
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -263,7 +263,7 @@ function ajax_forms_test_advanced_comman
 
   $commands = array();
   $commands[] = ajax_command_before($selector, "Before text");
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -271,7 +271,7 @@ function ajax_forms_test_advanced_comman
  */
 function ajax_forms_test_advanced_commands_changed_callback($form, $form_state) {
   $commands[] = ajax_command_changed('#changed_div');
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 /**
  * AJAX callback for 'changed' with asterisk marking inner div.
@@ -279,7 +279,7 @@ function ajax_forms_test_advanced_comman
 function ajax_forms_test_advanced_commands_changed_asterisk_callback($form, $form_state) {
   $commands = array();
   $commands[] = ajax_command_changed('#changed_div', '#changed_div_mark_this');
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -291,7 +291,7 @@ function ajax_forms_test_advanced_comman
 
   $commands = array();
   $commands[] = ajax_command_css($selector, array('background-color' => $color));
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -302,7 +302,7 @@ function ajax_forms_test_advanced_comman
 
   $commands = array();
   $commands[] = ajax_command_data($selector, 'testkey', 'testvalue');
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -311,7 +311,7 @@ function ajax_forms_test_advanced_comman
 function ajax_forms_test_advanced_commands_html_callback($form, $form_state) {
   $commands = array();
   $commands[] = ajax_command_html('#html_div', 'replacement text');
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -320,7 +320,7 @@ function ajax_forms_test_advanced_comman
 function ajax_forms_test_advanced_commands_prepend_callback($form, $form_state) {
   $commands = array();
   $commands[] = ajax_command_prepend('#prepend_div', "prepended text");
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -329,7 +329,7 @@ function ajax_forms_test_advanced_comman
 function ajax_forms_test_advanced_commands_remove_callback($form, $form_state) {
   $commands = array();
   $commands[] = ajax_command_remove('#remove_text');
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
@@ -338,5 +338,5 @@ function ajax_forms_test_advanced_comman
 function ajax_forms_test_advanced_commands_restripe_callback($form, $form_state) {
   $commands = array();
   $commands[] = ajax_command_restripe('#restripe_table');
-  return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
Index: modules/simpletest/tests/ajax_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax_test.module,v
retrieving revision 1.2
diff -u -p -r1.2 ajax_test.module
--- modules/simpletest/tests/ajax_test.module	4 Dec 2009 16:49:47 -0000	1.2
+++ modules/simpletest/tests/ajax_test.module	19 Feb 2010 01:58:08 -0000
@@ -13,12 +13,14 @@ function ajax_test_menu() {
   $items['ajax-test/render'] = array(
     'title' => 'ajax_render',
     'page callback' => 'ajax_test_render',
+    'delivery callback' => 'ajax_deliver',
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
   $items['ajax-test/render-error'] = array(
     'title' => 'ajax_render_error',
-    'page callback' => 'ajax_test_render_error',
+    'page callback' => 'ajax_test_error',
+    'delivery callback' => 'ajax_deliver',
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
@@ -26,7 +28,7 @@ function ajax_test_menu() {
 }
 
 /**
- * Menu callback; Copies $_GET['commands'] into $commands and ajax_render()s that.
+ * Menu callback; Returns $_GET['commands'] suitable for use by ajax_deliver().
  *
  * Additionally ensures that ajax_render() incorporates JavaScript settings
  * by invoking drupal_add_js() with a dummy setting.
@@ -40,20 +42,16 @@ function ajax_test_render() {
   // Add a dummy JS setting.
   drupal_add_js(array('ajax' => 'test'), 'setting');
 
-  // Output AJAX commands and end the request.
-  ajax_render($commands);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
- * Menu callback; Invokes ajax_render_error().
- *
- * Optionally passes $_GET['message'] to ajax_render_error().
+ * Menu callback; Returns AJAX element with #error property set.
  */
-function ajax_test_render_error() {
+function ajax_test_error() {
   $message = '';
   if (!empty($_GET['message'])) {
     $message = $_GET['message'];
   }
-  ajax_render_error($message);
+  return array('#type' => 'ajax', '#error' => $message);
 }
-
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.890
diff -u -p -r1.890 system.module
--- modules/system/system.module	17 Feb 2010 09:09:30 -0000	1.890
+++ modules/system/system.module	19 Feb 2010 01:58:09 -0000
@@ -308,8 +308,10 @@ function system_element_info() {
   // HTML page, so we don't provide defaults for #theme or #theme_wrappers.
   // However, modules can set these properties (for example, to provide an HTML
   // debugging page that displays rather than executes AJAX commands).
-  $types['ajax_commands'] = array(
-    '#ajax_commands' => array(),
+  $types['ajax'] = array(
+    '#header' => TRUE,
+    '#commands' => array(),
+    '#error' => NULL,
   );
 
   $types['html_tag'] = array(
