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 ''; + } + + // 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.