Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.40
diff -u -p -r1.40 ajax.inc
--- includes/ajax.inc	17 Dec 2010 01:03:58 -0000	1.40
+++ includes/ajax.inc	20 Dec 2010 22:43:57 -0000
@@ -407,19 +407,51 @@ function ajax_base_page_theme() {
 /**
  * Package and send the result of a page callback to the browser as an AJAX response.
  *
+ * This function is the equivalent of drupal_deliver_html_page(), but for AJAX
+ * requests. Like that function, it:
+ * - Adds needed HTTP headers.
+ * - Prints rendered output.
+ * - Performs end-of-request tasks.
+ *
  * @param $page_callback_result
  *   The result of a page callback. Can be one of:
  *   - NULL: to indicate no content.
  *   - An integer menu status constant: to indicate an error condition.
  *   - A string of HTML content.
  *   - A renderable array of content.
+ *
+ * @see drupal_deliver_html_page()
  */
 function ajax_deliver($page_callback_result) {
-  $commands = array();
-  $header = TRUE;
+  // Emit an appropriate default Content-Type HTTP header if one hasn't already
+  // been added (for example, by the page callback, or by an alternate delivery
+  // callback that emits custom headers, and then calls this function).
+  if (is_null(drupal_get_http_header('Content-Type'))) {
+    // The standard media type for a JSON string is application/json
+    // (http://www.ietf.org/rfc/rfc4627.txt?number=4627). However, because
+    // browsers restrict JavaScript from reading the contents of a user's local
+    // files, jQuery submits to an IFRAME rather than using XHR for forms that
+    // contain file input elements (http://malsup.com/jquery/form/#file-upload).
+    // When Internet Explorer receives application/json content in an IFRAME, it
+    // treats it as a file download, prompting the user to save it, rather than
+    // silently rendering it in the window and allowing jQuery to read it. To
+    // get around this limitation, we return the content as text/plain. We only
+    // do this for POST requests, because there is no reason for jQuery to not
+    // use XHR for a GET request, and we don't want the incorrect mime type
+    // ending up in the page cache or proxy server caches.
+    // @see http://drupal.org/node/995854
+    $iframe_upload = !isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest';
+    if ($iframe_upload && $_SERVER['REQUEST_METHOD'] == 'POST') {
+      drupal_add_http_header('Content-Type', 'text/plain; charset=utf-8');
+    }
+    else {
+      drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
+    }
+  }
 
   // Normalize whatever was returned by the page callback to an AJAX commands
   // array.
+  $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.
@@ -444,7 +476,6 @@ 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'];
     $error = $page_callback_result['#error'];
     if (isset($error) && $error !== FALSE) {
       if ((empty($error) || $error === TRUE)) {
@@ -470,24 +501,10 @@ function ajax_deliver($page_callback_res
     $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
   }
 
-  // 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;
+  // Unlike the recommendation in http://malsup.com/jquery/form/#file-upload, we
+  // do not have to wrap the JSON string in a TEXTAREA, because
+  // drupal_json_encode() returns an HTML-safe JSON string.
+  print ajax_render($commands);
   ajax_footer();
 }
 
@@ -544,6 +561,8 @@ function ajax_process_form($element, &$f
  *   An associative array containing the properties of the element.
  *   Properties used:
  *   - #ajax['event']
+ *   - #ajax['keypress']
+ *   - #ajax['secondary_event']
  *   - #ajax['path']
  *   - #ajax['options']
  *   - #ajax['wrapper']
@@ -572,13 +591,20 @@ function ajax_pre_render_element($elemen
       case 'submit':
       case 'button':
       case 'image_button':
-        // Use the mousedown instead of the click event because form
-        // submission via pressing the enter key triggers a click event on
-        // submit inputs, inappropriately triggering AJAX behaviors.
+        // Use the mousedown instead of the click event because form submission
+        // via pressing the enter key triggers a click event on submit inputs,
+        // inappropriately triggering AJAX behaviors.
+        // @todo Clarify this. What does the browser do that's inappropriate if
+        //   the AJAX handler is bound to the click event?
         $element['#ajax']['event'] = 'mousedown';
-        // Attach an additional event handler so that AJAX behaviors
-        // can be triggered still via keyboard input.
+        // Attach an additional event handler so that AJAX behaviors can be
+        // triggered still via keyboard input.
         $element['#ajax']['keypress'] = TRUE;
+        // Ensure that if preventDefault() or stopPropagation() are called on
+        // the mousedown or keypress events, that they are also called on the
+        // click event. Otherwise, the browser issues a non-AJAX form submission
+        // even if the primary AJAX event handler returns false.
+        $element['#ajax']['secondary_event'] = 'click';
         break;
 
       case 'password':
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.33
diff -u -p -r1.33 ajax.js
--- misc/ajax.js	17 Dec 2010 01:03:58 -0000	1.33
+++ misc/ajax.js	20 Dec 2010 22:43:58 -0000
@@ -101,6 +101,7 @@ Drupal.ajax = function (base, element, e
     url: 'system/ajax',
     event: 'mousedown',
     keypress: true,
+    secondary_event: null,
     selector: '#' + base,
     effect: 'none',
     speed: 'none',
@@ -176,17 +177,27 @@ Drupal.ajax = function (base, element, e
 
   // Bind the ajaxSubmit function to the element event.
   $(ajax.element).bind(element_settings.event, function (event) {
+    ajax.triggeringEvent = event;
     return ajax.eventResponse(this, event);
   });
 
-  // If necessary, enable keyboard submission so that AJAX behaviors
-  // can be triggered through keyboard input as well as e.g. a mousedown
-  // action.
+  // If necessary, enable keyboard submission so that AJAX behaviors can be
+  // triggered through keyboard input as well as e.g. a mousedown action.
   if (element_settings.keypress) {
-    $(element_settings.element).keypress(function (event) {
+    $(ajax.element).keypress(function (event) {
+      ajax.triggeringEvent = event;
       return ajax.keypressResponse(this, event);
     });
   }
+
+  // Allow event termination from ajax.eventResponse() to terminate a dependant
+  // event. For example, for a button, 'event' might be 'mousedown' and
+  // 'secondary_event' might be 'click'.
+  if (element_settings.secondary_event) {
+    $(ajax.element).bind(element_settings.secondary_event, function (event) {
+      return ajax.secondaryEventResponse(this, event, ajax.triggeringEvent);
+    });
+  }
 };
 
 /**
@@ -262,6 +273,31 @@ Drupal.ajax.prototype.eventResponse = fu
 };
 
 /**
+ * Chains event termination from the triggering event to a dependant event.
+ *
+ * During ajax.eventResponse(), event.preventDefault(), event.stopPropagation(),
+ * and/or event.stopImmediatePropagation() may be called, or the handler may
+ * return false (implicitly invoking the first two), in order to prevent default
+ * actions (such as submitting a form or following a link) from running.
+ * However, that only affects the original triggering event. In some cases, it's
+ * necessary to also perform the same termination on a dependant event (for
+ * example, the triggering event might be 'mousedown', and the 'click' event
+ * needs to be terminated).
+ */
+Drupal.ajax.prototype.secondaryEventResponse = function (element, event, triggeringEvent) {
+  var map = {
+    isDefaultPrevented: 'preventDefault',
+    isPropagationStopped: 'stopPropagation',
+    isImmediatePropagationStopped: 'stopImmediatePropagation'
+  };
+  for (var key in map) {
+    if (triggeringEvent[key]()) {
+      event[map[key]]();
+    }
+  }
+}
+
+/**
  * Handler for the form serialization.
  *
  * Runs before the beforeSend() handler (see below), and unlike that one, runs
@@ -312,8 +348,9 @@ Drupal.ajax.prototype.beforeSubmit = fun
  * Prepare the AJAX request before it is sent.
  */
 Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
-  // Disable the element that received the change.
-  $(this.element).addClass('progress-disabled').attr('disabled', true);
+  // Mark the element that received the change as disabled. We cannot actually
+  // disable it or it will not appear in the form submission anymore.
+  $(this.element).addClass('progress-disabled');
 
   // Insert progressbar or throbber.
   if (this.progress.type == 'bar') {
@@ -348,7 +385,7 @@ Drupal.ajax.prototype.success = function
   if (this.progress.object) {
     this.progress.object.stopMonitoring();
   }
-  $(this.element).removeClass('progress-disabled').removeAttr('disabled');
+  $(this.element).removeClass('progress-disabled');
 
   Drupal.freezeHeight();
 
@@ -416,7 +453,7 @@ Drupal.ajax.prototype.error = function (
   // Undo hide.
   $(this.wrapper).show();
   // Re-enable the element.
-  $(this.element).removeClass('progress-disabled').removeAttr('disabled');
+  $(this.element).removeClass('progress-disabled');
   // Reattach behaviors, if they were detached in beforeSerialize().
   if (this.form) {
     var settings = response.settings || this.settings || Drupal.settings;
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.48
diff -u -p -r1.48 file.module
--- modules/file/file.module	11 Dec 2010 01:32:20 -0000	1.48
+++ modules/file/file.module	20 Dec 2010 22:45:06 -0000
@@ -237,7 +237,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'));
-    return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
+    return array('#type' => 'ajax', '#commands' => $commands);
   }
 
   list($form, $form_state) = ajax_get_form();
@@ -247,7 +247,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'));
-    return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
+    return array('#type' => 'ajax', '#commands' => $commands);
   }
 
   // Get the current element and count the number of files.
@@ -280,7 +280,7 @@ function file_ajax_upload() {
 
   $commands = array();
   $commands[] = ajax_command_replace(NULL, $output, $settings);
-  return array('#type' => 'ajax', '#commands' => $commands, '#header' => FALSE);
+  return array('#type' => 'ajax', '#commands' => $commands);
 }
 
 /**
