Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.31 diff -u -p -r1.31 ajax.inc --- includes/ajax.inc 30 Apr 2010 08:07:54 -0000 1.31 +++ includes/ajax.inc 1 Sep 2010 23:40:09 -0000 @@ -193,11 +193,65 @@ * functions. */ function ajax_render($commands = array()) { + // AJAX responses aren't rendered with html.tpl.php, so we have to call + // drupal_get_css() and drupal_get_js() here, in order to have new files added + // during this request to be loaded by the page. We only want to send back + // files that the page hasn't already loaded, so we implement simple diffing + // logic using array_diff_key(). + foreach (array('css', 'js') as $type) { + // It is highly suspicious if $_POST['ajax_page_state'][$type] is empty, + // since the base page ought to have at least one JS file and one CSS file + // loaded. It probably indicates an error, and rather than making the page + // reload all of the files, instead we return no new files. + if (empty($_POST['ajax_page_state'][$type])) { + $items[$type] = array(); + } + else { + $function = 'drupal_add_' . $type; + $items[$type] = $function(); + drupal_alter($type, $items[$type]); + // @todo Inline CSS and JS items are indexed numerically. These can't be + // reliably diffed with array_diff_key(), since the number can change due + // to factors unrelated to the inline content, so for now, we strip the + // inline items from AJAX responses, and can add support for them when + // drupal_add_css() and drupal_add_js() are changed to using md5() or some + // other hash of the inline content. + foreach ($items[$type] as $key => $item) { + if (is_numeric($key)) { + unset($items[$type][$key]); + } + } + // Ensure that the page doesn't reload what it already has. + $items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]); + } + } + // Settings are handled separately (see below). + unset($items['js']['settings']); + + // Render the HTML to load these files, and add AJAX commands to insert this + // HTML in the page. + $styles = drupal_get_css($items['css'], TRUE); + $scripts_footer = drupal_get_js('footer', $items['js'], TRUE); + $scripts_header = drupal_get_js('header', $items['js'], TRUE); + $extra_commands = array(); + if (!empty($styles)) { + $extra_commands[] = ajax_command_prepend('head', $styles); + } + if (!empty($scripts_header)) { + $extra_commands[] = ajax_command_prepend('head', $scripts_header); + } + if (!empty($scripts_footer)) { + $extra_commands[] = ajax_command_append('body', $scripts_footer); + } + if (!empty($extra_commands)) { + $commands = array_merge($extra_commands, $commands); + } + // Automatically extract any 'settings' added via drupal_add_js() and make // them the first command. - $scripts = drupal_add_js(NULL, NULL); + $scripts = drupal_add_js(); if (!empty($scripts['settings'])) { - array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $scripts['settings']['data']))); + array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $scripts['settings']['data']), TRUE)); } // Allow modules to alter any AJAX response. @@ -284,6 +338,30 @@ function ajax_form_callback() { } /** + * Theme callback for AJAX requests. + * + * Many different pages can invoke an AJAX request to system/ajax or another + * generic AJAX path. It is almost always desired for an AJAX response to be + * rendered using the same theme as the base page, and it is therefore + * recommended for all AJAX paths to list this function for its 'theme + * callback', as is done in system_menu() for the 'system/ajax' path. + */ +function ajax_base_page_theme() { + // It's an error for a client to initiate an AJAX request without submitting + // these variables, so allow PHP to trigger notices. + list($theme, $token) = array($_POST['ajax_page_state']['theme'], $_POST['ajax_page_state']['theme_token']); + + // Prevent a request forgery from giving a person access to a theme they + // shouldn't be otherwise allowed to see. However, since everyone is allowed + // to see the default theme, token validation isn't required for that, and + // bypassing it allows most use-cases to work even when accessed from the page + // cache. + if ($theme === variable_get('theme_default', 'bartik') || drupal_valid_token($token, $theme)) { + return $theme; + } +} + +/** * Package and send the result of a page callback to the browser as an AJAX response. * * @param $page_callback_result Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1211 diff -u -p -r1.1211 common.inc --- includes/common.inc 27 Aug 2010 11:54:32 -0000 1.1211 +++ includes/common.inc 1 Sep 2010 23:40:10 -0000 @@ -2811,16 +2811,22 @@ function drupal_add_css($data = NULL, $o * @param $css * (optional) An array of CSS files. If no array is provided, the default * stylesheets array is used instead. + * @param $skip_alter + * (optional) If set to TRUE, this function skips calling drupal_alter() on + * $css, useful when the calling function passes a $css array that has already + * been altered. * @return * A string of XHTML CSS tags. */ -function drupal_get_css($css = NULL) { +function drupal_get_css($css = NULL, $skip_alter = FALSE) { if (!isset($css)) { $css = drupal_add_css(); } // Allow modules and themes to alter the CSS items. - drupal_alter('css', $css); + if (!$skip_alter) { + drupal_alter('css', $css); + } // Sort CSS items according to their weights. uasort($css, 'drupal_sort_weight'); @@ -2844,6 +2850,10 @@ function drupal_get_css($css = NULL) { '#type' => 'styles', '#items' => $css, ); + // Provide the page with information about the individual CSS files used, + // information not otherwise available when CSS aggregation is enabled. + $setting['ajaxPageState']['css'] = array_fill_keys(array_keys($css), TRUE); + $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); return drupal_render($styles); } @@ -3780,13 +3790,17 @@ function drupal_js_defaults($data = NULL * @param $javascript * (optional) An array with all JavaScript code. Defaults to the default * JavaScript array for the given scope. + * @param $skip_alter + * (optional) If set to TRUE, this function skips calling drupal_alter() on + * $javascript, useful when the calling function passes a $javascript array + * that has already been altered. * @return * All JavaScript code segments and includes for the scope as HTML tags. * @see drupal_add_js() * @see locale_js_alter() * @see drupal_js_defaults() */ -function drupal_get_js($scope = 'header', $javascript = NULL) { +function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALSE) { if (!isset($javascript)) { $javascript = drupal_add_js(); } @@ -3795,7 +3809,9 @@ function drupal_get_js($scope = 'header' } // Allow modules to alter the JavaScript. - drupal_alter('js', $javascript); + if (!$skip_alter) { + drupal_alter('js', $javascript); + } // Filter out elements of the given scope. $items = array(); @@ -3834,6 +3850,20 @@ function drupal_get_js($scope = 'header' // Sort the JavaScript by weight so that it appears in the correct order. uasort($items, 'drupal_sort_weight'); + // Provide the page with information about the individual JavaScript files + // used, information not otherwise available when JS aggregation is enabled. + $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), TRUE); + drupal_add_js($setting, 'setting'); + // If we're outputting the header scope, then this might be the final time + // that drupal_get_js() is running, so add the setting to this output as well + // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's + // because drupal_get_js() was intentionally passed a $javascript argument + // stripped of settings, potentially in order to override how settings get + // output, so in this case, do not add the setting to this output. + if ($scope == 'header' && isset($items['settings'])) { + $items['settings']['data'][] = $setting; + } + // Loop through the JavaScript to construct the rendered output. $element = array( '#tag' => 'script', Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.606 diff -u -p -r1.606 theme.inc --- includes/theme.inc 22 Aug 2010 12:46:21 -0000 1.606 +++ includes/theme.inc 1 Sep 2010 23:40:11 -0000 @@ -102,6 +102,15 @@ function drupal_theme_initialize() { // Themes can have alter functions, so reset the drupal_alter() cache. drupal_static_reset('drupal_alter'); + + // Provide the page with information about the theme that's used, so that a + // later AJAX request can be rendered using the same theme. + // @see ajax_base_page_theme() + $setting['ajaxPageState'] = array( + 'theme' => $theme_key, + 'themeToken' => drupal_get_token($theme_key), + ); + drupal_add_js($setting, 'setting'); } /** Index: misc/ajax.js =================================================================== RCS file: /cvs/drupal/drupal/misc/ajax.js,v retrieving revision 1.18 diff -u -p -r1.18 ajax.js --- misc/ajax.js 25 Jun 2010 20:34:07 -0000 1.18 +++ misc/ajax.js 1 Sep 2010 23:40:12 -0000 @@ -212,6 +212,17 @@ Drupal.ajax.prototype.beforeSubmit = fun form_values.push({ name: 'ajax_html_ids[]', value: this.id }); }); + // Allow Drupal to return new JavaScript and CSS files to load without + // returning the ones already loaded. + form_values.push({ name: 'ajax_page_state[theme]', value: Drupal.settings.ajaxPageState.theme }); + form_values.push({ name: 'ajax_page_state[theme_token]', value: Drupal.settings.ajaxPageState.themeToken }); + for (var key in Drupal.settings.ajaxPageState.css) { + form_values.push({ name: 'ajax_page_state[css][' + key + ']', value: 1 }); + } + for (var key in Drupal.settings.ajaxPageState.js) { + form_values.push({ name: 'ajax_page_state[js][' + key + ']', value: 1 }); + } + // Insert progressbar or throbber. if (this.progress.type == 'bar') { var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback)); Index: modules/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.37 diff -u -p -r1.37 file.module --- modules/file/file.module 27 Aug 2010 11:54:32 -0000 1.37 +++ modules/file/file.module 1 Sep 2010 23:40:14 -0000 @@ -41,12 +41,14 @@ function file_menu() { 'page callback' => 'file_ajax_upload', 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access content'), + 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, ); $items['file/progress'] = array( 'page callback' => 'file_ajax_progress', 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access content'), + 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, ); Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.957 diff -u -p -r1.957 system.module --- modules/system/system.module 22 Aug 2010 13:52:58 -0000 1.957 +++ modules/system/system.module 1 Sep 2010 23:40:18 -0000 @@ -509,6 +509,7 @@ function system_menu() { 'page callback' => 'ajax_form_callback', 'delivery callback' => 'ajax_deliver', 'access callback' => TRUE, + 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, 'file path' => 'includes', 'file' => 'form.inc',