? ajax_get.patch
? sites/default/files
? sites/default/settings.php
Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.37
diff -u -p -r1.37 ajax.inc
--- includes/ajax.inc	21 Oct 2010 19:31:39 -0000	1.37
+++ includes/ajax.inc	28 Oct 2010 18:56:05 -0000
@@ -213,55 +213,12 @@
  *   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, later in this function, so that changes to
-  // the ajaxPageState setting that occur during drupal_get_css() and
-  // drupal_get_js() get included, and because the jQuery.extend() code produced
-  // by drupal_get_js() for adding settings isn't appropriate during an AJAX
-  // response, because it does not pass TRUE for the "deep" parameter, and
-  // therefore, can clobber existing settings on the page.
-  if (isset($items['js']['settings'])) {
-    unset($items['js']['settings']);
-  }
-
   // Render the HTML to load these files, and add AJAX commands to insert this
   // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
   // data from being altered again, as we already altered it above.
-  $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);
+  $styles = drupal_get_css(NULL, TRUE);
+  $scripts_footer = drupal_get_js('footer', NULL, TRUE);
+  $scripts_header = drupal_get_js('header', NULL, TRUE);
 
   $extra_commands = array();
   if (!empty($styles)) {
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1247
diff -u -p -r1.1247 common.inc
--- includes/common.inc	28 Oct 2010 02:00:27 -0000	1.1247
+++ includes/common.inc	28 Oct 2010 18:56:05 -0000
@@ -2849,21 +2849,35 @@ 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.
+ * @param $ajax_request
+ *   (optional) If set to TRUE process CSS files intelligently using the
+ *   request AJAX page state, returning only CSS that should be reloaded by
+ *   the requesting page.
  * @return
  *   A string of XHTML CSS tags.
  */
-function drupal_get_css($css = NULL, $skip_alter = FALSE) {
+function drupal_get_css($css = NULL, $ajax_request = FALSE) {
+  $page_state = &drupal_static(__FUNCTION__);
+
   if (!isset($css)) {
     $css = drupal_add_css();
   }
 
   // Allow modules and themes to alter the CSS items.
-  if (!$skip_alter) {
-    drupal_alter('css', $css);
+  drupal_alter('css', $css);
+
+  // @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.
+  if ($ajax_request) {
+    foreach ($css as $key => $item) {
+      if (is_numeric($key)) {
+        unset($css[$key]);
+      }
+    }
   }
 
   // Sort CSS items, so that they appear in the correct order.
@@ -2883,17 +2897,41 @@ function drupal_get_css($css = NULL, $sk
     }
   }
 
+  // Provide the page with information about the individual files used for
+  // the AJAX response, information not otherwise available when aggregation
+  // is enabled.
+  $response_state = array_fill_keys(array_keys($css), 1);
+  $cid = 'page_state_css_' . md5(serialize($response_state));
+
+  // Because of an array_merge_recursive() bug in drupal_add_js check to ensure
+  // the page state is set only once.
+  if (!isset($page_state)) {
+    drupal_add_js(array('ajaxPageState' => array('css' => $cid)), 'setting');
+    $page_state = TRUE;
+  }
+
+  // Store a cid => file list cache for this particular response state. If
+  // a subsequent AJAX request is made using this response state cid, the
+  // corresponding list of files that was provided for that page state can
+  // be retrieved and used to selectively return files to be reloaded.
+  if (!cache_get($cid)) {
+    cache_set($cid, $response_state, 'cache', CACHE_TEMPORARY);
+  }
+
+  // Retrieve list of files served for the given AJAX page state and remove
+  // those files to ensure that the page doesn't reload what it already has.
+  if ($ajax_request) {
+    if (isset($_REQUEST['ajax_page_state']['css']) && substr($_REQUEST['ajax_page_state']['css'], 0, 15) === 'page_state_css_' && $page_state = cache_get($_REQUEST['ajax_page_state']['css'])) {
+      $css = array_diff_key($css, $page_state->data);
+    }
+  }
+
   // Render the HTML needed to load the CSS.
   $styles = array(
     '#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), 1);
-  $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting);
-
   return drupal_render($styles);
 }
 
@@ -3613,48 +3651,8 @@ function drupal_html_class($class) {
  *   The cleaned ID.
  */
 function drupal_html_id($id) {
-  // If this is an AJAX request, then content returned by this page request will
-  // be merged with content already on the base page. The HTML ids must be
-  // unique for the fully merged content. Therefore, initialize $seen_ids to
-  // take into account ids that are already in use on the base page.
-  $seen_ids_init = &drupal_static(__FUNCTION__ . ':init');
-  if (!isset($seen_ids_init)) {
-    // Ideally, Drupal would provide an API to persist state information about
-    // prior page requests in the database, and we'd be able to add this
-    // function's $seen_ids static variable to that state information in order
-    // to have it properly initialized for this page request. However, no such
-    // page state API exists, so instead, ajax.js adds all of the in-use HTML
-    // ids to the POST data of AJAX submissions. Direct use of $_POST is
-    // normally not recommended as it could open up security risks, but because
-    // the raw POST data is cast to a number before being returned by this
-    // function, this usage is safe.
-    if (empty($_POST['ajax_html_ids'])) {
-      $seen_ids_init = array();
-    }
-    else {
-      // This function ensures uniqueness by appending a counter to the base id
-      // requested by the calling function after the first occurrence of that
-      // requested id. $_POST['ajax_html_ids'] contains the ids as they were
-      // returned by this function, potentially with the appended counter, so
-      // we parse that to reconstruct the $seen_ids array.
-      foreach ($_POST['ajax_html_ids'] as $seen_id) {
-        // We rely on '--' being used solely for separating a base id from the
-        // counter, which this function ensures when returning an id.
-        $parts = explode('--', $seen_id, 2);
-        if (!empty($parts[1]) && is_numeric($parts[1])) {
-          list($seen_id, $i) = $parts;
-        }
-        else {
-          $i = 1;
-        }
-        if (!isset($seen_ids_init[$seen_id]) || ($i > $seen_ids_init[$seen_id])) {
-          $seen_ids_init[$seen_id] = $i;
-        }
-      }
-    }
-  }
-  $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init);
-
+  $seen_ids = &drupal_static(__FUNCTION__, array());
+  $hash = &drupal_static(__FUNCTION__ . '_hash');
   $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
 
   // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
@@ -3665,19 +3663,17 @@ function drupal_html_id($id) {
   // characters as well.
   $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
 
-  // Removing multiple consecutive hyphens.
-  $id = preg_replace('/\-+/', '-', $id);
-  // Ensure IDs are unique by appending a counter after the first occurrence.
-  // The counter needs to be appended with a delimiter that does not exist in
-  // the base ID. Requiring a unique delimiter helps ensure that we really do
-  // return unique IDs and also helps us re-create the $seen_ids array during
-  // AJAX requests.
-  if (isset($seen_ids[$id])) {
-    $id = $id . '--' . ++$seen_ids[$id];
-  }
-  else {
-    $seen_ids[$id] = 1;
+  // Ensure IDs are unique by appending a random short hash. This provides a
+  // reliable method of ensuring uniqueness for AJAX requests where there is not
+  // necessarily a way to compare id's from the original page delivery with the
+  // AJAX response.
+  if (isset($seen_ids[$id]) || isset($_REQUEST['ajax_page_state'])) {
+    while (!isset($hash) || isset($seen_ids[$id . '-'. $hash])) {
+      $hash = substr(md5(uniqid(mt_rand())), 0, 8);
+    }
+    $id = $id .'-'. $hash;
   }
+  $seen_ids[$id] = 1;
 
   return $id;
 }
@@ -3965,17 +3961,19 @@ 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.
+* @param $ajax_request
+ *   (optional) If set to TRUE process JS files intelligently using the
+ *   request AJAX page state, returning only JS that should be reloaded by
+ *   the requesting page.
  * @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, $skip_alter = FALSE) {
+function drupal_get_js($scope = 'header', $javascript = NULL, $ajax_request = FALSE) {
+  $page_state = &drupal_static(__FUNCTION__);
+
   if (!isset($javascript)) {
     $javascript = drupal_add_js();
   }
@@ -3984,8 +3982,44 @@ function drupal_get_js($scope = 'header'
   }
 
   // Allow modules to alter the JavaScript.
-  if (!$skip_alter) {
-    drupal_alter('js', $javascript);
+  drupal_alter('js', $javascript);
+
+  // @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.
+  $state_items = array();
+  foreach ($javascript as $key => $item) {
+    if (!is_numeric($key)) {
+      $state_items[$key] = $item;
+    }
+  }
+  // Use inline-stripped items if this is an AJAX request.
+  if ($ajax_request) {
+    $javascript = $state_items;
+  }
+
+  // Provide the page with information about the individual files used for
+  // the AJAX response, information not otherwise available when aggregation
+  // is enabled.
+  $response_state = array_fill_keys(array_keys($state_items), 1);
+  $cid = 'page_state_js_' . md5(serialize($response_state));
+
+  // Because of an array_merge_recursive() bug in drupal_add_js check to ensure
+  // the page state is set only once.
+  if (!isset($page_state)) {
+    drupal_add_js(array('ajaxPageState' => array('js' => $cid)), 'setting');
+    $page_state = TRUE;
+  }
+
+  // Store a cid => file list cache for this particular response state. If
+  // a subsequent AJAX request is made using this response state cid, the
+  // corresponding list of files that was provided for that page state can
+  // be retrieved and used to selectively return files to be reloaded.
+  if (!cache_get($cid)) {
+    cache_set($cid, $response_state, 'cache', CACHE_TEMPORARY);
   }
 
   // Filter out elements of the given scope.
@@ -4025,20 +4059,21 @@ function drupal_get_js($scope = 'header'
   // Sort the JavaScript so that it appears in the correct order.
   uasort($items, 'drupal_sort_css_js');
 
-  // Provide the page with information about the individual JavaScript files
-  // used, information not otherwise available when aggregation is enabled.
-  $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1);
-  unset($setting['ajaxPageState']['js']['settings']);
-  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 off 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;
+  // Retrieve list of files served for the given AJAX page state and remove
+  // those files to ensure that the page doesn't reload what it already has.
+  if ($ajax_request) {
+    // Settings are handled separately in AJAX requests so that changes to
+    // the ajaxPageState setting that occur during drupal_get_css() and
+    // drupal_get_js() get included, and because the jQuery.extend() code produced
+    // by drupal_get_js() for adding settings isn't appropriate during an AJAX
+    // response, because it does not pass TRUE for the "deep" parameter, and
+    // therefore, can clobber existing settings on the page.
+    if (isset($items['settings'])) {
+      unset($items['settings']);
+    }
+    if (isset($_REQUEST['ajax_page_state']['js']) && substr($_REQUEST['ajax_page_state']['js'], 0, 14) === 'page_state_js_' && $page_state = cache_get($_REQUEST['ajax_page_state']['js'])) {
+      $items = array_diff_key($items, $page_state->data);
+    }
   }
 
   // Loop through the JavaScript to construct the rendered output.
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.27
diff -u -p -r1.27 ajax.js
--- misc/ajax.js	25 Oct 2010 00:23:47 -0000	1.27
+++ misc/ajax.js	28 Oct 2010 18:56:06 -0000
@@ -218,13 +218,6 @@ Drupal.ajax.prototype.beforeSerialize = 
     Drupal.detachBehaviors(this.form, settings, 'serialize');
   }
 
-  // Prevent duplicate HTML ids in the returned markup.
-  // @see drupal_html_id()
-  options.data['ajax_html_ids[]'] = [];
-  $('[id]').each(function () {
-    options.data['ajax_html_ids[]'].push(this.id);
-  });
-
   // Allow Drupal to return new JavaScript and CSS files to load without
   // returning the ones already loaded.
   // @see ajax_base_page_theme()
@@ -232,12 +225,8 @@ Drupal.ajax.prototype.beforeSerialize = 
   // @see drupal_get_js()
   options.data['ajax_page_state[theme]'] = Drupal.settings.ajaxPageState.theme;
   options.data['ajax_page_state[theme_token]'] = Drupal.settings.ajaxPageState.theme_token;
-  for (var key in Drupal.settings.ajaxPageState.css) {
-    options.data['ajax_page_state[css][' + key + ']'] = 1;
-  }
-  for (var key in Drupal.settings.ajaxPageState.js) {
-    options.data['ajax_page_state[js][' + key + ']'] = 1;
-  }
+  options.data['ajax_page_state[css]'] = Drupal.settings.ajaxPageState.css;
+  options.data['ajax_page_state[js]'] = Drupal.settings.ajaxPageState.js;
 };
 
 /**
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.245
diff -u -p -r1.245 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	25 Oct 2010 15:51:21 -0000	1.245
+++ modules/simpletest/drupal_web_test_case.php	28 Oct 2010 18:56:06 -0000
@@ -1834,9 +1834,17 @@ class DrupalWebTestCase extends DrupalTe
         $extra_post .= '&' . urlencode($key) . '=' . urlencode($value);
       }
     }
-    foreach ($this->xpath('//*[@id]') as $element) {
-      $id = (string) $element['id'];
-      $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id);
+    // We check whether the page state values are arrays because
+    // array_merge_recursive() used in the drupalPostAJAX() implementation of
+    // the settings command generates arrays from non-array values when
+    // merging. This is wrong, as $.extend() (used in ajax.js) does not merge
+    // scalar values into an array.
+    // @TODO Use something like drupal_array_merge_deep() found in
+    // http://drupal.org/node/208611.
+    $page_states = array('theme', 'theme_token', 'css', 'js');
+    foreach ($page_states as $key) {
+      $value = is_array($drupal_settings['ajaxPageState'][$key]) ? end($drupal_settings['ajaxPageState'][$key]) : $drupal_settings['ajaxPageState'][$key];
+      $extra_post .= '&' . urlencode('ajax_page_state[' . $key . ']') . '=' . urlencode($value);
     }
 
     // Unless a particular path is specified, use the one specified by the
Index: modules/simpletest/tests/ajax.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v
retrieving revision 1.20
diff -u -p -r1.20 ajax.test
--- modules/simpletest/tests/ajax.test	21 Oct 2010 19:31:39 -0000	1.20
+++ modules/simpletest/tests/ajax.test	28 Oct 2010 18:56:06 -0000
@@ -357,10 +357,11 @@ class AJAXMultiFormTestCase extends AJAX
     // each AJAX submission, but these variables are stable and help target the
     // desired elements.
     $field_name = 'field_ajax_test';
-    $field_xpaths = array(
-      'page-node-form' => '//form[@id="page-node-form"]//div[contains(@class, "field-name-field-ajax-test")]',
-      'page-node-form--2' => '//form[@id="page-node-form--2"]//div[contains(@class, "field-name-field-ajax-test")]',
+    $form_xpaths = array(
+      '//form[@id="page-node-form"]',
+      '//form[starts-with(@id, "page-node-form-")]',
     );
+    $field_xpath = '//div[contains(@class, "field-name-field-ajax-test")]';
     $button_name = $field_name . '_add_more';
     $button_value = t('Add another item');
     $button_xpath_suffix = '//input[@name="' . $button_name . '"]';
@@ -369,20 +370,26 @@ class AJAXMultiFormTestCase extends AJAX
     // Ensure the initial page contains both node forms and the correct number
     // of field items and "add more" button for the multi-valued field within
     // each form.
-    $this->drupalGet('form-test/two-instances-of-same-form');
-    foreach ($field_xpaths as $form_html_id => $field_xpath) {
-      $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == 1, t('Found the correct number of field items on the initial page.'));
-      $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button on the initial page.'));
+    $output = $this->drupalGet('form-test/two-instances-of-same-form');
+    foreach ($form_xpaths as $form_xpath) {
+      $this->assert(count($this->xpath($form_xpath . $field_xpath . $field_items_xpath_suffix)) == 1, t('Found the correct number of field items on the initial page.'));
+      $this->assertFieldByXPath($form_xpath . $field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button on the initial page.'));
     }
     $this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other');
 
     // Submit the "add more" button of each form twice. After each corresponding
     // page update, ensure the same as above.
-    foreach ($field_xpaths as $form_html_id => $field_xpath) {
+    foreach ($form_xpaths as $form_xpath) {
       for ($i = 0; $i < 2; $i++) {
+        if ($elements = $this->xpath($form_xpath)) {
+          $form_html_id = $elements[0]->attributes()->id;
+        }
+        else {
+          $form_html_id = '';
+        }
         $this->drupalPostAJAX(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_html_id);
-        $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, t('Found the correct number of field items after an AJAX submission.'));
-        $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button after an AJAX submission.'));
+        $this->assert(count($this->xpath($form_xpath . $field_xpath . $field_items_xpath_suffix)) == $i+2, t('Found the correct number of field items after an AJAX submission.'));
+        $this->assertFieldByXPath($form_xpath . $field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button after an AJAX submission.'));
         $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');
       }
     }
