Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.41
diff -u -p -r1.41 ajax.inc
--- includes/ajax.inc	23 Dec 2010 04:26:31 -0000	1.41
+++ includes/ajax.inc	31 Dec 2010 21:44:16 -0000
@@ -423,32 +423,67 @@ function ajax_base_page_theme() {
  * @see drupal_deliver_html_page()
  */
 function ajax_deliver($page_callback_result) {
+  // Browsers do not allow JavaScript to read the contents of a user's local
+  // files. To work around that, the jQuery Form plugin submits forms containing
+  // a file input element to an IFRAME, instead of using XHR. Browsers do not
+  // normally expect JSON strings as content within an IFRAME, so the response
+  // must be customized accordingly.
+  // @see http://malsup.com/jquery/form/#file-upload
+  // @see Drupal.ajax.prototype.beforeSend()
+  $iframe_upload = !empty($_POST['ajax_iframe_upload']);
+
   // Emit a Content-Type HTTP header if none has been added by the page callback
   // or by a wrapping delivery callback.
   if (is_null(drupal_get_http_header('Content-Type'))) {
-    // The standard header for JSON is application/json.
-    // @see http://www.ietf.org/rfc/rfc4627.txt?number=4627
-    // However, browsers do not allow JavaScript to read the contents of a
-    // user's local files. To work around that, jQuery submits forms containing
-    // a file input element to an IFRAME, instead of using XHR.
-    // @see http://malsup.com/jquery/form/#file-upload
-    // When Internet Explorer receives application/json content in an IFRAME, it
-    // treats it as a file download and prompts the user to save it. To prevent
-    // that, we return the content as text/plain. But only for POST requests,
-    // since jQuery should always use XHR for GET requests and the incorrect
-    // mime type should not end up in page 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');
+    if (!$iframe_upload) {
+      // Standard JSON can be returned to a browser's XHR object, and to
+      // non-browser user agents.
+      // @see http://www.ietf.org/rfc/rfc4627.txt?number=4627
+      drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
     }
     else {
-      drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
+      // Browser IFRAMEs expect HTML. With most other content types, Internet
+      // Explorer presents the user with a download prompt.
+      drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
     }
   }
 
-  // Normalize whatever was returned by the page callback to an AJAX commands
-  // array.
+  // Print the response.
+  $commands = ajax_prepare_response($page_callback_result);
+  $json = ajax_render($commands);
+  if (!$iframe_upload) {
+    // Standard JSON can be returned to a browser's XHR object, and to
+    // non-browser user agents.
+    print $json;
+  }
+  else {
+    // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification
+    // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into
+    // links. This corrupts the JSON response. Protect the integrity of the
+    // JSON data by making it the value of a textarea.
+    // @see http://malsup.com/jquery/form/#file-upload
+    // @see http://drupal.org/node/1009382
+    print '<textarea>' . $json . '</textarea>';
+  }
+
+  // Perform end-of-request tasks.
+  ajax_footer();
+}
+
+/**
+ * Converts the return value of a page callback into an AJAX commands array.
+ *
+ * @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.
+ *
+ * @return
+ *   An AJAX commands array that can be passed to ajax_render().
+ */
+function ajax_prepare_response($page_callback_result) {
   $commands = array();
   if (!isset($page_callback_result)) {
     // Simply delivering an empty commands array is sufficient. This results
@@ -499,11 +534,7 @@ function ajax_deliver($page_callback_res
     $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
   }
 
-  // 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();
+  return $commands;
 }
 
 /**
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.35
diff -u -p -r1.35 ajax.js
--- misc/ajax.js	23 Dec 2010 04:26:31 -0000	1.35
+++ misc/ajax.js	31 Dec 2010 21:44:17 -0000
@@ -318,20 +318,38 @@ Drupal.ajax.prototype.beforeSubmit = fun
  * Prepare the AJAX request before it is sent.
  */
 Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
+  // For forms without file inputs, the jQuery Form plugin serializes the form
+  // values, and then calls jQuery's $.ajax() function, which invokes this
+  // handler. In this circumstance, options.extraData is never used. For forms
+  // with file inputs, the jQuery Form plugin uses the browser's normal form
+  // submission mechanism, but captures the response in a hidden IFRAME. In this
+  // circumstance, it calls this handler first, and then appends hidden fields
+  // to the form to submit the values in options.extraData. There is no simple
+  // way to know which submission mechanism will be used, so we add to extraData
+  // regardless, and allow it to be ignored in the former case.
+  if (this.form) {
+    options.extraData = options.extraData || {};
+
+    // Let the server know when the IFRAME submission mechanism is used. The
+    // server can use this information to wrap the JSON response in a TEXTAREA,
+    // as per http://jquery.malsup.com/form/#file-upload.
+    options.extraData.ajax_iframe_upload = '1';
+
+    // The triggering element is about to be disabled (see below), but if it
+    // contains a value (e.g., a checkbox, textfield, select, etc.), ensure that
+    // value is included in the submission. As per above, submissions that use
+    // $.ajax() are already serialized prior to the element being disabled, so
+    // this is only needed for IFRAME submissions.
+    var v = $.fieldValue(this.element);
+    if (v !== null) {
+      options.extraData[this.element.name] = v;
+    }
+  }
+
   // Disable the element that received the change to prevent user interface
   // interaction while the AJAX request is in progress. ajax.ajaxing prevents
   // the element from triggering a new request, but does not prevent the user
   // from changing its value.
-  // Forms without file inputs are already serialized before this function is
-  // called. Forms with file inputs use an IFRAME to perform a POST request
-  // similar to a browser, so disabled elements are not contained in the
-  // submitted values. Therefore, we manually add the element's value to
-  // options.extraData.
-  var v = $.fieldValue(this.element);
-  if (v !== null) {
-    options.extraData = options.extraData || {};
-    options.extraData[this.element.name] = v;
-  }
   $(this.element).addClass('progress-disabled').attr('disabled', true);
 
   // Insert progressbar or throbber.
