From e658064759d54e0d43872a5430ad0381366b0bcd Mon Sep 17 00:00:00 2001 From: Angie Byron <webchick@24967.no-reply.drupal.org> Date: Thu, 23 Dec 2010 04:26:31 +0000 Subject: [PATCH] #995854 by rfay, effulgentsia, sun, merlinofchaos, Damien Tournoud, manimejia: Fixed #ajax doesn't work at all if a file element (or enctype => 'multipart/form-data') is included in the form --- includes/ajax.inc | 57 +++++++++++++++++++++++++--------------- misc/ajax.js | 19 +++++++++++--- modules/file/file.module | 6 ++--- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/includes/ajax.inc b/includes/ajax.inc index af7e6da22a10..a119e17702ed 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -407,19 +407,49 @@ 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 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'); + } + 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 +474,6 @@ function ajax_deliver($page_callback_result) { // 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 +499,10 @@ function ajax_deliver($page_callback_result) { $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(); } diff --git a/misc/ajax.js b/misc/ajax.js index 57a16048e7ea..b17e64a9f7e3 100644 --- a/misc/ajax.js +++ b/misc/ajax.js @@ -152,9 +152,9 @@ Drupal.ajax = function (base, element, element_settings) { ajax.ajaxing = true; return ajax.beforeSubmit(form_values, element_settings, options); }, - beforeSend: function (xmlhttprequest) { + beforeSend: function (xmlhttprequest, options) { ajax.ajaxing = true; - return ajax.beforeSend(xmlhttprequest, ajax.options); + return ajax.beforeSend(xmlhttprequest, options); }, success: function (response, status) { // Sanity check for browser support (object expected). @@ -318,7 +318,20 @@ Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) { * Prepare the AJAX request before it is sent. */ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) { - // Disable the element that received the change. + // 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. diff --git a/modules/file/file.module b/modules/file/file.module index 81c6000e5b6f..bbf3a1ae414f 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -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); } /** -- GitLab