diff --git a/.htaccess b/.htaccess
index 710f72d2908b4488d86b9cafc46fb3532bdd993e..82d1d2ca5c1e76412484f6e9d9de00c290b2b020 100644
--- a/.htaccess
+++ b/.htaccess
@@ -38,6 +38,8 @@ DirectoryIndex index.php index.html index.htm
   php_value mbstring.http_input             pass
   php_value mbstring.http_output            pass
   php_flag mbstring.encoding_translation    off
+  # Report all PHP errors, including compile-time errors.
+  php_value error_reporting                 -1
 </IfModule>
 
 # Requires mod_expires to be enabled.
@@ -111,4 +113,4 @@ DirectoryIndex index.php index.html index.htm
   RewriteRule ^ index.php [L]
 </IfModule>
 
-# $Id: .htaccess,v 1.107 2010/02/07 05:20:21 webchick Exp $
+# $Id: .htaccess,v 1.108 2010/04/11 18:33:43 dries Exp $
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 8121364e6d6e27f8a49837e264b7c4b31dc40f10..505fb32488b10b8025adfa7452f10654feeaa3e4 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,6 @@
-// $Id: CHANGELOG.txt,v 1.360 2010/03/21 16:51:26 webchick Exp $
+// $Id: CHANGELOG.txt,v 1.363 2010/04/26 21:24:54 webchick Exp $
 
-Drupal 7.0 alpha 3, 2010-03-21
+Drupal 7.0 alpha 4, 2010-04-26
 ----------------------
 - Database:
     * Fully rewritten database layer utilizing PHP 5's PDO abstraction layer.
@@ -84,6 +84,8 @@ Drupal 7.0 alpha 3, 2010-03-21
     * If your site is being upgraded from Drupal 6 and you do not have the
       contributed date or event modules installed, user time zone settings will
       fallback to the system time zone and will have to be reconfigured by each user.
+    * User-configured time zones now serve as the default time zone for PHP
+      date/time functions.
 - Filter system:
     * Revamped the filter API and text format storage.
     * Added support for default text formats to be assigned on a per-role basis.
diff --git a/INSTALL.pgsql.txt b/INSTALL.pgsql.txt
index b57036ca71f76ab550bfccef5da00fc16e39ccbd..c6ee6e75dd7955cc1d39f3bf4f76d2f4483fed92 100644
--- a/INSTALL.pgsql.txt
+++ b/INSTALL.pgsql.txt
@@ -1,4 +1,4 @@
-// $Id: INSTALL.pgsql.txt,v 1.8 2009/07/27 19:42:54 dries Exp $
+// $Id: INSTALL.pgsql.txt,v 1.9 2010/04/07 15:07:58 dries Exp $
 
 CREATE THE PostgreSQL DATABASE
 ------------------------------
@@ -26,3 +26,19 @@ Note that the database must be created with UTF-8 (Unicode) encoding.
      createdb --encoding=UTF8 --owner=username databasename
 
    If there are no errors then the command was successful
+
+3. CREATE A SCHEMA OR SCHEMAS (Optional advanced)
+
+  Drupal will run across different schemas within your database if you so wish.
+  By default, Drupal runs inside the 'public' schema but you can use $db_prefix
+  inside settings.php to define a schema for Drupal to inside of or specify tables
+  that are shared inside of a separate schema. Drupal will not create schemas for
+  you, infact the user that Drupal runs as should not be allowed to. You'll need
+  execute the SQL below as a superuser (such as a postgres user) and replace
+  'drupaluser' with the username that Drupal uses to connect to PostgreSQL with
+  and replace schema_name with a schema name you wish to use such as 'shared':
+
+    CREATE SCHEMA schema_name AUTHORIZATION drupaluser;
+
+  Do this for as many schemas as you need. See default.settings.php for how to
+  set which tables use which schemas.
diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt
index 31a0ae22cbbc5cca68a6a0272ce65015716003b3..841153f88247ae25877411e28577dee46a257377 100644
--- a/MAINTAINERS.txt
+++ b/MAINTAINERS.txt
@@ -1,4 +1,4 @@
-// $Id: MAINTAINERS.txt,v 1.40 2010/03/21 03:57:20 webchick Exp $
+// $Id: MAINTAINERS.txt,v 1.41 2010/04/20 07:13:34 webchick Exp $
 
 Drupal core is maintained by the community.  To participate, go to
 
@@ -229,6 +229,7 @@ RDF module
 
 Search module
 - Jennifer Hodgdon 'jhodgdon' <http://drupal.org/user/155601>
+- Doug Green 'douggreen' <http://drupal.org/user/29191>
 
 Shortcut module
 - David Rothstein 'David_Rothstein' <http://drupal.org/user/124982>
diff --git a/UPGRADE.txt b/UPGRADE.txt
index 90f4a4d8988f82f5f59b924511a10cc2ebb7e1ff..eae2bd154cbf48cb8f9aef4a6e26edf74fbab733 100644
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -1,4 +1,4 @@
-// $Id: UPGRADE.txt,v 1.19 2010/01/28 07:09:03 webchick Exp $
+// $Id: UPGRADE.txt,v 1.20 2010/04/22 08:15:39 webchick Exp $
 
 UPGRADING
 ---------
@@ -49,7 +49,10 @@ Let's begin!
 
 4.  If using a custom or contributed theme, switch to Garland.
 
-5.  Disable all custom and contributed modules.
+5.  Disable all custom and contributed modules. This includes any modules that
+    are not listed under 'Core - required' or 'Core - optional' on
+    http://www.example.com/?q=admin/build/modules (replace www.example.com with
+    your installation's domain name and path).
 
 6.  Remove all old files and directories from the Drupal installation directory.
 
diff --git a/authorize.php b/authorize.php
index 0a667ec7944eb201b01f54585cf12060930aebd0..02cf32ff642ebdba773c01d7bc8b5be95e1dda85 100644
--- a/authorize.php
+++ b/authorize.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: authorize.php,v 1.7 2010/03/06 06:31:23 dries Exp $
+// $Id: authorize.php,v 1.8 2010/04/22 10:16:24 webchick Exp $
 
 /**
  * @file
@@ -158,7 +158,8 @@ if (authorize_access_allowed()) {
     }
     elseif (!$batch = batch_get()) {
       // We have a batch to process, show the filetransfer form.
-      $output = drupal_render(drupal_get_form('authorize_filetransfer_form'));
+      $elements = drupal_get_form('authorize_filetransfer_form');
+      $output = drupal_render($elements);
     }
   }
   // We defer the display of messages until all operations are done.
diff --git a/includes/ajax.inc b/includes/ajax.inc
index 1d4ea778345bf26b291b14afe81eb14c29c1d162..c5f30232141ccbd92f197e6de55769fd464f2412 100644
--- a/includes/ajax.inc
+++ b/includes/ajax.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: ajax.inc,v 1.26 2010/03/13 06:55:50 dries Exp $
+// $Id: ajax.inc,v 1.29 2010/03/31 19:34:56 dries Exp $
 
 /**
  * @file
@@ -257,28 +257,12 @@ function ajax_get_form() {
  * The Form API #ajax property can be set both for buttons and other input
  * elements.
  *
- * ajax_process_form() defines an additional 'formPath' JavaScript setting
- * that is used by Drupal.ajax.prototype.beforeSubmit() to automatically inject
- * an additional field 'ajax_triggering_element' to the submitted form values,
- * which contains the array #parents of the element in the form structure.
- * This additional field allows ajax_form_callback() to determine which
- * element triggered the action, as non-submit form elements do not
- * provide this information in $form_state['clicked_button'], which can
- * also be used to determine triggering element, but only submit-type
- * form elements.
- *
  * This function is also the canonical example of how to implement
  * #ajax['path']. If processing is required that cannot be accomplished with
  * a callback, re-implement this function and set #ajax['path'] to the
  * enhanced function.
  */
 function ajax_form_callback() {
-  // Find the triggering element, which was set up for us on the client side.
-  if (!empty($_REQUEST['ajax_triggering_element'])) {
-    $triggering_element_path = $_REQUEST['ajax_triggering_element'];
-    // Remove the value for form validation.
-    unset($_REQUEST['ajax_triggering_element']);
-  }
   list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
 
   // Build, validate and if possible, submit the form.
@@ -286,34 +270,13 @@ function ajax_form_callback() {
 
   // This call recreates the form relying solely on the $form_state that
   // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
-
-  // $triggering_element_path in a simple form might just be 'myselect', which
-  // would mean we should use the element $form['myselect']. For nested form
-  // elements we need to recurse into the form structure to find the triggering
-  // element, so we can retrieve the #ajax['callback'] from it.
-  if (!empty($triggering_element_path)) {
-    if (!isset($form['#access']) || $form['#access']) {
-      $triggering_element = $form;
-      foreach (explode('/', $triggering_element_path) as $key) {
-        if (!empty($triggering_element[$key]) && (!isset($triggering_element[$key]['#access']) || $triggering_element[$key]['#access'])) {
-          $triggering_element = $triggering_element[$key];
-        }
-        else {
-          // We did not find the $triggering_element or do not have #access,
-          // so break out and do not provide it.
-          $triggering_element = NULL;
-          break;
-        }
-      }
-    }
-  }
-  if (empty($triggering_element)) {
-    $triggering_element = $form_state['clicked_button'];
-  }
-  // Now that we have the element, get a callback if there is one.
-  if (!empty($triggering_element)) {
-    $callback = $triggering_element['#ajax']['callback'];
+  $form = drupal_rebuild_form($form_id, $form_state, $form);
+
+  // As part of drupal_process_form(), the element that triggered the form
+  // submission is determined, and in the case of AJAX, it might not be a
+  // button. This lets us route to the appropriate callback.
+  if (!empty($form_state['triggering_element'])) {
+    $callback = $form_state['triggering_element']['#ajax']['callback'];
   }
   if (!empty($callback) && function_exists($callback)) {
     return $callback($form, $form_state);
@@ -448,8 +411,6 @@ function ajax_footer() {
  *   drupal_add_js().
  */
 function ajax_process_form($element, &$form_state) {
-  $js_added = &drupal_static(__FUNCTION__, array());
-
   // Nothing to do if there is neither a callback nor a path.
   if (!(isset($element['#ajax']['callback']) || isset($element['#ajax']['path']))) {
     return $element;
@@ -487,9 +448,8 @@ function ajax_process_form($element, &$form_state) {
     }
   }
 
-  // Adding the same JavaScript settings twice will cause a recursion error,
-  // we avoid the problem by checking if the JavaScript has already been added.
-  if (!isset($js_added[$element['#id']]) && isset($element['#ajax']['event'])) {
+  // Attach JavaScript settings to the element.
+  if (isset($element['#ajax']['event'])) {
     $element['#attached']['library'][] = array('system', 'form');
     $element['#attached']['js']['misc/ajax.js'] = array('weight' => JS_LIBRARY + 2);
 
@@ -502,13 +462,40 @@ function ajax_process_form($element, &$form_state) {
       'speed' => 'none',
       'method' => 'replace',
       'progress' => array('type' => 'throbber'),
-      'formPath' => implode('/', $element['#array_parents']),
     );
 
-    // Process special settings.
+    // Change path to url.
     $settings['url'] = isset($settings['path']) ? url($settings['path']) : url('system/ajax');
     unset($settings['path']);
-    $settings['button'] = isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE;
+
+    // Add special data to $settings['submit'] so that when this element
+    // triggers an AJAX submission, Drupal's form processing can determine which
+    // element triggered it.
+    // @see _form_element_triggered_scripted_submission()
+    if (isset($settings['trigger_as'])) {
+      // An element can add a 'trigger_as' key within #ajax to make the element
+      // submit as though another one (for example, a non-button can use this
+      // to submit the form as though a button were clicked). When using this,
+      // the 'name' key is always required to identify the element to trigger
+      // as. The 'value' key is optional, and only needed when multiple elements
+      // share the same name, which is commonly the case for buttons.
+      $settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name'];
+      if (isset($settings['trigger_as']['value'])) {
+        $settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value'];
+      }
+      unset($settings['trigger_as']);
+    }
+    else {
+      // Most of the time, elements can submit as themselves, in which case the
+      // 'trigger_as' key isn't needed, and the element's name is used.
+      $settings['submit']['_triggering_element_name'] = $element['#name'];
+      // If the element is a (non-image) button, its name may not identify it
+      // uniquely, in which case a match on value is also needed.
+      // @see _form_button_was_clicked()
+      if (isset($element['#button_type']) && empty($element['#has_garbage_value'])) {
+        $settings['submit']['_triggering_element_value'] = $element['#value'];
+      }
+    }
 
     // Convert a simple #ajax['progress'] string into an array.
     if (is_string($settings['progress'])) {
@@ -524,12 +511,11 @@ function ajax_process_form($element, &$form_state) {
       $element['#attached']['js']['misc/progress.js'] = array('cache' => FALSE);
     }
 
-    // @todo This is incompatible with drupal_render() caching, but cannot be
-    //   assigned to #attached, because AJAX callbacks render the form in a way
-    //   so that #attached settings are not taken over.
-    drupal_add_js(array('ajax' => array($element['#id'] => $settings)), 'setting');
+    $element['#attached']['js'][] = array(
+      'type' => 'setting',
+      'data' => array('ajax' => array($element['#id'] => $settings)),
+    );
 
-    $js_added[$element['#id']] = TRUE;
     $form_state['cache'] = TRUE;
   }
   return $element;
diff --git a/includes/authorize.inc b/includes/authorize.inc
index a6c916bbc60bf19594c65492cf7501986fe02729..341fc311059ec198c40ebf8ae3442a2286e11fcf 100644
--- a/includes/authorize.inc
+++ b/includes/authorize.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: authorize.inc,v 1.9 2010/01/30 07:59:24 dries Exp $
+// $Id: authorize.inc,v 1.10 2010/04/23 05:21:19 webchick Exp $
 
 /**
  * @file
@@ -159,7 +159,7 @@ function authorize_filetransfer_form_submit($form, &$form_state) {
       // to make sure it is available since this code could potentially (will
       // likely) be called during the installation process, before the
       // database is set up.
-      if (db_is_active()) {
+      try {
         $connection_settings = array();
         foreach ($form_state['values']['connection_settings'][$filetransfer_backend] as $key => $value) {
           // We do *not* want to store passwords in the database, unless the
@@ -186,6 +186,11 @@ function authorize_filetransfer_form_submit($form, &$form_state) {
         // Now run the operation.
         authorize_run_operation($filetransfer);
       }
+      catch (Exception $e) {
+        // If there is no database available, we don't care and just skip
+        // this part entirely.
+      }
+
       break;
 
     case 'enter_connection_settings':
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index f3379b9c602ddd0800d550d4a5d9b4c9a48043e4..843c475a7b74bc5c964f9aca0dcb63927ab7d431 100644
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: bootstrap.inc,v 1.364 2010/03/21 16:51:26 webchick Exp $
+// $Id: bootstrap.inc,v 1.377 2010/04/26 21:24:54 webchick Exp $
 
 /**
  * @file
@@ -9,7 +9,7 @@
 /**
  * The current system version.
  */
-define('VERSION', '7.0-alpha3');
+define('VERSION', '7.0-alpha4');
 
 /**
  * Core API compatibility.
@@ -352,7 +352,7 @@ function timer_stop($name) {
  *
  * $sites = array(
  *   'devexample.com' => 'example.com',
- *   'localhost/example' => 'example.com',
+ *   'localhost.example' => 'example.com',
  * );
  *
  * The above array will cause Drupal to look for a directory named
@@ -598,15 +598,15 @@ function drupal_settings_initialize() {
     // in drupal_settings_initialize().
     if (!empty($_SERVER['HTTP_HOST'])) {
       $cookie_domain = $_SERVER['HTTP_HOST'];
+      // Strip leading periods, www., and port numbers from cookie domain.
+      $cookie_domain = ltrim($cookie_domain, '.');
+      if (strpos($cookie_domain, 'www.') === 0) {
+        $cookie_domain = substr($cookie_domain, 4);
+      }
+      $cookie_domain = explode(':', $cookie_domain);
+      $cookie_domain = '.' . $cookie_domain[0];
     }
   }
-  // Strip leading periods, www., and port numbers from cookie domain.
-  $cookie_domain = ltrim($cookie_domain, '.');
-  if (strpos($cookie_domain, 'www.') === 0) {
-    $cookie_domain = substr($cookie_domain, 4);
-  }
-  $cookie_domain = explode(':', $cookie_domain);
-  $cookie_domain = '.' . $cookie_domain[0];
   // Per RFC 2109, cookie domains must contain at least one dot other than the
   // first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
   if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
@@ -752,10 +752,12 @@ function variable_initialize($conf = array()) {
  *   The name of the variable to return.
  * @param $default
  *   The default value to use if this variable has never been set.
+ *
  * @return
  *   The value of the variable.
  *
- * @see variable_del(), variable_set()
+ * @see variable_del()
+ * @see variable_set()
  */
 function variable_get($name, $default = NULL) {
   global $conf;
@@ -772,7 +774,8 @@ function variable_get($name, $default = NULL) {
  *   The value to set. This can be any PHP data type; these functions take care
  *   of serialization as necessary.
  *
- * @see variable_del(), variable_get()
+ * @see variable_del()
+ * @see variable_get()
  */
 function variable_set($name, $value) {
   global $conf;
@@ -790,7 +793,8 @@ function variable_set($name, $value) {
  * @param $name
  *   The name of the variable to undefine.
  *
- * @see variable_get(), variable_set()
+ * @see variable_get()
+ * @see variable_set()
  */
 function variable_del($name) {
   global $conf;
@@ -855,10 +859,10 @@ function drupal_page_is_cacheable($allow_caching = NULL) {
 }
 
 /**
- * Call all init or exit hooks without including all modules.
+ * Invoke a bootstrap hook in all bootstrap modules that implement it.
  *
  * @param $hook
- *   The name of the bootstrap hook we wish to invoke.
+ *   The name of the bootstrap hook to invoke.
  */
 function bootstrap_invoke_all($hook) {
   // _drupal_bootstrap_page_cache() already loaded the bootstrap modules, so we
@@ -1159,7 +1163,7 @@ function drupal_serve_page_from_cache(stdClass $cache) {
  * Define the critical hooks that force modules to always be loaded.
  */
 function bootstrap_hooks() {
-  return array('boot', 'exit', 'watchdog');
+  return array('boot', 'exit', 'watchdog', 'language_init');
 }
 
 /**
@@ -1376,6 +1380,8 @@ function drupal_unpack($obj, $field = 'data') {
  *       belongs to.
  * @return
  *   The translated string.
+ *
+ * @ingroup sanitization
  */
 function t($string, array $args = array(), array $options = array()) {
   global $language;
@@ -1438,10 +1444,13 @@ function t($string, array $args = array(), array $options = array()) {
  *
  * @param $text
  *   The text to be checked or processed.
+ *
  * @return
  *   An HTML safe version of $text, or an empty string if $text is not
  *   valid UTF-8.
- * @see drupal_validate_utf8().
+ *
+ * @see drupal_validate_utf8()
+ * @ingroup sanitization
  */
 function check_plain($text) {
   // We do not want to use drupal_static() since PHP version will never change
@@ -1455,8 +1464,8 @@ function check_plain($text) {
   // drupal_validate_utf8() here. This avoids the overhead of an additional
   // function call, since check_plain() may be called hundreds of times during
   // a request. For PHP 5.2.5+, this check for valid UTF-8 should be handled
-  // internally by PHP in htmlspecialchars().
-  // @see http://www.php.net/releases/5_2_5.php
+  // internally by PHP in htmlspecialchars(). 
+  // See http://www.php.net/releases/5_2_5.php.
   // @todo remove this when support for either IE6 or PHP < 5.2.5 is dropped.
 
   if ($php525) {
@@ -1681,7 +1690,7 @@ function drupal_is_denied($ip) {
   // won't be denied. However the user asked explicitly not to use the
   // database and also in this case it's quite likely that the user relies
   // on higher performance solutions like a firewall.
-  elseif (function_exists('db_is_active')) {
+  elseif (class_exists('Database', FALSE)) {
     $denied = (bool)db_query("SELECT 1 FROM {blocked_ips} WHERE ip = :ip", array(':ip' => $ip))->fetchField();
   }
   return $denied;
@@ -1810,6 +1819,21 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
   return $stored_phase;
 }
 
+/**
+ * Return the time zone of the current user.
+ */
+function drupal_get_user_timezone() {
+  global $user;
+  if (variable_get('configurable_timezones', 1) && $user->uid && $user->timezone) {
+    return $user->timezone;
+  }
+  else {
+    // Ignore PHP strict notice if time zone has not yet been set in the php.ini
+    // configuration.
+    return variable_get('date_default_timezone', @date_default_timezone_get());
+  }
+}
+
 /**
  * Custom PHP error handler.
  *
@@ -1841,8 +1865,21 @@ function _drupal_error_handler($error_level, $message, $filename, $line, $contex
  */
 function _drupal_exception_handler($exception) {
   require_once DRUPAL_ROOT . '/includes/errors.inc';
-  // Log the message to the watchdog and return an error page to the user.
-  _drupal_log_error(_drupal_decode_exception($exception), TRUE);
+
+  try {
+    // Log the message to the watchdog and return an error page to the user.
+    _drupal_log_error(_drupal_decode_exception($exception), TRUE);
+  }
+  catch (Exception $exception2) {
+    // Another uncaught exception was thrown while handling the first one.
+    // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
+    $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
+    if ($error_level == ERROR_REPORTING_DISPLAY_ALL || (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update')) {
+      print 'Additional uncaught exception thrown while handling exception.<br /><br />';
+      print '<b>Original</b><br />' . _drupal_render_exception_safe($exception) . '<br /><br />';
+      print '<b>Additional</b><br />' . _drupal_render_exception_safe($exception2) . '<br /><br />';
+    }
+  }
 }
 
 /**
@@ -1891,6 +1928,7 @@ function _drupal_bootstrap_page_cache() {
     $cache = drupal_page_get_cache();
     // If there is a cached page, display it.
     if (is_object($cache)) {
+      date_default_timezone_set(drupal_get_user_timezone());
       // If the skipping of the bootstrap hooks is not enforced, call
       // hook_boot.
       if (variable_get('page_cache_invoke_hooks', TRUE)) {
@@ -1928,6 +1966,10 @@ function _drupal_bootstrap_database() {
   Database::setLoggingCallback('watchdog', WATCHDOG_NOTICE, WATCHDOG_ERROR);
 
   // Register autoload functions so that we can access classes and interfaces.
+  // The database autoload routine comes first so that we can load the database
+  // system without hitting the database. That is especially important during
+  // the install or upgrade process.
+  spl_autoload_register('db_autoload');
   spl_autoload_register('drupal_autoload_class');
   spl_autoload_register('drupal_autoload_interface');
 }
@@ -2067,7 +2109,7 @@ function drupal_language_initialize() {
     }
     // Allow modules to react on language system initialization in multilingual
     // environments.
-    module_invoke_all('language_init', $types);
+    bootstrap_invoke_all('language_init');
   }
 }
 
@@ -2539,7 +2581,7 @@ function registry_update() {
  * - http://php.net/manual/en/language.variables.scope.php#language.variables.scope.static
  * - http://php.net/manual/en/language.variables.scope.php#language.variables.scope.references
  * The example below shows the syntax needed to work around both limitations.
- * For benchmarks and more information, @see http://drupal.org/node/619666.
+ * For benchmarks and more information, see http://drupal.org/node/619666.
  *
  * Example:
  * @code
@@ -2667,7 +2709,7 @@ function &drupal_register_shutdown_function($callback = NULL, $parameters = NULL
     $args = func_get_args();
     array_shift($args);
     // Save callback and arguments
-    $callbacks[] = array('callback' => $callback, 'arguments' => $args);
+    $callbacks[] = array('callback' => $callback, 'arguments' => $args, 'cwd' => getcwd());
   }
   return $callbacks;
 }
@@ -2680,15 +2722,17 @@ function _drupal_shutdown_function() {
 
   try {
     while (list($key, $callback) = each($callbacks)) {
+      chdir($callback['cwd']);
       call_user_func_array($callback['callback'], $callback['arguments']);
     }
   }
-  catch(Exception $exception) {
-    require_once DRUPAL_ROOT . '/includes/errors.inc';
-    // The theme has already been printed so it doesn't make much sense to use
-    // drupal_exception_handler() because that would display the maintenaince
-    // page below the usual page. For now, just print the error for debugging.
-    // @todo: Improve this.
-    print t('%type: %message in %function (line %line of %file).', _drupal_decode_exception($exception));
+  catch (Exception $exception) {
+    // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
+    $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
+    if ($error_level == ERROR_REPORTING_DISPLAY_ALL || (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update')) {
+      require_once DRUPAL_ROOT . '/includes/errors.inc';
+      print 'Uncaught exception thrown in shutdown function.<br /><br />';
+      print _drupal_render_exception_safe($exception) . '<br /><br />';
+    }
   }
 }
diff --git a/includes/cache-install.inc b/includes/cache-install.inc
index a3d05cde3a919d795b39bce27c1e3803dd525edc..3beebf3ab2f91707775043735fb428a9b52e2480 100644
--- a/includes/cache-install.inc
+++ b/includes/cache-install.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: cache-install.inc,v 1.7 2009/12/30 08:16:55 dries Exp $
+// $Id: cache-install.inc,v 1.8 2010/04/11 17:16:45 dries Exp $
 
 /**
  * @file
@@ -41,8 +41,7 @@ class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterfac
     // subtle bugs, some of which would not be fixed unless the site
     // administrator cleared the cache manually.
     try {
-      if (function_exists('drupal_install_initialize_database')) {
-        drupal_install_initialize_database();
+      if (class_exists('Database')) {
         parent::clear($cid, $wildcard);
       }
     }
diff --git a/includes/common.inc b/includes/common.inc
index b0e872c42572e856ebf632c5983a8698b6461045..b340f02806ce751a415e3a18d010394b6187eabd 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: common.inc,v 1.1129 2010/03/18 19:15:02 dries Exp $
+// $Id: common.inc,v 1.1151 2010/04/24 14:53:59 dries Exp $
 
 /**
  * @file
@@ -250,14 +250,17 @@ function drupal_get_breadcrumb() {
 }
 
 /**
- * Return a string containing RDF namespace declarations for use in XML and
+ * Returns a string containing RDF namespace declarations for use in XML and
  * XHTML output.
  */
 function drupal_get_rdf_namespaces() {
-  // Serialize the RDF namespaces used in RDFa annotation.
   $xml_rdf_namespaces = array();
-  foreach (module_invoke_all('rdf_namespaces') as $prefix => $uri) {
-    $xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"';
+
+  // Serializes the RDF namespaces in XML namespace syntax.
+  if (function_exists('rdf_get_namespaces')) {
+    foreach (rdf_get_namespaces() as $prefix => $uri) {
+      $xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"';
+    }
   }
   return implode("\n  ", $xml_rdf_namespaces);
 }
@@ -792,6 +795,7 @@ function drupal_http_request($url, array $options = array()) {
 
   switch ($uri['scheme']) {
     case 'http':
+    case 'feed':
       $port = isset($uri['port']) ? $uri['port'] : 80;
       $host = $uri['host'] . ($port != 80 ? ':' . $port : '');
       $fp = @fsockopen($uri['host'], $port, $errno, $errstr, $options['timeout']);
@@ -818,7 +822,7 @@ function drupal_http_request($url, array $options = array()) {
     // Mark that this request failed. This will trigger a check of the web
     // server's ability to make outgoing HTTP requests the next time that
     // requirements checking is performed.
-    // @see system_requirements()
+    // See system_requirements()
     variable_set('drupal_http_request_fails', TRUE);
 
     return $result;
@@ -1071,7 +1075,7 @@ function valid_url($url, $absolute = FALSE) {
   if ($absolute) {
     return (bool)preg_match("
       /^                                                      # Start at the beginning of the text
-      (?:ftp|https?):\/\/                                     # Look for ftp, http, or https schemes
+      (?:ftp|https?|feed):\/\/                                # Look for ftp, http, https or feed schemes
       (?:                                                     # Userinfo (optional) which is typically
         (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)*      # a username or a username and password
         (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@          # combination
@@ -1173,14 +1177,13 @@ function flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL)
   return ($number < $threshold);
 }
 
-function check_file($filename) {
-  return is_uploaded_file($filename);
-}
-
 /**
  * @defgroup sanitization Sanitization functions
  * @{
  * Functions to sanitize values.
+ *
+ * See http://drupal.org/writing-secure-code for information
+ * on writing secure code.
  */
 
 /**
@@ -1768,13 +1771,7 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL
   $timezones = &$drupal_static_fast['timezones'];
 
   if (!isset($timezone)) {
-    global $user;
-    if (variable_get('configurable_timezones', 1) && $user->uid && $user->timezone) {
-      $timezone = $user->timezone;
-    }
-    else {
-      $timezone = variable_get('date_default_timezone', 'UTC');
-    }
+    $timezone = date_default_timezone_get();
   }
   // Store DateTimeZone objects in an array rather than repeatedly
   // constructing identical objects over the life of a request.
@@ -1903,21 +1900,37 @@ function format_username($account) {
  */
 
 /**
- * Generate a URL.
+ * Generates an internal or external URL.
+ *
+ * When creating links in modules, consider whether l() could be a better
+ * alternative than url().
  *
  * @param $path
- *   The Drupal path being linked to, such as "admin/content", or an existing
- *   URL like "http://drupal.org/". The special path '<front>' may also be given
- *   and will generate the site's base URL.
+ *   The internal path or external URL being linked to, such as "node/34" or
+ *   "http://example.com/foo". A few notes:
+ *   - If you provide a full URL, it will be considered an external URL.
+ *   - If you provide only the path (e.g. "node/34"), it will be
+ *     considered an internal link. In this case, it should be a system URL,
+ *     and it will be replaced with the alias, if one exists. Additional query
+ *     arguments for internal paths must be supplied in $options['query'], not
+ *     included in $path.
+ *   - If you provide an internal path and $options['alias'] is set to TRUE, the
+ *     path is assumed already to be the correct path alias, and the alias is
+ *     not looked up.
+ *   - The special string '<front>' generates a link to the site's base URL.
+ *   - If your external URL contains a query (e.g. http://example.com/foo?a=b),
+ *     then you can either URL encode the query keys and values yourself and
+ *     include them in $path, or use $options['query'] to let this function
+ *     URL encode them.
  * @param $options
- *   An associative array of additional options, with the following keys:
+ *   An associative array of additional options, with the following elements:
  *   - 'query': An array of query key/value-pairs (without any URL-encoding) to
- *     append to the link.
- *   - 'fragment': A fragment identifier (or named anchor) to append to the
- *     link. Do not include the leading '#' character.
+ *     append to the URL.
+ *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
+ *     Do not include the leading '#' character.
  *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
  *     absolute link (beginning with http:). Useful for links that will be
- *     displayed outside the site, such as in a RSS feed.
+ *     displayed outside the site, such as in an RSS feed.
  *   - 'alias': Defaults to FALSE. Whether the given path is a URL alias
  *     already.
  *   - 'external': Whether the given path is an external URL.
@@ -1936,9 +1949,6 @@ function format_username($account) {
  *
  * @return
  *   A string containing a URL to the given path.
- *
- * When creating links in modules, consider whether l() could be a better
- * alternative than url().
  */
 function url($path = NULL, array $options = array()) {
   // Merge in defaults.
@@ -1996,19 +2006,6 @@ function url($path = NULL, array $options = array()) {
   }
 
   global $base_url, $base_secure_url, $base_insecure_url;
-  // Use the advanced drupal_static() pattern, since this is called very often.
-  static $drupal_static_fast;
-  if (!isset($drupal_static_fast)) {
-    $drupal_static_fast['script'] = &drupal_static(__FUNCTION__);
-  }
-  $script = &$drupal_static_fast['script'];
-
-  if (!isset($script)) {
-    // On some web servers, such as IIS, we can't omit "index.php". So, we
-    // generate "index.php?q=foo" instead of "?q=foo" on anything that is not
-    // Apache.
-    $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : '';
-  }
 
   // The base_url might be rewritten from the language rewrite in domain mode.
   if (!isset($options['base_url'])) {
@@ -2065,6 +2062,16 @@ function url($path = NULL, array $options = array()) {
       $query += $options['query'];
     }
     if ($query) {
+      // On some web servers, such as IIS, we can't omit "index.php". So, we
+      // generate "index.php?q=foo" instead of "?q=foo" on anything that is not
+      // Apache. strpos() is fast, so there is no performance benefit to
+      // statically caching its result.
+      // @todo This needs to be re-evaluated with modern web servers. Since we
+      //   do not add $script when there aren't query parameters, we're already
+      //   assuming that index.php is setup as a default document on the web
+      //   server. If that's the case, it should be possible to omit "index.php"
+      //   even when there are query parameters: http://drupal.org/node/437228.
+      $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : '';
       return $base . $script . '?' . drupal_http_build_query($query) . $options['fragment'];
     }
     else {
@@ -2128,50 +2135,36 @@ function drupal_attributes(array $attributes = array()) {
 }
 
 /**
- * Format an internal Drupal link.
+ * Formats an internal or external URL link as an HTML anchor tag.
  *
- * This function correctly handles aliased paths, and allows themes to highlight
- * links to the current page correctly, so all internal links output by modules
- * should be generated by this function if possible.
+ * This function correctly handles aliased paths, and adds an 'active' class
+ * attribute to links that point to the current page (for theming), so all
+ * internal links output by modules should be generated by this function if
+ * possible.
  *
  * @param $text
- *   The text to be enclosed with the anchor tag.
+ *   The link text for the anchor tag.
  * @param $path
- *   The Drupal path being linked to, such as "admin/content". Can be an
- *   external or internal URL.
- *     - If you provide the full URL, it will be considered an external URL.
- *     - If you provide only the path (e.g. "admin/content"), it is
- *       considered an internal link. In this case, it must be a system URL
- *       as the url() function will generate the alias.
- *     - If you provide '<front>', it generates a link to the site's
- *       base URL (again via the url() function).
- *     - If you provide a path, and 'alias' is set to TRUE (see below), it is
- *       used as is.
- * @param $options
- *   An associative array of additional options, with the following keys:
- *     - attributes: An associative array of HTML attributes to apply to the
- *       anchor tag.
- *     - query: A query string to append to the link, or an array of query
- *       key/value properties.
- *     - fragment: A fragment identifier (named anchor) to append to the link.
- *       Do not include the '#' character.
- *     - absolute: (default FALSE) Whether to force the output to be an absolute
- *       link (beginning with http:). Useful for links that will be displayed
- *       outside the site, such as in an RSS feed.
- *     - html: (default FALSE) Whether $text is HTML, or just plain-text. For
- *       example for making an image a link, this must be set to TRUE, or else
- *       you will see the escaped HTML.
- *     - alias: (default FALSE) Whether the given path is an alias already.
- *     - language: An optional language object. If the path being linked to is
- *       internal to the site, $options['language'] is used to look up the alias
- *       for the URL, and to determine whether the link is "active", or pointing
- *       to the current page (the language as well as the path must match). If
- *       $options['language'] is omitted, the language defined by the current
- *       page's URL will be used; this depends on the site's language
- *       negotiation settings (sub-domain or URL prefix).
+ *   The internal path or external URL being linked to, such as "node/34" or
+ *   "http://example.com/foo". After the url() function is called to construct
+ *   the URL from $path and $options, the resulting URL is passed through
+ *   check_plain() before it is inserted into the HTML anchor tag, to ensure
+ *   well-formed HTML. See url() for more information and notes.
+ * @param array $options
+ *   An associative array of additional options, with the following elements:
+ *   - 'attributes': An associative array of HTML attributes to apply to the
+ *     anchor tag.
+ *   - 'html' (default FALSE): Whether $text is HTML or just plain-text. For
+ *     example, to make an image tag into a link, this must be set to TRUE, or
+ *     you will see the escaped HTML image tag.
+ *   - 'language': An optional language object. If the path being linked to is
+ *     internal to the site, $options['language'] is used to determine whether
+ *     the link is "active", or pointing to the current page (the language as
+ *     well as the path must match). This element is also used by url().
+ *   - Additional $options elements used by the url() function.
  *
  * @return
- *   an HTML string containing a link to the given path.
+ *   An HTML string containing a link to the given path.
  */
 function l($text, $path, array $options = array()) {
   global $language_url;
@@ -2635,6 +2628,12 @@ function drupal_add_html_head_link($attributes, $header = FALSE) {
  *   have any or all of the following keys:
  *   - 'type': The type of stylesheet being added. Available options are 'file',
  *     'inline' or 'external'. Defaults to 'file'.
+ *   - 'basename': Force a basename for the file being added. Modules are
+ *     expected to use stylesheets with unique filenames, but integration of
+ *     external libraries may make this impossible. The basename of
+ *     'modules/node/node.css' is 'node.css'. If the external library "node.js"
+ *     ships with a 'node.css', then a different, unique basename would be
+ *     'node.js.css'.
  *   - 'weight': The weight of the stylesheet specifies the order in which the
  *     CSS will appear when presented on the page. Available constants are:
  *     - CSS_SYSTEM: Any system-layer CSS.
@@ -2742,7 +2741,8 @@ function drupal_get_css($css = NULL) {
   $previous_item = array();
   foreach ($css as $key => $item) {
     if ($item['type'] == 'file') {
-      $basename = basename($item['data']);
+      // If defined, force a unique basename for this file.
+      $basename = isset($item['basename']) ? $item['basename'] : basename($item['data']);
       if (isset($previous_item[$basename])) {
         // Remove the previous item that shared the same base name.
         unset($css[$previous_item[$basename]]);
@@ -2873,8 +2873,6 @@ function drupal_group_css($css) {
  */
 function drupal_aggregate_css(&$css_groups) {
   $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
-  $directory = file_directory_path('public');
-  $is_writable = is_dir($directory) && is_writable($directory);
 
   // For each group that needs aggregation, aggregate its items.
   foreach ($css_groups as $key => $group) {
@@ -2882,7 +2880,7 @@ function drupal_aggregate_css(&$css_groups) {
       // If a file group can be aggregated into a single file, do so, and set
       // the group's data property to the file path of the aggregate file.
       case 'file':
-        if ($group['preprocess'] && $preprocess_css && $is_writable) {
+        if ($group['preprocess'] && $preprocess_css) {
           // Prefix filename to prevent blocking by firewalls which reject files
           // starting with "ad*".
           $filename = 'css_' . md5(serialize($group['items'])) . '.css';
@@ -2974,7 +2972,7 @@ function drupal_pre_render_styles($elements) {
   // browser-caching. The string changes on every update or full cache
   // flush, forcing browsers to load a new copy of the files, as the
   // URL changed.
-  $query_string = substr(variable_get('css_js_query_string', '0'), 0, 2);
+  $query_string = variable_get('css_js_query_string', '0');
 
   // Defaults for LINK and STYLE elements.
   $link_element_defaults = array(
@@ -3102,15 +3100,16 @@ function drupal_pre_render_styles($elements) {
  * @param $filename
  *   The name of the aggregate CSS file.
  * @return
- *   The name of the CSS file.
+ *   The name of the CSS file, or FALSE if the file could not be saved.
  */
 function drupal_build_css_cache($css, $filename) {
   $data = '';
 
   // Create the css/ within the files folder.
   $csspath = 'public://css';
-  file_prepare_directory($csspath, FILE_CREATE_DIRECTORY);
-  if (!file_exists($csspath . '/' . $filename)) {
+  $uri = $csspath . '/' . $filename;
+
+  if (!file_exists($uri)) {
     // Build aggregate CSS file.
     foreach ($css as $stylesheet) {
       // Only 'file' stylesheets can be aggregated.
@@ -3132,9 +3131,12 @@ function drupal_build_css_cache($css, $filename) {
     $data = implode('', $matches[0]) . $data;
 
     // Create the CSS file.
-    file_unmanaged_save_data($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE);
+    file_prepare_directory($csspath, FILE_CREATE_DIRECTORY);
+    if (!file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
+      return FALSE;
+    }
   }
-  return $csspath . '/' . $filename;
+  return $uri;
 }
 
 /**
@@ -3315,13 +3317,73 @@ function drupal_html_class($class) {
 /**
  * Prepare a string for use as a valid HTML ID and guarantee uniqueness.
  *
+ * This function ensures that each passed HTML ID value only exists once on the
+ * page. By tracking the already returned ids, this function enables forms,
+ * blocks, and other content to be output multiple times on the same page,
+ * without breaking (X)HTML validation.
+ *
+ * For already existing ids, a counter is appended to the id string. Therefore,
+ * JavaScript and CSS code should not rely on any value that was generated by
+ * this function and instead should rely on manually added CSS classes or
+ * similarly reliable constructs.
+ *
+ * Two consecutive hyphens separate the counter from the original id. To manage
+ * uniqueness across multiple AJAX requests on the same page, AJAX requests
+ * POST an array of all IDs currently present on the page, which are used to
+ * prime this function's cache upon first invocation.
+ *
+ * To allow reverse-parsing of ids submitted via AJAX, any multiple consecutive
+ * hyphens in the originally passed $id are replaced with a single hyphen.
+ *
  * @param $id
  *   The ID to clean.
+ *
  * @return
  *   The cleaned ID.
  */
 function drupal_html_id($id) {
-  $seen_ids = &drupal_static(__FUNCTION__, array());
+  // 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);
+
   $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
 
   // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
@@ -3332,14 +3394,15 @@ function drupal_html_id($id) {
   // characters as well.
   $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
 
-  // Ensure IDs are unique. The first occurrence is held but left alone.
-  // Subsequent occurrences get a number appended to them. This incrementing
-  // will almost certainly break code that relies on explicit HTML IDs in forms
-  // that appear more than once on the page, but the alternative is outputting
-  // duplicate IDs, which would break JS code and XHTML validity anyways. For
-  // now, it's an acceptable stopgap solution.
+  // 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];
+    $id = $id . '--' . ++$seen_ids[$id];
   }
   else {
     $seen_ids[$id] = 1;
@@ -3371,33 +3434,28 @@ function drupal_region_class($region) {
 }
 
 /**
- * Add a JavaScript file, setting or inline code to the page.
+ * Adds a JavaScript file, setting, or inline code to the page.
  *
  * The behavior of this function depends on the parameters it is called with.
  * Generally, it handles the addition of JavaScript to the page, either as
  * reference to an existing file or as inline code. The following actions can be
  * performed using this function:
- *
- * - Add a file ('file'):
- *   Adds a reference to a JavaScript file to the page.
- *
- * - Add inline JavaScript code ('inline'):
- *   Executes a piece of JavaScript code on the current page by placing the code
- *   directly in the page. This can, for example, be useful to tell the user that
- *   a new message arrived, by opening a pop up, alert box etc. This should only
- *   be used for JavaScript which cannot be placed and executed from a file.
- *   When adding inline code, make sure that you are not relying on $ being jQuery.
- *   Wrap your code in (function ($) { ... })(jQuery); or use jQuery instead of $.
- *
- * - Add external JavaScript ('external'):
- *   Allows the inclusion of external JavaScript files that are not hosted on the
- *   local server. Note that these external JavaScript references do not get
- *   aggregated when preprocessing is on.
- *
- * - Add settings ('setting'):
- *   Adds a setting to Drupal's global storage of JavaScript settings. Per-page
- *   settings are required by some modules to function properly. All settings
- *   will be accessible at Drupal.settings.
+ * - Add a file ('file'): Adds a reference to a JavaScript file to the page.
+ * - Add inline JavaScript code ('inline'): Executes a piece of JavaScript code
+ *   on the current page by placing the code directly in the page (for example,
+ *   to tell the user that a new message arrived, by opening a pop up, alert
+ *   box, etc.). This should only be used for JavaScript that cannot be executed
+ *   from a file. When adding inline code, make sure that you are not relying on
+ *   $() being the jQuery function.  Wrap your code in
+ *   @code (function ($) {... })(jQuery); @endcode
+ *   or use jQuery() instead of $().
+ * - Add external JavaScript ('external'): Allows the inclusion of external
+ *   JavaScript files that are not hosted on the local server. Note that these
+ *   external JavaScript references do not get aggregated when preprocessing is
+ *   on.
+ * - Add settings ('setting'): Adds settings to Drupal's global storage of
+ *   JavaScript settings. Per-page settings are required by some modules to
+ *   function properly. All settings will be accessible at Drupal.settings.
  *
  * Examples:
  * @code
@@ -3420,58 +3478,49 @@ function drupal_region_class($region) {
  *   - 'external': The absolute path to an external JavaScript file that is not
  *     hosted on the local server. These files will not be aggregated if
  *     JavaScript aggregation is enabled.
- *   - 'setting': An array with configuration options as associative array. The
- *       array is directly placed in Drupal.settings. All modules should wrap
- *       their actual configuration settings in another variable to prevent
- *       the pollution of the Drupal.settings namespace.
+ *   - 'setting': An associative array with configuration options. The array is
+ *     directly placed in Drupal.settings. All modules should wrap their actual
+ *     configuration settings in another variable to prevent conflicts in the
+ *     Drupal.settings namespace.
  * @param $options
- *   (optional) A string defining the type of JavaScript that is being added
- *   in the $data parameter ('file'/'setting'/'inline'), or an array which
- *   can have any or all of the following keys. JavaScript settings should
- *   always pass the string 'setting' only.
- *   - type
- *       The type of JavaScript that is to be added to the page. Allowed
- *       values are 'file', 'inline', 'external' or 'setting'. Defaults
- *       to 'file'.
- *   - scope
- *       The location in which you want to place the script. Possible values
- *       are 'header' or 'footer'. If your theme implements different regions,
- *       however, you can also use these. Defaults to 'header'.
- *   - weight
- *       A number defining the order in which the JavaScript is added to the
- *       page. In some cases, the order in which the JavaScript is presented
- *       on the page is very important. jQuery, for example, must be added to
- *       to the page before any jQuery code is run, so jquery.js uses a weight
- *       of JS_LIBRARY - 2, drupal.js uses a weight of JS_LIBRARY - 1, and all
- *       following scripts depending on jQuery and Drupal behaviors are simply
- *       added using the default weight of JS_DEFAULT.
- *
- *       Available constants are:
- *       - JS_LIBRARY: Any libraries, settings, or jQuery plugins.
- *       - JS_DEFAULT: Any module-layer JavaScript.
- *       - JS_THEME: Any theme-layer JavaScript.
- *
- *       If you need to invoke a JavaScript file before any other module's
- *       JavaScript, for example, you would use JS_DEFAULT - 1.
- *       Note that inline JavaScripts are simply appended to the end of the
- *       specified scope (region), so they always come last.
- *   - defer
- *       If set to TRUE, the defer attribute is set on the &lt;script&gt; tag.
- *       Defaults to FALSE.
- *   - cache
- *       If set to FALSE, the JavaScript file is loaded anew on every page
- *       call, that means, it is not cached. Used only when 'type' references
- *       a JavaScript file. Defaults to TRUE.
- *   - preprocess
- *       Aggregate the JavaScript if the JavaScript optimization setting has
- *       been toggled in admin/config/development/performance. Note that
- *       JavaScript of type 'external' is not aggregated. Defaults to TRUE.
- *   - version
- *       If not empty, the version is added as a query string instead of the
- *       incremental query string that changes on cache clearing. Primarily
- *       used for libraries.
+ *   (optional) A string defining the type of JavaScript that is being added in
+ *   the $data parameter ('file'/'setting'/'inline'/'external'), or an
+ *   associative array. JavaScript settings should always pass the string
+ *   'setting' only. Other types can have the following elements in the array:
+ *   - type: The type of JavaScript that is to be added to the page. Allowed
+ *     values are 'file', 'inline', 'external' or 'setting'. Defaults
+ *     to 'file'.
+ *   - scope: The location in which you want to place the script. Possible
+ *     values are 'header' or 'footer'. If your theme implements different
+ *     regions, you can also use these. Defaults to 'header'.
+ *   - weight: A number defining the order in which the JavaScript is added to
+ *     the page. In some cases, the order in which the JavaScript is presented
+ *     on the page is very important. jQuery, for example, must be added to
+ *     the page before any jQuery code is run, so jquery.js uses a weight of
+ *     JS_LIBRARY - 20, jquery.once.js (a library drupal.js depends on) uses
+ *     a weight of JS_LIBRARY - 19, drupal.js uses a weight of JS_LIBRARY - 1,
+ *     and all following scripts depending on jQuery and Drupal behaviors are
+ *     simply added using the default weight of JS_DEFAULT. Available constants
+ *     are:
+ *     - JS_LIBRARY: Any libraries, settings, or jQuery plugins.
+ *     - JS_DEFAULT: Any module-layer JavaScript.
+ *     - JS_THEME: Any theme-layer JavaScript.
+ *     If you need to invoke a JavaScript file before any other module's
+ *     JavaScript, for example, you would use JS_DEFAULT - 1.
+ *   - defer: If set to TRUE, the defer attribute is set on the &lt;script&gt;
+ *     tag.  Defaults to FALSE.
+ *   - cache: If set to FALSE, the JavaScript file is loaded anew on every page
+ *     call; in other words, it is not cached. Used only when 'type' references
+ *     a JavaScript file. Defaults to TRUE.
+ *   - preprocess: Aggregate the JavaScript if the JavaScript optimization
+ *     setting has been toggled in admin/config/development/performance. Note
+ *     that JavaScript of type 'external' is not aggregated. Defaults to TRUE.
+ *
  * @return
- *   The constructed array of JavaScript files.
+ *   The current array of JavaScript files, settings, and in-line code,
+ *   including Drupal defaults, anything previously added with calls to
+ *   drupal_add_js(), and this function call's additions.
+ *
  * @see drupal_get_js()
  */
 function drupal_add_js($data = NULL, $options = NULL) {
@@ -3617,8 +3666,6 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
   $processed = array();
   $files = array();
   $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
-  $directory = file_directory_path('public');
-  $is_writable = is_dir($directory) && is_writable($directory);
 
   // A dummy query-string is added to filenames, to gain control over
   // browser-caching. The string changes on every update or full cache
@@ -3626,7 +3673,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
   // URL changed. Files that should not be cached (see drupal_add_js())
   // get REQUEST_TIME as query-string instead, to enforce reload on every
   // page request.
-  $default_query_string = substr(variable_get('css_js_query_string', '0'), 0, 2);
+  $default_query_string = variable_get('css_js_query_string', '0');
 
   // For inline Javascript to validate as XHTML, all Javascript containing
   // XHTML needs to be wrapped in CDATA. To make that backwards compatible
@@ -3669,12 +3716,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
         $js_element['#value_prefix'] = $embed_prefix;
         $js_element['#value'] = $item['data'];
         $js_element['#value_suffix'] = $embed_suffix;
-        $output .= theme('html_tag', array('element' => $js_element));
+        $processed[$index++] = theme('html_tag', array('element' => $js_element));
         break;
 
       case 'file':
         $js_element = $element;
-        if (!$item['preprocess'] || !$is_writable || !$preprocess_js) {
+        if (!$item['preprocess'] || !$preprocess_js) {
           if ($item['defer']) {
             $js_element['#attributes']['defer'] = 'defer';
           }
@@ -3704,15 +3751,20 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
   }
 
   // Aggregate any remaining JS files that haven't already been output.
-  if ($is_writable && $preprocess_js && count($files) > 0) {
+  if ($preprocess_js && count($files) > 0) {
     // Prefix filename to prevent blocking by firewalls which reject files
     // starting with "ad*".
     foreach ($files as $key => $file_set) {
       $filename = 'js_' . md5(serialize($file_set)) . '.js';
-      $preprocess_file = file_create_url(drupal_build_js_cache($file_set, $filename)) . '?' . $default_query_string;
-      $js_element = $element;
-      $js_element['#attributes']['src'] = $preprocess_file;
-      $processed[$key] = theme('html_tag', array('element' => $js_element));
+      $uri = drupal_build_js_cache($file_set, $filename);
+      // Only include the file if was written successfully. Errors are logged
+      // using watchdog.
+      if ($uri) {
+        $preprocess_file = file_create_url($uri) . '?' . $default_query_string;
+        $js_element = $element;
+        $js_element['#attributes']['src'] = $preprocess_file;
+        $processed[$key] = theme('html_tag', array('element' => $js_element));
+      }
     }
   }
 
@@ -3755,14 +3807,15 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
  *   When TRUE, will exit if a given library's dependencies are missing. When
  *   set to FALSE, will continue to add the libraries, even though one of the
  *   dependencies are missing. Defaults to FALSE.
+ *
  * @return
  *   Will return FALSE if there were any missing library dependencies. TRUE will
  *   be returned if all library dependencies were met.
  *
- * @see drupal_add_library().
- * @see drupal_add_js().
- * @see drupal_add_css().
- * @see drupal_render().
+ * @see drupal_add_library()
+ * @see drupal_add_js()
+ * @see drupal_add_css()
+ * @see drupal_render()
  */
 function drupal_process_attached($elements, $weight = JS_DEFAULT, $dependency_check = FALSE) {
   // Add defaults to the special attached structures that should be processed differently.
@@ -4111,16 +4164,16 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro
  * @param $filename
  *   The name of the aggregate JS file.
  * @return
- *   The name of the JS file.
+ *   The name of the JS file, or FALSE if the file could not be saved.
  */
 function drupal_build_js_cache($files, $filename) {
   $contents = '';
 
   // Create the js/ within the files folder.
   $jspath = 'public://js';
-  file_prepare_directory($jspath, FILE_CREATE_DIRECTORY);
+  $uri = $jspath . '/' . $filename;
 
-  if (!file_exists($jspath . '/' . $filename)) {
+  if (!file_exists($uri)) {
     // Build aggregate JS file.
     foreach ($files as $path => $info) {
       if ($info['preprocess']) {
@@ -4130,10 +4183,12 @@ function drupal_build_js_cache($files, $filename) {
     }
 
     // Create the JS file.
-    file_unmanaged_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE);
+    file_prepare_directory($jspath, FILE_CREATE_DIRECTORY);
+    if (!file_unmanaged_save_data($contents, $uri, FILE_EXISTS_REPLACE)) {
+      return FALSE;
+    }
   }
-
-  return $jspath . '/' . $filename;
+  return $uri;
 }
 
 /**
@@ -4539,86 +4594,6 @@ function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1)
   return $files;
 }
 
-/**
- * Hands off alterable variables to type-specific *_alter implementations.
- *
- * This dispatch function hands off the passed in variables to type-specific
- * hook_TYPE_alter() implementations in modules. It ensures a consistent
- * interface for all altering operations.
- *
- * A maximum of 2 alterable arguments is supported. In case more arguments need
- * to be passed and alterable, modules provide additional variables assigned by
- * reference in the last $context argument:
- * @code
- *   $context = array(
- *     'alterable' => &$alterable,
- *     'unalterable' => $unalterable,
- *     'foo' => 'bar',
- *   );
- *   drupal_alter('mymodule_data', $alterable1, $alterable2, $context);
- * @endcode
- *
- * Note that objects are always passed by reference in PHP5. If it is absolutely
- * required that no implementation alters a passed object in $context, then an
- * object needs to be cloned:
- * @code
- *   $context = array(
- *     'unalterable_object' => clone $object,
- *   );
- *   drupal_alter('mymodule_data', $data, $context);
- * @endcode
- *
- * @param $type
- *   A string describing the data type of the alterable $data. 'form', 'links',
- *   'node_content', and so on are several examples.
- * @param &$data
- *   The primary data to be altered.
- * @param &$context1
- *   (optional) An additional variable that is passed by reference.
- * @param &$context2
- *   (optional) An additional variable that is passed by reference. If more
- *   context needs to be provided to implementations, then this should be an
- *   keyed array as described above.
- */
-function drupal_alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
-  // Use the advanced drupal_static() pattern, since this is called very often.
-  static $drupal_static_fast;
-  if (!isset($drupal_static_fast)) {
-    $drupal_static_fast['functions'] = &drupal_static(__FUNCTION__);
-  }
-  $functions = &$drupal_static_fast['functions'];
-
-  // Some alter hooks are invoked many times per page request, so statically
-  // cache the list of functions to call, and on subsequent calls, iterate
-  // through them quickly.
-  if (!isset($functions[$type])) {
-    $functions[$type] = array();
-    $hook = $type . '_alter';
-    foreach (module_implements($hook) as $module) {
-      $functions[$type][] = $module . '_' . $hook;
-    }
-    // Allow the theme to alter variables after the theme system has been
-    // initialized.
-    global $theme, $base_theme_info;
-    if (isset($theme)) {
-      $theme_keys = array();
-      foreach ($base_theme_info as $base) {
-        $theme_keys[] = $base->name;
-      }
-      $theme_keys[] = $theme;
-      foreach ($theme_keys as $theme_key) {
-        $function = $theme_key . '_' . $hook;
-        if (function_exists($function)) {
-          $functions[$type][] = $function;
-        }
-      }
-    }
-  }
-  foreach ($functions[$type] as $function) {
-    $function($data, $context1, $context2);
-  }
-}
-
 /**
  * Set the main page content value for later use.
  *
@@ -4776,7 +4751,7 @@ function drupal_pre_render_markup($elements) {
  *   - #show_messages: Suppress drupal_get_message() items. Used by Batch API (optional).
  *
  * @see hook_page_alter()
- * @see element_info('page')
+ * @see element_info()
  */
 function drupal_render_page($page) {
   $main_content_display = &drupal_static('system_main_content_added', FALSE);
@@ -4842,8 +4817,8 @@ function drupal_render_page($page) {
  * improve performance. To use drupal_render() caching, set the element's #cache
  * property to an associative array with one or several of the following keys:
  *    - 'keys': An array of one or more keys that identify the element. If 'keys'
- *       is set, the cache ID is created automatically from these keys.
- *       @see drupal_render_cid_create()
+ *       is set, the cache ID is created automatically from these keys. See
+ *       drupal_render_cid_create().
  *    - 'granularity' (optional): Define the cache granularity using binary
  *       combinations of the cache granularity constants, e.g. DRUPAL_CACHE_PER_USER
  *       to cache for each user separately or
@@ -4861,6 +4836,13 @@ function drupal_render_page($page) {
  * using uasort(). Since this is expensive, when passing already sorted
  * elements to drupal_render(), for example from a database query, set
  * $elements['#sorted'] = TRUE to avoid sorting them a second time.
+ * 
+ * drupal_render() flags each element with a '#printed' status to indicate that
+ * the element has been rendered, which allows individual elements of a given
+ * array to be rendered independently and prevents them from being rendered
+ * more than once on subsequent calls to drupal_render() (e.g., as part of a
+ * larger array). If the same array or array element is passed more than once
+ * to drupal_render(), it simply returns a NULL value.
  *
  * @param $elements
  *   The structured array describing the data to be rendered.
@@ -5242,7 +5224,12 @@ function element_sort($a, $b) {
  * Retrieve the default properties for the defined element type.
  */
 function element_info($type) {
-  $cache = &drupal_static(__FUNCTION__);
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
+  }
+  $cache = &$drupal_static_fast['cache'];
 
   if (!isset($cache)) {
     $cache = module_invoke_all('element_info');
@@ -6074,16 +6061,7 @@ function drupal_flush_all_caches() {
   registry_rebuild();
   drupal_clear_css_cache();
   drupal_clear_js_cache();
-
-  // If invoked from update.php, we must not update the theme information in the
-  // database, or this will result in all themes being disabled.
-  if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
-    _system_rebuild_theme_data();
-  }
-  else {
-    system_rebuild_theme_data();
-  }
-
+  system_rebuild_theme_data();
   drupal_theme_rebuild();
   menu_rebuild();
   node_types_rebuild();
@@ -6099,21 +6077,12 @@ function drupal_flush_all_caches() {
 /**
  * Helper function to change query-strings on css/js files.
  *
- * Changes the character added to all css/js files as dummy query-string,
- * so that all browsers are forced to reload fresh files. We keep
- * 20 characters history (FIFO) to avoid repeats, but only the first two
- * (newest) characters are actually used on urls, to keep them short.
- * This is also called from update.php.
+ * Changes the character added to all css/js files as dummy query-string, so
+ * that all browsers are forced to reload fresh files.
  */
 function _drupal_flush_css_js() {
-  $string_history = variable_get('css_js_query_string', '00000000000000000000');
-  $new_character = $string_history[0];
-  // Not including 'q' to allow certain JavaScripts to re-use query string.
-  $characters = 'abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
-  while (strpos($string_history, $new_character) !== FALSE) {
-    $new_character = $characters[mt_rand(0, strlen($characters) - 1)];
-  }
-  variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19));
+  // The timestamp is converted to base 36 in order to make it more compact.
+  variable_set('css_js_query_string', base_convert(REQUEST_TIME, 10, 36));
 }
 
 /**
@@ -6253,20 +6222,20 @@ function entity_get_info($entity_type = NULL) {
           'fieldable' => FALSE,
           'controller class' => 'DrupalDefaultEntityController',
           'static cache' => TRUE,
+          'field cache' => TRUE,
           'load hook' => $name . '_load',
           'bundles' => array(),
           'view modes' => array(),
-          'object keys' => array(),
-          'cacheable' => TRUE,
+          'entity keys' => array(),
           'translation' => array(),
         );
-        $entity_info[$name]['object keys'] += array(
+        $entity_info[$name]['entity keys'] += array(
           'revision' => '',
           'bundle' => '',
         );
         // If no bundle key is provided, assume a single bundle, named after
         // the entity type.
-        if (empty($entity_info[$name]['object keys']['bundle']) && empty($entity_info[$name]['bundles'])) {
+        if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) {
           $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label']));
         }
       }
@@ -6306,18 +6275,16 @@ function entity_info_cache_clear() {
  *   0: primary id of the entity
  *   1: revision id of the entity, or NULL if $entity_type is not versioned
  *   2: bundle name of the entity
- *   3: whether $entity_type's fields should be cached (TRUE/FALSE)
  */
 function entity_extract_ids($entity_type, $entity) {
   $info = entity_get_info($entity_type);
   // Objects being created might not have id/vid yet.
-  $id = isset($entity->{$info['object keys']['id']}) ? $entity->{$info['object keys']['id']} : NULL;
-  $vid = ($info['object keys']['revision'] && isset($entity->{$info['object keys']['revision']})) ? $entity->{$info['object keys']['revision']} : NULL;
+  $id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL;
+  $vid = ($info['entity keys']['revision'] && isset($entity->{$info['entity keys']['revision']})) ? $entity->{$info['entity keys']['revision']} : NULL;
   // If no bundle key provided, then we assume a single bundle, named after the
   // entity type.
-  $bundle = $info['object keys']['bundle'] ? $entity->{$info['object keys']['bundle']} : $entity_type;
-  $cacheable = $info['cacheable'];
-  return array($id, $vid, $bundle, $cacheable);
+  $bundle = $info['entity keys']['bundle'] ? $entity->{$info['entity keys']['bundle']} : $entity_type;
+  return array($id, $vid, $bundle);
 }
 
 /**
@@ -6339,12 +6306,12 @@ function entity_extract_ids($entity_type, $entity) {
 function entity_create_stub_entity($entity_type, $ids) {
   $entity = new stdClass();
   $info = entity_get_info($entity_type);
-  $entity->{$info['object keys']['id']} = $ids[0];
-  if (isset($info['object keys']['revision']) && !is_null($ids[1])) {
-    $entity->{$info['object keys']['revision']} = $ids[1];
+  $entity->{$info['entity keys']['id']} = $ids[0];
+  if (isset($info['entity keys']['revision']) && !is_null($ids[1])) {
+    $entity->{$info['entity keys']['revision']} = $ids[1];
   }
-  if ($info['object keys']['bundle']) {
-    $entity->{$info['object keys']['bundle']} = $ids[2];
+  if ($info['entity keys']['bundle']) {
+    $entity->{$info['entity keys']['bundle']} = $ids[2];
   }
   return $entity;
 }
@@ -6453,11 +6420,42 @@ function entity_prepare_view($entity_type, $entities) {
  *   uri of its own.
  */
 function entity_uri($entity_type, $entity) {
-  $info = entity_get_info($entity_type);
-  if (isset($info['uri callback']) && function_exists($info['uri callback'])) {
-    return $info['uri callback']($entity) + array('options' => array());
+  // This check enables the URI of an entity to be easily overridden from what
+  // the callback for the entity type or bundle would return, and it helps
+  // minimize performance overhead when entity_uri() is called multiple times
+  // for the same entity.
+  if (!isset($entity->uri)) {
+    $info = entity_get_info($entity_type);
+    list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+    // A bundle-specific callback takes precedence over the generic one for the
+    // entity type.
+    if (isset($info['bundles'][$bundle]['uri callback'])) {
+      $uri_callback = $info['bundles'][$bundle]['uri callback'];
+    }
+    elseif (isset($info['uri callback'])) {
+      $uri_callback = $info['uri callback'];
+    }
+    else {
+      $uri_callback = NULL;
+    }
+
+    // Invoke the callback to get the URI. If there is no callback, set the
+    // entity's 'uri' property to FALSE to indicate that it is known to not have
+    // a URI.
+    if (isset($uri_callback) && function_exists($uri_callback)) {
+      $entity->uri = $uri_callback($entity);
+      if (!isset($entity->uri['options'])) {
+        $entity->uri['options'] = array();
+      }
+    }
+    else {
+      $entity->uri = FALSE;
+    }
   }
+  return $entity->uri ? $entity->uri : NULL;
 }
+
 /**
  * Invokes entity insert/update hooks.
  *
diff --git a/includes/database/database.inc b/includes/database/database.inc
index 9d8eff70b4d43fdd1b52625f4df166e62c1f0cc9..5c771b18dce8c2ef76334e2fc9e22f4df86ee3e6 100644
--- a/includes/database/database.inc
+++ b/includes/database/database.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: database.inc,v 1.103 2010/03/20 15:06:51 dries Exp $
+// $Id: database.inc,v 1.119 2010/04/26 14:12:59 dries Exp $
 
 /**
  * @file
@@ -48,7 +48,6 @@
  * database servers, so that is abstracted into db_query_range() arguments.
  * Finally, note the PDO-based ability to foreach() over the result set.
  *
- *
  * All queries are passed as a prepared statement string. A
  * prepared statement is a "template" of a query that omits literal or variable
  * values in favor of placeholders. The values to place into those
@@ -98,7 +97,6 @@
  * considerably more secure against SQL injection than trying to remember
  * which values need quotation marks and string escaping and which don't.
  *
- *
  * INSERT, UPDATE, and DELETE queries need special care in order to behave
  * consistently across all different databases. Therefore, they use a special
  * object-oriented API for defining a query structurally. For example, rather
@@ -115,7 +113,6 @@
  * while also allowing optimizations such as multi-insert queries. UPDATE and
  * DELETE queries have a similar pattern.
  *
- *
  * Drupal also supports transactions, including a transparent fallback for
  * databases that do not support transactions. To start a new transaction,
  * simply call $txn = db_transaction(); in your own code. The transaction will
@@ -126,7 +123,6 @@
  * scope, that is, all relevant queries completed successfully.
  *
  * Example:
- *
  * @code
  * function my_transaction_function() {
  *   // The transaction opens here.
@@ -166,7 +162,6 @@
  *   }
  * }
  * @endcode
- *
  */
 
 
@@ -205,16 +200,9 @@ abstract class DatabaseConnection extends PDO {
    * nested calls to transactions and collapse them into a single
    * transaction.
    *
-   * @var int
-   */
-  protected $transactionLayers = 0;
-
-  /**
-   * Whether or not the active transaction (if any) will be rolled back.
-   *
-   * @var boolean
+   * @var array
    */
-  protected $willRollback;
+  protected $transactionLayers = array();
 
   /**
    * Array of argument arrays for logging post-rollback.
@@ -224,56 +212,11 @@ abstract class DatabaseConnection extends PDO {
   protected $rollbackLogs = array();
 
   /**
-   * The name of the Select class for this connection.
-   *
-   * Normally this and the following class names would be static variables,
-   * but statics in methods are still global and shared by all instances.
+   * Index of what driver-specific class to use for various operations.
    *
-   * @var string
-   */
-  protected $selectClass = NULL;
-
-  /**
-   * The name of the Delete class for this connection.
-   *
-   * @var string
-   */
-  protected $deleteClass = NULL;
-
-  /**
-   * The name of the Truncate class for this connection.
-   *
-   * @var string
-   */
-  protected $truncateClass = NULL;
-
-  /**
-   * The name of the Insert class for this connection.
-   *
-   * @var string
-   */
-  protected $insertClass = NULL;
-
-  /**
-   * The name of the Merge class for this connection.
-   *
-   * @var string
-   */
-  protected $mergeClass = NULL;
-
-  /**
-   * The name of the Update class for this connection.
-   *
-   * @var string
-   */
-  protected $updateClass = NULL;
-
-  /**
-   * The name of the Transaction class for this connection.
-   *
-   * @var string
+   * @var array
    */
-  protected $transactionClass = NULL;
+  protected $driverClasses = array();
 
   /**
    * The name of the Statement class for this connection.
@@ -336,47 +279,39 @@ abstract class DatabaseConnection extends PDO {
    * Returns the default query options for any given query.
    *
    * A given query can be customized with a number of option flags in an
-   * associative array.
-   *
-   *   target - The database "target" against which to execute a query. Valid
+   * associative array:
+   * - target: The database "target" against which to execute a query. Valid
    *   values are "default" or "slave". The system will first try to open a
    *   connection to a database specified with the user-supplied key. If one
    *   is not available, it will silently fall back to the "default" target.
    *   If multiple databases connections are specified with the same target,
    *   one will be selected at random for the duration of the request.
-   *
-   *   fetch - This element controls how rows from a result set will be
+   * - fetch: This element controls how rows from a result set will be
    *   returned. Legal values include PDO::FETCH_ASSOC, PDO::FETCH_BOTH,
    *   PDO::FETCH_OBJ, PDO::FETCH_NUM, or a string representing the name of a
    *   class. If a string is specified, each record will be fetched into a new
    *   object of that class. The behavior of all other values is defined by PDO.
    *   See http://www.php.net/PDOStatement-fetch
-   *
-   *   return - Depending on the type of query, different return values may be
+   * - return: Depending on the type of query, different return values may be
    *   meaningful. This directive instructs the system which type of return
    *   value is desired. The system will generally set the correct value
    *   automatically, so it is extremely rare that a module developer will ever
    *   need to specify this value. Setting it incorrectly will likely lead to
    *   unpredictable results or fatal errors. Legal values include:
-   *
-   *     Database::RETURN_STATEMENT - Return the prepared statement object for
+   *   - Database::RETURN_STATEMENT: Return the prepared statement object for
    *     the query. This is usually only meaningful for SELECT queries, where
    *     the statement object is how one accesses the result set returned by the
    *     query.
-   *
-   *     Database::RETURN_AFFECTED - Return the number of rows affected by an
+   *   - Database::RETURN_AFFECTED: Return the number of rows affected by an
    *     UPDATE or DELETE query. Be aware that means the number of rows actually
    *     changed, not the number of rows matched by the WHERE clause.
-   *
-   *     Database::RETURN_INSERT_ID - Return the sequence ID (primary key)
+   *   - Database::RETURN_INSERT_ID: Return the sequence ID (primary key)
    *     created by an INSERT statement on a table that contains a serial
    *     column.
-   *
-   *     Database::RETURN_NULL - Do not return anything, as there is no
+   *   - Database::RETURN_NULL: Do not return anything, as there is no
    *     meaningful value to return. That is the case for INSERT queries on
    *     tables that do not contain a serial column.
-   *
-   *   throw_exception - By default, the database system will catch any errors
+   * - throw_exception: By default, the database system will catch any errors
    *   on a query as an Exception, log it, and then rethrow it so that code
    *   further up the call chain can take an appropriate action. To suppress
    *   that behavior and simply return NULL on failure, set this option to
@@ -448,6 +383,26 @@ abstract class DatabaseConnection extends PDO {
     }
   }
 
+  /**
+   * Find the prefix for a table.
+   *
+   * This function is for when you want to know the prefix of a table. This
+   * is not used in prefixTables due to performance reasons.
+   */
+  public function tablePrefix($table = 'default') {
+    global $db_prefix;
+    if (is_array($db_prefix)) {
+      if (isset($db_prefix[$table])) {
+        return $db_prefix[$table];
+      }
+      elseif (isset($db_prefix['default'])) {
+        return $db_prefix['default'];
+      }
+      return '';
+    }
+    return $db_prefix;
+  }
+
   /**
    * Prepares a query string and returns the prepared statement.
    *
@@ -548,7 +503,6 @@ abstract class DatabaseConnection extends PDO {
    *   DatabaseStatementInterface may also be passed in order to allow calling
    *   code to manually bind variables to a query. If a
    *   DatabaseStatementInterface is passed, the $args array will be ignored.
-   *
    *   It is extremely rare that module code will need to pass a statement
    *   object to this method. It is used primarily for database drivers for
    *   databases that require special LOB field handling.
@@ -638,41 +592,56 @@ abstract class DatabaseConnection extends PDO {
   protected function expandArguments(&$query, &$args) {
     $modified = FALSE;
 
-    foreach ($args as $key => $data) {
-      // If the placeholder value to insert is an array, assume that we need
-      // to expand it out into a comma-delimited set of placeholders.
-      if (is_array($data)) {
-        $new_keys = array();
-        foreach ($data as $i => $value) {
-          // This assumes that there are no other placeholders that use the same
-          // name.  For example, if the array placeholder is defined as :example
-          // and there is already an :example_2 placeholder, this will generate
-          // a duplicate key.  We do not account for that as the calling code
-          // is already broken if that happens.
-          $new_keys[$key . '_' . $i] = $value;
-        }
-
-        // Update the query with the new placeholders.
-        // preg_replace is a little bit slower than str_replace, but it is
-        // necessary to ensure the replacement does not affect placeholders
-        // that start with the same exact text. For example, if the query
-        // contains the placeholders :foo and :foobar, and :foo has an array
-        // of values, using str_replace would affect both placeholders, but
-        // using the following preg_replace would only affect :foo because it
-        // is followed by a non-word character.
-        $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
-
-        // Update the args array with the new placeholders.
-        unset($args[$key]);
-        $args += $new_keys;
-
-        $modified = TRUE;
+    // If the placeholder value to insert is an array, assume that we need
+    // to expand it out into a comma-delimited set of placeholders.
+    foreach (array_filter($args, 'is_array') as $key => $data) {
+      $new_keys = array();
+      foreach ($data as $i => $value) {
+        // This assumes that there are no other placeholders that use the same
+        // name.  For example, if the array placeholder is defined as :example
+        // and there is already an :example_2 placeholder, this will generate
+        // a duplicate key.  We do not account for that as the calling code
+        // is already broken if that happens.
+        $new_keys[$key . '_' . $i] = $value;
       }
+
+      // Update the query with the new placeholders.
+      // preg_replace is necessary to ensure the replacement does not affect
+      // placeholders that start with the same exact text. For example, if the
+      // query contains the placeholders :foo and :foobar, and :foo has an
+      // array of values, using str_replace would affect both placeholders,
+      // but using the following preg_replace would only affect :foo because
+      // it is followed by a non-word character.
+      $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
+
+      // Update the args array with the new placeholders.
+      unset($args[$key]);
+      $args += $new_keys;
+
+      $modified = TRUE;
     }
 
     return $modified;
   }
 
+  /**
+   * Gets the driver-specific override class if any for the specified class.
+   *
+   * @param string $class
+   *   The class for which we want the potentially driver-specific class.
+   * @return string
+   *   The name of the class that should be used for this driver.
+   */
+  protected function getDriverClass($class) {
+    if (empty($this->driverClasses[$class])) {
+      $this->driverClasses[$class] = $class . '_' . $this->driver();
+      if (!class_exists($this->driverClasses[$class])) {
+        $this->driverClasses[$class] = $class;
+      }
+    }
+    return $this->driverClasses[$class];
+  }
+
   /**
    * Prepares and returns a SELECT query object with the specified ID.
    *
@@ -693,17 +662,7 @@ abstract class DatabaseConnection extends PDO {
    * @see SelectQuery
    */
   public function select($table, $alias = NULL, array $options = array()) {
-    if (empty($this->selectClass)) {
-      $this->selectClass = 'SelectQuery_' . $this->driver();
-      if (!class_exists($this->selectClass)) {
-        $this->selectClass = 'SelectQuery';
-      }
-    }
-    $class = $this->selectClass;
-    // new is documented as the highest precedence operator so this will
-    // create a class named $class and pass the arguments into the constructor,
-    // instead of calling a function named $class with the arguments listed and
-    // then creating using the return value as the class name.
+    $class = $this->getDriverClass('SelectQuery');
     return new $class($table, $alias, $this, $options);
   }
 
@@ -719,13 +678,7 @@ abstract class DatabaseConnection extends PDO {
    * @see InsertQuery
    */
   public function insert($table, array $options = array()) {
-    if (empty($this->insertClass)) {
-      $this->insertClass = 'InsertQuery_' . $this->driver();
-      if (!class_exists($this->insertClass)) {
-        $this->insertClass = 'InsertQuery';
-      }
-    }
-    $class = $this->insertClass;
+    $class = $this->getDriverClass('InsertQuery');
     return new $class($this, $table, $options);
   }
 
@@ -741,13 +694,7 @@ abstract class DatabaseConnection extends PDO {
    * @see MergeQuery
    */
   public function merge($table, array $options = array()) {
-    if (empty($this->mergeClass)) {
-      $this->mergeClass = 'MergeQuery_' . $this->driver();
-      if (!class_exists($this->mergeClass)) {
-        $this->mergeClass = 'MergeQuery';
-      }
-    }
-    $class = $this->mergeClass;
+    $class = $this->getDriverClass('MergeQuery');
     return new $class($this, $table, $options);
   }
 
@@ -764,13 +711,7 @@ abstract class DatabaseConnection extends PDO {
    * @see UpdateQuery
    */
   public function update($table, array $options = array()) {
-    if (empty($this->updateClass)) {
-      $this->updateClass = 'UpdateQuery_' . $this->driver();
-      if (!class_exists($this->updateClass)) {
-        $this->updateClass = 'UpdateQuery';
-      }
-    }
-    $class = $this->updateClass;
+    $class = $this->getDriverClass('UpdateQuery');
     return new $class($this, $table, $options);
   }
 
@@ -786,13 +727,7 @@ abstract class DatabaseConnection extends PDO {
    * @see DeleteQuery
    */
   public function delete($table, array $options = array()) {
-    if (empty($this->deleteClass)) {
-      $this->deleteClass = 'DeleteQuery_' . $this->driver();
-      if (!class_exists($this->deleteClass)) {
-        $this->deleteClass = 'DeleteQuery';
-      }
-    }
-    $class = $this->deleteClass;
+    $class = $this->getDriverClass('DeleteQuery');
     return new $class($this, $table, $options);
   }
 
@@ -808,19 +743,12 @@ abstract class DatabaseConnection extends PDO {
    * @see TruncateQuery
    */
   public function truncate($table, array $options = array()) {
-    if (empty($this->truncateClass)) {
-      $this->truncateClass = 'TruncateQuery_' . $this->driver();
-      if (!class_exists($this->truncateClass)) {
-        $this->truncateClass = 'TruncateQuery';
-      }
-    }
-    $class = $this->truncateClass;
+    $class = $this->getDriverClass('TruncateQuery');
     return new $class($this, $table, $options);
   }
 
   /**
-   * Returns a DatabaseSchema object for manipulating the schema of this
-   * database.
+   * Returns a DatabaseSchema object for manipulating the schema.
    *
    * This method will lazy-load the appropriate schema library file.
    *
@@ -829,8 +757,10 @@ abstract class DatabaseConnection extends PDO {
    */
   public function schema() {
     if (empty($this->schema)) {
-      $class_type = 'DatabaseSchema_' . $this->driver();
-      $this->schema = new $class_type($this);
+      $class = $this->getDriverClass('DatabaseSchema');
+      if (class_exists($class)) {
+        $this->schema = new $class($this);
+      }
     }
     return $this->schema;
   }
@@ -846,7 +776,7 @@ abstract class DatabaseConnection extends PDO {
    *   The sanitized table name string.
    */
   public function escapeTable($table) {
-    return preg_replace('/[^A-Za-z0-9_]+/', '', $table);
+    return preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
   }
 
   /**
@@ -885,29 +815,37 @@ abstract class DatabaseConnection extends PDO {
    *   TRUE if we're currently in a transaction, FALSE otherwise.
    */
   public function inTransaction() {
-    return ($this->transactionLayers > 0);
+    return ($this->transactionDepth() > 0);
+  }
+
+  /**
+   * Determines current transaction depth.
+   */
+  public function transactionDepth() {
+    return count($this->transactionLayers);
   }
 
   /**
    * Returns a new DatabaseTransaction object on this connection.
    *
+   * @param $name
+   *   Optional name of the savepoint.
+   *
    * @see DatabaseTransaction
    */
-  public function startTransaction() {
-    if (empty($this->transactionClass)) {
-      $this->transactionClass = 'DatabaseTransaction_' . $this->driver();
-      if (!class_exists($this->transactionClass)) {
-        $this->transactionClass = 'DatabaseTransaction';
-      }
-    }
-    return new $this->transactionClass($this);
+  public function startTransaction($name = '') {
+    $class = $this->getDriverClass('DatabaseTransaction');
+    return new $class($this, $name);
   }
 
   /**
-   * Schedules the current transaction for rollback.
+   * Rolls back the transaction entirely or to a named savepoint.
    *
    * This method throws an exception if no transaction is active.
    *
+   * @param $savepoint_name
+   *   The name of the savepoint. The default, 'drupal_transaction', will roll
+   *   the entire transaction back.
    * @param $type
    *   The category to which the rollback message belongs.
    * @param $message
@@ -927,9 +865,14 @@ abstract class DatabaseConnection extends PDO {
    * @see DatabaseTransaction::rollback()
    * @see watchdog()
    */
-  public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
-    if ($this->transactionLayers == 0) {
-      throw new NoActiveTransactionException();
+  public function rollback($savepoint_name = 'drupal_transaction', $type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+    // A previous rollback to an earlier savepoint may mean that the savepoint
+    // in question has already been rolled back.
+    if (!in_array($savepoint_name, $this->transactionLayers)) {
+      return;
     }
 
     // Set the severity to the configured default if not specified.
@@ -955,25 +898,54 @@ abstract class DatabaseConnection extends PDO {
       );
     }
 
-    $this->willRollback = TRUE;
+    // We need to find the point we're rolling back to, all other savepoints
+    // before are no longer needed.
+    while ($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint == $savepoint_name) {
+        // If it is the last the transaction in the stack, then it is not a
+        // savepoint, it is the transaction itself so we will need to roll back
+        // the transaction rather than a savepoint.
+        if (empty($this->transactionLayers)) {
+          break;
+        }
+        $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint);
+        return;
+      }
+    }
+    if ($this->supportsTransactions()) {
+      parent::rollBack();
+    }
+    else {
+      // Log unsupported rollback.
+      $this->rollbackLogs[] = array(
+        'type' => 'database',
+        'message' => t('Explicit rollback failed: not supported on active connection.'),
+        'variables' => array(),
+      );
+    }
+    $this->logRollback();
   }
 
   /**
-   * Determines if this transaction will roll back.
-   *
-   * Use this function to skip further operations if the current transaction
-   * is already scheduled to roll back. Throws an exception if no transaction
-   * is active.
-   *
-   * @return
-   *   TRUE if the transaction will roll back, FALSE otherwise.
+   * Logs messages from rollback().
    */
-  public function willRollback() {
-    if ($this->transactionLayers == 0) {
-      throw new NoActiveTransactionException();
+  protected function logRollback() {
+    $logging = Database::getLoggingCallback();
+    // If there is no callback defined. We can't do anything.
+    if (!is_array($logging)) {
+      return;
     }
 
-    return $this->willRollback;
+    $logging_callback = $logging['callback'];
+
+    // Play back the logged errors to the specified logging callback post-
+    // rollback.
+    foreach ($this->rollbackLogs as $log_item) {
+      call_user_func($logging_callback, $log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']);
+    }
+
+    // Reset the error logs.
+    $this->rollbackLogs = array();
   }
 
   /**
@@ -983,72 +955,57 @@ abstract class DatabaseConnection extends PDO {
    *
    * @see DatabaseTransaction
    */
-  public function pushTransaction() {
-    ++$this->transactionLayers;
-
-    if ($this->transactionLayers == 1) {
-      if ($this->supportsTransactions()) {
-        parent::beginTransaction();
-      }
+  public function pushTransaction($name) {
+    if (!$this->supportsTransactions()) {
+      return;
     }
+    if (isset($this->transactionLayers[$name])) {
+      throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
+    }
+    // If we're already in a transaction then we want to create a savepoint
+    // rather than try to create another transaction.
+    if ($this->inTransaction()) {
+      $this->query('SAVEPOINT ' . $name);
+    }
+    else {
+      parent::beginTransaction();
+    }
+    $this->transactionLayers[$name] = $name;
   }
 
   /**
    * Decreases the depth of transaction nesting.
    *
-   * This function first attempts to decrease the number of layers of
-   * transaction nesting by one. If there was no active transaction, the
-   * function throws an exception. If this was the last transaction layer, the
-   * function either rolls back or commits the transaction, depending on whether
-   * the transaction was marked for rollback or not.
+   * If we pop off the last transaction layer, then we either commit or roll
+   * back the transaction as necessary. If no transaction is active, we return
+   * because the transaction may have manually been rolled back.
+   *
+   * @param $name
+   *   The name of the savepoint
    *
    * @see DatabaseTransaction
    */
-  public function popTransaction() {
-    if ($this->transactionLayers == 0) {
-      throw new NoActiveTransactionException();
+  public function popTransaction($name) {
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
     }
 
-    --$this->transactionLayers;
-
-    if ($this->transactionLayers == 0) {
-      if ($this->willRollback) {
-        // Reset the rollback status so that the next transaction starts clean.
-        $this->willRollback = FALSE;
-
-        // Reset the error log.
-        $rollback_logs = $this->rollbackLogs;
-        $this->rollbackLogs = array();
-
-        $logging = Database::getLoggingCallback();
-        $logging_callback = NULL;
-        if (is_array($logging)) {
-          $logging_callback = $logging['callback'];
-        }
-
-        if ($this->supportsTransactions()) {
-          parent::rollBack();
-        }
-        else {
-          if (isset($logging_callback)) {
-            // Log the failed rollback.
-            $logging_callback('database', 'Explicit rollback failed: not supported on active connection.', array(), $logging['error_severity']);
-          }
-
-          // It would be nice to throw an exception here if logging failed,
-          // but throwing exceptions in destructors is not supported.
-        }
+    // Commit everything since SAVEPOINT $name.
+    while($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint != $name) continue;
 
-        if (isset($logging_callback)) {
-          // Play back the logged errors to the specified logging callback post-
-          // rollback.
-          foreach ($rollback_logs as $log_item) {
-            $logging_callback($log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']);
-          }
+      // If there are no more layers left then we should commit.
+      if (empty($this->transactionLayers)) {
+        if (!parent::commit()) {
+          throw new DatabaseTransactionCommitFailedException();
         }
       }
-      elseif ($this->supportsTransactions()) {
-        parent::commit();
+      else {
+        $this->query('RELEASE SAVEPOINT ' . $name);
+        break;
       }
     }
   }
@@ -1181,7 +1138,7 @@ abstract class DatabaseConnection extends PDO {
    * @see DatabaseTransaction
    */
   public function commit() {
-    throw new ExplicitTransactionsNotSupportedException();
+    throw new DatabaseTransactionExplicitCommitNotAllowedException();
   }
 
   /**
@@ -1410,8 +1367,7 @@ abstract class Database {
       // If necessary, a new connection is opened.
       self::$connections[$key][$target] = self::openConnection($key, $target);
     }
-
-    return isset(self::$connections[$key][$target]) ? self::$connections[$key][$target] : NULL;
+    return self::$connections[$key][$target];
   }
 
   /**
@@ -1454,27 +1410,27 @@ abstract class Database {
 
     _db_check_install_needed();
 
-    $databaseInfo = $databases;
-    foreach ($databaseInfo as $index => $info) {
-      foreach ($databaseInfo[$index] as $target => $value) {
+    $database_info = is_array($databases) ? $databases : array();
+    foreach ($database_info as $index => $info) {
+      foreach ($database_info[$index] as $target => $value) {
         // If there is no "driver" property, then we assume it's an array of
         // possible connections for this target. Pick one at random. That allows
         //  us to have, for example, multiple slave servers.
         if (empty($value['driver'])) {
-          $databaseInfo[$index][$target] = $databaseInfo[$index][$target][mt_rand(0, count($databaseInfo[$index][$target]) - 1)];
+          $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)];
         }
       }
     }
 
     if (!is_array(self::$databaseInfo)) {
-      self::$databaseInfo = $databaseInfo;
+      self::$databaseInfo = $database_info;
     }
 
-    // Merge the new $databaseInfo into the existing.
+    // Merge the new $database_info into the existing.
     // array_merge_recursive() cannot be used, as it would make multiple
     // database, user, and password keys in the same database array.
     else {
-      foreach ($databaseInfo as $database_key => $database_values) {
+      foreach ($database_info as $database_key => $database_values) {
         foreach ($database_values as $target => $target_values) {
           self::$databaseInfo[$database_key][$target] = $target_values;
         }
@@ -1544,11 +1500,11 @@ abstract class Database {
       // If the requested database does not exist then it is an unrecoverable
       // error.
       if (!isset(self::$databaseInfo[$key])) {
-        throw new Exception('DB does not exist');
+        throw new DatabaseConnectionNotDefinedException('The specified database connection is not defined: ' . $key);
       }
 
       if (!$driver = self::$databaseInfo[$key][$target]['driver']) {
-        throw new Exception('Drupal is not set up');
+        throw new DatabaseDriverNotSpecifiedException('Driver not specified for this database connection: ' . $key);
       }
 
       // We cannot rely on the registry yet, because the registry requires an
@@ -1626,7 +1582,17 @@ abstract class Database {
 /**
  * Exception for when popTransaction() is called with no active transaction.
  */
-class NoActiveTransactionException extends Exception { }
+class DatabaseTransactionNoActiveException extends Exception { }
+
+/**
+ * Exception thrown when a savepoint or transaction name occurs twice.
+ */
+class DatabaseTransactionNameNonUniqueException extends Exception { }
+
+/**
+ * Exception thrown when a commit() function fails.
+ */
+class DatabaseTransactionCommitFailedException extends Exception { }
 
 /**
  * Exception to deny attempts to explicitly manage transactions.
@@ -1634,7 +1600,7 @@ class NoActiveTransactionException extends Exception { }
  * This exception will be thrown when the PDO connection commit() is called.
  * Code should never call this method directly.
  */
-class ExplicitTransactionsNotSupportedException extends Exception { }
+class DatabaseTransactionExplicitCommitNotAllowedException extends Exception { }
 
 /**
  * Exception thrown for merge queries that do not make semantic sense.
@@ -1657,6 +1623,17 @@ class FieldsOverlapException extends Exception {}
  */
 class NoFieldsException extends Exception {}
 
+/**
+ * Exception thrown if an undefined database connection is requested.
+ */
+class DatabaseConnectionNotDefinedException extends Exception {}
+
+/**
+ * Exception thrown if no driver is specified for a database connection.
+ */
+class DatabaseDriverNotSpecifiedException extends Exception {}
+
+
 /**
  * A wrapper class for creating and managing database transactions.
  *
@@ -1685,13 +1662,51 @@ class DatabaseTransaction {
    */
   protected $connection;
 
-  public function __construct(DatabaseConnection &$connection) {
+  /**
+   * A boolean value to indicate whether this transaction has been rolled back.
+   *
+   * @var Boolean
+   */
+  protected $rolledBack = FALSE;
+
+  /**
+   * The name of the transaction.
+   *
+   * This is used to label the transaction savepoint. It will be overridden to
+   * 'drupal_transaction' if there is no transaction depth.
+   */
+  protected $name;
+
+  public function __construct(DatabaseConnection &$connection, $name = NULL) {
     $this->connection = &$connection;
-    $this->connection->pushTransaction();
+    // If there is no transaction depth, then no transaction has started. Name
+    // the transaction 'drupal_transaction'.
+    if (!$depth = $connection->transactionDepth()) {
+      $this->name = 'drupal_transaction';
+    }
+    // Within transactions, savepoints are used. Each savepoint requires a
+    // name. So if no name is present we need to create one.
+    elseif (!$name) {
+      $this->name = 'savepoint_' . $depth;
+    }
+    else {
+      $this->name = $name;
+    }
+    $this->connection->pushTransaction($this->name);
   }
 
   public function __destruct() {
-    $this->connection->popTransaction();
+    // If we rolled back then the transaction would have already been popped.
+    if ($this->connection->inTransaction() && !$this->rolledBack) {
+      $this->connection->popTransaction($this->name);
+    }
+  }
+
+  /**
+   * Retrieves the name of the transaction or savepoint.
+   */
+  public function name() {
+    return $this->name;
   }
 
   /**
@@ -1719,22 +1734,15 @@ class DatabaseTransaction {
    * @see watchdog()
    */
   public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
+    $this->rolledBack = TRUE;
     if (!isset($severity)) {
       $logging = Database::getLoggingCallback();
       if (is_array($logging)) {
         $severity = $logging['default_severity'];
       }
     }
-    $this->connection->rollback($type, $message, $variables, $severity, $link);
+    $this->connection->rollback($this->name, $type, $message, $variables, $severity, $link);
   }
-
-  /**
-   * Determines if this transaction will roll back.
-   */
-  public function willRollback() {
-    return $this->connection->willRollback();
-  }
-
 }
 
 /**
@@ -1751,8 +1759,7 @@ class DatabaseTransaction {
  * @code
  * class DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {}
  * @endcode
- *
- * …or implement their own class, but in that case they will also have to
+ * or implement their own class, but in that case they will also have to
  * implement the Iterator or IteratorArray interfaces before
  * DatabaseStatementInterface:
  * @code
@@ -1802,12 +1809,12 @@ interface DatabaseStatementInterface extends Traversable {
    *   One of the PDO::FETCH_* constants.
    * @param $a1
    *   An option depending of the fetch mode specified by $mode:
-   *    - for PDO::FETCH_COLUMN, it is the index of the column to fetch,
-   *    - for PDO::FETCH_CLASS, it is the name of the class to create, and
-   *    - for PDO::FETCH_INTO, it is the object to add the data to.
+   *   - for PDO::FETCH_COLUMN, the index of the column to fetch
+   *   - for PDO::FETCH_CLASS, the name of the class to create
+   *   - for PDO::FETCH_INTO, the object to add the data to
    * @param $a2
-   *  In case of when mode is PDO::FETCH_CLASS, the optional arguments to
-   *  pass to the constructor.
+   *   If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   constructor.
    */
   // public function setFetchMode($mode, $a1 = NULL, $a2 = array());
 
@@ -2111,6 +2118,73 @@ class DatabaseStatementEmpty implements Iterator, DatabaseStatementInterface {
   }
 }
 
+/**
+ * Autoload callback for the database system.
+ */
+function db_autoload($class) {
+  static $base_path = '';
+  static $checked = array();
+
+  static $files = array(
+    'query.inc' => array(
+      'QueryPlaceholderInterface',
+      'QueryConditionInterface', 'DatabaseCondition',
+      'Query', 'DeleteQuery', 'InsertQuery', 'UpdateQuery', 'MergeQuery', 'TruncateQuery',
+      'QueryAlterableInterface',
+    ),
+    'select.inc' => array('QueryAlterableInterface', 'SelectQueryInterface', 'SelectQuery', 'SelectQueryExtender'),
+    'database.inc' => array('DatabaseConnection'),
+    'log.inc' => array('DatabaseLog'),
+    'prefetch.inc' => array('DatabaseStatementPrefetch'),
+    'schema.inc' => array('DatabaseSchema'),
+  );
+
+  // If a class doesn't exist, it may get checked a second time
+  // by class_exists().  If so, just bail out now.
+  if (isset($checked[$class])) {
+    return;
+  }
+  $checked[$class] = TRUE;
+
+  if (empty($base_path)) {
+    $base_path = dirname(realpath(__FILE__));
+  }
+
+  // If there is an underscore in the class name, we know it's a
+  // driver-specific file so check for those.  If not, it's a generic.
+  // Note that we use require_once here instead of require because of a
+  // quirk in class_exists().  By default, class_exists() will try to
+  // autoload a class if it's not found.  However, we cannot tell
+  // at this point whether or not the class is going to exist, only
+  // the file that it would be in if it does exist.  That means we may
+  // try to include a file that was already included by another
+  // autoload call, which would break.  Using require_once() neatly
+  // avoids that issue.
+  if (strpos($class, '_') !== FALSE) {
+    list($base, $driver) = explode('_', $class);
+
+    // Drivers have an extra file, and may put their SelectQuery implementation
+    // in the main query file since it's so small.
+    $driver_files = $files;
+    $driver_files['query.inc'][] = 'SelectQuery';
+    $driver_files['install.inc'] = array('DatabaseTasks');
+
+    foreach ($driver_files as $file => $classes) {
+      if (in_array($base, $classes)) {
+        require_once "{$base_path}/{$driver}/{$file}";
+        return;
+      }
+    }
+  }
+  else {
+    foreach ($files as $file => $classes) {
+      if (in_array($class, $classes)) {
+        require_once $base_path . '/' . $file;
+        return;
+      }
+    }
+  }
+}
 
 /**
  * The following utility functions are simply convenience wrappers.
@@ -2326,22 +2400,20 @@ function db_select($table, $alias = NULL, array $options = array()) {
 /**
  * Returns a new transaction object for the active database.
  *
- * @param $required
- *   TRUE if the calling code will not function properly without transaction
- *   support.  If set to TRUE and the active database does not support
- *   transactions, a TransactionsNotSupportedException exception will be thrown.
- * @param $options
- *   An array of options to control how the transaction operates.  Only the
- *   target key has any meaning in this case.
+ * @param string $name
+ *   Optional name of the transaction.
+ * @param array $options
+ *   An array of options to control how the transaction operates:
+ *   - target: The database target name.
  *
  * @return DatabaseTransaction
  *   A new DatabaseTransaction object for this connection.
  */
-function db_transaction($required = FALSE, Array $options = array()) {
+function db_transaction($name = NULL, array $options = array()) {
   if (empty($options['target'])) {
     $options['target'] = 'default';
   }
-  return Database::getConnection($options['target'])->startTransaction($required);
+  return Database::getConnection($options['target'])->startTransaction($name);
 }
 
 /**
@@ -2357,20 +2429,6 @@ function db_set_active($key = 'default') {
   return Database::setActiveConnection($key);
 }
 
-/**
- * Determines if there is an active connection.
- *
- * Note that this method will return FALSE if no connection has been established
- * yet, even if one could be.
- *
- * @return
- *   TRUE if there is at least one database connection established, FALSE
- *   otherwise.
- */
-function db_is_active() {
-  return Database::isActiveConnection();
-}
-
 /**
  * Restricts a dynamic table, column, or constraint name to safe characters.
  *
@@ -2429,7 +2487,7 @@ function db_driver() {
  * Closes the active database connection.
  *
  * @param $options
- *   An array of options to control which connection is closed.  Only the target
+ *   An array of options to control which connection is closed. Only the target
  *   key has any meaning in this case.
  */
 function db_close(array $options = array()) {
@@ -2442,8 +2500,9 @@ function db_close(array $options = array()) {
 /**
  * Retrieves a unique id.
  *
- * Use this function if for some reason you can't use a serial field, normally a
- * serial field with db_last_insert_id is preferred.
+ * Use this function if for some reason you can't use a serial field. Using a
+ * serial field is preferred, and InsertQuery::execute() returns the value of
+ * the last ID inserted.
  *
  * @param $existing_id
  *   After a database import, it might be that the sequences table is behind, so
@@ -2457,6 +2516,47 @@ function db_next_id($existing_id = 0) {
   return Database::getConnection()->nextId($existing_id);
 }
 
+/**
+ * Returns a new DatabaseCondition, set to "OR" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_or() {
+  return new DatabaseCondition('OR');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "AND" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_and() {
+  return new DatabaseCondition('AND');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "XOR" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_xor() {
+  return new DatabaseCondition('XOR');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to the specified conjunction.
+ *
+ * Internal API function call.  The db_and(), db_or(), and db_xor()
+ * functions are preferred.
+ *
+ * @param $conjunction
+ *   The conjunction to use for query conditions (AND, OR or XOR).
+ * @return DatabaseCondition
+ */
+function db_condition($conjunction) {
+  return new DatabaseCondition($conjunction);
+}
+
 /**
  * @} End of "defgroup database".
  */
@@ -2467,7 +2567,6 @@ function db_next_id($existing_id = 0) {
  * @{
  */
 
-
 /**
  * Creates a new table from a Drupal table definition.
  *
@@ -2529,14 +2628,14 @@ function db_table_exists($table) {
  *
  * @param $table
  *   The name of the table in drupal (no prefixing).
- * @param $name
- *   The name of the column.
+ * @param $field
+ *   The name of the field.
  *
  * @return
  *   TRUE if the given column exists, otherwise FALSE.
  */
-function db_column_exists($table, $column) {
-  return Database::getConnection()->schema()->columnExists($table, $column);
+function db_field_exists($table, $field) {
+  return Database::getConnection()->schema()->fieldExists($table, $field);
 }
 
 /**
@@ -2784,7 +2883,7 @@ function db_change_field($table, $field, $field_new, $spec, $keys_new = array())
  * @} End of "ingroup schemaapi".
  */
 
- /**
+/**
  * Sets a session variable specifying the lag time for ignoring a slave server.
  */
 function db_ignore_slave() {
diff --git a/includes/database/log.inc b/includes/database/log.inc
index 1c86eba7f8172c7b3ffffb10335a4765e73d8e1b..e579bbcce322a552c332c2d8e2c2314bdd31af66 100644
--- a/includes/database/log.inc
+++ b/includes/database/log.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: log.inc,v 1.7 2010/01/18 17:10:03 dries Exp $
+// $Id: log.inc,v 1.8 2010/03/31 15:09:21 dries Exp $
 
 /**
  * @file
@@ -154,7 +154,6 @@ class DatabaseLog {
           'type' => isset($stack[$i + 1]['type']) ? $stack[$i + 1]['type'] : NULL,
           'args' => $stack[$i + 1]['args'],
         );
-        return $stack[$i];
       }
     }
   }
diff --git a/includes/database/mysql/query.inc b/includes/database/mysql/query.inc
index da4f9e691142da0b1e45f9f06d5d67f3b6980349..cbf6dd074a15dacdc25f369ea982ed427016a3b1 100644
--- a/includes/database/mysql/query.inc
+++ b/includes/database/mysql/query.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: query.inc,v 1.14 2009/10/11 13:43:36 dries Exp $
+// $Id: query.inc,v 1.15 2010/04/19 04:43:05 webchick Exp $
 
 /**
  * @ingroup database
@@ -44,18 +44,16 @@ class InsertQuery_mysql extends InsertQuery {
 
   public function __toString() {
 
-    $delay = $this->queryOptions['delay'] ? 'DELAYED' : '';
-
     // Default fields are always placed first for consistency.
     $insert_fields = array_merge($this->defaultFields, $this->insertFields);
 
     // If we're selecting from a SelectQuery, finish building the query and
     // pass it back, as any remaining options are irrelevant.
     if (!empty($this->fromQuery)) {
-      return "INSERT $delay INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+      return "INSERT INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
     }
 
-    $query = "INSERT $delay INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+    $query = "INSERT INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
 
     $max_placeholder = 0;
     $values = array();
diff --git a/includes/database/mysql/schema.inc b/includes/database/mysql/schema.inc
index edd22d55851e953959b563d49b70f153ccd69257..56fca57d03d85664d504e63ee13f3594fb4650d1 100644
--- a/includes/database/mysql/schema.inc
+++ b/includes/database/mysql/schema.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: schema.inc,v 1.33 2010/03/09 11:39:07 dries Exp $
+// $Id: schema.inc,v 1.36 2010/04/07 15:07:59 dries Exp $
 
 /**
  * @file
@@ -24,6 +24,26 @@ class DatabaseSchema_mysql extends DatabaseSchema {
    */
   const COMMENT_MAX_COLUMN = 255;
 
+  /**
+   * Get information about the table and database name from the db_prefix.
+   *
+   * @return
+   *   A keyed array with information about the database, table name and prefix.
+   */
+  protected function getPrefixInfo($table = 'default') {
+    $info = array('prefix' => $this->connection->tablePrefix($table));
+    if (($pos = strpos($info['prefix'], '.')) !== FALSE) {
+      $info['database'] = substr($info['prefix'], 0, $pos);
+      $info['table'] = substr($info['prefix'], ++$pos) . $table;
+    }
+    else {
+      $db_info = Database::getConnectionInfo();
+      $info['database'] = $db_info['default']['database'];
+      $info['table'] = $info['prefix'] . $table;
+    }
+    return $info;
+  }
+
   /**
    * Build a condition to match a table name against a standard information_schema.
    *
@@ -35,16 +55,11 @@ class DatabaseSchema_mysql extends DatabaseSchema {
   protected function buildTableNameCondition($table_name, $operator = '=') {
     $info = $this->connection->getConnectionOptions();
 
-    if (strpos($table_name, '.')) {
-      list($schema, $table_name) = explode('.', $table_name);
-    }
-    else {
-      $schema = $info['database'];
-    }
+    $table_info = $this->getPrefixInfo($table_name);
 
     $condition = new DatabaseCondition('AND');
-    $condition->condition('table_schema', $schema);
-    $condition->condition('table_name', $table_name, $operator);
+    $condition->condition('table_schema', $table_info['database']);
+    $condition->condition('table_name', $table_info['table'], $operator);
     return $condition;
   }
 
@@ -273,7 +288,8 @@ class DatabaseSchema_mysql extends DatabaseSchema {
       throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
     }
 
-    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}');
+    $info = $this->getPrefixInfo($new_name);
+    return $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
   }
 
   public function dropTable($table) {
@@ -289,7 +305,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
     if (!$this->tableExists($table)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
     }
-    if ($this->columnExists($table, $field)) {
+    if ($this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
     }
 
@@ -316,7 +332,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
   }
 
   public function dropField($table, $field) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       return FALSE;
     }
 
@@ -325,7 +341,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
   }
 
   public function fieldSetDefault($table, $field, $default) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
     }
 
@@ -340,7 +356,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
   }
 
   public function fieldSetNoDefault($table, $field) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
     }
 
@@ -415,10 +431,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
   }
 
   public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
     }
-    if (($field != $field_new) && $this->columnExists($table, $field_new)) {
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
       throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
     }
 
@@ -446,7 +462,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
    * Retrieve a table or column comment.
    */
   public function getComment($table, $column = NULL) {
-    $condition = $this->buildTableNameCondition($this->connection->prefixTables('{' . $table . '}'));
+    $condition = $this->buildTableNameCondition($table);
     if (isset($column)) {
       $condition->condition('column_name', $column);
       $condition->compile($this->connection, $this);
@@ -460,6 +476,40 @@ class DatabaseSchema_mysql extends DatabaseSchema {
     return preg_replace('/; InnoDB free:.*$/', '', $comment);
   }
 
+  public function tableExists($table) {
+    // The information_schema table is very slow to query under MySQL 5.0.
+    // Instead, we try to select from the table in question.  If it fails,
+    // the most likely reason is that it does not exist. That is dramatically
+    // faster than using information_schema.
+    // @link http://bugs.mysql.com/bug.php?id=19588
+    // @todo: This override should be removed once we require a version of MySQL
+    // that has that bug fixed.
+    try {
+      $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
+      return TRUE;
+    }
+    catch (Exception $e) {
+      return FALSE;
+    }
+  }
+
+  public function fieldExists($table, $column) {
+    // The information_schema table is very slow to query under MySQL 5.0.
+    // Instead, we try to select from the table and field in question. If it
+    // fails, the most likely reason is that it does not exist. That is
+    // dramatically faster than using information_schema.
+    // @link http://bugs.mysql.com/bug.php?id=19588
+    // @todo: This override should be removed once we require a version of MySQL
+    // that has that bug fixed.
+    try {
+      $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
+      return TRUE;
+    }
+    catch (Exception $e) {
+      return FALSE;
+    }
+  }
+
 }
 
 /**
diff --git a/includes/database/pgsql/schema.inc b/includes/database/pgsql/schema.inc
index 9c4d88f88dbdb72f93ac61b334ea322edb3bfdda..44dccdc3a68e4c41476a38c999b95a235ea8aee9 100644
--- a/includes/database/pgsql/schema.inc
+++ b/includes/database/pgsql/schema.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: schema.inc,v 1.32 2010/03/06 06:39:00 dries Exp $
+// $Id: schema.inc,v 1.34 2010/04/07 15:07:59 dries Exp $
 
 /**
  * @file
@@ -95,7 +95,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
     }
     if (isset($table['unique keys']) && is_array($table['unique keys'])) {
       foreach ($table['unique keys'] as $key_name => $key) {
-        $sql_keys[] = 'CONSTRAINT {' . $name . '}_' . $key_name . '_key UNIQUE (' . implode(', ', $key) . ')';
+        $sql_keys[] = 'CONSTRAINT ' . $this->prefixNonTable($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
       }
     }
 
@@ -297,7 +297,9 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
     }
 
     // Now rename the table.
-    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}');
+    // Ensure the new table name does not include schema syntax.
+    $prefixInfo = $this->getPrefixInfo($new_name);
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
   }
 
   public function dropTable($table) {
@@ -313,7 +315,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
     if (!$this->tableExists($table)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
     }
-    if ($this->columnExists($table, $field)) {
+    if ($this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
     }
 
@@ -343,7 +345,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
   }
 
   public function dropField($table, $field) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       return FALSE;
     }
 
@@ -352,7 +354,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
   }
 
   public function fieldSetDefault($table, $field, $default) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
     }
 
@@ -367,7 +369,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
   }
 
   public function fieldSetNoDefault($table, $field) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
     }
 
@@ -409,7 +411,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
       return FALSE;
     }
 
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT {' . $table . '}_pkey');
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->prefixNonTable($table, 'pkey'));
     return TRUE;
   }
 
@@ -421,8 +423,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
       throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
     }
 
-    $name = '{' . $table . '}_' . $name . '_key';
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $name . '" UNIQUE (' . implode(',', $fields) . ')');
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
   }
 
   public function dropUniqueKey($table, $name) {
@@ -430,8 +431,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
       return FALSE;
     }
 
-    $name = '{' . $table . '}_' . $name . '_key';
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $name . '"');
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '"');
     return TRUE;
   }
 
@@ -451,16 +451,15 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
       return FALSE;
     }
 
-    $name = '{' . $table . '}_' . $name . '_idx';
-    $this->connection->query('DROP INDEX ' . $name);
+    $this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name, 'idx'));
     return TRUE;
   }
 
   public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
     }
-    if (($field != $field_new) && $this->columnExists($table, $field_new)) {
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
       throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
     }
 
@@ -495,7 +494,7 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
   }
 
   protected function _createIndexSql($table, $name, $fields) {
-    $query = 'CREATE INDEX "{' . $table . '}_' . $name . '_idx" ON {' . $table . '} (';
+    $query = 'CREATE INDEX "' . $this->prefixNonTable($table, $name, 'idx') . '" ON {' . $table . '} (';
     $query .= $this->_createKeySql($fields) . ')';
     return $query;
   }
@@ -520,13 +519,13 @@ class DatabaseSchema_pgsql extends DatabaseSchema {
    * Retrieve a table or column comment.
    */
   public function getComment($table, $column = NULL) {
-    $table = $this->connection->prefixTables('{' . $table . '}');
+    $info = $this->getPrefixInfo($table);
     // Don't use {} around pg_class, pg_attribute tables.
     if (isset($column)) {
-      return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', array($table, $column))->fetchField();
+      return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', array($info['table'], $column))->fetchField();
     }
     else {
-      return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $table))->fetchField();
+      return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $info['table']))->fetchField();
     }
   }
 }
diff --git a/includes/database/prefetch.inc b/includes/database/prefetch.inc
index d6dc571e97df5fd550735e945b34bee1fa12d74a..4a07e784d604811ce00024e6ff8b973f7cf03b49 100644
--- a/includes/database/prefetch.inc
+++ b/includes/database/prefetch.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: prefetch.inc,v 1.7 2009/12/04 16:31:04 dries Exp $
+// $Id: prefetch.inc,v 1.8 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -175,9 +175,8 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface
     // as soon as possible.
     $this->rowCount = $statement->rowCount();
     $this->data = $statement->fetchAll(PDO::FETCH_ASSOC);
-    // Destroy the statement as soon as possible.
-    // See DatabaseConnection_sqlite::PDOPrepare() for explanation.
-    // @see DatabaseConnection_sqlite::PDOPrepare()
+    // Destroy the statement as soon as possible. See 
+    // DatabaseConnection_sqlite::PDOPrepare() for explanation.
     unset($statement);
 
     $this->resultRowCount = count($this->data);
diff --git a/includes/database/query.inc b/includes/database/query.inc
index cf63fd40339f841083167c5717fe781c7e084ac5..ead5318b6c4e7aca0cbf874587fde3ce3539048f 100644
--- a/includes/database/query.inc
+++ b/includes/database/query.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: query.inc,v 1.43 2010/03/01 11:30:37 dries Exp $
+// $Id: query.inc,v 1.46 2010/04/19 04:43:05 webchick Exp $
 
 /**
  * @ingroup database
@@ -414,33 +414,6 @@ class InsertQuery extends Query {
     return $this;
   }
 
-  /**
-   * Flag this query as being delay-safe or not.
-   *
-   * If this method is never called, it is assumed that the query must be
-   * executed immediately. If delay is set to TRUE, then the query will be
-   * flagged to run "delayed" or "low priority" on databases that support such
-   * capabilities. In that case, the database will return immediately and the
-   * query will be run at some point in the future. That makes it useful for
-   * logging-style queries.
-   *
-   * If the database does not support delayed INSERT queries, this method
-   * has no effect.
-   *
-   * Note that for a delayed query there is no serial ID returned, as it won't
-   * be created until later when the query runs. It should therefore not be
-   * used if the value of the ID is known.
-   *
-   * @param $delay
-   *   If TRUE, this query is delay-safe and will run delayed on supported databases.
-   * @return InsertQuery
-   *   The called object.
-   */
-  public function delay($delay = TRUE) {
-    $this->delay = $delay;
-    return $this;
-  }
-
   public function from(SelectQueryInterface $query) {
     $this->fromQuery = $query;
     return $this;
@@ -498,7 +471,7 @@ class InsertQuery extends Query {
     $insert_fields = array_merge($this->defaultFields, $this->insertFields);
 
     if (!empty($this->fromQuery)) {
-      return "INSERT $delay INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+      return "INSERT INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
     }
 
     // For simplicity, we will use the $placeholders array to inject
@@ -1357,47 +1330,6 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
 
 }
 
-/**
- * Returns a new DatabaseCondition, set to "OR" all conditions together.
- *
- * @return DatabaseCondition
- */
-function db_or() {
-  return new DatabaseCondition('OR');
-}
-
-/**
- * Returns a new DatabaseCondition, set to "AND" all conditions together.
- *
- * @return DatabaseCondition
- */
-function db_and() {
-  return new DatabaseCondition('AND');
-}
-
-/**
- * Returns a new DatabaseCondition, set to "XOR" all conditions together.
- *
- * @return DatabaseCondition
- */
-function db_xor() {
-  return new DatabaseCondition('XOR');
-}
-
-/**
- * Returns a new DatabaseCondition, set to the specified conjunction.
- *
- * Internal API function call.  The db_and(), db_or(), and db_xor()
- * functions are preferred.
- *
- * @param $conjunction
- *   The conjunction to use for query conditions (AND, OR or XOR).
- * @return DatabaseCondition
- */
-function db_condition($conjunction) {
-  return new DatabaseCondition($conjunction);
-}
-
 /**
  * @} End of "ingroup database".
  */
diff --git a/includes/database/schema.inc b/includes/database/schema.inc
index a2674246bff6f5d28cd6e70f2d6181e5eaa466ae..012b74afa4cf0c209d021fc863fda4327a534cce 100644
--- a/includes/database/schema.inc
+++ b/includes/database/schema.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: schema.inc,v 1.31 2010/03/09 11:39:07 dries Exp $
+// $Id: schema.inc,v 1.34 2010/04/07 15:07:59 dries Exp $
 
 /**
  * @file
@@ -24,7 +24,6 @@
  * the module defines.
  *
  * The following keys are defined:
- *
  *   - 'description': A string in non-markup plain text describing this table
  *     and its purpose. References to other tables should be enclosed in
  *     curly-brackets. For example, the node_revisions table
@@ -33,7 +32,6 @@
  *   - 'fields': An associative array ('fieldname' => specification)
  *     that describes the table's database columns. The specification
  *     is also an array. The following specification parameters are defined:
- *
  *     - 'description': A string in non-markup plain text describing this field
  *       and its purpose. References to other tables should be enclosed in
  *       curly-brackets. For example, the node table vid field
@@ -52,7 +50,6 @@
  *       datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
  *       'normal', the default, selects the base type (e.g. on MySQL,
  *       INT, VARCHAR, BLOB, etc.).
- *
  *       Not all sizes are available for all data types. See
  *       DatabaseSchema::getFieldTypeMap() for possible combinations.
  *     - 'not null': If true, no NULL values will be allowed in this
@@ -71,10 +68,8 @@
  *       the precision (total number of significant digits) and scale
  *       (decimal digits right of the decimal point). Both values are
  *       mandatory. Ignored for other field types.
- *
  *     All parameters apart from 'type' are optional except that type
  *     'numeric' columns must specify 'precision' and 'scale'.
- *
  *  - 'primary key': An array of one or more key column specifiers (see below)
  *    that form the primary key.
  *  - 'unique keys': An associative array of unique keys ('keyname' =>
@@ -155,6 +150,16 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    */
   protected $placeholder = 0;
 
+  /**
+   * Definition of prefixInfo array structure.
+   *
+   * Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
+   * by defining the defaultSchema variable only MySQL has to re-write the
+   * method.
+   *
+   * @see DatabaseSchema::getPrefixInfo()
+   */
+  protected $defaultSchema = 'public';
 
   public function __construct($connection) {
     $this->connection = $connection;
@@ -164,6 +169,48 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
     return $this->placeholder++;
   }
 
+  /**
+   * Get information about the table name and schema from the db_prefix.
+   *
+   * @param
+   *   Name of table to look prefix up for. Defaults to 'default' because thats
+   *   default key for db_prefix.
+   * @return
+   *   A keyed array with information about the schema, table name and prefix.
+   */
+  protected function getPrefixInfo($table = 'default') {
+    $info = array(
+      'schema' => $this->defaultSchema,
+      'prefix' => $this->connection->tablePrefix($table),
+    );
+    // If the prefix contains a period in it, then that means the prefix also
+    // contains a schema reference in which case we will change the schema key
+    // to the value before the period in the prefix. Everything after the dot
+    // will be prefixed onto the front of the table.
+    if ($pos = strpos($info['prefix'], '.') !== FALSE) {
+      // Grab everything before the period.
+      $info['schema'] = substr($info['prefix'], 0, $pos);
+      // Grab everything after the dot, and prefix on to the table.
+      $info['table'] = substr($info['prefix'], ++$pos) . $table;
+    }
+    else {
+      $info['table'] = $info['prefix'] . $table;
+    }
+    return $info;
+  }
+
+  /**
+   * Create names for indexes, primary keys and constraints.
+   *
+   * This prevents using {} around non-table names like indexes and keys.
+   */
+  function prefixNonTable($table) {
+    $args = func_get_args();
+    $info = $this->getPrefixInfo($table);
+    $args[0] = $info['table'];
+    return implode('_', $args);
+  }
+
   /**
    * Build a condition to match a table name against a standard information_schema.
    *
@@ -180,27 +227,23 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    * to make all the others work. For example see includes/databases/mysql/schema.inc.
    *
    * @param $table_name
-   *   The name of the table to explode.
+   *   The name of the table in question.
    * @param $operator
    *   The operator to apply on the 'table' part of the condition.
+   *
    * @return QueryConditionInterface
    *   A DatabaseCondition object.
    */
   protected function buildTableNameCondition($table_name, $operator = '=') {
     $info = $this->connection->getConnectionOptions();
 
-    // The table name may describe the schema eg. schema.table.
-    if (strpos($table_name, '.')) {
-      list($schema, $table_name) = explode('.', $table_name);
-    }
-    else {
-      $schema = 'public';
-    }
+    // Retrive the table name and schema
+    $table_info = $this->getPrefixInfo($table_name);
 
     $condition = new DatabaseCondition('AND');
     $condition->condition('table_catalog', $info['database']);
-    $condition->condition('table_schema', $schema);
-    $condition->condition('table_name', $table_name, $operator);
+    $condition->condition('table_schema', $table_info['schema']);
+    $condition->condition('table_name', $table_info['table'], $operator);
     return $condition;
   }
 
@@ -209,11 +252,12 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *
    * @param $table
    *   The name of the table in drupal (no prefixing).
+   *
    * @return
    *   TRUE if the given table exists, otherwise FALSE.
    */
   public function tableExists($table) {
-    $condition = $this->buildTableNameCondition($this->connection->prefixTables('{' . $table . '}'));
+    $condition = $this->buildTableNameCondition($table);
     $condition->compile($this->connection, $this);
     // Normally, we would heartily discourage the use of string
     // concatenation for conditionals like this however, we
@@ -229,6 +273,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    * @param $table_expression
    *   An SQL expression, for example "simpletest%" (without the quotes).
    *   BEWARE: this is not prefixed, the caller should take care of that.
+   *
    * @return
    *   Array, both the keys and the values are the matching tables.
    */
@@ -250,11 +295,12 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The name of the table in drupal (no prefixing).
    * @param $name
    *   The name of the column.
+   *
    * @return
    *   TRUE if the given column exists, otherwise FALSE.
    */
-  public function columnExists($table, $column) {
-    $condition = $this->buildTableNameCondition($this->connection->prefixTables('{' . $table . '}'));
+  public function fieldExists($table, $column) {
+    $condition = $this->buildTableNameCondition($table);
     $condition->condition('column_name', $column);
     $condition->compile($this->connection, $this);
     // Normally, we would heartily discourage the use of string
@@ -266,15 +312,15 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
   }
 
   /**
-  * Returns a mapping of Drupal schema field names to DB-native field types.
-  *
-  * Because different field types do not map 1:1 between databases, Drupal has
-  * its own normalized field type names. This function returns a driver-specific
-  * mapping table from Drupal names to the native names for each database.
-  *
-  * @return array
-  *   An array of Schema API field types to driver-specific field types.
-  */
+   * Returns a mapping of Drupal schema field names to DB-native field types.
+   *
+   * Because different field types do not map 1:1 between databases, Drupal has
+   * its own normalized field type names. This function returns a driver-specific
+   * mapping table from Drupal names to the native names for each database.
+   *
+   * @return array
+   *   An array of Schema API field types to driver-specific field types.
+   */
   abstract public function getFieldTypeMap();
 
   /**
@@ -284,6 +330,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The table to be renamed.
    * @param $new_name
    *   The new name for the table.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table doesn't exist.
    * @throws DatabaseSchemaObjectExistsException
@@ -296,6 +343,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *
    * @param $table
    *   The table to be dropped.
+   *
    * @return
    *   TRUE if the table was successfully dropped, FALSE if there was no table
    *   by that name to begin with.
@@ -322,6 +370,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   adding a type 'serial' field, you MUST specify at least one key
    *   or index including it in this array. See db_change_field() for more
    *   explanation why.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table doesn't exist.
    * @throws DatabaseSchemaObjectExistsException
@@ -336,6 +385,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The table to be altered.
    * @param $field
    *   The field to be dropped.
+   *
    * @return
    *   TRUE if the field was successfully dropped, FALSE if there was no field
    *   by that name to begin with.
@@ -351,6 +401,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The field to be altered.
    * @param $default
    *   Default value to be set. NULL for 'default NULL'.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table or field doesn't exist.
    */
@@ -363,6 +414,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The table to be altered.
    * @param $field
    *   The field to be altered.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table or field doesn't exist.
    */
@@ -375,6 +427,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The name of the table in drupal (no prefixing).
    * @param $name
    *   The name of the index in drupal (no prefixing).
+   *
    * @return
    *   TRUE if the given index exists, otherwise FALSE.
    */
@@ -387,6 +440,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The table to be altered.
    * @param $fields
    *   Fields for the primary key.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table doesn't exist.
    * @throws DatabaseSchemaObjectExistsException
@@ -399,6 +453,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *
    * @param $table
    *   The table to be altered.
+   *
    * @return
    *   TRUE if the primary key was successfully dropped, FALSE if there was no
    *   primary key on this table to begin with.
@@ -414,6 +469,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The name of the key.
    * @param $fields
    *   An array of field names.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table doesn't exist.
    * @throws DatabaseSchemaObjectExistsException
@@ -428,6 +484,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The table to be altered.
    * @param $name
    *   The name of the key.
+   *
    * @return
    *   TRUE if the key was successfully dropped, FALSE if there was no key by
    *   that name to begin with.
@@ -443,6 +500,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The name of the index.
    * @param $fields
    *   An array of field names.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table doesn't exist.
    * @throws DatabaseSchemaObjectExistsException
@@ -457,6 +515,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The table to be altered.
    * @param $name
    *   The name of the index.
+   *
    * @return
    *   TRUE if the index was successfully dropped, FALSE if there was no index
    *   by that name to begin with.
@@ -522,6 +581,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   Optional keys and indexes specification to be created on the
    *   table along with changing the field. The format is the same as a
    *   table specification but without the 'fields' element.
+   *
    * @throws DatabaseSchemaObjectDoesNotExistException
    *   If the specified table or source field doesn't exist.
    * @throws DatabaseSchemaObjectExistsException
@@ -536,6 +596,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The name of the table to create.
    * @param $table
    *   A Schema API table definition array.
+   *
    * @throws DatabaseSchemaObjectExistsException
    *   If the specified table already exists.
    */
@@ -557,6 +618,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *
    * @param $fields
    *   An array of key/index column specifiers.
+   *
    * @return
    *   An array of field names.
    */
@@ -580,6 +642,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    *   The comment string to prepare.
    * @param $length
    *   Optional upper limit on the returned string length.
+   *
    * @return
    *   The prepared comment.
    */
diff --git a/includes/database/select.inc b/includes/database/select.inc
index fd3efd331c2b2d556f7dba58f9fb53c327722490..61c85c68d0a2ea08f6c9bc9d611a2daa3bfc6000 100644
--- a/includes/database/select.inc
+++ b/includes/database/select.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: select.inc,v 1.34 2010/02/17 05:24:53 webchick Exp $
+// $Id: select.inc,v 1.35 2010/04/11 18:33:43 dries Exp $
 
 /**
  * @ingroup database
@@ -501,11 +501,11 @@ class SelectQueryExtender implements SelectQueryInterface {
   }
 
   public function hasAllTags() {
-    return call_user_func_array(array($this->query, 'hasAllTags', func_get_args()));
+    return call_user_func_array(array($this->query, 'hasAllTags'), func_get_args());
   }
 
   public function hasAnyTag() {
-    return call_user_func_array(array($this->query, 'hasAnyTags', func_get_args()));
+    return call_user_func_array(array($this->query, 'hasAnyTags'), func_get_args());
   }
 
   public function addMetaData($key, $object) {
diff --git a/includes/database/sqlite/database.inc b/includes/database/sqlite/database.inc
index 8d7e83036714c504dd34c370094d27c9d3c6177f..60625909faa6f9c98068367c25f5b41c98879ee9 100644
--- a/includes/database/sqlite/database.inc
+++ b/includes/database/sqlite/database.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: database.inc,v 1.28 2010/03/07 08:03:45 webchick Exp $
+// $Id: database.inc,v 1.30 2010/04/06 16:54:15 dries Exp $
 
 /**
  * @file
@@ -18,6 +18,23 @@ include_once DRUPAL_ROOT . '/includes/database/prefetch.inc';
  */
 class DatabaseConnection_sqlite extends DatabaseConnection {
 
+  /**
+   * Whether this database connection supports savepoints.
+   *
+   * Version of sqlite lower then 3.6.8 can't use savepoints.
+   * See http://www.sqlite.org/releaselog/3_6_8.html
+   *
+   * @var bool
+   */
+  protected $savepointSupport = FALSE;
+
+  /**
+   * Whether or not the active transaction (if any) will be rolled back.
+   *
+   * @var boolean
+   */
+  protected $willRollback;
+
   public function __construct(array $connection_options = array()) {
     // We don't need a specific PDOStatement class here, we simulate it below.
     $this->statementClass = NULL;
@@ -34,6 +51,10 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
 
     $this->exec('PRAGMA encoding="UTF-8"');
 
+    // Detect support for SAVEPOINT.
+    $version = $this->query('SELECT sqlite_version()')->fetchField();
+    $this->savepointSupport = (version_compare($version, '3.6.8') >= 0);
+
     // Create functions needed by SQLite.
     $this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf'));
     $this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest'));
@@ -188,6 +209,122 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
     // because it gets out of scope.
     return (int) $this->query('SELECT value FROM {sequences}')->fetchField();
   }
+
+  public function rollback($savepoint_name = 'drupal_transaction', $type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
+    if ($this->savepointSupport) {
+      return parent::rollBack($savepoint_name, $type, $message, $variables, $severity, $link);
+    }
+
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+    // A previous rollback to an earlier savepoint may mean that the savepoint
+    // in question has already been rolled back.
+    if (!in_array($savepoint_name, $this->transactionLayers)) {
+      return;
+    }
+
+    // Set the severity to the configured default if not specified.
+    if (!isset($severity)) {
+      $logging = Database::getLoggingCallback();
+      if (is_array($logging)) {
+        $severity = $logging['default_severity'];
+      }
+    }
+
+    // Record in an array to send to the log after transaction rollback.
+    // Messages written directly to a log (with a database back-end) will roll
+    // back during the following transaction rollback. This is an array because
+    // rollback could be requested multiple times during a transaction, and all
+    // such errors ought to be logged.
+    if (isset($message)) {
+      $this->rollbackLogs[] = array(
+        'type' => $type,
+        'message' => $message,
+        'variables' => $variables,
+        'severity' => $severity,
+        'link' => $link,
+      );
+    }
+
+    // We need to find the point we're rolling back to, all other savepoints
+    // before are no longer needed.
+    while ($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint == $savepoint_name) {
+        // Mark whole stack of transactions as needed roll back.
+        $this->willRollback = TRUE;
+        // If it is the last the transaction in the stack, then it is not a
+        // savepoint, it is the transaction itself so we will need to roll back
+        // the transaction rather than a savepoint.
+        if (empty($this->transactionLayers)) {
+          break;
+        }
+        return;
+      }
+    }
+    if ($this->supportsTransactions()) {
+      PDO::rollBack();
+    }
+    else {
+      // Log unsupported rollback.
+      $this->rollbackLogs[] = array(
+        'type' => 'database',
+        'message' => t('Explicit rollback failed: not supported on active connection.'),
+        'variables' => array(),
+      );
+    }
+    $this->logRollback();
+  }
+
+  public function pushTransaction($name) {
+    if ($this->savepointSupport) {
+      return parent::pushTransaction($name);
+    }
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (isset($this->transactionLayers[$name])) {
+      throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
+    }
+    if (!$this->inTransaction()) {
+      PDO::beginTransaction();
+    }
+    $this->transactionLayers[$name] = $name;
+  }
+
+  public function popTransaction($name) {
+    if ($this->savepointSupport) {
+      return parent::popTransaction($name);
+    }
+    if (!$this->supportsTransactions()) {
+      return;
+    }
+    if (!$this->inTransaction()) {
+      throw new DatabaseTransactionNoActiveException();
+    }
+
+    // Commit everything since SAVEPOINT $name.
+    while($savepoint = array_pop($this->transactionLayers)) {
+      if ($savepoint != $name) continue;
+
+      // If there are no more layers left then we should commit or rollback.
+      if (empty($this->transactionLayers)) {
+        // If there was any rollback() we should roll back whole transaction.
+        if ($this->willRollback) {
+          $this->willRollback = FALSE;
+          PDO::rollBack();
+          $this->logRollback();
+        }
+        elseif (!PDO::commit()) {
+          throw new DatabaseTransactionCommitFailedException();
+        }
+      }
+      else {
+        break;
+      }
+    }
+  }
+
 }
 
 /**
@@ -243,7 +380,7 @@ class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iter
               // in the automatic cast.
               $value = sprintf('%F', $value);
             }
-            
+
             // We will remove this placeholder from the query as PDO throws an
             // exception if the number of placeholders in the query and the
             // arguments does not match.
diff --git a/includes/database/sqlite/schema.inc b/includes/database/sqlite/schema.inc
index 517cdafb0025609e4309266f3c08cb09c00a800e..515fc6aff17efbff7054e5e2a6662418be23b2c9 100644
--- a/includes/database/sqlite/schema.inc
+++ b/includes/database/sqlite/schema.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: schema.inc,v 1.14 2010/03/01 06:27:58 dries Exp $
+// $Id: schema.inc,v 1.16 2010/04/07 15:07:59 dries Exp $
 
 /**
  * @file
@@ -14,12 +14,17 @@
 
 class DatabaseSchema_sqlite extends DatabaseSchema {
 
+	/**
+   * Override DatabaseSchema::$defaultSchema
+   */
+  protected $defaultSchema = 'main';
+
   public function tableExists($table) {
     // Don't use {} around sqlite_master table.
     return (bool) $this->connection->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE '{" . $table . "}'", array(), array())->fetchField();
   }
 
-  public function columnExists($table, $column) {
+  public function fieldExists($table, $column) {
     $schema = $this->introspectSchema($table);
     return !empty($schema['fields'][$column]);
   }
@@ -45,14 +50,19 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
    */
   protected function createIndexSql($tablename, $schema) {
     $sql = array();
+    $info = $this->getPrefixInfo($tablename);
     if (!empty($schema['unique keys'])) {
       foreach ($schema['unique keys'] as $key => $fields) {
-        $sql[] = 'CREATE UNIQUE INDEX "{' . $tablename . '}_' . $key . '" ON {' . $tablename . '} (' . $this->createKeySql($fields) . "); \n";
+        // Normally we don't escape double quotes (we use single quotes) but
+        // describing the index name like this is faster and is readable.
+        $index = "\"{$info['schema']}\".\"{$info['table']}_$key\"";
+        $sql[] = 'CREATE UNIQUE INDEX ' . $index . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
       }
     }
     if (!empty($schema['indexes'])) {
-      foreach ($schema['indexes'] as $index => $fields) {
-        $sql[] = 'CREATE INDEX "{' . $tablename . '}_' . $index . '" ON {' . $tablename . '} (' . $this->createKeySql($fields) . "); \n";
+      foreach ($schema['indexes'] as $key => $fields) {
+        $index = "\"{$info['schema']}\".\"{$info['table']}_$key\"";
+        $sql[] = 'CREATE INDEX ' . $index . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
       }
     }
     return $sql;
@@ -228,7 +238,13 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
 
     $schema = $this->introspectSchema($table);
 
-    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}');
+    // SQLite doesn't allow you to rename tables outside of the current
+    // database. So the syntax '...RENAME TO database.table' would fail.
+    // So we must determine the full table name here rather than surrounding
+    // the table with curly braces incase the db_prefix contains a reference
+    // to a database outside of our existsing database.
+    $info = $this->getPrefixInfo($new_name);
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
 
     // Drop the indexes, there is no RENAME INDEX command in SQLite.
     if (!empty($schema['unique keys'])) {
@@ -262,7 +278,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
     if (!$this->tableExists($table)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
     }
-    if ($this->columnExists($table, $field)) {
+    if ($this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
     }
 
@@ -377,7 +393,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
   }
 
   public function dropField($table, $field) {
-    if ($this->columnExists($table, $field)) {
+    if ($this->fieldExists($table, $field)) {
       return FALSE;
     }
 
@@ -399,10 +415,10 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
   }
 
   public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
     }
-    if (($field != $field_new) && $this->columnExists($table, $field_new)) {
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
       throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
     }
 
@@ -445,7 +461,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
       return FALSE;
     }
 
-    $this->connection->query('DROP INDEX ' . '{' . $table . '}_' . $name);
+    $this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name));
     return TRUE;
   }
 
@@ -469,7 +485,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
       return FALSE;
     }
 
-    $this->connection->query('DROP INDEX ' . '{' . $table . '}_' . $name);
+    $this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name));
     return TRUE;
   }
 
@@ -499,7 +515,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
   }
 
   public function fieldSetDefault($table, $field, $default) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
     }
 
@@ -509,7 +525,7 @@ class DatabaseSchema_sqlite extends DatabaseSchema {
   }
 
   public function fieldSetNoDefault($table, $field) {
-    if (!$this->columnExists($table, $field)) {
+    if (!$this->fieldExists($table, $field)) {
       throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
     }
 
diff --git a/includes/entity.inc b/includes/entity.inc
index b7356d89a2a07e0c190f0da69419b04a2229e9ce..7c1ddf6738dcb2d8c7f269a4941c30a2d60a462e 100644
--- a/includes/entity.inc
+++ b/includes/entity.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: entity.inc,v 1.6 2010/03/10 14:04:26 dries Exp $
+// $Id: entity.inc,v 1.7 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * Interface for entity controller classes.
@@ -64,11 +64,11 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     $this->entityInfo = entity_get_info($entityType);
     $this->entityCache = array();
     $this->hookLoadArguments = array();
-    $this->idKey = $this->entityInfo['object keys']['id'];
+    $this->idKey = $this->entityInfo['entity keys']['id'];
 
     // Check if the entity type supports revisions.
-    if (!empty($this->entityInfo['object keys']['revision'])) {
-      $this->revisionKey = $this->entityInfo['object keys']['revision'];
+    if (!empty($this->entityInfo['entity keys']['revision'])) {
+      $this->revisionKey = $this->entityInfo['entity keys']['revision'];
       $this->revisionTable = $this->entityInfo['revision table'];
     }
     else {
diff --git a/includes/errors.inc b/includes/errors.inc
index 3c5b916ad4d89b7bc048bb64a9f931a485e37bbe..1c0e71cc98e8d8eea9f12870a739bd1285be16ce 100644
--- a/includes/errors.inc
+++ b/includes/errors.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: errors.inc,v 1.2 2010/03/06 06:31:24 dries Exp $
+// $Id: errors.inc,v 1.5 2010/04/11 18:33:43 dries Exp $
 
 /**
  * @file
@@ -87,7 +87,8 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
  *
  * @param $exception
  *   The exception object that was thrown.
- * @return An error in the format expected by _drupal_log_error().
+ * @return
+ *   An error in the format expected by _drupal_log_error().
  */
 function _drupal_decode_exception($exception) {
   $message = $exception->getMessage();
@@ -125,6 +126,18 @@ function _drupal_decode_exception($exception) {
   );
 }
 
+/**
+ * Render an error message for an exception without any possibility of a further exception occuring.
+ *
+ * @param $exception
+ *   The exception object that was thrown.
+ * @return
+ *   An error message.
+ */
+function _drupal_render_exception_safe($exception) {
+  return strtr('%type: %message in %function (line %line of %file).', _drupal_decode_exception($exception));
+}
+
 /**
  * Log a PHP error or exception, display an error page in fatal cases.
  *
@@ -163,18 +176,20 @@ function _drupal_log_error($error, $fatal = FALSE) {
     $number++;
   }
 
-  try {
-    watchdog('php', '%type: %message in %function (line %line of %file).', $error, $error['severity_level']);
-  }
-  catch (Exception $e) {
-    // Ignore any additional watchdog exception, as that probably means
-    // that the database was not initialized correctly.
-  }
+  watchdog('php', '%type: %message in %function (line %line of %file).', $error, $error['severity_level']);
 
   if ($fatal) {
     drupal_add_http_header('Status', '500 Service unavailable (with message)');
   }
 
+  if (drupal_is_cli()) {
+    if ($fatal) {
+      // When called from CLI, simply output a plain text message.
+      print html_entity_decode(strip_tags(t('%type: %message in %function (line %line of %file).', $error))). "\n";
+      exit;
+    }
+  }
+
   if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
     if ($fatal) {
       // When called from JavaScript, simply output the error message.
@@ -186,7 +201,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
     // Display the message if the current error reporting level allows this type
     // of message to be displayed, and unconditionnaly in update.php.
     $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
-    $display_error = $error_level == ERROR_REPORTING_DISPLAY_ALL || ($error_level == ERROR_REPORTING_DISPLAY_SOME && $error['%type'] != 'Notice');
+    $display_error = $error_level == ERROR_REPORTING_DISPLAY_ALL || ($error_level == ERROR_REPORTING_DISPLAY_SOME && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning');
     if ($display_error || (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update')) {
       $class = 'error';
 
diff --git a/includes/file.inc b/includes/file.inc
index 1677e2903c17cd1d12251e9765374fb2e6602d2b..3e4ec953a3f60bf9e16deb88abedb398d9e9f208 100644
--- a/includes/file.inc
+++ b/includes/file.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: file.inc,v 1.205 2010/02/01 19:07:07 dries Exp $
+// $Id: file.inc,v 1.207 2010/04/10 17:30:15 dries Exp $
 
 /**
  * @file
@@ -509,13 +509,13 @@ function file_save(stdClass $file) {
   $file->filesize = filesize($file->uri);
 
   if (empty($file->fid)) {
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
     // Inform modules about the newly added file.
     module_invoke_all('file_insert', $file);
     entity_invoke('insert', 'file', $file);
   }
   else {
-    drupal_write_record('file', $file, 'fid');
+    drupal_write_record('file_managed', $file, 'fid');
     // Inform modules that the file has been updated.
     module_invoke_all('file_update', $file);
     entity_invoke('update', 'file', $file);
@@ -536,8 +536,8 @@ function file_save(stdClass $file) {
  * - If file already exists in $destination either the call will error out,
  *   replace the file or rename the file based on the $replace parameter.
  * - Adds the new file to the files database. If the source file is a
- *   temporary file, the resulting file will also be a temporary file.
- *   @see file_save_upload() for details on temporary files.
+ *   temporary file, the resulting file will also be a temporary file. See
+ *   file_save_upload() for details on temporary files.
  *
  * @param $source
  *   A file object.
@@ -553,6 +553,7 @@ function file_save(stdClass $file) {
  *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
  *       unique.
  *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
  * @return
  *   File object if the copy is successful, or FALSE in the event of an error.
  *
@@ -973,7 +974,7 @@ function file_delete(stdClass $file, $force = FALSE) {
   // Make sure the file is deleted before removing its row from the
   // database, so UIs can still find the file in the database.
   if (file_unmanaged_delete($file->uri)) {
-    db_delete('file')->condition('fid', $file->fid)->execute();
+    db_delete('file_managed')->condition('fid', $file->fid)->execute();
     return TRUE;
   }
   return FALSE;
@@ -1068,9 +1069,9 @@ function file_unmanaged_delete_recursive($path) {
  *   An integer containing the number of bytes used.
  */
 function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
-  $query = db_select('file', 'f');
+  $query = db_select('file_managed', 'f');
   // Use separate placeholders for the status to avoid a bug in some versions
-  // of PHP. @see http://drupal.org/node/352956
+  // of PHP. See http://drupal.org/node/352956.
   $query->where('f.status & :status1 = :status2', array(':status1' => $status, ':status2' => $status));
   $query->addExpression('SUM(f.filesize)', 'filesize');
   if (!is_null($uid)) {
@@ -1082,9 +1083,9 @@ function file_space_used($uid = NULL, $status = FILE_STATUS_PERMANENT) {
 /**
  * Saves a file upload to a new location.
  *
- * The file will be added to the {file} table as a temporary file. Temporary
- * files are periodically cleaned. To make the file a permanent file, assign
- * the status and use file_save() to save the changes.
+ * The file will be added to the {file_managed} table as a temporary file.
+ * Temporary files are periodically cleaned. To make the file a permanent file,
+ * assign the status and use file_save() to save the changes.
  *
  * @param $source
  *   A string specifying the filepath or URI of the uploaded file to save.
@@ -1127,7 +1128,7 @@ function file_save_upload($source, $validators = array(), $destination = FALSE,
 
   // Check for file upload errors and return FALSE if a lower level system
   // error occurred. For a complete list of errors:
-  // @see http://php.net/manual/en/features.file-upload.errors.php
+  // See http://php.net/manual/en/features.file-upload.errors.php.
   switch ($_FILES['files']['error'][$source]) {
     case UPLOAD_ERR_INI_SIZE:
     case UPLOAD_ERR_FORM_SIZE:
@@ -1503,7 +1504,8 @@ function file_save_data($data, $destination = NULL, $replace = FILE_EXISTS_RENAM
  * Save a string to the specified destination without invoking file API.
  *
  * This function is identical to file_save_data() except the file will not be
- * saved to the {file} table and none of the file_* hooks will be called.
+ * saved to the {file_managed} table and none of the file_* hooks will be
+ * called.
  *
  * @param $data
  *   A string containing the contents of the file.
@@ -1734,8 +1736,8 @@ function file_upload_max_size() {
  * @return
  *   The internet media type registered for the extension or
  *   application/octet-stream for unknown extensions.
- * @see
- *   file_default_mimetype_mapping()
+ *
+ * @see file_default_mimetype_mapping()
  */
 function file_get_mimetype($uri, $mapping = NULL) {
   if ($wrapper = file_stream_wrapper_get_instance_by_uri($uri)) {
@@ -1818,6 +1820,7 @@ function drupal_chmod($uri, $mode = NULL) {
  * @param $uri
  *   A string containing the URI to verify. If this value is omitted,
  *   Drupal's public files directory will be used [public://].
+ *
  * @return
  *   The absolute pathname, or FALSE on failure.
  *
@@ -1853,6 +1856,7 @@ function drupal_realpath($uri) {
  *
  * @param $uri
  *   A URI or path.
+ *
  * @return
  *   A string containing the directory name.
  *
@@ -1894,6 +1898,7 @@ function drupal_dirname($uri) {
  *   Default to FALSE.
  * @param $context
  *   Refer to http://php.net/manual/en/ref.stream.php
+ *
  * @return
  *   Boolean TRUE on success, or FALSE on failure.
  *
@@ -1929,6 +1934,7 @@ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
  * @param $prefix
  *   The prefix of the generated temporary filename.
  *   Note: Windows uses only the first three characters of prefix.
+ *
  * @return
  *   The new temporary filename, or FALSE on failure.
  *
diff --git a/includes/filetransfer/filetransfer.inc b/includes/filetransfer/filetransfer.inc
index 6bbeadf13b077f0569fa255add7fc6f75be901ea..4396de6e1874a31ec90e4967ee3f858391b8dfae 100644
--- a/includes/filetransfer/filetransfer.inc
+++ b/includes/filetransfer/filetransfer.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: filetransfer.inc,v 1.8 2010/01/25 10:38:34 dries Exp $
+// $Id: filetransfer.inc,v 1.9 2010/04/11 18:33:43 dries Exp $
 
 /*
  * Base FileTransfer class.
@@ -24,7 +24,16 @@ abstract class FileTransfer {
     $this->jail = $jail;
   }
 
-  abstract static function factory($jail, $settings);
+  /**
+   * Classes that extend this class must override the factory() static method.
+   *
+   * @param string $jail
+   * @param array $settings
+   * @return object New instance of the appropriate FileTransfer subclass.
+   */
+  static function factory($jail, $settings) {
+    throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
+  }
 
   /**
    * Implementation of the magic __get() method.
diff --git a/includes/form.inc b/includes/form.inc
index 4013f14b07a9a83ff96aad7c75e5d429cf990947..a57d7ba873679933474bf0259825fcf7bfff1441 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: form.inc,v 1.442 2010/03/13 22:33:05 webchick Exp $
+// $Id: form.inc,v 1.454 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @defgroup forms Form builder functions
@@ -61,6 +61,7 @@
  *   drupal_get_form(), including the unique form constructor function. For
  *   example, the node_edit form requires that a node object is passed in here
  *   when it is called.
+ *
  * @return
  *   The form array.
  *
@@ -191,13 +192,13 @@ function drupal_build_form($form_id, &$form_state) {
     if (!isset($form)) {
       // Record the filepath of the include file containing the original form,
       // so the form builder callbacks can be loaded when the form is being
-      // rebuilt from cache on a different path (such as 'system/ajax').
-      // @see form_get_cache()
-      // menu_get_item() is not available at installation time.
+      // rebuilt from cache on a different path (such as 'system/ajax'). See
+      // form_get_cache().
+      // $menu_get_item() is not available at installation time.
       if (!isset($form_state['build_info']['file']) && !defined('MAINTENANCE_MODE')) {
         $item = menu_get_item();
-        if (!empty($item['file'])) {
-          $form_state['build_info']['file'] = $item['file'];
+        if (!empty($item['include_file'])) {
+          $form_state['build_info']['file'] = $item['include_file'];
         }
       }
 
@@ -280,19 +281,21 @@ function form_state_defaults() {
     'cache'=> FALSE,
     'method' => 'post',
     'groups' => array(),
+    'buttons' => array(),
   );
 }
 
 /**
  * Retrieves a form, caches it and processes it again.
  *
- * If your AHAH callback simulates the pressing of a button, then your AHAH
- * callback will need to do the same as what drupal_get_form would do when the
+ * If your AJAX callback simulates the pressing of a button, then your AJAX
+ * callback will need to do the same as what drupal_get_form() would do when the
  * button is pressed: get the form from the cache, run drupal_process_form over
- * it and then if it needs rebuild, run drupal_rebuild_form over it. Then send
+ * it and then if it needs rebuild, run drupal_rebuild_form() over it. Then send
  * back a part of the returned form.
- * $form_state['clicked_button']['#array_parents'] will help you to find which
- * part.
+ * $form_state['triggering_element']['#array_parents'] will help you to find
+ * which part.
+ * @see ajax_form_callback() for an example.
  *
  * @param $form_id
  *   The unique string identifying the desired form. If a function
@@ -303,14 +306,16 @@ function form_state_defaults() {
  *   may be found in node_forms(), search_forms(), and user_forms().
  * @param $form_state
  *   A keyed array containing the current state of the form.
- * @param $form_build_id
- *   If the AHAH callback calling this function only alters part of the form,
- *   then pass in the existing form_build_id so we can re-cache with the same
- *   csid.
+ * @param $old_form
+ *   (optional) A previously built $form. Used to retain the #build_id and
+ *   #action properties in AJAX callbacks and similar partial form rebuilds.
+ *   Should not be passed for regular rebuilds, for which the entire $form
+ *   should be rebuilt freshly.
+ *
  * @return
  *   The newly built form.
  */
-function drupal_rebuild_form($form_id, &$form_state, $form_build_id = NULL) {
+function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
   // AJAX and other contexts may call drupal_rebuild_form() even when
   // $form_state['rebuild'] isn't set, but _form_builder_handle_input_element()
   // needs to distinguish a rebuild from an initial build in order to process
@@ -320,17 +325,27 @@ function drupal_rebuild_form($form_id, &$form_state, $form_build_id = NULL) {
 
   $form = drupal_retrieve_form($form_id, $form_state);
 
-  if (!isset($form_build_id)) {
-    // We need a new build_id for the new version of the form.
-    $form_build_id = 'form-' . md5(mt_rand());
+  // If only parts of the form will be returned to the browser (e.g. AJAX or
+  // RIA clients), re-use the old #build_id to not require client-side code to
+  // manually update the hidden 'build_id' input element.
+  // Otherwise, a new #build_id is generated, to not clobber the previous
+  // build's data in the form cache; also allowing the user to go back to an
+  // earlier build, make changes, and re-submit.
+  $form['#build_id'] = isset($old_form['#build_id']) ? $old_form['#build_id'] : 'form-' . md5(mt_rand());
+
+  // #action defaults to request_uri(), but in case of AJAX and other partial
+  // rebuilds, the form is submitted to an alternate URL, and the original
+  // #action needs to be retained.
+  if (isset($old_form['#action'])) {
+    $form['#action'] = $old_form['#action'];
   }
-  $form['#build_id'] = $form_build_id;
+
   drupal_prepare_form($form_id, $form, $form_state);
 
   if (empty($form_state['no_cache'])) {
     // We cache the form structure and the form state so it can be retrieved
     // later for validation.
-    form_set_cache($form_build_id, $form, $form_state);
+    form_set_cache($form['#build_id'], $form, $form_state);
   }
 
   // Clear out all group associations as these might be different when
@@ -357,7 +372,7 @@ function form_get_cache($form_build_id, &$form_state) {
         $form_state = $cached->data + $form_state;
 
         // If the original form is contained in an include file, load the file.
-        // @see drupal_build_form()
+        // See drupal_build_form().
         if (!empty($form_state['build_info']['file']) && file_exists($form_state['build_info']['file'])) {
           require_once DRUPAL_ROOT . '/' . $form_state['build_info']['file'];
         }
@@ -402,6 +417,7 @@ function form_state_keys_no_cache() {
     'temporary',
     // Internal properties defined by form processing.
     'buttons',
+    'triggering_element',
     'clicked_button',
     'complete form',
     'groups',
@@ -443,7 +459,20 @@ function form_state_keys_no_cache() {
  *   Any additional arguments are passed on to the functions called by
  *   drupal_form_submit(), including the unique form constructor function.
  *   For example, the node_edit form requires that a node object be passed
- *   in here when it is called.
+ *   in here when it is called. Arguments that need to be passed by reference
+ *   should not be included here, but rather placed directly in the $form_state
+ *   build info array so that the reference can be preserved. For example, a
+ *   form builder function with the following signature:
+ *   @code
+ *   function mymodule_form($form, &$form_state, &$object) {
+ *   }
+ *   @endcode
+ *   would be called via drupal_form_submit() as follows:
+ *   @code
+ *   $form_state['values'] = $my_form_values;
+ *   $form_state['build_info']['args'] = array(&$object);
+ *   drupal_form_submit('mymodule_form', $form_state);
+ *   @endcode
  * For example:
  * @code
  * // register a new user
@@ -553,8 +582,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
   // builder function to pre-populate the $form array with form elements, which
   // the actual form builder function ($callback) expects. This allows for
   // pre-populating a form with common elements for certain forms, such as
-  // back/next/save buttons in multi-step form wizards.
-  // @see drupal_build_form()
+  // back/next/save buttons in multi-step form wizards. See drupal_build_form().
   if (isset($form_state['wrapper_callback']) && function_exists($form_state['wrapper_callback'])) {
     $form = call_user_func_array($form_state['wrapper_callback'], $args);
     // Put the prepopulated $form into $args.
@@ -640,7 +668,7 @@ function drupal_process_form($form_id, &$form, &$form_state) {
         // - The form is multistep.
         // In other cases, we only need the information expected by
         // drupal_redirect_form().
-        if ($batch['has_form_submits'] || !empty($form_state['rebuild']) || !empty($form_state['storage'])) {
+        if ($batch['has_form_submits'] || !empty($form_state['rebuild'])) {
           $batch['form_state'] = $form_state;
         }
         else {
@@ -935,27 +963,40 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
     // to form_set_error() be suppressed and not result in a form error, so
     // that a button that implements low-risk functionality (such as "Previous"
     // or "Add more") that doesn't require all user input to be valid can still
-    // have its submit handlers triggered. The clicked button's
+    // have its submit handlers triggered. The triggering element's
     // #limit_validation_errors property contains the information for which
     // errors are needed, and all other errors are to be suppressed. The
-    // #limit_validation_errors property is ignored if the button doesn't also
-    // define its own submit handlers, because it's too large a security risk to
-    // have any invalid user input when executing form-level submit handlers.
-    if (isset($form_state['clicked_button']['#limit_validation_errors']) && isset($form_state['clicked_button']['#submit'])) {
-      form_set_error(NULL, '', $form_state['clicked_button']['#limit_validation_errors']);
-    }
+    // #limit_validation_errors property is ignored if submit handlers will run,
+    // but the element doesn't have a #submit property, because it's too large a
+    // security risk to have any invalid user input when executing form-level
+    // submit handlers.
+    if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) {
+      form_set_error(NULL, '', $form_state['triggering_element']['#limit_validation_errors']);
+    }
+    // If submit handlers won't run (due to the submission having been triggered
+    // by an element whose #executes_submit_callback property isn't TRUE), then
+    // it's safe to suppress all validation errors, and we do so by default,
+    // which is particularly useful during an AJAX submission triggered by a
+    // non-button. An element can override this default by setting the
+    // #limit_validation_errors property. For button element types,
+    // #limit_validation_errors defaults to FALSE (via system_element_info()),
+    // so that full validation is their default behavior.
+    elseif (isset($form_state['triggering_element']) && !isset($form_state['triggering_element']['#limit_validation_errors']) && !$form_state['submitted']) {
+      form_set_error(NULL, '', array());
+    }
+    // As an extra security measure, explicitly turn off error suppression if
+    // one of the above conditions wasn't met. Since this is also done at the
+    // end of this function, doing it here is only to handle the rare edge case
+    // where a validate handler invokes form processing of another form.
     else {
-      // As an extra security measure, explicitly turn off error suppression.
-      // Since this is also done at the end of this function, doing it here is
-      // only to handle the rare edge case where a validate handler invokes form
-      // processing of another form.
       drupal_static_reset('form_set_error:limit_validation_errors');
     }
+
     // Make sure a value is passed when the field is required.
     // A simple call to empty() will not cut it here as some fields, like
     // checkboxes, can return a valid value of '0'. Instead, check the
     // length if it's a string, and the item count if it's an array.
-    // An unchecked checkbox has a #value of numeric 0, different than string
+    // An unchecked checkbox has a #value of integer 0, different than string
     // '0', which could be a valid value.
     if (isset($elements['#needs_validation']) && $elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
       form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
@@ -1088,7 +1129,7 @@ function form_execute_handlers($type, &$form, &$form_state) {
  *   assume that certain data exists within $form_state['values'], and while not
  *   doing anything with that data that requires it to be valid, PHP errors
  *   would be triggered if the input processing and validation steps were fully
- *   skipped. @see http://drupal.org/node/370537.
+ *   skipped. See http://drupal.org/node/370537.
  *
  * @return
  *   Return value is for internal use only. To get a list of errors, use
@@ -1196,7 +1237,7 @@ function form_builder($form_id, $element, &$form_state) {
   $element['#processed'] = FALSE;
 
   // Use element defaults.
-  if (isset($element['#type']) && ($info = element_info($element['#type']))) {
+  if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = element_info($element['#type']))) {
     // Overlay $info onto $element, retaining preexisting keys in $element.
     $element += $info;
     $element['#defaults_loaded'] = TRUE;
@@ -1274,6 +1315,12 @@ function form_builder($form_id, $element, &$form_state) {
     $array_parents[] = $key;
     $element[$key]['#array_parents'] = $array_parents;
 
+    // Prior to handling #weight, default element properties need to be applied.
+    if (isset($element[$key]['#type']) && empty($element[$key]['#defaults_loaded']) && ($info = element_info($element[$key]['#type']))) {
+      $element[$key] += $info;
+      $element[$key]['#defaults_loaded'] = TRUE;
+    }
+
     // Assign a decimal placeholder weight to preserve original array order.
     if (!isset($element[$key]['#weight'])) {
       $element[$key]['#weight'] = $count/1000;
@@ -1296,22 +1343,61 @@ function form_builder($form_id, $element, &$form_state) {
     $element['#after_build_done'] = TRUE;
   }
 
-  // Now that we've processed everything, we can go back to handle the funky
-  // Internet Explorer button-click scenario.
-  _form_builder_ie_cleanup($element, $form_state);
-
   // If there is a file element, we need to flip a flag so later the
   // form encoding can be set.
   if (isset($element['#type']) && $element['#type'] == 'file') {
     $form_state['has_file_element'] = TRUE;
   }
 
+  // Final tasks for the form element after form_builder() has run for all other
+  // elements.
   if (isset($element['#type']) && $element['#type'] == 'form') {
-    // We are on the top form.
     // If there is a file element, we set the form encoding.
     if (isset($form_state['has_file_element'])) {
       $element['#attributes']['enctype'] = 'multipart/form-data';
     }
+
+    // If a form contains a single textfield, and the ENTER key is pressed
+    // within it, Internet Explorer submits the form with no POST data
+    // identifying any submit button. Other browsers submit POST data as though
+    // the user clicked the first button. Therefore, to be as consistent as we
+    // can be across browsers, if no 'triggering_element' has been identified
+    // yet, default it to the first button.
+    if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) {
+      $form_state['triggering_element'] = $form_state['buttons'][0];
+    }
+
+    // If the triggering element specifies "button-level" validation and submit
+    // handlers to run instead of the default form-level ones, then add those to
+    // the form state.
+    foreach (array('validate', 'submit') as $type) {
+      if (isset($form_state['triggering_element']['#' . $type])) {
+        $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type];
+      }
+    }
+
+    // If the triggering element executes submit handlers, then set the form
+    // state key that's needed for those handlers to run.
+    if (!empty($form_state['triggering_element']['#executes_submit_callback'])) {
+      $form_state['submitted'] = TRUE;
+    }
+
+    // Special processing if the triggering element is a button.
+    if (isset($form_state['triggering_element']['#button_type'])) {
+      // Because there are several ways in which the triggering element could
+      // have been determined (including from input variables set by JavaScript
+      // or fallback behavior implemented for IE), and because buttons often
+      // have their #name property not derived from their #parents property, we
+      // can't assume that input processing that's happened up until here has
+      // resulted in $form_state['values'][BUTTON_NAME] being set. But it's
+      // common for forms to have several buttons named 'op' and switch on
+      // $form_state['values']['op'] during submit handler execution.
+      $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value'];
+
+      // @todo Legacy support. Remove in Drupal 8.
+      $form_state['clicked_button'] = $form_state['triggering_element'];
+    }
+
     // Update the copy of the complete form for usage in validation handlers.
     $form_state['complete form'] = $element;
   }
@@ -1338,15 +1424,45 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
     array_unshift($element['#parents'], $name);
   }
 
+  // Setting #disabled to TRUE results in user input being ignored, regardless
+  // of how the element is themed or whether JavaScript is used to change the
+  // control's attributes. However, it's good UI to let the user know that input
+  // is not wanted for the control. HTML supports two attributes for this:
+  // http://www.w3.org/TR/html401/interact/forms.html#h-17.12. If a form wants
+  // to start a control off with one of these attributes for UI purposes only,
+  // but still allow input to be processed if it's sumitted, it can set the
+  // desired attribute in #attributes directly rather than using #disabled.
+  // However, developers should think carefully about the accessibility
+  // implications of doing so: if the form expects input to be enterable under
+  // some condition triggered by JavaScript, how would someone who has
+  // JavaScript disabled trigger that condition? Instead, developers should
+  // consider whether a multi-step form would be more appropriate (#disabled can
+  // be changed from step to step). If one still decides to use JavaScript to
+  // affect when a control is enabled, then it is best for accessibility for the
+  // control to be enabled in the HTML, and disabled by JavaScript on document
+  // ready.
   if (!empty($element['#disabled'])) {
-    $element['#attributes']['disabled'] = 'disabled';
+    if (!empty($element['#allow_focus'])) {
+      $element['#attributes']['readonly'] = 'readonly';
+    }
+    else {
+      $element['#attributes']['disabled'] = 'disabled';
+    }
   }
 
   // Set the element's #value property.
   if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
     $value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
 
-    if ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))) {
+    // With JavaScript or other easy hacking, input can be submitted even for
+    // elements with #access=FALSE or #disabled=TRUE. For security, these must
+    // not be processed. Forms that set #disabled=TRUE on an element do not
+    // expect input for the element, and even forms submitted with
+    // drupal_form_submit() must not be able to get around this. Forms that set
+    // #access=FALSE on an element usually allow access for some users, so forms
+    // submitted with drupal_form_submit() may bypass access restriction and be
+    // treated as high-privelege users instead.
+    if (empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])))) {
       // Get the input for the current element. NULL values in the input need to
       // be explicitly distinguished from missing input. (see below)
       $input = $form_state['input'];
@@ -1408,33 +1524,42 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
     }
   }
 
-  // Determine which button (if any) was clicked to submit the form.
-  // We compare the incoming values with the buttons defined in the form,
-  // and flag the one that matches. We have to do some funky tricks to
-  // deal with Internet Explorer's handling of single-button forms, though.
-  if (!empty($form_state['input']) && isset($element['#executes_submit_callback'])) {
-    // First, accumulate a collection of buttons, divided into two bins:
-    // those that execute full submit callbacks and those that only validate.
-    $button_type = $element['#executes_submit_callback'] ? 'submit' : 'button';
-    $form_state['buttons'][$button_type][] = $element;
-
-    if (_form_button_was_clicked($element, $form_state)) {
-      $form_state['submitted'] = $form_state['submitted'] || $element['#executes_submit_callback'];
-
-      // In most cases, we want to use form_set_value() to manipulate
-      // the global variables. In this special case, we want to make sure that
-      // the value of this element is listed in $form_variables under 'op'.
-      $form_state['values'][$element['#name']] = $element['#value'];
-      $form_state['clicked_button'] = $element;
-
-      if (isset($element['#validate'])) {
-        $form_state['validate_handlers'] = $element['#validate'];
+  // Determine which element (if any) triggered the submission of the form and
+  // keep track of all the buttons in the form for form_state_values_clean().
+  // @todo We need to add a #access check here, so that someone can't fake the
+  //   click of a button they shouldn't have access to, but first we need to
+  //   fix file.module's managed_file element pipeline to handle the click of
+  //   the remove button in a submit handler instead of in a #process function.
+  //   During the first run of form_builder() after the form is submitted,
+  //   #process functions need to return the expanded element with child
+  //   elements' #access properties matching what they were when the form was
+  //   displayed to the user, since that is what we are processing input for.
+  //   Changes to the form (like toggling the upload/remove button) need to wait
+  //   until form rebuild: http://drupal.org/node/736298.
+  if (!empty($form_state['input'])) {
+    // Detect if the element triggered the submission via AJAX.
+    if (_form_element_triggered_scripted_submission($element, $form_state)) {
+      $form_state['triggering_element'] = $element;
+    }
+
+    // If the form was submitted by the browser rather than via AJAX, then it
+    // can only have been triggered by a button, and we need to determine which
+    // button within the constraints of how browsers provide this information.
+    if (isset($element['#button_type'])) {
+      // All buttons in the form need to be tracked for
+      // form_state_values_clean() and for the form_builder() code that handles
+      // a form submission containing no button information in $_POST.
+      // @todo When #access is checked in an outer if statement (see above), it
+      //   won't need to be checked here.
+      if ($form_state['programmed'] || !isset($element['#access']) || $element['#access']) {
+        $form_state['buttons'][] = $element;
       }
-      if (isset($element['#submit'])) {
-        $form_state['submit_handlers'] = $element['#submit'];
+      if (_form_button_was_clicked($element, $form_state)) {
+        $form_state['triggering_element'] = $element;
       }
     }
   }
+
   // Set the element's value in $form_state['values'], but only, if its key
   // does not exist yet (a #value_callback may have already populated it).
   $values = $form_state['values'];
@@ -1447,21 +1572,52 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
 }
 
 /**
- * Helper function to handle the sometimes-convoluted logic of button
- * click detection.
+ * Helper function to handle the convoluted logic of button click detection.
  *
- * In Internet Explorer, if ONLY one submit button is present, AND the
- * enter key is used to submit the form, no form value is sent for it
- * and we'll never detect a match. That special case is handled by
- * _form_builder_ie_cleanup().
+ * This detects button or non-button controls that trigger a form submission via
+ * AJAX or some other scriptable environment. These environments can set the
+ * special input key '_triggering_element_name' to identify the triggering
+ * element. If the name alone doesn't identify the element uniquely, the input
+ * key '_triggering_element_value' may also be set to require a match on element
+ * value. An example where this is needed is if there are several buttons all
+ * named 'op', and only differing in their value.
  */
-function _form_button_was_clicked($form, &$form_state) {
+function _form_element_triggered_scripted_submission($element, &$form_state) {
+  if (!empty($form_state['input']['_triggering_element_name']) && $element['#name'] == $form_state['input']['_triggering_element_name']) {
+    if (empty($form_state['input']['_triggering_element_value']) || $form_state['input']['_triggering_element_value'] == $element['#value']) {
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Helper function to handle the convoluted logic of button click detection.
+ *
+ * This detects button controls that trigger a form submission by being clicked
+ * and having the click processed by the browser rather than being captured by
+ * JavaScript. Essentially, it detects if the button's name and value are part
+ * of the POST data, but with extra code to deal with the convoluted way in
+ * which browsers submit data for image button clicks.
+ *
+ * This does not detect button clicks processed by AJAX (that is done in
+ * _form_element_triggered_scripted_submission()) and it does not detect form
+ * submissions from Internet Explorer in response to an ENTER key pressed in a
+ * textfield (form_builder() has extra code for that).
+ *
+ * Because this function contains only part of the logic needed to determine
+ * $form_state['triggering_element'], it should not be called from anywhere
+ * other than within the Form API. Form validation and submit handlers needing
+ * to know which button was clicked should get that information from
+ * $form_state['triggering_element'].
+ */
+function _form_button_was_clicked($element, &$form_state) {
   // First detect normal 'vanilla' button clicks. Traditionally, all
   // standard buttons on a form share the same name (usually 'op'),
   // and the specific return value is used to determine which was
   // clicked. This ONLY works as long as $form['#name'] puts the
   // value at the top level of the tree of $_POST data.
-  if (isset($form_state['input'][$form['#name']]) && $form_state['input'][$form['#name']] == $form['#value']) {
+  if (isset($form_state['input'][$element['#name']]) && $form_state['input'][$element['#name']] == $element['#value']) {
     return TRUE;
   }
   // When image buttons are clicked, browsers do NOT pass the form element
@@ -1469,41 +1625,12 @@ function _form_button_was_clicked($form, &$form_state) {
   // coordinates of the click on the button image. This means that image
   // buttons MUST have unique $form['#name'] values, but the details of
   // their $_POST data should be ignored.
-  elseif (!empty($form['#has_garbage_value']) && isset($form['#value']) && $form['#value'] !== '') {
+  elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') {
     return TRUE;
   }
   return FALSE;
 }
 
-/**
- * In IE, if only one submit button is present, AND the enter key is
- * used to submit the form, no form value is sent for it and our normal
- * button detection code will never detect a match. We call this
- * function after all other button-detection is complete to check
- * for the proper conditions, and treat the single button on the form
- * as 'clicked' if they are met.
- */
-function _form_builder_ie_cleanup($form, &$form_state) {
-  // Quick check to make sure we're always looking at the full form
-  // and not a sub-element.
-  if (!empty($form['#type']) && $form['#type'] == 'form') {
-    // If we haven't recognized a submission yet, and there's a single
-    // submit button, we know that we've hit the right conditions. Grab
-    // the first one and treat it as the clicked button.
-    if (empty($form_state['submitted']) && !empty($form_state['buttons']['submit']) && empty($form_state['buttons']['button'])) {
-      $button = $form_state['buttons']['submit'][0];
-
-      // Set up all the $form_state information that would have been
-      // populated had the button been recognized earlier.
-      $form_state['submitted'] = TRUE;
-      $form_state['submit_handlers'] = empty($button['#submit']) ? NULL : $button['#submit'];
-      $form_state['validate_handlers'] = empty($button['#validate']) ? NULL : $button['#validate'];
-      $form_state['values'][$button['#name']] = $button['#value'];
-      $form_state['clicked_button'] = $button;
-    }
-  }
-}
-
 /**
  * Removes internal Form API elements and buttons from submitted form values.
  *
@@ -1527,37 +1654,35 @@ function form_state_values_clean(&$form_state) {
   unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']);
 
   // Remove button values.
-  // form_builder() collects all button elements in a form, keyed by button
-  // type. We remove the button value separately for each button element.
-  foreach ($form_state['buttons'] as $button_type => $buttons) {
-    foreach ($buttons as $button) {
-      // Remove this button's value from the submitted form values by finding
-      // the value corresponding to this button.
-      // We iterate over the #parents of this button and move a reference to
-      // each parent in $form_state['values']. For example, if #parents is:
-      //   array('foo', 'bar', 'baz')
-      // then the corresponding $form_state['values'] part will look like this:
-      // array(
-      //   'foo' => array(
-      //     'bar' => array(
-      //       'baz' => 'button_value',
-      //     ),
-      //   ),
-      // )
-      // We start by (re)moving 'baz' to $last_parent, so we are able unset it
-      // at the end of the iteration. Initially, $values will contain a
-      // reference to $form_state['values'], but in the iteration we move the
-      // reference to $form_state['values']['foo'], and finally to
-      // $form_state['values']['foo']['bar'], which is the level where we can
-      // unset 'baz' (that is stored in $last_parent).
-      $parents = $button['#parents'];
-      $values = &$form_state['values'];
-      $last_parent = array_pop($parents);
-      foreach ($parents as $parent) {
-        $values = &$values[$parent];
-      }
-      unset($values[$last_parent]);
-    }
+  // form_builder() collects all button elements in a form. We remove the button
+  // value separately for each button element.
+  foreach ($form_state['buttons'] as $button) {
+    // Remove this button's value from the submitted form values by finding
+    // the value corresponding to this button.
+    // We iterate over the #parents of this button and move a reference to
+    // each parent in $form_state['values']. For example, if #parents is:
+    //   array('foo', 'bar', 'baz')
+    // then the corresponding $form_state['values'] part will look like this:
+    // array(
+    //   'foo' => array(
+    //     'bar' => array(
+    //       'baz' => 'button_value',
+    //     ),
+    //   ),
+    // )
+    // We start by (re)moving 'baz' to $last_parent, so we are able unset it
+    // at the end of the iteration. Initially, $values will contain a
+    // reference to $form_state['values'], but in the iteration we move the
+    // reference to $form_state['values']['foo'], and finally to
+    // $form_state['values']['foo']['bar'], which is the level where we can
+    // unset 'baz' (that is stored in $last_parent).
+    $parents = $button['#parents'];
+    $values = &$form_state['values'];
+    $last_parent = array_pop($parents);
+    foreach ($parents as $parent) {
+      $values = &$values[$parent];
+    }
+    unset($values[$last_parent]);
   }
 }
 
@@ -1622,18 +1747,11 @@ function form_type_image_button_value($form, $input, $form_state) {
  */
 function form_type_checkbox_value($element, $input = FALSE) {
   if ($input !== FALSE) {
-    if (empty($element['#disabled'])) {
-      // Successful (checked) checkboxes are present with a value (possibly '0').
-      // http://www.w3.org/TR/html401/interact/forms.html#successful-controls
-      // For an unchecked checkbox, we return numeric 0, so we can explicitly
-      // test for a value different than string '0'.
-      return isset($input) ? $element['#return_value'] : 0;
-    }
-    else {
-      // Disabled form controls are not submitted by the browser. Ignore any
-      // submitted value and always return default.
-      return $element['#default_value'];
-    }
+    // Successful (checked) checkboxes are present with a value (possibly '0').
+    // http://www.w3.org/TR/html401/interact/forms.html#successful-controls
+    // For an unchecked checkbox, we return integer 0, so we can explicitly
+    // test for a value different than string '0'.
+    return isset($input) ? $element['#return_value'] : 0;
   }
 }
 
@@ -1830,7 +1948,11 @@ function _form_options_flatten($array) {
 }
 
 /**
- * Theme select form element.
+ * Returns HTML for a select form element.
+ *
+ * It is possible to group options together; to do this, change the format of
+ * $options to an associative array in which the keys are group labels, and the
+ * values are associative arrays in the normal $options format.
  *
  * @param $variables
  *   An associative array containing:
@@ -1838,14 +1960,7 @@ function _form_options_flatten($array) {
  *     Properties used: #title, #value, #options, #description, #extra,
  *     #multiple, #required, #name, #attributes, #size.
  *
- * @return
- *   A themed HTML string representing the form element.
- *
  * @ingroup themeable
- *
- * It is possible to group options together; to do this, change the format of
- * $options to an associative array in which the keys are group labels, and the
- * values are associative arrays in the normal $options format.
  */
 function theme_select($variables) {
   $element = $variables['element'];
@@ -1950,7 +2065,7 @@ function form_get_options($element, $key) {
 }
 
 /**
- * Theme a fieldset form element.
+ * Returns HTML for a fieldset form element and its children.
  *
  * @param $variables
  *   An associative array containing:
@@ -1958,9 +2073,6 @@ function form_get_options($element, $key) {
  *     Properties used: #attributes, #children, #collapsed, #collapsible,
  *     #description, #id, #title, #value.
  *
- * @return
- *   A themed HTML string representing the group of items.
- *
  * @ingroup themeable
  */
 function theme_fieldset($variables) {
@@ -1985,7 +2097,7 @@ function theme_fieldset($variables) {
 }
 
 /**
- * Theme a radio button form element.
+ * Returns HTML for a radio button form element.
  *
  * @param $variables
  *   An associative array containing:
@@ -1993,9 +2105,6 @@ function theme_fieldset($variables) {
  *     Properties used: #required, #return_value, #value, #attributes, #title,
  *     #description
  *
- * @return
- *   A themed HTML string representing the form item group.
- *
  * @ingroup themeable
  */
 function theme_radio($variables) {
@@ -2012,7 +2121,7 @@ function theme_radio($variables) {
 }
 
 /**
- * Theme a set of radio button form elements.
+ * Returns HTML for a set of radio button form elements.
  *
  * @param $variables
  *   An associative array containing:
@@ -2020,20 +2129,19 @@ function theme_radio($variables) {
  *     Properties used: #title, #value, #options, #description, #required,
  *     #attributes, #children.
  *
- * @return
- *   A themed HTML string representing the radio button set.
- *
  * @ingroup themeable
  */
 function theme_radios($variables) {
   $element = $variables['element'];
-  $class = 'form-radios';
+  $attributes = array();
+  if (!empty($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  $attributes['class'] = 'form-radios';
   if (!empty($element['#attributes']['class'])) {
-    $class .= ' ' . implode(' ', $element['#attributes']['class']);
+    $attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']);
   }
-  $element['#children'] = '<div class="' . $class . '">' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
-
-  return $element['#children'];
+  return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
 }
 
 /**
@@ -2090,7 +2198,7 @@ function password_confirm_validate($element, &$element_state) {
 }
 
 /**
- * Theme a date selection form element.
+ * Returns HTML for a date selection form element.
  *
  * @param $variables
  *   An associative array containing:
@@ -2098,9 +2206,6 @@ function password_confirm_validate($element, &$element_state) {
  *     Properties used: #title, #value, #options, #description, #required,
  *     #attributes.
  *
- * @return
- *   A themed HTML string representing the date selection boxes.
- *
  * @ingroup themeable
  */
 function theme_date($variables) {
@@ -2216,9 +2321,12 @@ function form_process_radios($element) {
           '#attributes' => $element['#attributes'],
           '#parents' => $element['#parents'],
           '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
-          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
-          '#disabled' => isset($element['#disabled']) ? $element['#disabled'] : NULL,
         );
+        foreach (array('#ajax', '#disabled', '#allow_focus') as $property) {
+          if (isset($element[$property])) {
+            $element[$key][$property] = $element[$property];
+          }
+        }
       }
     }
   }
@@ -2226,7 +2334,7 @@ function form_process_radios($element) {
 }
 
 /**
- * Theme a checkbox form element.
+ * Returns HTML for a checkbox form element.
  *
  * @param $variables
  *   An associative array containing:
@@ -2234,9 +2342,6 @@ function form_process_radios($element) {
  *     Properties used: #title, #value, #return_value, #description, #required,
  *     #attributes.
  *
- * @return
- *   A themed HTML string representing the checkbox.
- *
  * @ingroup themeable
  */
 function theme_checkbox($variables) {
@@ -2248,7 +2353,7 @@ function theme_checkbox($variables) {
   $checkbox .= 'name="' . $element['#name'] . '" ';
   $checkbox .= 'id="' . $element['#id'] . '" ' ;
   $checkbox .= 'value="' . $element['#return_value'] . '" ';
-  // Unchecked checkbox has #value of numeric 0.
+  // Unchecked checkbox has #value of integer 0.
   if ($element['#value'] !== 0 && $element['#value'] == $element['#return_value']) {
     $checkbox .= 'checked="checked" ';
   }
@@ -2258,27 +2363,26 @@ function theme_checkbox($variables) {
 }
 
 /**
- * Theme a set of checkbox form elements.
+ * Returns HTML for a set of checkbox form elements.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #children, #attributes.
  *
- * @return
- *   A themed HTML string representing the checkbox set.
- *
  * @ingroup themeable
  */
 function theme_checkboxes($variables) {
   $element = $variables['element'];
-  $class = 'form-checkboxes';
+  $attributes = array();
+  if (!empty($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  $attributes['class'] = 'form-checkboxes';
   if (!empty($element['#attributes']['class'])) {
-    $class .= ' ' . implode(' ', $element['#attributes']['class']);
+    $attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']);
   }
-  $element['#children'] = '<div class="' . $class . '">' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
-
-  return $element['#children'];
+  return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
 }
 
 /**
@@ -2297,7 +2401,6 @@ function form_pre_render_conditional_form_element($element) {
   }
 
   if (isset($element['#title']) || isset($element['#description'])) {
-    unset($element['#id']);
     $element['#theme_wrappers'][] = 'form_element';
   }
   return $element;
@@ -2319,15 +2422,35 @@ function form_process_checkboxes($element) {
           '#return_value' => $key,
           '#default_value' => isset($value[$key]) ? $key : NULL,
           '#attributes' => $element['#attributes'],
-          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
-          '#disabled' => isset($element['#disabled']) ? $element['#disabled'] : NULL,
         );
+        foreach (array('#ajax', '#disabled', '#allow_focus') as $property) {
+          if (isset($element[$property])) {
+            $element[$key][$property] = $element[$property];
+          }
+        }
       }
     }
   }
   return $element;
 }
 
+/**
+ * Processes a form actions container element.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   form actions container.
+ * @param $form_state
+ *   The $form_state array for the form this element belongs to.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_actions($element, &$form_state) {
+  $element['#attributes']['class'][] = 'form-actions';
+  return $element;
+}
+
 /**
  * Processes a container element.
  *
@@ -2348,14 +2471,12 @@ function form_process_container($element, &$form_state) {
 }
 
 /**
- * Adds a container for grouped items.
+ * Returns HTML for a container for grouped form items.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #id, #attributes, #children.
- * @return
- *   A themed HTML string representing the form element.
  *
  * @ingroup themeable
  */
@@ -2370,16 +2491,9 @@ function theme_container($variables) {
 }
 
 /**
- * Formats a table with radio buttons or checkboxes.
+ * Returns HTML for a table with radio buttons or checkboxes.
  *
- * @param $variables
- *   An associative array containing:
- *   - element: An associative array containing the properties and children of
- *     the tableselect element. Properties used: #header, #options, #empty,
- *     and #js_select. The #options property is an array of selection options;
- *     each array element of #options is an array of properties. These
- *     properties can include #attributes, which is added to the
- *     table row's HTML attributes (see theme_table()). Example:
+ * An example of per-row options:
  * @code
  * $options = array();
  * $options[0]['title'] = "A red row"
@@ -2394,8 +2508,14 @@ function theme_container($variables) {
  * );
  * @endcode
  *
- * @return
- *   A themed HTML string representing the table.
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties and children of
+ *     the tableselect element. Properties used: #header, #options, #empty,
+ *     and #js_select. The #options property is an array of selection options;
+ *     each array element of #options is an array of properties. These
+ *     properties can include #attributes, which is added to the
+ *     table row's HTML attributes; see theme_table().
  *
  * @ingroup themeable
  */
@@ -2471,8 +2591,6 @@ function form_process_tableselect($element) {
             '#return_value' => $key,
             '#default_value' => isset($value[$key]) ? $key : NULL,
             '#attributes' => $element['#attributes'],
-            '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
-            '#disabled' => isset($element['#disabled']) ? $element['#disabled'] : NULL,
           );
         }
         else {
@@ -2487,10 +2605,13 @@ function form_process_tableselect($element) {
             '#attributes' => $element['#attributes'],
             '#parents' => $element['#parents'],
             '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
-            '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
-            '#disabled' => isset($element['#disabled']) ? $element['#disabled'] : NULL,
           );
         }
+        foreach (array('#ajax', '#disabled', '#allow_focus') as $property) {
+          if (isset($element[$property])) {
+            $element[$key][$property] = $element[$property];
+          }
+        }
       }
     }
   }
@@ -2537,7 +2658,7 @@ function form_process_fieldset(&$element, &$form_state) {
     if (!isset($element['#attributes']['class'])) {
       $element['#attributes']['class'] = array();
     }
-
+    $element['#attributes']['class'][] = 'form-wrapper';
     $element['#attributes']['class'][] = 'collapsible';
     if (!empty($element['#collapsed'])) {
       $element['#attributes']['class'][] = 'collapsed';
@@ -2638,16 +2759,13 @@ function form_process_vertical_tabs($element, &$form_state) {
 }
 
 /**
- * Makes the element's children fieldsets be vertical tabs.
+ * Returns HTML for an element's children fieldsets as vertical tabs.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties and children of the
  *     fieldset. Properties used: #children.
  *
- * @return
- *   A themed HTML string representing the form element.
- *
  * @ingroup themeable
  */
 function theme_vertical_tabs($variables) {
@@ -2655,20 +2773,19 @@ function theme_vertical_tabs($variables) {
   // Add required JavaScript and Stylesheet.
   drupal_add_library('system', 'vertical-tabs');
 
-  return '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
+  $output = '<h2 class="element-invisible">' . t('Vertical Tabs') . '</h2>';
+  $output .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
+  return $output;
 }
 
 /**
- * Theme a submit button form element.
+ * Returns HTML for a submit button form element.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #attributes, #button_type, #name, #value.
  *
- * @return
- *   A themed HTML string representing the form element.
- *
  * @ingroup themeable
  */
 function theme_submit($variables) {
@@ -2677,16 +2794,13 @@ function theme_submit($variables) {
 }
 
 /**
- * Theme a button form element.
+ * Returns HTML for a button form element.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #attributes, #button_type, #name, #value.
  *
- * @return
- *   A themed HTML string representing the form element.
- *
  * @ingroup themeable
  */
 function theme_button($variables) {
@@ -2697,15 +2811,13 @@ function theme_button($variables) {
 }
 
 /**
- * Theme a image button form element.
+ * Returns HTML for an image button form element.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #attributes, #button_type, #name, #value, #title, #src.
  *
- * @return
- *   A themed HTML string representing the form element.
  * @ingroup themeable
  */
 function theme_image_button($variables) {
@@ -2722,16 +2834,13 @@ function theme_image_button($variables) {
 }
 
 /**
- * Theme a hidden form element.
+ * Returns HTML for a hidden form element.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #name, #value, #attributes.
  *
- * @return
- *   A themed HTML string representing the form element.
- *
  * @ingroup themeable
  */
 function theme_hidden($variables) {
@@ -2740,7 +2849,7 @@ function theme_hidden($variables) {
 }
 
 /**
- * Theme a textfield form element.
+ * Returns HTML for a textfield form element.
  *
  * @param $variables
  *   An associative array containing:
@@ -2748,9 +2857,6 @@ function theme_hidden($variables) {
  *     Properties used: #title, #value, #description, #size, #maxlength,
  *     #required, #attributes, #autocomplete_path.
  *
- * @return
- *   A themed HTML string representing the textfield.
- *
  * @ingroup themeable
  */
 function theme_textfield($variables) {
@@ -2774,16 +2880,13 @@ function theme_textfield($variables) {
 }
 
 /**
- * Theme a form.
+ * Returns HTML for a form.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #action, #method, #attributes, #children
  *
- * @return
- *   A themed HTML string representing the form.
- *
  * @ingroup themeable
  */
 function theme_form($variables) {
@@ -2794,7 +2897,7 @@ function theme_form($variables) {
 }
 
 /**
- * Theme a textarea form element.
+ * Returns HTML for a textarea form element.
  *
  * @param $variables
  *   An associative array containing:
@@ -2802,9 +2905,6 @@ function theme_form($variables) {
  *     Properties used: #title, #value, #description, #rows, #cols, #required,
  *     #attributes
  *
- * @return
- *   A themed HTML string representing the textarea.
- *
  * @ingroup themeable
  */
 function theme_textarea($variables) {
@@ -2830,7 +2930,7 @@ function theme_textarea($variables) {
 }
 
 /**
- * Theme a password form element.
+ * Returns HTML for a password form element.
  *
  * @param $variables
  *   An associative array containing:
@@ -2838,9 +2938,6 @@ function theme_textarea($variables) {
  *     Properties used: #title, #value, #description, #size, #maxlength,
  *     #required, #attributes.
  *
- * @return
- *   A themed HTML string representing the form element.
- *
  * @ingroup themeable
  */
 function theme_password($variables) {
@@ -2868,7 +2965,10 @@ function form_process_weight($element) {
 }
 
 /**
- * Theme a file upload form element.
+ * Returns HTML for a file upload form element.
+ *
+ * For assistance with handling the uploaded file correctly, see the API
+ * provided by file.inc.
  *
  * @param $variables
  *   An associative array containing:
@@ -2876,13 +2976,7 @@ function form_process_weight($element) {
  *     Properties used: #title, #name, #size, #description, #required,
  *     #attributes.
  *
- * @return
- *   A themed HTML string representing the field.
- *
  * @ingroup themeable
- *
- * For assistance with handling the uploaded file correctly, see the API
- * provided by file.inc.
  */
 function theme_file($variables) {
   $element = $variables['element'];
@@ -2891,7 +2985,7 @@ function theme_file($variables) {
 }
 
 /**
- * Theme a form element.
+ * Returns HTML for a form element.
  *
  * Each form element is wrapped in a DIV with #type and #name classes. In
  * addition to the element itself, the div contains a label before or after
@@ -2905,6 +2999,9 @@ function theme_file($variables) {
  *   for radio and checkbox #type elements as set in system_element_info().
  *   If the #title is empty but the field is #required, the label will
  *   contain only the required marker.
+ * - invisible: Labels are critical for screen readers to enable them to
+ *   properly navigate through forms but can be visually distracting. This
+ *   property hides the label for everyone except screen readers.
  * - attribute: Set the title attribute on the element to create a tooltip
  *   but output no label element. This is supported only for checkboxes
  *   and radios in form_pre_render_conditional_form_element(). It is used
@@ -2925,9 +3022,6 @@ function theme_file($variables) {
  *     Properties used: #title, #title_display, #description, #id, #required,
  *     #children, #type, #name.
  *
- * @return
- *   A string representing the form element.
- *
  * @ingroup themeable
  */
 function theme_form_element($variables) {
@@ -2961,6 +3055,7 @@ function theme_form_element($variables) {
       $output .= ' ' . $element['#children'] . "\n";
       break;
 
+    case 'invisible':
     case 'after':
       $output .= ' ' . $element['#children'];
       $output .= ' ' . theme('form_element_label', $variables) . "\n";
@@ -2987,13 +3082,11 @@ function theme_form_element($variables) {
 }
 
 /**
- * Theme the marker for required form elements.
+ * Returns HTML for a marker for required form elements.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
- * @return
- *   A string representing the marker to identify required form elements.
  *
  * @ingroup themeable
  */
@@ -3008,7 +3101,7 @@ function theme_form_required_marker($variables) {
 }
 
 /**
- * Theme a form element label and required marker.
+ * Returns HTML for a form element label and required marker.
  *
  * Form element labels include the #title and a #required marker. The label is
  * associated with the element itself by the element #id. Labels may appear
@@ -3026,8 +3119,6 @@ function theme_form_required_marker($variables) {
  *   An associative array containing:
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #required, #title, #id, #value, #description.
- * @return
- *   A string representing the form element label.
  *
  * @ingroup themeable
  */
@@ -3047,10 +3138,15 @@ function theme_form_element_label($variables) {
   $title = filter_xss_admin($element['#title']);
 
   $attributes = array();
+  // Style the label as class option to display inline with the element.
   if ($element['#title_display'] == 'after') {
-    // Style the label as class option to display inline with the element.
     $attributes['class'] = 'option';
   }
+  // Show label only to screen readers to avoid disruption in visual flows.
+  elseif ($element['#title_display'] == 'invisible') {
+    $attributes['class'] = 'element-invisible';
+  }
+
   if (!empty($element['#id'])) {
     $attributes['for'] = $element['#id'];
   }
diff --git a/includes/graph.inc b/includes/graph.inc
index efa6b19222af31f41a7b32a152d09e845f3a8261..01a273391504e35387554ebc091af18d87ae9cf5 100644
--- a/includes/graph.inc
+++ b/includes/graph.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: graph.inc,v 1.3 2009/07/28 19:06:15 dries Exp $
+// $Id: graph.inc,v 1.4 2010/04/22 19:21:12 dries Exp $
 
 /**
  * @file
@@ -27,7 +27,7 @@
  *   On return you will also have:
  *   @code
  *     $graph[1]['paths'][2] = 1;
- *     $graph[1]['paths'][3] = 2;
+ *     $graph[1]['paths'][3] = 1;
  *     $graph[2]['reverse_paths'][1] = 1;
  *     $graph[3]['reverse_paths'][1] = 1;
  *   @endcode
diff --git a/includes/install.core.inc b/includes/install.core.inc
index 28a77c945a54d8c68c0d09606b888ff6b19fd292..6fed3728dc6b4dcc06b95e560350f75df2597ee2 100644
--- a/includes/install.core.inc
+++ b/includes/install.core.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: install.core.inc,v 1.5 2010/03/17 14:04:38 dries Exp $
+// $Id: install.core.inc,v 1.13 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -43,7 +43,7 @@ define('INSTALL_TASK_RUN_IF_REACHED', 2);
 define('INSTALL_TASK_RUN_IF_NOT_COMPLETED', 3);
 
 /**
- * Install Drupal either interactively or via an array of passed-in settings.
+ * Installs Drupal either interactively or via an array of passed-in settings.
  *
  * The Drupal installation happens in a series of steps, which may be spread
  * out over multiple page requests. Each request begins by trying to determine
@@ -107,7 +107,7 @@ function install_drupal($settings = array()) {
 }
 
 /**
- * Return an array of default settings for the global installation state.
+ * Returns an array of default settings for the global installation state.
  *
  * The installation state is initialized with these settings at the beginning
  * of each page request. They may evolve during the page request, but they are
@@ -278,6 +278,7 @@ function install_begin_request(&$install_state) {
     // Initialize the database system. Note that the connection
     // won't be initialized until it is actually requested.
     require_once DRUPAL_ROOT . '/includes/database/database.inc';
+    spl_autoload_register('db_autoload');
 
     // Verify the last completed task in the database, if there is one.
     $task = install_verify_completed_task();
@@ -312,7 +313,7 @@ function install_begin_request(&$install_state) {
 }
 
 /**
- * Run all tasks for the current installation request.
+ * Runs all tasks for the current installation request.
  *
  * In the case of an interactive installation, all tasks will be attempted
  * until one is reached that has output which needs to be displayed to the
@@ -322,6 +323,7 @@ function install_begin_request(&$install_state) {
  * @param $install_state
  *   An array of information about the current installation state. This is
  *   passed along to each task, so it can be modified if necessary.
+ *
  * @return
  *   HTML output from the last completed task.
  */
@@ -349,7 +351,6 @@ function install_run_tasks(&$install_state) {
       $install_state['tasks_performed'][] = $task_name;
       $install_state['installation_finished'] = empty($tasks_to_perform);
       if ($install_state['database_tables_exist'] && ($task['run'] == INSTALL_TASK_RUN_IF_NOT_COMPLETED || $install_state['installation_finished'])) {
-        drupal_install_initialize_database();
         variable_set('install_task', $install_state['installation_finished'] ? 'done' : $task_name);
       }
     }
@@ -363,13 +364,14 @@ function install_run_tasks(&$install_state) {
 }
 
 /**
- * Run an individual installation task.
+ * Runs an individual installation task.
  *
  * @param $task
  *   An array of information about the task to be run.
  * @param $install_state
  *   An array of information about the current installation state. This is
  *   passed in by reference so that it can be modified by the task.
+ *
  * @return
  *   The output of the task function, if there is any.
  */
@@ -404,8 +406,14 @@ function install_run_task($task, &$install_state) {
       // For non-interactive forms, submit the form programmatically with the
       // values taken from the installation state. Throw an exception if any
       // errors were encountered.
-      $form_state = array('values' => !empty($install_state['forms'][$function]) ? $install_state['forms'][$function] : array());
-      drupal_form_submit($function, $form_state, $install_state);
+      $form_state = array(
+        'values' => !empty($install_state['forms'][$function]) ? $install_state['forms'][$function] : array(),
+        // We need to pass $install_state by reference in order for forms to
+        // modify it, since the form API will use it in call_user_func_array(),
+        // which requires that referenced variables be passed explicitly.
+        'build_info' => array('args' => array(&$install_state)),
+      );
+      drupal_form_submit($function, $form_state);
       $errors = form_get_errors();
       if (!empty($errors)) {
         throw new Exception(implode("\n", $errors));
@@ -469,7 +477,7 @@ function install_run_task($task, &$install_state) {
 }
 
 /**
- * Return a list of tasks to perform during the current installation request.
+ * Returns a list of tasks to perform during the current installation request.
  *
  * Note that the list of tasks can change based on the installation state as
  * the page request evolves (for example, if an installation profile hasn't
@@ -477,6 +485,7 @@ function install_run_task($task, &$install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   A list of tasks to be performed, with associated metadata.
  */
@@ -499,7 +508,7 @@ function install_tasks_to_perform($install_state) {
 }
 
 /**
- * Return a list of all tasks the installer currently knows about.
+ * Returns a list of all tasks the installer currently knows about.
  *
  * This function will return tasks regardless of whether or not they are
  * intended to run on the current page request. However, the list can change
@@ -509,6 +518,7 @@ function install_tasks_to_perform($install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   A list of tasks, with associated metadata.
  */
@@ -610,13 +620,14 @@ function install_tasks($install_state) {
 }
 
 /**
- * Return a list of tasks that should be displayed to the end user.
+ * Returns a list of tasks that should be displayed to the end user.
  *
  * The output of this function is a list suitable for sending to
  * theme_task_list().
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   A list of tasks, with keys equal to the machine-readable task name and
  *   values equal to the name that should be displayed.
@@ -634,12 +645,13 @@ function install_tasks_to_display($install_state) {
 }
 
 /**
- * Return the URL that should be redirected to during an installation request.
+ * Returns the URL that should be redirected to during an installation request.
  *
  * The output of this function is suitable for sending to install_goto().
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The URL to redirect to.
  *
@@ -650,11 +662,11 @@ function install_redirect_url($install_state) {
 }
 
 /**
- * Return the complete URL that should be redirected to during an installation
- * request.
+ * Returns the complete URL redirected to during an installation request.
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The complete URL to redirect to.
  *
@@ -666,7 +678,7 @@ function install_full_redirect_url($install_state) {
 }
 
 /**
- * Display themed installer output and end the page request.
+ * Displays themed installer output and ends the page request.
  *
  * Installation tasks should use drupal_set_title() to set the desired page
  * title, but otherwise this function takes care of theming the overall page
@@ -697,6 +709,7 @@ function install_display_output($output, $install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   A themed status report, or an exception if there are requirement errors.
  *   Otherwise, no output is returned, so that the next task can be run
@@ -784,7 +797,7 @@ function install_verify_completed_task() {
 }
 
 /**
- * Verify existing settings.php
+ * Verifies the existing settings in settings.php.
  */
 function install_verify_settings() {
   global $db_prefix, $databases;
@@ -816,6 +829,7 @@ function install_verify_pdo() {
  *   An associative array containing the current state of the form.
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The form API definition for the database configuration form.
  */
@@ -832,13 +846,14 @@ function install_settings_form($form, &$form_state, &$install_state) {
   drupal_set_title(st('Database configuration'));
 
   $drivers = drupal_detect_database_types();
+  $drivers_keys = array_keys($drivers);
 
   $form['driver'] = array(
     '#type' => 'radios',
     '#title' => st('Database type'),
     '#required' => TRUE,
     '#options' => $drivers,
-    '#default_value' => !empty($database['driver']) ? $database['driver'] : current(array_keys($drivers)),
+    '#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers_keys),
     '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_distribution_name())),
   );
   if (count($drivers) == 1) {
@@ -846,7 +861,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     $form['driver']['#description'] .= ' ' . st('Your PHP configuration only supports the %driver database type so it has been automatically selected.', array('%driver' => current($drivers)));
   }
 
-  // Database name
+  // Database name.
   $form['database'] = array(
     '#type' => 'textfield',
     '#title' => st('Database name'),
@@ -856,7 +871,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     '#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())),
   );
 
-  // Database username
+  // Database username.
   $form['username'] = array(
     '#type' => 'textfield',
     '#title' => st('Database username'),
@@ -864,7 +879,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     '#size' => 45,
   );
 
-  // Database password
+  // Database password.
   $form['password'] = array(
     '#type' => 'password',
     '#title' => st('Database password'),
@@ -880,7 +895,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider.")
   );
 
-  // Database host
+  // Database host.
   $form['advanced_options']['host'] = array(
     '#type' => 'textfield',
     '#title' => st('Database host'),
@@ -892,7 +907,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     '#description' => st('If your database is located on a different server, change this.'),
   );
 
-  // Database port
+  // Database port.
   $form['advanced_options']['port'] = array(
     '#type' => 'textfield',
     '#title' => st('Database port'),
@@ -903,7 +918,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     '#description' => st('If your database server is listening to a non-standard port, enter its number.'),
   );
 
-  // Table prefix
+  // Table prefix.
   $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
   $form['advanced_options']['db_prefix'] = array(
     '#type' => 'textfield',
@@ -913,7 +928,7 @@ function install_settings_form($form, &$form_state, &$install_state) {
     '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)),
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['save'] = array(
     '#type' => 'submit',
     '#value' => st('Save and continue'),
@@ -938,12 +953,12 @@ function install_settings_form_validate($form, &$form_state) {
 }
 
 /**
- * Check a database connection and return any errors.
+ * Checks a database connection and returns any errors.
  */
 function install_database_errors($database, $settings_file) {
   global $databases;
   $errors = array();
-  // Verify the table prefix
+  // Verify the table prefix.
   if (!empty($database['db_prefix']) && is_string($database['db_prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['db_prefix'])) {
     $errors['db_prefix'] = st('The database table prefix you have entered, %db_prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%db_prefix' => $database['db_prefix']));
   }
@@ -952,7 +967,7 @@ function install_database_errors($database, $settings_file) {
     $errors['db_port'] = st('Database port must be a number.');
   }
 
-  // Check database type
+  // Check database type.
   $database_types = drupal_detect_database_types();
   $driver = $database['driver'];
   if (!isset($database_types[$driver])) {
@@ -960,7 +975,7 @@ function install_database_errors($database, $settings_file) {
   }
   else {
     // Run tasks associated with the database type. Any errors are caught in the
-    // calling function
+    // calling function.
     $databases['default']['default'] = $database;
     // Just changing the global doesn't get the new information processed.
     // We tell tell the Database class to re-parse $databases.
@@ -986,7 +1001,7 @@ function install_settings_form_submit($form, &$form_state) {
   global $install_state;
 
   $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port')));
-  // Update global settings array and save
+  // Update global settings array and save.
   $settings['databases'] = array(
     'value'    => array('default' => array('default' => $database)),
     'required' => TRUE,
@@ -1009,7 +1024,7 @@ function install_settings_form_submit($form, &$form_state) {
 }
 
 /**
- * Find all .profile files.
+ * Finds all .profile files.
  */
 function install_find_profiles() {
   return file_scan_directory('./profiles', '/\.profile$/', array('key' => 'name'));
@@ -1022,6 +1037,7 @@ function install_find_profiles() {
  *   An array of information about the current installation state. The chosen
  *   profile will be added here, if it was not already selected previously, as
  *   will a list of all available profiles.
+ *
  * @return
  *   For interactive installations, a form allowing the profile to be selected,
  *   if the user has a choice that needs to be made. Otherwise, an exception is
@@ -1130,7 +1146,7 @@ function install_select_profile_form($form, &$form_state, $profile_files) {
       '#parents' => array('profile'),
     );
   }
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] =  array(
     '#type' => 'submit',
     '#value' => st('Save and continue'),
@@ -1154,6 +1170,7 @@ function install_find_locales($profilename) {
  *   An array of information about the current installation state. The chosen
  *   locale will be added here, if it was not already selected previously, as
  *   will a list of all available locales.
+ *
  * @return
  *   For interactive installations, a form or other page output allowing the
  *   locale to be selected or providing information about locale selection, if
@@ -1194,7 +1211,8 @@ function install_select_locale(&$install_state) {
         }
         else {
           include_once DRUPAL_ROOT . '/includes/form.inc';
-          $output = drupal_render(drupal_get_form('install_select_locale_form', $locales, $profilename));
+          $elements = drupal_get_form('install_select_locale_form', $locales, $profilename);
+          $output = drupal_render($elements);
         }
         return $output;
       }
@@ -1227,7 +1245,8 @@ function install_select_locale(&$install_state) {
       if ($install_state['interactive']) {
         drupal_set_title(st('Choose language'));
         include_once DRUPAL_ROOT . '/includes/form.inc';
-        return drupal_render(drupal_get_form('install_select_locale_form', $locales, $profilename));
+        $elements = drupal_get_form('install_select_locale_form', $locales, $profilename);
+        return drupal_render($elements);
       }
       else {
         throw new Exception(st('Sorry, you must select a language to continue the installation.'));
@@ -1243,7 +1262,7 @@ function install_select_locale_form($form, &$form_state, $locales, $profilename
   include_once DRUPAL_ROOT . '/includes/iso.inc';
   $languages = _locale_get_predefined_list();
   foreach ($locales as $locale) {
-    // Try to use verbose locale name
+    // Try to use verbose locale name.
     $name = $locale->name;
     if (isset($languages[$name])) {
       $name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . st('(@language)', array('@language' => $languages[$name][1])) : '');
@@ -1261,7 +1280,7 @@ function install_select_locale_form($form, &$form_state, $locales, $profilename
       '#markup' => '<p><a href="install.php?profile=' . $profilename . '&amp;localize=true">' . st('Learn how to install Drupal in other languages') . '</a></p>',
     );
   }
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] =  array(
     '#type' => 'submit',
     '#value' => st('Save and continue'),
@@ -1270,7 +1289,7 @@ function install_select_locale_form($form, &$form_state, $locales, $profilename
 }
 
 /**
- * Indicate that there are no profiles available.
+ * Indicates that there are no profiles available.
  */
 function install_no_profile_error() {
   drupal_set_title(st('No profiles available'));
@@ -1278,7 +1297,7 @@ function install_no_profile_error() {
 }
 
 /**
- * Indicate that Drupal has already been installed.
+ * Indicates that Drupal has already been installed.
  */
 function install_already_done_error() {
   global $base_url;
@@ -1315,7 +1334,6 @@ function install_load_profile(&$install_state) {
 function install_bootstrap_full(&$install_state) {
   // Bootstrap newly installed Drupal, while preserving existing messages.
   $messages = isset($_SESSION['messages']) ? $_SESSION['messages'] : '';
-  drupal_install_initialize_database();
 
   drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
   $_SESSION['messages'] = $messages;
@@ -1326,6 +1344,7 @@ function install_bootstrap_full(&$install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The batch definition.
  */
@@ -1350,6 +1369,7 @@ function install_profile_modules(&$install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The batch definition, if there are language files to import.
  */
@@ -1374,6 +1394,7 @@ function install_import_locales(&$install_state) {
  *   An associative array containing the current state of the form.
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The form API definition for the site configuration form.
  */
@@ -1428,6 +1449,7 @@ function install_configure_form($form, &$form_state, &$install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   The batch definition, if there are language files to import.
  */
@@ -1447,6 +1469,7 @@ function install_import_locales_remaining(&$install_state) {
  *
  * @param $install_state
  *   An array of information about the current installation state.
+ *
  * @return
  *   A message informing the user that the installation is complete.
  */
@@ -1457,10 +1480,7 @@ function install_finished(&$install_state) {
   $output .= '<p>' . (isset($messages['error']) ? st('Review the messages above before visiting <a href="@url">your new site</a>.', array('@url' => url(''))) : st('<a href="@url">Visit your new site</a>.', array('@url' => url('')))) . '</p>';
 
   // Rebuild the module and theme data, in case any newly-installed modules
-  // need to modify it via hook_system_info_alter(). We need to clear the
-  // theme static cache first, to make sure that the theme data is actually
-  // rebuilt.
-  drupal_static_reset('_system_rebuild_theme_data');
+  // need to modify it via hook_system_info_alter().
   system_rebuild_module_data();
   system_rebuild_theme_data();
 
@@ -1507,7 +1527,7 @@ function _install_module_batch($module, $module_name, &$context) {
 }
 
 /**
- * Check installation requirements and report any errors.
+ * Checks installation requirements and reports any errors.
  */
 function install_check_requirements($install_state) {
   $profile = $install_state['parameters']['profile'];
@@ -1567,7 +1587,7 @@ function install_check_requirements($install_state) {
 }
 
 /**
- * Form API array definition for site configuration.
+ * Forms API array definition for site configuration.
  */
 function _install_configure_form($form, &$form_state, &$install_state) {
   include_once DRUPAL_ROOT . '/includes/locale.inc';
@@ -1669,7 +1689,7 @@ function _install_configure_form($form, &$form_state, &$install_state) {
     '#weight' => 15,
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => st('Save and continue'),
@@ -1680,7 +1700,7 @@ function _install_configure_form($form, &$form_state, &$install_state) {
 }
 
 /**
- * Form API validate for the site configuration form.
+ * Forms API validate for the site configuration form.
  */
 function install_configure_form_validate($form, &$form_state) {
   if ($error = user_validate_name($form_state['values']['account']['name'])) {
@@ -1695,7 +1715,7 @@ function install_configure_form_validate($form, &$form_state) {
 }
 
 /**
- * Form API submit for the site configuration form.
+ * Forms API submit for the site configuration form.
  */
 function install_configure_form_submit($form, &$form_state) {
   global $user;
@@ -1739,4 +1759,3 @@ function install_configure_form_submit($form, &$form_state) {
   // Record when this install ran.
   variable_set('install_time', $_SERVER['REQUEST_TIME']);
 }
-
diff --git a/includes/install.inc b/includes/install.inc
index 0b1afbef7d7e824abf907147cbae3017390a37ed..a88d05c179314391b6fd53192030fb6108463abf 100644
--- a/includes/install.inc
+++ b/includes/install.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: install.inc,v 1.130 2010/03/07 06:53:26 webchick Exp $
+// $Id: install.inc,v 1.132 2010/04/24 14:53:59 dries Exp $
 
 /**
  * Indicates that a module has not been installed yet.
@@ -219,20 +219,14 @@ function drupal_detect_baseurl($file = 'install.php') {
 function drupal_detect_database_types() {
   $databases = array();
 
-  // Initialize the database system if it has not been
-  // initialized yet. We do not initialize it earlier to make
-  // requirements check during the installation.
-  require_once DRUPAL_ROOT . '/includes/database/database.inc';
-
   // We define a driver as a directory in /includes/database that in turn
   // contains a database.inc file. That allows us to drop in additional drivers
   // without modifying the installer.
   // Because we have no registry yet, we need to also include the install.inc
   // file for the driver explicitly.
   require_once DRUPAL_ROOT . '/includes/database/database.inc';
+  spl_autoload_register('db_autoload');
   foreach (file_scan_directory(DRUPAL_ROOT . '/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) {
-    include_once "{$file->uri}/install.inc";
-    include_once "{$file->uri}/database.inc";
     $drivers[$file->filename] = $file->uri;
   }
 
@@ -546,28 +540,6 @@ function drupal_verify_profile($install_state) {
   return $requirements;
 }
 
-/**
- * Manually include all files for the active database.
- *
- * Because we have no registry yet, we need to manually include the
- * necessary database include files.
- */
-function drupal_install_initialize_database() {
-  static $included = FALSE;
-
-  if (!$included) {
-    $connection_info = Database::getConnectionInfo();
-    $driver = $connection_info['default']['driver'];
-    require_once DRUPAL_ROOT . '/includes/database/query.inc';
-    require_once DRUPAL_ROOT . '/includes/database/select.inc';
-    require_once DRUPAL_ROOT . '/includes/database/schema.inc';
-    foreach (glob(DRUPAL_ROOT . '/includes/database/' . $driver . '/*.inc') as $include_file) {
-      require_once $include_file;
-    }
-    $included = TRUE;
-  }
-}
-
 /**
  * Callback to install the system module.
  *
@@ -577,7 +549,6 @@ function drupal_install_initialize_database() {
 function drupal_install_system() {
   $system_path = dirname(drupal_get_filename('module', 'system', NULL));
   require_once DRUPAL_ROOT . '/' . $system_path . '/system.install';
-  drupal_install_initialize_database();
   module_invoke('system', 'install');
 
   $system_versions = drupal_get_schema_versions('system');
@@ -876,6 +847,7 @@ function install_goto($path) {
  * system is possibly not yet available.
  *
  * @see t()
+ * @ingroup sanitization
  */
 function st($string, $args = array()) {
   static $locale_strings = NULL;
diff --git a/includes/iso.inc b/includes/iso.inc
index 319f9984be2f304462cbcc5d5f0c8a82ddbb1e0d..6b5baf0d75482b0de93d4816a41475c9d608e8cf 100644
--- a/includes/iso.inc
+++ b/includes/iso.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: iso.inc,v 1.8 2010/03/18 19:07:42 dries Exp $
+// $Id: iso.inc,v 1.9 2010/04/17 12:55:08 dries Exp $
 
 /**
  * @file
@@ -376,7 +376,7 @@ function _locale_get_predefined_list() {
     'ku' => array('Kurdish', 'Kurdî'),
     'kv' => array('Komi'),
     'kw' => array('Cornish'),
-    'ky' => array('Kirghiz', 'Кыргыз'),
+    'ky' => array('Kyrgyz', 'Кыргыз тили'),
     'la' => array('Latin', 'Latina'),
     'lb' => array('Luxembourgish'),
     'lg' => array('Luganda'),
diff --git a/includes/locale.inc b/includes/locale.inc
index ca031c9ec34b9ecdb2fa5113d6afe7516d8f5b8d..b5fe671de51ad191a2f9ae814a28744c774e0fd8 100644
--- a/includes/locale.inc
+++ b/includes/locale.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: locale.inc,v 1.250 2010/03/10 14:23:31 dries Exp $
+// $Id: locale.inc,v 1.251 2010/04/09 12:14:25 dries Exp $
 
 /**
  * @file
@@ -386,6 +386,8 @@ function locale_add_language($langcode, $name = NULL, $native = NULL, $direction
   _locale_invalidate_js($langcode);
 
   watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
+
+  module_invoke_all('multilingual_settings_changed');
 }
 /**
  * @} End of "locale-api-add"
diff --git a/includes/menu.inc b/includes/menu.inc
index d805abbfcde9888c1ed5c28d504235a55a01d3de..975a162e9ffef342c150ef6cf05d530bfd349e02 100644
--- a/includes/menu.inc
+++ b/includes/menu.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: menu.inc,v 1.382 2010/03/12 05:14:14 webchick Exp $
+// $Id: menu.inc,v 1.387 2010/04/26 14:06:23 dries Exp $
 
 /**
  * @file
@@ -287,6 +287,7 @@ define('MENU_MAX_DEPTH', 9);
  * @param $parts
  *   An array of path parts, for the above example
  *   array('node', '12345', 'edit').
+ *
  * @return
  *   An array which contains the ancestors and placeholders. Placeholders
  *   simply contain as many '%s' as the ancestors.
@@ -341,6 +342,7 @@ function menu_get_ancestors($parts) {
  *   A serialized array.
  * @param @map
  *   An array of potential replacements.
+ *
  * @return
  *   The $data array unserialized and mapped.
  */
@@ -383,6 +385,7 @@ function menu_set_item($path, $router_item) {
  *   node/% item and return that.
  * @param $router_item
  *   Internal use only.
+ *
  * @return
  *   The router item, an associate array corresponding to one row in the
  *   menu_router table. The value of key map holds the loaded objects. The
@@ -460,8 +463,8 @@ function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
       // to make alterations just for this request.
       drupal_alter('menu_active_handler', $router_item, $path);
       if ($router_item['access']) {
-        if ($router_item['file']) {
-          require_once DRUPAL_ROOT . '/' . $router_item['file'];
+        if ($router_item['include_file']) {
+          require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
         }
         $page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
       }
@@ -492,6 +495,7 @@ function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
  *   A menu router or menu link item
  * @param $map
  *   An array of path arguments (ex: array('node', '5'))
+ *
  * @return
  *   Returns TRUE for success, FALSE if an object cannot be loaded.
  *   Names of object loading functions are placed in $item['load_functions'].
@@ -601,6 +605,7 @@ function _menu_check_access(&$item, $map) {
  * @param $link_translate
  *   TRUE if we are translating a menu link item; FALSE if we are
  *   translating a menu router item.
+ *
  * @return
  *   No return value.
  *   $item['title'] is localized according to $item['title_callback'].
@@ -687,6 +692,7 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
  * @param $to_arg
  *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
  *   path from the menu table, for example tabs.
+ *
  * @return
  *   Returns the map with objects loaded as defined in the
  *   $item['load_functions']. $item['access'] becomes TRUE if the item is
@@ -761,6 +767,7 @@ function menu_tail_to_arg($arg, $map, $index) {
  *
  * @param $item
  *   A menu link
+ *
  * @return
  *   Returns the map of path arguments with objects loaded as defined in the
  *   $item['load_functions'].
@@ -853,11 +860,12 @@ function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  * Render a menu tree based on the current path.
  *
  * The tree is expanded based on the current path and dynamic paths are also
- * changed according to the defined to_arg functions (for example the 'My account'
- * link is changed from user/% to a link with the current user's uid).
+ * changed according to the defined to_arg functions (for example the 'My
+ * account' link is changed from user/% to a link with the current user's uid).
  *
  * @param $menu_name
  *   The name of the menu.
+ *
  * @return
  *   The rendered HTML of that menu on the current page.
  */
@@ -881,6 +889,7 @@ function menu_tree($menu_name) {
  *
  * @param $tree
  *   A data structure representing the tree as returned from menu_tree_data.
+ *
  * @return
  *   A structured array to be rendered by drupal_render().
  */
@@ -1317,6 +1326,7 @@ function _menu_tree_check_access(&$tree) {
  *   to the root of the menu tree.
  * @param $depth
  *   The minimum depth of any link in the $links array.
+ *
  * @return
  *   See menu_tree_page_data for a description of the data structure.
  */
@@ -1362,24 +1372,20 @@ function _menu_tree_data(&$links, $parents, $depth) {
 }
 
 /**
- * Preprocess the rendered tree for theme_menu_tree.
- *
- * @ingroup themeable
+ * Preprocesses the rendered tree for theme_menu_tree().
  */
 function template_preprocess_menu_tree(&$variables) {
   $variables['tree'] = $variables['tree']['#children'];
 }
 
 /**
- * Theme wrapper for the HTML output for a menu sub-tree.
+ * Returns HTML for a wrapper for a menu sub-tree.
  *
  * @param $variables
  *   An associative array containing:
- *   - tree: @todo: document
- *
- * @return
- *   A themed HTML string.
+ *   - tree: An HTML string containing the tree's items.
  *
+ * @see template_preprocess_menu_tree()
  * @ingroup themeable
  */
 function theme_menu_tree($variables) {
@@ -1387,15 +1393,12 @@ function theme_menu_tree($variables) {
 }
 
 /**
- * Generate the HTML output for a menu link and submenu.
+ * Returns HTML for a menu link and submenu.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: Structured array data for a menu link.
  *
- * @return
- *   A themed HTML string.
- *
  * @ingroup themeable
  */
 function theme_menu_link(array $variables) {
@@ -1410,13 +1413,14 @@ function theme_menu_link(array $variables) {
 }
 
 /**
- * Generate the HTML output for a single local task link.
+ * Returns HTML for a single local task link.
  *
  * @param $variables
  *   An associative array containing:
- *   - #link: A menu link array with 'title', 'href', and 'localized_options'
- *     keys.
- *   - #active: A boolean indicating whether the local task is active.
+ *   - element: A render element containing:
+ *     - #link: A menu link array with 'title', 'href', and 'localized_options'
+ *       keys.
+ *     - #active: A boolean indicating whether the local task is active.
  *
  * @ingroup themeable
  */
@@ -1441,12 +1445,13 @@ function theme_menu_local_task($variables) {
 }
 
 /**
- * Generate the HTML output for a single local action link.
+ * Returns HTML for a single local action link.
  *
  * @param $variables
  *   An associative array containing:
- *   - #link: A menu link array with 'title', 'href', and 'localized_options'
- *     keys.
+ *   - element: A render element containing:
+ *     - #link: A menu link array with 'title', 'href', and 'localized_options'
+ *       keys.
  *
  * @ingroup themeable
  */
@@ -1604,6 +1609,7 @@ function menu_secondary_menu() {
  *   The name of the menu.
  * @param $level
  *   Optional, the depth of the menu to be returned.
+ *
  * @return
  *   An array of links of the specified menu and level.
  */
@@ -1651,6 +1657,7 @@ function menu_navigation_links($menu_name, $level = 0) {
  *
  * @param $level
  *   The level of tasks you ask for. Primary tasks are 0, secondary are 1.
+ *
  * @return
  *   An array containing
  *   - tabs: Local tasks for the requested level:
@@ -2036,6 +2043,7 @@ function menu_set_active_item($path) {
  * @param $new_trail
  *   Menu trail to set, or NULL to use previously-set or calculated trail. If
  *   supplying a trail, use the same format as the return value (see below).
+ *
  * @return
  *   Path to menu root of the current page, as an array of menu link items,
  *   starting with the site's home page. Each link item is an associative array
@@ -2096,7 +2104,8 @@ function menu_set_active_trail($new_trail = NULL) {
         $found[] = $menu->menu_name;
       }
       // The $menu_names array is ordered, so take the first one that matches.
-      $name = current(array_intersect($menu_names, $found));
+      $found_menu_names = array_intersect($menu_names, $found);
+      $name = current($found_menu_names);
       if ($name !== FALSE) {
         $tree = menu_tree_page_data($name);
         list($key, $curr) = each($tree);
@@ -2187,6 +2196,7 @@ function menu_get_active_title() {
  *
  * @param $mlid
  *   The mlid of the menu item.
+ *
  * @return
  *   A menu link, with $item['access'] filled and link translated for
  *   rendering.
@@ -2592,13 +2602,14 @@ function _menu_delete_item($item, $force = FALSE) {
  * @param $item
  *   An array representing a menu link item. The only mandatory keys are
  *   link_path and link_title. Possible keys are:
- *   - menu_name   default is navigation
- *   - weight      default is 0
- *   - expanded    whether the item is expanded.
- *   - options     An array of options, @see l for more.
- *   - mlid        Set to an existing value, or 0 or NULL to insert a new link.
- *   - plid        The mlid of the parent.
- *   - router_path The path of the relevant router item.
+ *   - menu_name: Default is navigation
+ *   - weight: Default is 0
+ *   - expanded: Whether the item is expanded.
+ *   - options: An array of options, see l() for more.
+ *   - mlid: Set to an existing value, or 0 or NULL to insert a new link.
+ *   - plid: The mlid of the parent.
+ *   - router_path: The path of the relevant router item.
+ *
  * @return
  *   The mlid of the saved menu link, or FALSE if the menu link could not be
  *   saved.
@@ -2820,6 +2831,7 @@ function _menu_set_expanded_menus() {
  *
  * @param $link_path
  *  The path for we are looking up its router path.
+ *
  * @return
  *  A path from $menu keys or empty if $link_path points to a nonexisting
  *  place.
@@ -2903,6 +2915,7 @@ function menu_link_maintain($module, $op, $link_path, $link_title) {
  *
  * @param $item
  *   An array representing a menu link item.
+ *
  * @return
  *   The relative depth, or zero.
  *
@@ -3153,12 +3166,15 @@ function _menu_router_build($callbacks) {
           if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
             $item['page arguments'] = $parent['page arguments'];
           }
-          if (!isset($item['file']) && isset($parent['file'])) {
-            $item['file'] = $parent['file'];
-          }
           if (!isset($item['file path']) && isset($parent['file path'])) {
             $item['file path'] = $parent['file path'];
           }
+          if (!isset($item['file']) && isset($parent['file'])) {
+            $item['file'] = $parent['file'];
+            if (empty($item['file path']) && isset($item['module']) && isset($parent['module']) && $item['module'] != $parent['module']) {
+              $item['file path'] = drupal_get_path('module', $parent['module']);
+            }
+          }
         }
         // Same for delivery callbacks.
         if (!isset($item['delivery callback']) && isset($parent['delivery callback'])) {
@@ -3254,7 +3270,7 @@ function _menu_router_save($menu, $masks) {
       'description',
       'position',
       'weight',
-      'file',
+      'include_file',
     ));
 
   $num_records = 0;
@@ -3285,7 +3301,7 @@ function _menu_router_save($menu, $masks) {
       'description' => $item['description'],
       'position' => $item['position'],
       'weight' => $item['weight'],
-      'file' => $item['include file'],
+      'include_file' => $item['include file'],
     ));
 
     // Execute in batches to avoid the memory overhead of all of those records
@@ -3313,6 +3329,7 @@ function _menu_router_save($menu, $masks) {
  *   If this is set to TRUE, the function will perform the access checks and
  *   return the site offline status, but not log the user out or display any
  *   messages.
+ *
  * @return
  *   FALSE if the site is not in maintenance mode, the user login page is
  *   displayed, or the user has the 'access site in maintenance mode'
diff --git a/includes/module.inc b/includes/module.inc
index 4af068e2da4d0b7246ddf02f86890a95ce670dd8..1733cff900bd1e5ef3dd8fabbf36b567dda0439c 100644
--- a/includes/module.inc
+++ b/includes/module.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: module.inc,v 1.184 2010/03/03 07:38:08 dries Exp $
+// $Id: module.inc,v 1.190 2010/04/22 22:36:01 webchick Exp $
 
 /**
  * @file
@@ -369,6 +369,8 @@ function module_enable($module_list, $enable_dependencies = TRUE) {
       registry_update();
       // Refresh the schema to include it.
       drupal_get_schema(NULL, TRUE);
+      // Clear entity cache.
+      entity_info_cache_clear();
 
       // Now install the module if necessary.
       if (drupal_get_installed_schema_version($module, TRUE) == SCHEMA_UNINSTALLED) {
@@ -503,6 +505,12 @@ function module_disable($module_list, $disable_dependents = TRUE) {
  * the module name in the hook definitions. For example, if the module file is
  * called example.module, then hook_help() as implemented by that module would
  * be defined as example_help().
+ *
+ * The example functions included are not part of the Drupal core, they are
+ * just models that you can modify. Only the hooks implemented within modules
+ * are executed when running Drupal.
+ *
+ * See also @link themeable the themeable group page. @endlink
  */
 
 /**
@@ -536,7 +544,7 @@ function module_hook($module, $hook) {
  * @return
  *   An array with the names of the modules which are implementing this hook.
  *
- * @see module_implements_write_cache().
+ * @see module_implements_write_cache()
  */
 function module_implements($hook, $sort = FALSE, $reset = FALSE) {
   // Use the advanced drupal_static() pattern, since this is called very often.
@@ -581,13 +589,18 @@ function module_implements($hook, $sort = FALSE, $reset = FALSE) {
     $implementations[$hook] = array();
     $list = module_list(FALSE, FALSE, $sort);
     foreach ($list as $module) {
-      $include_file = FALSE;
-      if (module_hook($module, $hook) || (isset($hook_info[$hook]['group']) && $include_file = module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']) && module_hook($module, $hook))) {
+      $include_file = isset($hook_info[$hook]['group']) && module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']);
+      if (module_hook($module, $hook)) {
         $implementations[$hook][$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
         // We added something to the cache, so write it when we are done.
         $implementations['#write_cache'] = TRUE;
       }
     }
+    // Allow modules to change the weight of specific implementations but avoid
+    // an infinite loop.
+    if ($hook != 'module_implements_alter') {
+      drupal_alter('module_implements', $implementations[$hook], $hook);
+    }
   }
   else {
     foreach ($implementations[$hook] as $module => $group) {
@@ -741,3 +754,83 @@ function drupal_required_modules() {
 
   return $required;
 }
+
+/**
+ * Hands off alterable variables to type-specific *_alter implementations.
+ *
+ * This dispatch function hands off the passed in variables to type-specific
+ * hook_TYPE_alter() implementations in modules. It ensures a consistent
+ * interface for all altering operations.
+ *
+ * A maximum of 2 alterable arguments is supported. In case more arguments need
+ * to be passed and alterable, modules provide additional variables assigned by
+ * reference in the last $context argument:
+ * @code
+ *   $context = array(
+ *     'alterable' => &$alterable,
+ *     'unalterable' => $unalterable,
+ *     'foo' => 'bar',
+ *   );
+ *   drupal_alter('mymodule_data', $alterable1, $alterable2, $context);
+ * @endcode
+ *
+ * Note that objects are always passed by reference in PHP5. If it is absolutely
+ * required that no implementation alters a passed object in $context, then an
+ * object needs to be cloned:
+ * @code
+ *   $context = array(
+ *     'unalterable_object' => clone $object,
+ *   );
+ *   drupal_alter('mymodule_data', $data, $context);
+ * @endcode
+ *
+ * @param $type
+ *   A string describing the data type of the alterable $data. 'form', 'links',
+ *   'node_content', and so on are several examples.
+ * @param &$data
+ *   The primary data to be altered.
+ * @param &$context1
+ *   (optional) An additional variable that is passed by reference.
+ * @param &$context2
+ *   (optional) An additional variable that is passed by reference. If more
+ *   context needs to be provided to implementations, then this should be an
+ *   keyed array as described above.
+ */
+function drupal_alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['functions'] = &drupal_static(__FUNCTION__);
+  }
+  $functions = &$drupal_static_fast['functions'];
+
+  // Some alter hooks are invoked many times per page request, so statically
+  // cache the list of functions to call, and on subsequent calls, iterate
+  // through them quickly.
+  if (!isset($functions[$type])) {
+    $functions[$type] = array();
+    $hook = $type . '_alter';
+    foreach (module_implements($hook) as $module) {
+      $functions[$type][] = $module . '_' . $hook;
+    }
+    // Allow the theme to alter variables after the theme system has been
+    // initialized.
+    global $theme, $base_theme_info;
+    if (isset($theme)) {
+      $theme_keys = array();
+      foreach ($base_theme_info as $base) {
+        $theme_keys[] = $base->name;
+      }
+      $theme_keys[] = $theme;
+      foreach ($theme_keys as $theme_key) {
+        $function = $theme_key . '_' . $hook;
+        if (function_exists($function)) {
+          $functions[$type][] = $function;
+        }
+      }
+    }
+  }
+  foreach ($functions[$type] as $function) {
+    $function($data, $context1, $context2);
+  }
+}
diff --git a/includes/pager.inc b/includes/pager.inc
index 12c9eedbcee778d398b40bafa9954ef61d3bd223..af0065cfa2c801b3aff83f67edae7707834da84d 100644
--- a/includes/pager.inc
+++ b/includes/pager.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: pager.inc,v 1.78 2009/12/05 20:17:02 dries Exp $
+// $Id: pager.inc,v 1.79 2010/04/13 15:23:02 dries Exp $
 
 /**
  * @file
@@ -186,11 +186,11 @@ function pager_get_query_parameters() {
 }
 
 /**
- * Format a query pager.
+ * Returns HTML for a query pager.
  *
  * Menu callbacks that display paged query results should call theme('pager') to
- * retrieve a pager control so that users can view other results.
- * Format a list of nearby pages with additional query results.
+ * retrieve a pager control so that users can view other results. Format a list
+ * of nearby pages with additional query results.
  *
  * @param $variables
  *   An associative array containing:
@@ -201,9 +201,6 @@ function pager_get_query_parameters() {
  *     the pager links.
  *   - quantity: The number of pages in the list.
  *
- * @return
- *   An HTML string that generates the query pager.
- *
  * @ingroup themeable
  */
 function theme_pager($variables) {
@@ -321,7 +318,7 @@ function theme_pager($variables) {
  */
 
 /**
- * Format a "first page" link.
+ * Returns HTML for the "first page" link in a query pager.
  *
  * @param $variables
  *   An associative array containing:
@@ -331,9 +328,6 @@ function theme_pager($variables) {
  *   - parameters: An associative array of query string parameters to append to
  *     the pager links.
  *
- * @return
- *   An HTML string that generates this piece of the query pager.
- *
  * @ingroup themeable
  */
 function theme_pager_first($variables) {
@@ -352,7 +346,7 @@ function theme_pager_first($variables) {
 }
 
 /**
- * Format a "previous page" link.
+ * Returns HTML for the "previous page" link in a query pager.
  *
  * @param $variables
  *   An associative array containing:
@@ -363,9 +357,6 @@ function theme_pager_first($variables) {
  *   - parameters: An associative array of query string parameters to append to
  *     the pager links.
  *
- * @return
- *   An HTML string that generates this piece of the query pager.
- *
  * @ingroup themeable
  */
 function theme_pager_previous($variables) {
@@ -394,7 +385,7 @@ function theme_pager_previous($variables) {
 }
 
 /**
- * Format a "next page" link.
+ * Returns HTML for the "next page" link in a query pager.
  *
  * @param $variables
  *   An associative array containing:
@@ -405,9 +396,6 @@ function theme_pager_previous($variables) {
  *   - parameters: An associative array of query string parameters to append to
  *     the pager links.
  *
- * @return
- *   An HTML string that generates this piece of the query pager.
- *
  * @ingroup themeable
  */
 function theme_pager_next($variables) {
@@ -435,7 +423,7 @@ function theme_pager_next($variables) {
 }
 
 /**
- * Format a "last page" link.
+ * Returns HTML for the "last page" link in query pager.
  *
  * @param $variables
  *   An associative array containing:
@@ -445,9 +433,6 @@ function theme_pager_next($variables) {
  *   - parameters: An associative array of query string parameters to append to
  *     the pager links.
  *
- * @return
- *   An HTML string that generates this piece of the query pager.
- *
  * @ingroup themeable
  */
 function theme_pager_last($variables) {
@@ -467,7 +452,7 @@ function theme_pager_last($variables) {
 
 
 /**
- * Format a link to a specific query result page.
+ * Returns HTML for a link to a specific query result page.
  *
  * @param $variables
  *   An associative array containing:
@@ -479,9 +464,6 @@ function theme_pager_last($variables) {
  *   - attributes: An associative array of HTML attributes to apply to a pager
  *     anchor tag.
  *
- * @return
- *   An HTML string that generates the link.
- *
  * @ingroup themeable
  */
 function theme_pager_link($variables) {
diff --git a/includes/path.inc b/includes/path.inc
index 68a92a7ed23259a71257ab9538f55349ad7266db..1f4d3557ade83bee71e908756b4ef38fe0fe4494 100644
--- a/includes/path.inc
+++ b/includes/path.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: path.inc,v 1.60 2010/03/10 19:36:14 webchick Exp $
+// $Id: path.inc,v 1.62 2010/04/24 15:11:27 dries Exp $
 
 /**
  * @file
@@ -238,7 +238,7 @@ function drupal_get_normal_path($path, $path_language = NULL) {
  * Return a component of the current Drupal path.
  *
  * When viewing a page at the path "admin/structure/types", for example, arg(0)
- * returns "admin", arg(1) returns "content", and arg(2) returns "types".
+ * returns "admin", arg(1) returns "structure", and arg(2) returns "types".
  *
  * Avoid use of this function where possible, as resulting code is hard to read.
  * In menu callback functions, attempt to use named arguments. See the explanation
@@ -362,7 +362,20 @@ function drupal_match_path($path, $patterns) {
   $regexps = &drupal_static(__FUNCTION__);
 
   if (!isset($regexps[$patterns])) {
-    $regexps[$patterns] = '/^(' . preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\<front\\\\>($|\|)/'), array('|', '.*', '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'), preg_quote($patterns, '/')) . ')$/';
+    // Convert path settings to a regular expression.
+    // Therefore replace newlines with a logical or, /* with asterisks and the <front> with the frontpage.
+    $to_replace = array(
+      '/(\r\n?|\n)/', // newlines
+      '/\\\\\*/',     // asterisks
+      '/(^|\|)\\\\<front\\\\>($|\|)/' // <front>
+    );
+    $replacements = array(
+      '|',
+      '.*',
+      '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'
+    );
+    $patterns_quoted = preg_quote($patterns, '/');
+    $regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
   }
   return (bool)preg_match($regexps[$patterns], $path);
 }
diff --git a/includes/registry.inc b/includes/registry.inc
index 125b50009ea9d842f17fa0b4fbc7837a6122ea63..2d2ded4fe13319b45f8dfd58a1022eec5c1ddb48 100644
--- a/includes/registry.inc
+++ b/includes/registry.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: registry.inc,v 1.28 2010/02/26 00:19:55 webchick Exp $
+// $Id: registry.inc,v 1.30 2010/04/04 13:44:49 dries Exp $
 
 /**
  * @file
@@ -17,7 +17,7 @@
  */
 
 /**
- * @see registry_update().
+ * Does the work for registry_update().
  */
 function _registry_update() {
 
@@ -162,7 +162,6 @@ function _registry_parse_files($files) {
  *   (optional) Weight of the module.
  */
 function _registry_parse_file($filename, $contents, $module = '', $weight = 0) {
-  static $map = array(T_CLASS => 'class', T_INTERFACE => 'interface');
   // Delete registry entries for this file, so we can insert the new resources.
   db_delete('registry')
     ->condition('filename', $filename)
diff --git a/includes/session.inc b/includes/session.inc
index 438d47dde21c494d6547f35882428995c275c8ff..542253e8604567603c0213139e34484b085d2d6e 100644
--- a/includes/session.inc
+++ b/includes/session.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: session.inc,v 1.79 2010/03/09 03:52:02 dries Exp $
+// $Id: session.inc,v 1.82 2010/04/13 15:13:40 dries Exp $
 
 /**
  * @file
@@ -104,7 +104,7 @@ function _drupal_session_read($sid) {
   // active user.
   if ($user && $user->uid > 0 && $user->status == 1) {
     // This is done to unserialize the data member of $user.
-    $user = drupal_unpack($user);
+    $user->data = unserialize($user->data);
 
     // Add roles element to $user.
     $user->roles = array();
@@ -208,6 +208,7 @@ function drupal_session_initialize() {
     $user = drupal_anonymous_user();
     session_id(md5(uniqid('', TRUE)));
   }
+  date_default_timezone_set(drupal_get_user_timezone());
 }
 
 /**
@@ -309,6 +310,7 @@ function drupal_session_regenerate() {
       ->condition('sid', $old_session_id)
       ->execute();
   }
+  date_default_timezone_set(drupal_get_user_timezone());
 }
 
 /**
diff --git a/includes/stream_wrappers.inc b/includes/stream_wrappers.inc
index 196b84a725957987426421003a642da49ec01e12..971a58fc696a8df6f70cb2f7800034ab8fa845b4 100644
--- a/includes/stream_wrappers.inc
+++ b/includes/stream_wrappers.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: stream_wrappers.inc,v 1.12 2010/02/06 05:49:35 webchick Exp $
+// $Id: stream_wrappers.inc,v 1.13 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -16,8 +16,8 @@
  * delimiter is in general just ":", not "://".  Because of this PHP limitation
  * and for consistency Drupal will only accept URIs of form "scheme://target".
  *
- * @link http://www.faqs.org/rfcs/rfc3986.html
- * @link http://bugs.php.net/bug.php?id=47070
+ * @see http://www.faqs.org/rfcs/rfc3986.html
+ * @see http://bugs.php.net/bug.php?id=47070
  */
 
 /**
diff --git a/includes/theme.inc b/includes/theme.inc
index 8e99515464b1e4d9a01ea9ef53b5fd13999835cc..a5d6aa1a26bc6062ec4fadebd20acabd5e3561ab 100644
--- a/includes/theme.inc
+++ b/includes/theme.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: theme.inc,v 1.584 2010/03/21 04:05:23 webchick Exp $
+// $Id: theme.inc,v 1.593 2010/04/26 17:54:49 webchick Exp $
 
 /**
  * @file
@@ -430,7 +430,7 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
           }
           foreach ($prefixes as $prefix) {
             // Only use non-hook-specific variable processors for theming hooks
-            // implemented as templates. @see theme().
+            // implemented as templates. See theme().
             if (isset($info['template']) && function_exists($prefix . '_' . $phase)) {
               $info[$phase_key][] = $prefix . '_' . $phase;
             }
@@ -467,7 +467,7 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
             $cache[$hook][$phase_key] = array();
           }
           // Only use non-hook-specific variable processors for theming hooks
-          // implemented as templates. @see theme().
+          // implemented as templates. See theme().
           if (isset($info['template']) && function_exists($name . '_' . $phase)) {
             $cache[$hook][$phase_key][] = $name . '_' . $phase;
           }
@@ -572,13 +572,19 @@ function list_themes($refresh = FALSE) {
     $themes = array();
     // Extract from the database only when it is available.
     // Also check that the site is not in the middle of an install or update.
-    if (!defined('MAINTENANCE_MODE') && db_is_active()) {
-      foreach (system_list('theme') as $theme) {
-        if (file_exists($theme->filename)) {
-          $theme->info = unserialize($theme->info);
-          $themes[] = $theme;
+    if (!defined('MAINTENANCE_MODE')) {
+      try {
+        foreach (system_list('theme') as $theme) {
+          if (file_exists($theme->filename)) {
+            $theme->info = unserialize($theme->info);
+            $themes[] = $theme;
+          }
         }
       }
+      catch (Exception $e) {
+        // If the database is not available, rebuild the theme data.
+        $themes = _system_rebuild_theme_data();
+      }
     }
     else {
       // Scan the installation when the database should not be read.
@@ -764,6 +770,14 @@ function list_themes($refresh = FALSE) {
  */
 function theme($hook, $variables = array()) {
   static $hooks = NULL;
+
+  // If called before all modules are loaded, we do not necessarily have a full
+  // theme registry to work with, and therefore cannot process the theme
+  // request properly. See also _theme_load_registry().
+  if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) {
+    throw new Exception(t('theme() may not be called until all modules are loaded.'));
+  }
+
   if (!isset($hooks)) {
     drupal_theme_initialize();
     $hooks = theme_get_registry();
@@ -1083,7 +1097,7 @@ function drupal_find_theme_templates($cache, $extension, $path) {
   }
 
   // Find templates that implement possible "suggestion" variants of registered
-  // theme hooks and add those as new registered theme hooks. @see
+  // theme hooks and add those as new registered theme hooks. See
   // drupal_find_theme_functions() for more information about suggestions and
   // the use of 'pattern' and 'base hook'.
   $patterns = array_keys($files);
@@ -1152,18 +1166,12 @@ function theme_get_setting($setting_name, $theme = NULL) {
       'favicon_path'                     =>  '',
       // Use the IANA-registered MIME type for ICO files as default.
       'favicon_mimetype'                 =>  'image/vnd.microsoft.icon',
-      'main_menu'                        =>  1,
-      'secondary_menu'                   =>  1,
-      'toggle_logo'                      =>  1,
-      'toggle_favicon'                   =>  1,
-      'toggle_name'                      =>  1,
-      'toggle_slogan'                    =>  1,
-      'toggle_node_user_picture'         =>  1,
-      'toggle_comment_user_picture'      =>  1,
-      'toggle_comment_user_verification' =>  1,
-      'toggle_main_menu'                 =>  1,
-      'toggle_secondary_menu'            =>  1,
     );
+    // Turn on all default features.
+    $features = _system_default_theme_features();
+    foreach ($features as $feature) {
+      $cache[$theme]['toggle_' . $feature] = 1;
+    }
 
     // Get the values for the theme-specific settings from the .info files of
     // the theme and all its base themes.
@@ -1193,6 +1201,16 @@ function theme_get_setting($setting_name, $theme = NULL) {
       // Get the saved theme-specific settings from the database.
       $cache[$theme] = array_merge($cache[$theme], variable_get('theme_' . $theme . '_settings', array()));
 
+      // If the theme does not support a particular feature, override the global
+      // setting and set the value to NULL.
+      if (!empty($theme_object->info['features'])) {
+        foreach ($features as $feature) {
+          if (!in_array($feature, $theme_object->info['features'])) {
+            $cache[$theme]['toggle_' . $feature] = NULL;
+          }
+        }
+      }
+
       // Generate the path to the logo image.
       if ($cache[$theme]['toggle_logo']) {
         if ($cache[$theme]['default_logo']) {
@@ -1317,8 +1335,7 @@ function theme_disable($theme_list) {
  */
 
 /**
- * Return a themed set of status and/or error messages. The messages are grouped
- * by type.
+ * Returns HTML for status and/or error messages, grouped by type.
  *
  * An invisible heading identifies the messages for assistive technology.
  * Sighted users see a colored box. See http://www.w3.org/TR/WCAG-TECHS/H69.html
@@ -1328,9 +1345,6 @@ function theme_disable($theme_list) {
  *   An associative array containing:
  *   - display: (optional) Set to 'status' or 'error' to display only messages
  *     of that type.
- *
- * @return
- *   A string containing the messages.
  */
 function theme_status_messages($variables) {
   $display = $variables['display'];
@@ -1362,11 +1376,11 @@ function theme_status_messages($variables) {
 }
 
 /**
- * Return a themed link.
+ * Returns HTML for a link.
  *
  * All Drupal code that outputs a link should call the l() function. That
- * function performs some initial preprocessing, and then, if necessary,
- * calls theme('link') for rendering the anchor tag.
+ * function performs some initial preprocessing, and then, if necessary, calls
+ * theme('link') for rendering the anchor tag.
  *
  * To optimize performance for sites that don't need custom theming of links,
  * the l() function includes an inline copy of this function, and uses that copy
@@ -1374,11 +1388,8 @@ function theme_status_messages($variables) {
  * or process functions or override this theme implementation.
  *
  * @param $variables
- *   An associative array containing the keys 'text', 'path', and 'options'.
- *   See the l() function for information about these variables.
- *
- * @return
- *   An HTML string containing a link to the given path.
+ *   An associative array containing the keys 'text', 'path', and 'options'. See
+ *   the l() function for information about these variables.
  *
  * @see l()
  */
@@ -1387,28 +1398,29 @@ function theme_link($variables) {
 }
 
 /**
- * Return a themed set of links.
+ * Returns HTML for a set of links.
  *
  * @param $variables
  *   An associative array containing:
  *   - links: A keyed array of links to be themed. The key for each link is used
  *     as its css class. Each link should be itself an array, with the following
  *     keys:
- *      - title: the link text
- *      - href: the link URL. If omitted, the 'title' is shown as a plain text
- *        item in the links list.
- *      - html: (optional) set this to TRUE if 'title' is HTML so it will be
- *        escaped.
+ *     - title: the link text
+ *     - href: the link URL. If omitted, the 'title' is shown as a plain text
+ *       item in the links list.
+ *     - html: (optional) set this to TRUE if 'title' is HTML so it will be
+ *       escaped.
  *     Array items are passed on to the l() function's $options parameter when
  *     creating the link.
  *   - attributes: A keyed array of attributes.
  *   - heading: An optional keyed array or a string for a heading to precede the
  *     links. When using an array the following keys can be used:
- *      - text: the heading text
- *      - level: the heading level (e.g. 'h2', 'h3')
- *      - class: (optional) an array of the CSS classes for the heading
+ *     - text: the heading text
+ *     - level: the heading level (e.g. 'h2', 'h3')
+ *     - class: (optional) an array of the CSS classes for the heading
  *     When using a string it will be used as the text of the heading and the
  *     level will default to 'h2'.
+ *
  *     Headings should be used on navigation menus and any list of links that
  *     consistently appears on multiple pages. To make the heading invisible
  *     use the 'element-invisible' CSS class. Do not use 'display:none', which
@@ -1416,9 +1428,6 @@ function theme_link($variables) {
  *     screen-reader and keyboard only users to navigate to or skip the links.
  *     See http://juicystudio.com/article/screen-readers-display-none.php
  *     and http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
- *
- * @return
- *   A string containing an unordered list of links.
  */
 function theme_links($variables) {
   $links = $variables['links'];
@@ -1497,7 +1506,7 @@ function theme_links($variables) {
 }
 
 /**
- * Return a themed image.
+ * Returns HTML for an image.
  *
  * @param $variables
  *   An associative array containing:
@@ -1509,9 +1518,6 @@ function theme_links($variables) {
  *   - attributes: Associative array of attributes to be placed in the img tag.
  *   - getsize: If set to TRUE, the image's dimension are fetched and added as
  *     width/height attributes.
- *
- * @return
- *   A string containing the image tag.
  */
 function theme_image($variables) {
   $path = $variables['path'];
@@ -1528,14 +1534,11 @@ function theme_image($variables) {
 }
 
 /**
- * Return a themed breadcrumb trail.
+ * Returns HTML for a breadcrumb trail.
  *
  * @param $variables
  *   An associative array containing:
  *   - breadcrumb: An array containing the breadcrumb links.
- *
- * @return
- *   A string containing the breadcrumb output.
  */
 function theme_breadcrumb($variables) {
   $breadcrumb = $variables['breadcrumb'];
@@ -1551,7 +1554,7 @@ function theme_breadcrumb($variables) {
 }
 
 /**
- * Return a themed table.
+ * Returns HTML for a table.
  *
  * @param $variables
  *   An associative array containing:
@@ -1622,9 +1625,6 @@ function theme_breadcrumb($variables) {
  *   - sticky: Use a "sticky" table header.
  *   - empty: The message to display in an extra row if table does not have any
  *     rows.
- *
- * @return
- *   An HTML string representing the table.
  */
 function theme_table($variables) {
   $header = $variables['header'];
@@ -1686,7 +1686,16 @@ function theme_table($variables) {
 
   // Add the 'empty' row message if available.
   if (!count($rows) && $empty) {
-    $rows[] = array(array('data' => $empty, 'colspan' => count($header), 'class' => array('empty', 'message')));
+    $header_count = 0;
+    foreach ($header as $header_cell) {
+      if (is_array($header_cell)) {
+        $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1;
+      }
+      else {
+        $header_count++;
+      }
+    }
+    $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message')));
   }
 
   // Format the table header:
@@ -1751,7 +1760,10 @@ function theme_table($variables) {
 }
 
 /**
- * Returns a header cell for tables that have a select all functionality.
+ * Returns attributes for a header cell of tables with select all functionality.
+ *
+ * @return
+ *   An array of attributes.
  */
 function theme_table_select_header_cell() {
   drupal_add_js('misc/tableselect.js');
@@ -1760,14 +1772,11 @@ function theme_table_select_header_cell() {
 }
 
 /**
- * Return a themed sort icon.
+ * Returns HTML for a sort icon.
  *
  * @param $variables
  *   An associative array containing:
- *   - style: Set to either asc or desc. This sets which icon to show.
- *
- * @return
- *   A themed sort icon.
+ *   - style: Set to either 'asc' or 'desc', this determines which icon to show.
  */
 function theme_tablesort_indicator($variables) {
   if ($variables['style'] == "asc") {
@@ -1779,16 +1788,12 @@ function theme_tablesort_indicator($variables) {
 }
 
 /**
- * Return a themed marker, useful for marking new or updated
- * content.
+ * Returns HTML for a marker for new or updated content.
  *
  * @param $variables
  *   An associative array containing:
- *   - type: Number representing the marker type to display.
- *     @see MARK_NEW, MARK_UPDATED, MARK_READ
- *
- * @return
- *   A string containing the marker.
+ *   - type: Number representing the marker type to display. See MARK_NEW,
+ *     MARK_UPDATED, MARK_READ.
  */
 function theme_mark($variables) {
   $type = $variables['type'];
@@ -1804,7 +1809,7 @@ function theme_mark($variables) {
 }
 
 /**
- * Return a themed list of items.
+ * Returns HTML for a list or nested list of items.
  *
  * @param $variables
  *   An associative array containing:
@@ -1817,9 +1822,6 @@ function theme_mark($variables) {
  *   - title: The title of the list.
  *   - type: The type of list to return (e.g. "ul", "ol").
  *   - attributes: The attributes applied to the list element.
- *
- * @return
- *   A string containing the list output.
  */
 function theme_item_list($variables) {
   $items = $variables['items'];
@@ -1873,14 +1875,18 @@ function theme_item_list($variables) {
 }
 
 /**
- * Returns code that emits the 'more help'-link.
+ * Returns HTML for a "more help" link.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - url: The url for the link.
  */
 function theme_more_help_link($variables) {
   return '<div class="more-help-link">' . t('<a href="@link">More help</a>', array('@link' => check_url($variables['url']))) . '</div>';
 }
 
 /**
- * Return code that emits an feed icon.
+ * Returns HTML for a feed icon.
  *
  * @param $variables
  *   An associative array containing:
@@ -1895,15 +1901,12 @@ function theme_feed_icon($variables) {
 }
 
 /**
- * Generate the output for a generic HTML tag with attributes.
- *
- * This theme function should be used for tags appearing in the HTML HEAD of a
- * document (specified via the #tag property in the passed $element):
+ * Returns HTML for a generic HTML tag with attributes.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: An associative array describing the tag:
- *     - #tag: The tag name to output.  Typical tags added to the HTML HEAD:
+ *     - #tag: The tag name to output. Typical tags added to the HTML HEAD:
  *       - meta: To provide meta information, such as a page refresh.
  *       - link: To refer to stylesheets and other contextual information.
  *       - script: To load JavaScript.
@@ -1935,96 +1938,19 @@ function theme_html_tag($variables) {
 }
 
 /**
- * Returns code that emits the 'more' link used on blocks.
+ * Returns HTML for a "more" link, like those used in blocks.
  *
  * @param $variables
  *   An associative array containing:
- *   - url: The url of the main page
- *   - title: A descriptive verb for the link, like 'Read more'
+ *   - url: The url of the main page.
+ *   - title: A descriptive verb for the link, like 'Read more'.
  */
 function theme_more_link($variables) {
   return '<div class="more-link">' . t('<a href="@link" title="@title">More</a>', array('@link' => check_url($variables['url']), '@title' => $variables['title'])) . '</div>';
 }
 
 /**
- * Preprocess variables for theme_username().
- *
- * Modules that make any changes to variables like 'name' or 'extra' must insure
- * that the final string is safe to include directly in the output by using
- * check_plain() or filter_xss().
- *
- * @see theme_username().
- */
-function template_preprocess_username(&$variables) {
-  $account = $variables['account'];
-
-  $variables['extra'] = '';
-  if (empty($account->uid)) {
-   $variables['uid'] = 0;
-   if (theme_get_setting('toggle_comment_user_verification')) {
-     $variables['extra'] = ' (' . t('not verified') . ')';
-   }
-  }
-  else {
-    $variables['uid'] = (int)$account->uid;
-  }
-
-  // Set the name to a formatted name that is safe for printing and
-  // that won't break tables by being too long. Keep an unshortened,
-  // unsanitized version, in case other preprocess functions want to implement
-  // their own shortening logic or add markup. If they do so, they must ensure
-  // that $variables['name'] is safe for printing.
-  $name = $variables['name_raw'] = format_username($account);
-  if (drupal_strlen($name) > 20) {
-    $name = drupal_substr($name, 0, 15) . '...';
-  }
-  $variables['name'] = check_plain($name);
-
-  $variables['profile_access'] = user_access('access user profiles');
-  $variables['link_attributes'] = array();
-  // Populate link path and attributes if appropriate.
-  if ($variables['uid'] && $variables['profile_access']) {
-    // We are linking to a local user.
-    $variables['link_attributes'] = array('title' => t('View user profile.'));
-    $variables['link_path'] = 'user/' . $variables['uid'];
-  }
-  elseif (!empty($account->homepage)) {
-    // Like the 'class' attribute, the 'rel' attribute can hold a
-    // space-separated set of values, so initialize it as an array to make it
-    // easier for other preprocess functions to append to it.
-    $variables['link_attributes'] = array('rel' => array('nofollow'));
-    $variables['link_path'] = $account->homepage;
-    $variables['homepage'] = $account->homepage;
-  }
-  // We do not want the l() function to check_plain() a second time.
-  $variables['link_options']['html'] = TRUE;
-  // Set a default class.
-  $variables['attributes_array'] = array('class' => array('username'));
-}
-
-/**
- * Process variables for theme_username().
- *
- * @see theme_username().
- */
-function template_process_username(&$variables) {
-  // Finalize the link_options array for passing to the l() function.
-  // This is done in the process phase so that attributes may be added by
-  // modules or the theme during the preprocess phase.
-  if (isset($variables['link_path'])) {
-    // $variables['attributes_array'] contains attributes that should be applied
-    // regardless of whether a link is being rendered or not.
-    // $variables['link_attributes'] contains attributes that should only be
-    // applied if a link is being rendered. Preprocess functions are encouraged
-    // to use the former unless they want to add attributes on the link only.
-    // If a link is being rendered, these need to be merged. Some attributes are
-    // themselves arrays, so the merging needs to be recursive.
-    $variables['link_options']['attributes'] = array_merge_recursive($variables['link_attributes'], $variables['attributes_array']);
-  }
-}
-
-/**
- * Format a username.
+ * Returns HTML for a username, potentially linked to the user's page.
  *
  * @param $variables
  *   An associative array containing:
@@ -2038,10 +1964,6 @@ function template_process_username(&$variables) {
  *   - attributes_array: An array of attributes to pass to the
  *     drupal_attributes() function if not linking to the user's page.
  *
- * @return
- *   A themed HTML string containing the user's name, potentially linked to the
- *   user's page.
- *
  * @see template_preprocess_username()
  * @see template_process_username()
  */
@@ -2062,15 +1984,12 @@ function theme_username($variables) {
 }
 
 /**
- * Return a themed progress bar.
+ * Returns HTML for a progress bar.
  *
  * @param $variables
  *   An associative array containing:
  *   - percent: The percentage of the progress.
  *   - message: A string containing information to be displayed.
- *
- * @return
- *   A themed HTML string representing the progress bar.
  */
 function theme_progress_bar($variables) {
   $output = '<div id="progress" class="progress">';
@@ -2083,14 +2002,11 @@ function theme_progress_bar($variables) {
 }
 
 /**
- * Create a standard indentation div. Used for drag and drop tables.
+ * Returns HTML for an indentation div; used for drag and drop tables.
  *
  * @param $variables
  *   An associative array containing:
  *   - size: Optional. The number of indentations to create.
- *
- * @return
- *   A string containing indentations.
  */
 function theme_indentation($variables) {
   $output = '';
@@ -2184,19 +2100,28 @@ function _template_preprocess_default_variables() {
     'title_prefix' => array(),
     'title_suffix' => array(),
     'user' => $user,
-    'db_is_active' => !defined('MAINTENANCE_MODE') && db_is_active(),
+    'db_is_active' => !defined('MAINTENANCE_MODE'),
+    'is_admin' => FALSE,
+    'logged_in' => FALSE,
   );
 
-  // Variables that depend on a database connection.
-  if ($variables['db_is_active']) {
+  // The user object has no uid property when the database does not exist during
+  // install. The user_access() check deals with issues when in maintenance mode
+  // as uid is set but the user.module has not been included.
+  if (isset($user->uid) && function_exists('user_access')) {
     $variables['is_admin'] = user_access('access administration pages');
-    $variables['is_front'] = drupal_is_front_page();
     $variables['logged_in'] = ($user->uid > 0);
   }
-  else {
-    $variables['is_admin'] = FALSE;
+
+  // drupal_is_front_page() might throw an exception.
+  try {
+    $variables['is_front'] = drupal_is_front_page();
+  }
+  catch (Exception $e) {
+    // If the database is not yet available, set default values for these
+    // variables.
     $variables['is_front'] = FALSE;
-    $variables['logged_in'] = FALSE;
+    $variables['db_is_active'] = FALSE;
   }
 
   return $variables;
@@ -2464,7 +2389,7 @@ function template_preprocess_maintenance_page(&$variables) {
 
   global $theme;
   // Retrieve the theme data to list all available regions.
-  $theme_data = _system_rebuild_theme_data();
+  $theme_data = list_themes();
   $regions = $theme_data[$theme]->info['regions'];
 
   // Get all region content set with drupal_add_region_content().
@@ -2571,3 +2496,80 @@ function template_preprocess_region(&$variables) {
   $variables['classes_array'][] = $region;
   $variables['theme_hook_suggestions'][] = 'region__' . $region;
 }
+
+/**
+ * Preprocesses variables for theme_username().
+ *
+ * Modules that make any changes to variables like 'name' or 'extra' must insure
+ * that the final string is safe to include directly in the output by using
+ * check_plain() or filter_xss().
+ *
+ * @see template_process_username()
+ */
+function template_preprocess_username(&$variables) {
+  $account = $variables['account'];
+
+  $variables['extra'] = '';
+  if (empty($account->uid)) {
+   $variables['uid'] = 0;
+   if (theme_get_setting('toggle_comment_user_verification')) {
+     $variables['extra'] = ' (' . t('not verified') . ')';
+   }
+  }
+  else {
+    $variables['uid'] = (int)$account->uid;
+  }
+
+  // Set the name to a formatted name that is safe for printing and
+  // that won't break tables by being too long. Keep an unshortened,
+  // unsanitized version, in case other preprocess functions want to implement
+  // their own shortening logic or add markup. If they do so, they must ensure
+  // that $variables['name'] is safe for printing.
+  $name = $variables['name_raw'] = format_username($account);
+  if (drupal_strlen($name) > 20) {
+    $name = drupal_substr($name, 0, 15) . '...';
+  }
+  $variables['name'] = check_plain($name);
+
+  $variables['profile_access'] = user_access('access user profiles');
+  $variables['link_attributes'] = array();
+  // Populate link path and attributes if appropriate.
+  if ($variables['uid'] && $variables['profile_access']) {
+    // We are linking to a local user.
+    $variables['link_attributes'] = array('title' => t('View user profile.'));
+    $variables['link_path'] = 'user/' . $variables['uid'];
+  }
+  elseif (!empty($account->homepage)) {
+    // Like the 'class' attribute, the 'rel' attribute can hold a
+    // space-separated set of values, so initialize it as an array to make it
+    // easier for other preprocess functions to append to it.
+    $variables['link_attributes'] = array('rel' => array('nofollow'));
+    $variables['link_path'] = $account->homepage;
+    $variables['homepage'] = $account->homepage;
+  }
+  // We do not want the l() function to check_plain() a second time.
+  $variables['link_options']['html'] = TRUE;
+  // Set a default class.
+  $variables['attributes_array'] = array('class' => array('username'));
+}
+
+/**
+ * Processes variables for theme_username().
+ *
+ * @see template_preprocess_username()
+ */
+function template_process_username(&$variables) {
+  // Finalize the link_options array for passing to the l() function.
+  // This is done in the process phase so that attributes may be added by
+  // modules or the theme during the preprocess phase.
+  if (isset($variables['link_path'])) {
+    // $variables['attributes_array'] contains attributes that should be applied
+    // regardless of whether a link is being rendered or not.
+    // $variables['link_attributes'] contains attributes that should only be
+    // applied if a link is being rendered. Preprocess functions are encouraged
+    // to use the former unless they want to add attributes on the link only.
+    // If a link is being rendered, these need to be merged. Some attributes are
+    // themselves arrays, so the merging needs to be recursive.
+    $variables['link_options']['attributes'] = array_merge_recursive($variables['link_attributes'], $variables['attributes_array']);
+  }
+}
diff --git a/includes/theme.maintenance.inc b/includes/theme.maintenance.inc
index b12eaaff5bfec9922c447994958875ab28e640b5..f2d416abcb7b0532b97b285db1a39cc84fac1d77 100644
--- a/includes/theme.maintenance.inc
+++ b/includes/theme.maintenance.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: theme.maintenance.inc,v 1.55 2010/03/17 14:04:38 dries Exp $
+// $Id: theme.maintenance.inc,v 1.58 2010/04/20 08:19:01 webchick Exp $
 
 /**
  * @file
@@ -7,11 +7,12 @@
  */
 
 /**
- * Sets up the theming system for site installs, updates and when the site is
- * in maintenance mode. It also applies when the database is unavailable.
+ * Sets up the theming system for maintenance page.
  *
- * Seven is always used for the initial install and update operations. In
- * other cases, Garland is used, but this can be overridden by setting a
+ * Used for site installs, updates and when the site is in maintenance mode.
+ * It also applies when the database is unavailable or bootstrap was not
+ * complete. Seven is always used for the initial install and update operations.
+ * In other cases, Garland is used, but this can be overridden by setting a
  * "maintenance_theme" key in the $conf variable in settings.php.
  */
 function _drupal_maintenance_theme() {
@@ -35,10 +36,16 @@ function _drupal_maintenance_theme() {
     $custom_theme = (isset($conf['maintenance_theme']) ? $conf['maintenance_theme'] : 'seven');
   }
   else {
-    if (!db_is_active()) {
-      // Because we are operating in a crippled environment, we need to
-      // bootstrap just enough to allow hook invocations to work.
+    // The bootstrap was not complete. So we are operating in a crippled
+    // environment, we need to bootstrap just enough to allow hook invocations
+    // to work. See _drupal_log_error().
+    if (!class_exists('Database', FALSE)) {
       require_once DRUPAL_ROOT . '/includes/database/database.inc';
+      spl_autoload_register('db_autoload');
+    }
+
+    // Ensure that system.module is loaded.
+    if (!function_exists('_system_rebuild_theme_data')) {
       $module_list['system']['filename'] = 'modules/system/system.module';
       module_list(TRUE, FALSE, FALSE, $module_list);
       drupal_load('module', 'system');
@@ -86,7 +93,12 @@ function _theme_load_offline_registry($theme, $base_theme = NULL, $theme_engine
 }
 
 /**
- * Return a themed list of maintenance tasks to perform.
+ * Returns HTML for a list of maintenance tasks to perform.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - items: An associative array of maintenance tasks.
+ *   - active: The key for the currently active maintenance task.
  *
  * @ingroup themeable
  */
@@ -119,7 +131,7 @@ function theme_task_list($variables) {
 }
 
 /**
- * Generate a themed installation page.
+ * Returns HTML for the installation page.
  *
  * Note: this function is not themeable.
  *
@@ -179,7 +191,7 @@ function theme_install_page($variables) {
 }
 
 /**
- * Generate a themed update page.
+ * Returns HTML for the update page.
  *
  * Note: this function is not themeable.
  *
@@ -225,10 +237,13 @@ function theme_update_page($variables) {
 }
 
 /**
- * Generate a report of the results from an operation run via authorize.php.
+ * Returns HTML for a report of the results from an operation run via authorize.php.
  *
- * @param array $variables
+ * @param $variables
+ *   An associative array containing:
  *   - messages: An array of result messages.
+ *
+ * @ingroup themeable
  */
 function theme_authorize_report($variables) {
   $messages = $variables['messages'];
@@ -250,11 +265,14 @@ function theme_authorize_report($variables) {
 }
 
 /**
- * Render a single log message from the authorize.php batch operation.
+ * Returns HTML for a single log message from the authorize.php batch operation.
  *
  * @param $variables
+ *   An associative array containing:
  *   - message: The log message.
  *   - success: A boolean indicating failure or success.
+ *
+ * @ingroup themeable
  */
 function theme_authorize_message($variables) {
   $output = '';
diff --git a/includes/update.inc b/includes/update.inc
index 121d251763d71f139fab5fd35a2b1fa326ec12b9..fe84dd5c2558d7662959740732cd449397e1fea9 100644
--- a/includes/update.inc
+++ b/includes/update.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: update.inc,v 1.40 2010/03/20 15:06:51 dries Exp $
+// $Id: update.inc,v 1.44 2010/04/21 07:05:44 webchick Exp $
 
 /**
  * @file
@@ -36,7 +36,10 @@ function update_check_incompatibility($name, $type = 'module') {
 
   // Store values of expensive functions for future use.
   if (empty($themes) || empty($modules)) {
-    $themes = _system_rebuild_theme_data();
+    // We need to do a full rebuild here to make sure the database reflects any
+    // code changes that were made in the filesystem before the update script
+    // was initiated.
+    $themes = system_rebuild_theme_data();
     $modules = system_rebuild_module_data();
   }
 
@@ -99,9 +102,35 @@ function update_prepare_d7_bootstrap() {
   // Allow the database system to work even if the registry has not been
   // created yet.
   drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
-  drupal_install_initialize_database();
-  spl_autoload_unregister('drupal_autoload_class');
-  spl_autoload_unregister('drupal_autoload_interface');
+
+  // Create the registry tables.
+  if (!db_table_exists('registry')) {
+    $schema['registry'] = array(
+      'fields' => array(
+        'name'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
+        'type'   => array('type' => 'varchar', 'length' => 9, 'not null' => TRUE, 'default' => ''),
+        'filename'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
+        'module'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
+        'weight'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      ),
+      'primary key' => array('name', 'type'),
+      'indexes' => array(
+        'hook' => array('type', 'weight', 'module'),
+      ),
+    );
+    db_create_table('registry', $schema['registry']);
+  }
+  if (!db_table_exists('registry_file')) {
+    $schema['registry_file'] = array(
+      'fields' => array(
+        'filename'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE),
+        'filectime'  => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+        'filemtime'  => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      ),
+      'primary key' => array('filename'),
+    );
+    db_create_table('registry_file', $schema['registry_file']);
+  }
 
   // The new cache_bootstrap bin is required to bootstrap to
   // DRUPAL_BOOTSTRAP_SESSION, so create it here rather than in
@@ -342,61 +371,9 @@ function update_fix_d7_requirements() {
     );
     db_create_table('role_permission', $schema['role_permission']);
 
-    // Add the {semaphore} table in case menu_rebuild() gets called during
-    // an update.
-    $schema['semaphore'] = array(
-      'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as Drupal variables since they must not be cached.',
-      'fields' => array(
-        'name' => array(
-          'description' => 'Primary Key: Unique name.',
-          'type' => 'varchar',
-          'length' => 255,
-          'not null' => TRUE,
-          'default' => ''
-        ),
-        'value' => array(
-          'description' => 'A value for the semaphore.',
-          'type' => 'varchar',
-          'length' => 255,
-          'not null' => TRUE,
-          'default' => ''
-        ),
-        'expire' => array(
-          'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
-          'type' => 'float',
-          'size' => 'big',
-          'not null' => TRUE
-        ),
-      ),
-      'indexes' => array('value' => array('value')),
-      'primary key' => array('name'),
-    );
-    db_create_table('semaphore', $schema['semaphore']);
-
-    // Add registry tables since these are required during an update.
-    $schema['registry'] = array(
-      'fields' => array(
-        'name'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
-        'type'   => array('type' => 'varchar', 'length' => 9, 'not null' => TRUE, 'default' => ''),
-        'filename'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
-        'module'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
-        'weight'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
-      ),
-      'primary key' => array('name', 'type'),
-      'indexes' => array(
-        'hook' => array('type', 'weight', 'module'),
-      ),
-    );
-    $schema['registry_file'] = array(
-      'fields' => array(
-        'filename'   => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE),
-        'filectime'  => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
-        'filemtime'  => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
-      ),
-      'primary key' => array('filename'),
-    );
-    db_create_table('registry', $schema['registry']);
-    db_create_table('registry_file', $schema['registry_file']);
+    // Drops and recreates semaphore value index.
+    db_drop_index('semaphore', 'expire');
+    db_add_index('semaphore', 'value', array('value'));
 
     $schema['date_format_type'] = array(
       'description' => 'Stores configured date format types.',
diff --git a/includes/xmlrpc.inc b/includes/xmlrpc.inc
index 146dea1228b8da9826d50535a5785b34d64c8dce..6edfe019df2dfb4959e3142b63a65f6c4b38beb5 100644
--- a/includes/xmlrpc.inc
+++ b/includes/xmlrpc.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: xmlrpc.inc,v 1.64 2009/12/31 13:43:36 dries Exp $
+// $Id: xmlrpc.inc,v 1.65 2010/03/31 15:56:53 dries Exp $
 
 /**
  * @file
@@ -150,7 +150,7 @@ function xmlrpc_message($message) {
 }
 
 /**
- * Parse an XML-RPC message.
+ * Parses an XML-RPC message.
  *
  * If parsing fails, the faultCode and faultString will be added to the message
  * object.
@@ -161,11 +161,6 @@ function xmlrpc_message($message) {
  *   TRUE if parsing succeeded; FALSE otherwise
  */
 function xmlrpc_message_parse($xmlrpc_message) {
-  // First remove the XML declaration
-  $xmlrpc_message->message = preg_replace('/<\?xml(.*)?\?' . '>/', '', $xmlrpc_message->message);
-  if (trim($xmlrpc_message->message) == '') {
-    return FALSE;
-  }
   $xmlrpc_message->_parser = xml_parser_create();
   // Set XML parser to take the case of tags into account.
   xml_parser_set_option($xmlrpc_message->_parser, XML_OPTION_CASE_FOLDING, FALSE);
@@ -177,9 +172,13 @@ function xmlrpc_message_parse($xmlrpc_message) {
     return FALSE;
   }
   xml_parser_free($xmlrpc_message->_parser);
-  // Grab the error messages, if any
+
+  // Grab the error messages, if any.
   $xmlrpc_message = xmlrpc_message_get();
-  if ($xmlrpc_message->messagetype == 'fault') {
+  if (!isset($xmlrpc_message->messagetype)) {
+    return FALSE;
+  }
+  elseif ($xmlrpc_message->messagetype == 'fault') {
     $xmlrpc_message->fault_code = $xmlrpc_message->params[0]['faultCode'];
     $xmlrpc_message->fault_string = $xmlrpc_message->params[0]['faultString'];
   }
diff --git a/includes/xmlrpcs.inc b/includes/xmlrpcs.inc
index 60955512faa1b7cf961934a5a67585a386e7f31a..6055a19bb5115fa36b58ca9add9cbf434b04b075 100644
--- a/includes/xmlrpcs.inc
+++ b/includes/xmlrpcs.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: xmlrpcs.inc,v 1.37 2010/01/28 07:06:57 webchick Exp $
+// $Id: xmlrpcs.inc,v 1.38 2010/03/31 15:33:08 dries Exp $
 
 /**
  * @file
@@ -75,7 +75,7 @@ function xmlrpc_server($callbacks) {
   xmlrpc_server_set($xmlrpc_server);
   $result = xmlrpc_server_call($xmlrpc_server, $xmlrpc_server->message->methodname, $xmlrpc_server->message->params);
 
-  if ($result->is_error) {
+  if (is_object($result) && !empty($result->is_error)) {
     xmlrpc_server_error($result);
   }
   // Encode the result
@@ -235,7 +235,7 @@ function xmlrpc_server_multicall($methodcalls) {
     elseif ($ok) {
       $result = xmlrpc_server_call($xmlrpc_server, $method, $params);
     }
-    if ($result->is_error) {
+    if (is_object($result) && !empty($result->is_error)) {
       $return[] = array(
         'faultCode' => $result->code,
         'faultString' => $result->message
diff --git a/misc/ajax.js b/misc/ajax.js
index c31b243a5f6d83ade990bc871096c64dfd160126..2ea89e65c31e7473360455945417dc47bda45d97 100644
--- a/misc/ajax.js
+++ b/misc/ajax.js
@@ -1,4 +1,4 @@
-// $Id: ajax.js,v 1.12 2010/03/10 15:14:38 dries Exp $
+// $Id: ajax.js,v 1.15 2010/04/07 17:30:43 dries Exp $
 (function ($) {
 
 /**
@@ -98,7 +98,7 @@ Drupal.ajax = function (base, element, element_settings) {
       type: 'bar',
       message: 'Please wait...'
     },
-    button: {}
+    submit: {}
   };
 
   $.extend(this, defaults, element_settings);
@@ -121,7 +121,7 @@ Drupal.ajax = function (base, element, element_settings) {
   var ajax = this;
   var options = {
     url: ajax.url,
-    data: ajax.button,
+    data: ajax.submit,
     beforeSerialize: function (element_settings, options) {
       return ajax.beforeSerialize(element_settings, options);
     },
@@ -200,9 +200,11 @@ Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) {
   // Disable the element that received the change.
   $(this.element).addClass('progress-disabled').attr('disabled', true);
 
-  // Server-side code needs to know what element triggered the call, so it can
-  // find the #ajax binding.
-  form_values.push({ name: 'ajax_triggering_element', value: this.formPath });
+  // Prevent duplicate HTML ids in the returned markup.
+  // @see drupal_html_id()
+  $('[id]').each(function () {
+    form_values.push({ name: 'ajax_html_ids[]', value: this.id });
+  });
 
   // Insert progressbar or throbber.
   if (this.progress.type == 'bar') {
@@ -427,12 +429,11 @@ Drupal.ajax.prototype.commands = {
   restripe: function (ajax, response, status) {
     // :even and :odd are reversed because jQuery counts from 0 and
     // we count from 1, so we're out of sync.
-    $('tbody tr:not(:hidden)', $(response.selector))
-      .removeClass('even').removeClass('odd')
-      .filter(':even')
-        .addClass('odd').end()
-      .filter(':odd')
-        .addClass('even');
+    // Match immediate children of the parent element to allow nesting.
+    $('> tbody > tr:visible, > tr:visible', $(response.selector))
+      .removeClass('odd even')
+      .filter(':even').addClass('odd').end()
+      .filter(':odd').addClass('even');
   }
 };
 
diff --git a/misc/collapse.js b/misc/collapse.js
index 52237130ea95d06f4069d40736bd105ede06ef14..8f7e7cbcadd3ab42415d34878e0c052d325aea66 100644
--- a/misc/collapse.js
+++ b/misc/collapse.js
@@ -1,4 +1,4 @@
-// $Id: collapse.js,v 1.28 2010/03/03 19:46:25 dries Exp $
+// $Id: collapse.js,v 1.30 2010/04/09 12:24:53 dries Exp $
 (function ($) {
 
 /**
@@ -40,8 +40,8 @@ Drupal.toggleFieldset = function (fieldset) {
  * Scroll a given fieldset into view as much as possible.
  */
 Drupal.collapseScrollIntoView = function (node) {
-  var h = self.innerHeight || document.documentElement.clientHeight || $('body')[0].clientHeight || 0;
-  var offset = self.pageYOffset || document.documentElement.scrollTop || $('body')[0].scrollTop || 0;
+  var h = document.documentElement.clientHeight || document.body.clientHeight || 0;
+  var offset = document.documentElement.scrollTop || document.body.scrollTop || 0;
   var posY = $(node).offset().top;
   var fudge = 55;
   if (posY + node.offsetHeight + fudge > h + offset) {
@@ -66,7 +66,7 @@ Drupal.behaviors.collapse = {
       var summary = $('<span class="summary"></span>');
       $fieldset.
         bind('summaryUpdated', function () {
-          var text = $.trim($fieldset.getSummary());
+          var text = $.trim($fieldset.drupalGetSummary());
           summary.html(text ? ' (' + text + ')' : '');
         })
         .trigger('summaryUpdated');
diff --git a/misc/form.js b/misc/form.js
index 1bb89c58f03130caa66dc61574d8a0300cc5a412..e5961f2e0cf1d43201676b2309b36bab41eea631 100644
--- a/misc/form.js
+++ b/misc/form.js
@@ -1,10 +1,10 @@
-// $Id: form.js,v 1.14 2010/03/07 23:14:20 webchick Exp $
+// $Id: form.js,v 1.15 2010/04/09 12:24:53 dries Exp $
 (function ($) {
 
 /**
  * Retrieves the summary for the first element.
  */
-$.fn.getSummary = function () {
+$.fn.drupalGetSummary = function () {
   var callback = this.data('summaryCallback');
   return (this[0] && callback) ? $.trim(callback(this[0])) : '';
 };
@@ -16,7 +16,7 @@ $.fn.getSummary = function () {
  *   Either a function that will be called each time the summary is
  *   retrieved or a string (which is returned each time).
  */
-$.fn.setSummary = function (callback) {
+$.fn.drupalSetSummary = function (callback) {
   var self = this;
 
   // To facilitate things, the callback should always be a function. If it's
diff --git a/misc/jquery.ba-bbq.js b/misc/jquery.ba-bbq.js
index 7a9f220c85a4b60a8aa138d736f6225a067743b4..c081a9379913e04ab4601997af50f3fc1fac4659 100644
--- a/misc/jquery.ba-bbq.js
+++ b/misc/jquery.ba-bbq.js
@@ -1,11 +1,20 @@
-// $Id: jquery.ba-bbq.js,v 1.1 2009/12/02 07:28:22 webchick Exp $
+// $Id: jquery.ba-bbq.js,v 1.2 2010/04/11 01:07:14 webchick Exp $
 
 /*
- * jQuery BBQ: Back Button & Query Library - v1.0.2 - 10/10/2009
+ * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010
  * http://benalman.com/projects/jquery-bbq-plugin/
  * 
- * Copyright (c) 2009 "Cowboy" Ben Alman
+ * Copyright (c) 2010 "Cowboy" Ben Alman
  * Dual licensed under the MIT and GPL licenses.
  * http://benalman.com/about/license/
  */
-(function($,c){var g,k=document.location,i=Array.prototype.slice,E=decodeURIComponent,a=$.param,m,d,p,n=$.bbq=$.bbq||{},o,e,z,b="hashchange",v="querystring",y="fragment",q="hash",x="elemUrlAttr",h="href",D="src",C=$.browser,l=C.msie&&C.version<8,j="on"+b in c&&!l,r=/^.*\?|#.*$/g,A=/^.*\#/,t={};function s(F){return typeof F==="string"}function w(G){var F=i.call(arguments,1);return function(){return G.apply(this,F.concat(i.call(arguments)))}}function f(G,O,F,H,K){var M,L,J,N,I;if(H!==g){J=F.match(G?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);I=J[3]||"";if(K===2&&s(H)){L=H.replace(O,"")}else{N=d(J[2]);H=s(H)?d[G?y:v](H):H;L=K===2?H:K===1?$.extend({},H,N):$.extend({},N,H);L=a(L)}M=J[1]+(G?"#":L||!J[1]?"?":"")+L+I}else{if(F){M=F.replace(O,"")}else{M=G?k[q]?k[h].replace(O,""):"":k.search.replace(/^\??/,"")}}return M}a[v]=w(f,0,r);a[y]=m=w(f,1,A);$.deparam=d=function(I,G){var H={},F={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(M,O){var L=O.split("="),P=E(L[0]),K,Q=H,N=0,R=P.split("]["),J=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[J])){R[J]=R[J].replace(/\]$/,"");R=R.shift().split("[").concat(R);J=R.length-1}else{J=0}if(L.length===2){K=E(L[1]);if(G){K=K&&!isNaN(K)?+K:K==="undefined"?g:F[K]!==g?F[K]:K}if(J){for(;N<=J;N++){P=R[N]===""?Q.length:R[N];Q=Q[P]=N<J?Q[P]||(R[N+1]&&isNaN(R[N+1])?{}:[]):K}}else{if($.isArray(H[P])){H[P].push(K)}else{if(H[P]!==g){H[P]=[H[P],K]}else{H[P]=K}}}}else{if(P){H[P]=G?g:""}}});return H};function u(I,H,G,F){if(G===g||typeof G==="boolean"){F=G;G=a[I]()}else{G=s(G)?G.replace(H,""):G}return d(G,F)}d[v]=w(u,v,r);d[y]=p=w(u,y,A);$[x]||($[x]=function(F){return $.extend(t,F)})({a:h,base:h,iframe:D,img:D,input:D,form:"action",link:h,script:D});e=$[x];function B(I,F,H,G){if(!s(H)&&typeof H!=="object"){G=H;H=F;F=g}return this.each(function(){var L=$(this),J=F||e()[(this.nodeName||"").toLowerCase()]||"",K=J&&L.attr(J)||"";L.attr(J,a[I](K,H,G))})}$.fn[v]=w(B,v);$.fn[y]=w(B,y);n.pushState=o=function(I,H){if(s(I)&&/^#/.test(I)&&H===g){H=2}var G=I!==g,F=m(k[h],G?I:{},G?H:2);k[h]=F+(/#/.test(F)?"":"#")};n.getState=function(G,F){return G===g||typeof G==="boolean"?p(G):p(F)[G]};n.pollDelay=100;$.event.special[b]={setup:function(){if(j){return false}z.start()},teardown:function(){if(j){return false}z.stop()},add:function(F,H,G){return function(J){var I=J[y]=m();J.getState=function(L,K){return L===g||typeof L==="boolean"?d(I,L):d(I,K)[L]};F.apply(this,arguments)}}};z=(function(){var G={},K,F,H,J;function I(){H=J=function(L){return L};if(l){F=$('<iframe src="javascript:0"/>').hide().appendTo("body")[0].contentWindow;J=function(){return F.document.location[q].replace(/^#/,"")};H=function(N,L){if(N!==L){var M=F.document;M.open();M.close();M.location[q]="#"+N}};H(m())}}G.start=function(){if(K){return}var M=m();H||I();(function L(){var O=m(),N=J(M);if(O!==M){H(M=O,N);$(c).trigger(b)}else{if(N!==M){o("#"+N)}}K=setTimeout(L,n.pollDelay)})()};G.stop=function(){if(!F){K&&clearTimeout(K);K=0}};return G})()})(jQuery,this);
\ No newline at end of file
+(function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M<N?O[P]||(R[M+1]&&isNaN(R[M+1])?{}:[]):J}}else{if($.isArray(H[P])){H[P].push(J)}else{if(H[P]!==i){H[P]=[H[P],J]}else{H[P]=J}}}}else{if(P){H[P]=F?i:""}}});return H};function z(H,F,G){if(F===i||typeof F==="boolean"){G=F;F=a[H?D:A]()}else{F=E(F)?F.replace(H?w:x,""):F}return l(F,G)}l[A]=B(z,0);l[D]=v=B(z,1);$[y]||($[y]=function(F){return $.extend(C,F)})({a:k,base:k,iframe:t,img:t,input:t,form:"action",link:k,script:t});j=$[y];function s(I,G,H,F){if(!E(H)&&typeof H!=="object"){F=H;H=G;G=i}return this.each(function(){var L=$(this),J=G||j()[(this.nodeName||"").toLowerCase()]||"",K=J&&L.attr(J)||"";L.attr(J,a[I](K,H,F))})}$.fn[A]=B(s,A);$.fn[D]=B(s,D);b.pushState=q=function(I,F){if(E(I)&&/^#/.test(I)&&F===i){F=2}var H=I!==i,G=c(p[g][k],H?I:{},H?F:2);p[g][k]=G+(/#/.test(G)?"":"#")};b.getState=u=function(F,G){return F===i||typeof F==="boolean"?v(F):v(G)[F]};b.removeState=function(F){var G={};if(F!==i){G=u();$.each($.isArray(F)?F:arguments,function(I,H){delete G[H]})}q(G,2)};e[d]=$.extend(e[d],{add:function(F){var H;function G(J){var I=J[D]=c();J.getState=function(K,L){return K===i||typeof K==="boolean"?l(I,K):l(I,L)[K]};H.apply(this,arguments)}if($.isFunction(F)){H=F;return G}else{H=F.handler;F.handler=G}}})})(jQuery,this);
+/*
+ * jQuery hashchange event - v1.2 - 2/11/2010
+ * http://benalman.com/projects/jquery-hashchange-plugin/
+ * 
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+(function($,i,b){var j,k=$.event.special,c="location",d="hashchange",l="href",f=$.browser,g=document.documentMode,h=f.msie&&(g===b||g<8),e="on"+d in i&&!h;function a(m){m=m||i[c][l];return m.replace(/^[^#]*#?(.*)$/,"$1")}$[d+"Delay"]=100;k[d]=$.extend(k[d],{setup:function(){if(e){return false}$(j.start)},teardown:function(){if(e){return false}$(j.stop)}});j=(function(){var m={},r,n,o,q;function p(){o=q=function(s){return s};if(h){n=$('<iframe src="javascript:0"/>').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this);
diff --git a/misc/tabledrag.js b/misc/tabledrag.js
index 30f5345ddfc61bdf7c0f543f0ada8a11f64a086d..91585d632b37533d14e43d078f93d058b5baa8fd 100644
--- a/misc/tabledrag.js
+++ b/misc/tabledrag.js
@@ -1,4 +1,4 @@
-// $Id: tabledrag.js,v 1.35 2010/03/10 20:31:59 webchick Exp $
+// $Id: tabledrag.js,v 1.36 2010/03/31 19:22:00 dries Exp $
 (function ($) {
 
 /**
@@ -640,7 +640,7 @@ Drupal.tableDrag.prototype.updateField = function (changedRow, group) {
       // Use the first row in the table as source, because it's guaranteed to
       // be at the root level. Find the first item, then compare this row
       // against it as a sibling.
-      sourceRow = $('tr.draggable:first').get(0);
+      sourceRow = $(this.table).find('tr.draggable:first').get(0);
       if (sourceRow == this.rowObject.element) {
         sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0);
       }
diff --git a/misc/vertical-tabs.css b/misc/vertical-tabs.css
index a86b7d02a03c12d7c5cfeee59642f62cc744e8d3..d0312d6a7c7d92d11a76da4b3601bd3eeb57f288 100644
--- a/misc/vertical-tabs.css
+++ b/misc/vertical-tabs.css
@@ -1,4 +1,4 @@
-/* $Id: vertical-tabs.css,v 1.6 2009/05/31 00:51:41 webchick Exp $ */
+/* $Id: vertical-tabs.css,v 1.7 2010/03/30 07:05:58 webchick Exp $ */
 
 div.vertical-tabs {
   margin: 1em 0 1em 15em;
@@ -40,23 +40,25 @@ div.vertical-tabs ul.vertical-tabs-list li a {
   line-height: 1.3em;
   height: 1%;
 }
-div.vertical-tabs ul.vertical-tabs-list li a:focus {
+div.vertical-tabs ul.vertical-tabs-list li a:focus strong,
+div.vertical-tabs ul.vertical-tabs-list li a:active strong,
+div.vertical-tabs ul.vertical-tabs-list li a:hover strong {
+  text-decoration: underline;
+}
+div.vertical-tabs ul.vertical-tabs-list li a:focus,
+div.vertical-tabs ul.vertical-tabs-list li a:active {
   position: relative;
   z-index: 5;
 }
 div.vertical-tabs ul.vertical-tabs-list li a:hover {
-  text-decoration: none;
+  outline: 1px dotted;
 }
 div.vertical-tabs ul.vertical-tabs-list li.selected {
-  background: #fff;
+  background-color: #fff;
   border-right-width: 0;
   position: relative;
 }
-div.vertical-tabs ul.vertical-tabs-list li.selected a:focus {
-  outline: 0;
-}
-div.vertical-tabs ul.vertical-tabs-list li.selected strong,
-div.vertical-tabs ul.vertical-tabs-list li.selected small {
+div.vertical-tabs ul.vertical-tabs-list span.selected strong {
   color: #000;
 }
 div.vertical-tabs ul.vertical-tabs-list .summary {
diff --git a/misc/vertical-tabs.js b/misc/vertical-tabs.js
index 9ab03fe1e913f69a333795653f7e3dcd8b119908..5fa13e5fedea12352b71f60f9ed314f966f921d2 100644
--- a/misc/vertical-tabs.js
+++ b/misc/vertical-tabs.js
@@ -1,4 +1,4 @@
-// $Id: vertical-tabs.js,v 1.8 2010/03/05 13:32:09 dries Exp $
+// $Id: vertical-tabs.js,v 1.12 2010/04/09 12:24:53 dries Exp $
 
 (function ($) {
 
@@ -17,32 +17,42 @@ Drupal.behaviors.verticalTabs = {
   attach: function (context) {
     $('.vertical-tabs-panes', context).once('vertical-tabs', function () {
       var focusID = $(':hidden.vertical-tabs-active-tab', this).val();
-      var focus;
+      var tab_focus;
+
+      // Check if there are some fieldsets that can be converted to vertical-tabs
+      var $fieldsets = $('> fieldset', this);
+      if ($fieldsets.length == 0) {
+        return;
+      }
+
       // Create the tab column.
-      var list = $('<ul class="vertical-tabs-list"></ul>');
-      $(this).wrap('<div class="vertical-tabs clearfix"></div>').before(list);
+      var tab_list = $('<ul class="vertical-tabs-list"></ul>');
+      $(this).wrap('<div class="vertical-tabs clearfix"></div>').before(tab_list);
 
       // Transform each fieldset into a tab.
-      $('> fieldset', this).each(function () {
-        var tab = new Drupal.verticalTab({ title: $('> legend', this).text(), fieldset: $(this) });
-        list.append(tab.item);
+      $fieldsets.each(function () {
+        var vertical_tab = new Drupal.verticalTab({
+          title: $('> legend', this).text(),
+          fieldset: $(this)
+        });
+        tab_list.append(vertical_tab.item);
         $(this)
           .removeClass('collapsible collapsed')
           .addClass('vertical-tabs-pane')
-          .data('verticalTab', tab);
+          .data('verticalTab', vertical_tab);
         if (this.id == focusID) {
-          focus = $(this);
+          tab_focus = $(this);
         }
       });
 
-      $('> li:first', list).addClass('first');
-      $('> li:last', list).addClass('last');
+      $('> li:first', tab_list).addClass('first');
+      $('> li:last', tab_list).addClass('last');
 
-      if (!focus) {
-        focus = $('> .vertical-tabs-pane:first', this);
+      if (!tab_focus) {
+        tab_focus = $('> .vertical-tabs-pane:first', this);
       }
-      if (focus.length) {
-        focus.data('verticalTab').focus();
+      if (tab_focus.length) {
+        tab_focus.data('verticalTab').focus();
       }
     });
   }
@@ -65,6 +75,27 @@ Drupal.verticalTab = function (settings) {
     return false;
   });
 
+  // Keyboard events added:
+  // Pressing the Enter key will open the tab pane.
+  this.link.keydown(function(event) {
+    if (event.keyCode == 13) {
+      self.focus();
+      // Set focus on the first input field of the visible fieldset/tab pane.
+      $("fieldset.vertical-tabs-pane :input:visible:enabled:first").focus();
+      return false;
+    }
+  });
+
+  // Pressing the Enter key lets you leave the tab again.
+  this.fieldset.keydown(function(event) {
+    // Enter key should not trigger inside <textarea> to allow for multi-line entries.
+    if (event.keyCode == 13 && event.target.nodeName != "TEXTAREA") {
+      // Set focus on the selected tab button again.
+      $(".vertical-tab-button.selected a").focus();
+      return false;
+    }
+  });
+
   this.fieldset
     .bind('summaryUpdated', function () {
       self.updateSummary();
@@ -73,7 +104,9 @@ Drupal.verticalTab = function (settings) {
 };
 
 Drupal.verticalTab.prototype = {
-  // Displays the tab's content pane.
+  /**
+   * Displays the tab's content pane.
+   */
   focus: function () {
     this.fieldset
       .siblings('fieldset.vertical-tabs-pane')
@@ -87,11 +120,55 @@ Drupal.verticalTab.prototype = {
       .siblings(':hidden.vertical-tabs-active-tab')
         .val(this.fieldset.attr('id'));
     this.item.addClass('selected');
+    // Mark the active tab for screen readers.
+    $('#active-vertical-tab').remove();
+    this.link.append('<span id="active-vertical-tab" class="element-invisible">' + Drupal.t('(active tab)') + '</span>');
   },
 
-  // Updates the tab's summary.
+  /**
+   * Updates the tab's summary.
+   */
   updateSummary: function () {
-    this.summary.html(this.fieldset.getSummary());
+	  this.summary.html(this.fieldset.drupalGetSummary());
+  },
+
+  /**
+   * Shows a vertical tab pane.
+   */
+  tabShow: function () {
+    // Display the tab.
+    this.item.show();
+    // Update .first marker for items. We need recurse from parent to retain the
+    // actual DOM element order as jQuery implements sortOrder, but not as public
+    // method.
+    this.item.parent().children('.vertical-tab-button').removeClass('first')
+      .filter(':visible:first').addClass('first');
+    // Display the fieldset.
+    this.fieldset.removeClass('vertical-tab-hidden').show();
+    // Focus this tab.
+    this.focus();
+    return this;
+  },
+
+  /**
+   * Hides a vertical tab pane.
+   */
+  tabHide: function () {
+    // Hide this tab.
+    this.item.hide();
+    // Update .first marker for items. We need recurse from parent to retain the
+    // actual DOM element order as jQuery implements sortOrder, but not as public
+    // method.
+    this.item.parent().children('.vertical-tab-button').removeClass('first')
+      .filter(':visible:first').addClass('first');
+    // Hide the fieldset.
+    this.fieldset.addClass('vertical-tab-hidden').hide();
+    // Focus the first visible tab (if there is one).
+    var $firstTab = this.fieldset.siblings('.vertical-tabs-pane:not(.vertical-tab-hidden):first');
+    if ($firstTab.length) {
+      $firstTab.data('verticalTab').focus();
+    }
+    return this;
   }
 };
 
@@ -110,10 +187,10 @@ Drupal.verticalTab.prototype = {
  */
 Drupal.theme.prototype.verticalTab = function (settings) {
   var tab = {};
-  tab.item = $('<li class="vertical-tab-button"></li>')
+  tab.item = $('<li class="vertical-tab-button" tabindex="-1"></li>')
     .append(tab.link = $('<a href="#"></a>')
       .append(tab.title = $('<strong></strong>').text(settings.title))
-      .append(tab.summary = $('<small class="summary"></small>')
+      .append(tab.summary = $('<span class="summary"></span>')
     )
   );
   return tab;
diff --git a/modules/aggregator/aggregator.admin.inc b/modules/aggregator/aggregator.admin.inc
index bc56ce67548a0fc99670f4226e2ddd387ccf9718..0ffa70b1e2d9997323e7999a45e46cb5458f3fbe 100644
--- a/modules/aggregator/aggregator.admin.inc
+++ b/modules/aggregator/aggregator.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: aggregator.admin.inc,v 1.50 2010/02/27 15:05:19 dries Exp $
+// $Id: aggregator.admin.inc,v 1.52 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -109,13 +109,14 @@ function aggregator_form_feed($form, &$form_state, stdClass $feed = NULL) {
       '#description' => t('New feed items are automatically filed in the checked categories.'),
     );
   }
-  $form['submit'] = array(
+
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
   );
-
   if (!empty($feed->fid)) {
-    $form['delete'] = array(
+    $form['actions']['delete'] = array(
       '#type' => 'submit',
       '#value' => t('Delete'),
     );
@@ -270,7 +271,8 @@ function aggregator_form_opml($form, &$form_state) {
       '#description' => t('New feed items are automatically filed in the checked categories.'),
     );
   }
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Import')
   );
@@ -478,7 +480,7 @@ function aggregator_admin_form($form, $form_state) {
   // Implementing modules will expect an array at $form['modules'].
   $form['modules'] = array();
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save configuration'),
@@ -488,7 +490,9 @@ function aggregator_admin_form($form, $form_state) {
 }
 
 function aggregator_admin_form_submit($form, &$form_state) {
-  $form_state['values']['aggregator_processors'] = array_filter($form_state['values']['aggregator_processors']);
+  if (isset($form_state['values']['aggregator_processors'])) {
+    $form_state['values']['aggregator_processors'] = array_filter($form_state['values']['aggregator_processors']);
+  }
   system_settings_form_submit($form, $form_state);
 }
 
@@ -510,10 +514,10 @@ function aggregator_form_category($form, &$form_state, $edit = array('title' =>
     '#title' => t('Description'),
     '#default_value' => $edit['description'],
   );
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
-
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
   if ($edit['cid']) {
-    $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+    $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
     $form['cid'] = array('#type' => 'hidden', '#value' => $edit['cid']);
   }
 
diff --git a/modules/aggregator/aggregator.info b/modules/aggregator/aggregator.info
index 64fa73005aff24b1c084a6840900c3e7b0fd14e2..14e94d5b12f55b2c0db0f113db22503157a43b65 100644
--- a/modules/aggregator/aggregator.info
+++ b/modules/aggregator/aggregator.info
@@ -14,8 +14,8 @@ files[] = aggregator.install
 files[] = aggregator.test
 configure = admin/config/services/aggregator/settings
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/aggregator/aggregator.module b/modules/aggregator/aggregator.module
index 2646422f4cdac4799a3c24555a1ff733415cda16..467423adfff6ad20f77d5f141987bc619f4f9261 100644
--- a/modules/aggregator/aggregator.module
+++ b/modules/aggregator/aggregator.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: aggregator.module,v 1.436 2010/02/26 00:11:58 webchick Exp $
+// $Id: aggregator.module,v 1.438 2010/04/13 15:23:02 dries Exp $
 
 /**
  * @file
@@ -209,7 +209,7 @@ function aggregator_menu() {
   $items['aggregator/categories/%aggregator_category/categorize'] = array(
     'title' => 'Categorize',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('aggregator_page_category', 2),
+    'page arguments' => array('aggregator_page_category_form', 2),
     'access arguments' => array('administer news feeds'),
     'type' => MENU_LOCAL_TASK,
     'file' => 'aggregator.pages.inc',
@@ -238,7 +238,7 @@ function aggregator_menu() {
   $items['aggregator/sources/%aggregator_feed/categorize'] = array(
     'title' => 'Categorize',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('aggregator_page_source', 2),
+    'page arguments' => array('aggregator_page_source_form', 2),
     'access arguments' => array('administer news feeds'),
     'type' => MENU_LOCAL_TASK,
     'file' => 'aggregator.pages.inc',
@@ -703,15 +703,13 @@ function aggregator_category_load($cid) {
 }
 
 /**
- * Format an individual feed item for display in the block.
+ * Returns HTML for an individual feed item for display in the block.
  *
  * @param $variables
  *   An associative array containing:
  *   - item: The item to be displayed.
  *   - feed: Not used.
  *
- * @return
- *   The item HTML.
  * @ingroup themeable
  */
 function theme_aggregator_block_item($variables) {
diff --git a/modules/aggregator/aggregator.pages.inc b/modules/aggregator/aggregator.pages.inc
index 180354a6aaa63cfab001f21962bbf8bbb81f5de1..08bfa675aef787e33aa4c380857b7f341eaba6ac 100644
--- a/modules/aggregator/aggregator.pages.inc
+++ b/modules/aggregator/aggregator.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: aggregator.pages.inc,v 1.39 2010/01/18 03:31:29 dries Exp $
+// $Id: aggregator.pages.inc,v 1.42 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -23,19 +23,13 @@ function aggregator_page_last() {
 /**
  * Menu callback; displays all the items captured from a particular feed.
  *
- * If there are two arguments then this function is the categorize form.
+ * @param $feed
+ *   The feed for which to display all items.
  *
- * @param $arg1
- *   If there are two arguments then $arg1 is $form_state. Otherwise, $arg1 is $feed.
- * @param $arg2
- *   If there are two arguments then $arg2 is feed.
  * @return
- *   The item's HTML.
+ *   The rendered list of items for a feed.
  */
-function aggregator_page_source($arg1, $arg2 = NULL) {
-  // If there are two arguments then this function is the categorize form, and
-  // $arg1 is $form_state and $arg2 is $feed. Otherwise, $arg1 is $feed.
-  $feed = is_object($arg2) ? $arg2 : $arg1;
+function aggregator_page_source($feed) {
   drupal_set_title($feed->title);
   $feed_source = theme('aggregator_feed_source', array('feed' => $feed));
 
@@ -47,22 +41,30 @@ function aggregator_page_source($arg1, $arg2 = NULL) {
 }
 
 /**
- * Menu callback; displays all the items aggregated in a particular category.
+ * Menu callback; displays a form with all items captured from a feed.
  *
- * If there are two arguments then this function is called as a form.
+ * @param $feed
+ *   The feed for which to list all the aggregated items.
  *
- * @param $arg1
- *   If there are two arguments then $arg1 is $form_state. Otherwise, $arg1 is $category.
- * @param $arg2
- *   If there are two arguments then $arg2 is $category.
  * @return
- *   The items HTML.
+ *   The rendered list of items for a feed.
+ *
+ * @see aggregator_page_source()
  */
-function aggregator_page_category($arg1, $arg2 = NULL) {
-  // If there are two arguments then we are called as a form, $arg1 is
-  // $form_state and $arg2 is $category. Otherwise, $arg1 is $category.
-  $category = is_array($arg2) ? $arg2 : $arg1;
+function aggregator_page_source_form($form, $form_state, $feed) {
+  return aggregator_page_source($feed);
+}
 
+/**
+ * Menu callback; displays all the items aggregated in a particular category.
+ *
+ * @param $category
+ *   The category for which to list all the aggregated items.
+ *
+ * @return
+*   The rendered list of items for a category.
+ */
+function aggregator_page_category($category) {
   drupal_add_feed(url('aggregator/rss/' . $category['cid']), variable_get('site_name', 'Drupal') . ' ' . t('aggregator - @title', array('@title' => $category['title'])));
 
   // It is safe to include the cid in the query because it's loaded from the
@@ -72,6 +74,21 @@ function aggregator_page_category($arg1, $arg2 = NULL) {
   return _aggregator_page_list($items, arg(3));
 }
 
+/**
+ * Menu callback; displays a form containing items aggregated in a category.
+ *
+ * @param $category
+ *   The category for which to list all the aggregated items.
+ *
+ * @return
+*   The rendered list of items for a category.
+ *
+ * @see aggregator_page_category()
+ */
+function aggregator_page_category_form($form, $form_state, $category) {
+  return aggregator_page_category($category);
+}
+
 /**
  * Load feed items
  *
@@ -182,7 +199,7 @@ function aggregator_categorize_items($items, $feed_source = '') {
       '#multiple' => TRUE
     );
   }
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save categories'));
 
   return $form;
@@ -217,14 +234,12 @@ function aggregator_categorize_items_submit($form, &$form_state) {
 }
 
 /**
- * Theme the page list form for assigning categories.
+ * Returns HTML for the aggregator page list form for assigning categories.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An associative array containing the structure of the form.
+ *   - form: A render element representing the form.
  *
- * @return
- *   The output HTML.
  * @ingroup themeable
  */
 function theme_aggregator_categorize_items($variables) {
@@ -354,13 +369,15 @@ function aggregator_page_rss() {
 }
 
 /**
- * Theme the RSS output.
+ * Prints the RSS page for a feed.
  *
  * @param $variables
  *   An associative array containing:
  *   - feeds: An array of the feeds to theme.
  *   - category: A common category, if any, for all the feeds.
  *
+ * @return void
+ *
  * @ingroup themeable
  */
 function theme_aggregator_page_rss($variables) {
@@ -420,12 +437,14 @@ function aggregator_page_opml($cid = NULL) {
 }
 
 /**
- * Theme the OPML feed output.
+ * Prints the OPML page for a feed.
  *
  * @param $variables
  *   An associative array containing:
  *   - feeds: An array of the feeds to theme.
  *
+ * @return void
+ *
  * @ingroup themeable
  */
 function theme_aggregator_page_opml($variables) {
diff --git a/modules/aggregator/aggregator.test b/modules/aggregator/aggregator.test
index d7860411a171180eb1c14a379bcd10211582320e..153711d767bba96cea412388db1f5685fd18aa39 100644
--- a/modules/aggregator/aggregator.test
+++ b/modules/aggregator/aggregator.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: aggregator.test,v 1.38 2010/02/26 00:11:58 webchick Exp $
+// $Id: aggregator.test,v 1.39 2010/03/24 08:51:42 dries Exp $
 
 /**
  * @file
@@ -283,6 +283,8 @@ class AddFeedTestCase extends AggregatorTestCase {
     $this->drupalGet('aggregator/sources/' . $feed->fid);
     $this->assertResponse(200, t('Feed source exists.'));
     $this->assertText($feed->title, t('Page title'));
+    $this->drupalGet('aggregator/sources/' . $feed->fid . '/categorize');
+    $this->assertResponse(200, t('Feed categorization page exists.'));
 
     // Delete feed.
     $this->deleteFeed($feed);
diff --git a/modules/aggregator/tests/aggregator_test.info b/modules/aggregator/tests/aggregator_test.info
index 7a10a80fa7e51f256e040e7f37e6e37415406f0f..6114806ac6b7bb1fe511fdeaa5de652b89e86947 100644
--- a/modules/aggregator/tests/aggregator_test.info
+++ b/modules/aggregator/tests/aggregator_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = aggregator_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/block/block.admin.inc b/modules/block/block.admin.inc
index 807ea2cff1bb4c02d9b3e63fd153dba200d4e753..3fbbe75a75210820174222024a56a1f8d42be01d 100644
--- a/modules/block/block.admin.inc
+++ b/modules/block/block.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: block.admin.inc,v 1.75 2010/03/09 12:09:52 dries Exp $
+// $Id: block.admin.inc,v 1.78 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -104,8 +104,7 @@ function block_admin_display_form($form, &$form_state, $blocks, $theme) {
 
   $form['actions'] = array(
     '#tree' => FALSE,
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
+    '#type' => 'actions',
   );
   $form['actions']['submit'] = array(
     '#type' => 'submit',
@@ -313,7 +312,7 @@ function block_admin_configure($form, &$form_state, $module, $delta) {
     ':module' => $block->module,
     ':delta' => $block->delta,
   ))->fetchCol();
-  $role_options = db_query('SELECT rid, name FROM {role} ORDER BY name')->fetchAllKeyed();
+  $role_options = array_map('check_plain', user_roles());
   $form['visibility']['role'] = array(
     '#type' => 'fieldset',
     '#title' => t('Roles'),
@@ -351,7 +350,7 @@ function block_admin_configure($form, &$form_state, $module, $delta) {
     '#default_value' => isset($block->custom) ? $block->custom : 0,
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save block'),
@@ -446,6 +445,8 @@ function block_add_block_form_submit($form, &$form_state) {
       'format' => $form_state['values']['format'],
     ))
     ->execute();
+  // Store block delta to allow other modules to work with new block.
+  $form_state['values']['delta'] = $delta;
 
   $query = db_insert('block')->fields(array('visibility', 'pages', 'custom', 'title', 'module', 'theme', 'status', 'weight', 'delta', 'cache'));
   foreach (list_themes() as $key => $theme) {
diff --git a/modules/block/block.api.php b/modules/block/block.api.php
index c98e2de8806b79504264e4eec54a15388d4e4f25..8bdab9f7dd59bf0d0baf2c76e065de9fa2e52279 100644
--- a/modules/block/block.api.php
+++ b/modules/block/block.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: block.api.php,v 1.9 2009/10/17 05:50:28 webchick Exp $
+// $Id: block.api.php,v 1.11 2010/04/22 09:12:35 webchick Exp $
 
 /**
  * @file
@@ -69,6 +69,25 @@ function hook_block_info() {
   return $blocks;
 }
 
+/**
+ * Change block definition before saving to the database.
+ *
+ * @param $blocks
+ *   A multidimensional array of blocks keyed by the defining module and delta
+ *   the value is a block as seen in hook_block_info(). This hook is fired
+ *   after the blocks are collected from hook_block_info() and the database,
+ *   right before saving back to the database.
+ * @param $theme
+ *   The theme these blocks belong to.
+ * @param $code_blocks
+ *   The blocks as defined in hook_block_info before overwritten by the
+ *   database data.
+ */
+function hook_block_info_alter(&$blocks, $theme, $code_blocks) {
+  // Disable the login block.
+  $blocks['user']['login']['status'] = 0;
+}
+
 /**
  * Configuration form for the block.
  *
@@ -240,7 +259,7 @@ function hook_block_view_MODULE_DELTA_alter(&$data, $block) {
  * This example shows how to achieve language specific visibility setting for
  * blocks.
  */
-function hook_block_info_alter(&$blocks) {
+function hook_block_list_alter(&$blocks) {
   global $language, $theme_key;
 
   $result = db_query('SELECT module, delta, language FROM {my_table}');
@@ -252,7 +271,7 @@ function hook_block_info_alter(&$blocks) {
   foreach ($blocks as $key => $block) {
     // Any module using this alter should inspect the data before changing it,
     // to ensure it is what they expect.
-    if ($block->theme != $theme_key || $block->status != 1) {
+    if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) {
       // This block was added by a contrib module, leave it in the list.
       continue;
     }
diff --git a/modules/block/block.info b/modules/block/block.info
index 9d5fcc98ebf42b17f3a7f9cb64ac3aab4bca3e72..5c23f4399572454fd64db43357f00dd632992dc2 100644
--- a/modules/block/block.info
+++ b/modules/block/block.info
@@ -10,8 +10,8 @@ files[] = block.install
 files[] = block.test
 configure = admin/structure/block
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/block/block.install b/modules/block/block.install
index 8658ca9658e5d7cf9469059fd62f67ec9619a12e..1d1496243069dd62f7eaaf791f881d8a475977cc 100644
--- a/modules/block/block.install
+++ b/modules/block/block.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: block.install,v 1.39 2010/03/04 21:42:00 dries Exp $
+// $Id: block.install,v 1.40 2010/03/28 11:16:29 dries Exp $
 
 /**
  * @file
@@ -131,34 +131,6 @@ function block_schema() {
     ),
   );
 
-  $schema['block_node_type'] = array(
-    'description' => 'Sets up display criteria for blocks based on content types',
-    'fields' => array(
-      'module' => array(
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => TRUE,
-        'description' => "The block's origin module, from {block}.module.",
-      ),
-      'delta' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'description' => "The block's unique delta within module, from {block}.delta.",
-      ),
-      'type' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'description' => "The machine-readable name of this type from {node_type}.type.",
-      ),
-    ),
-    'primary key' => array('module', 'delta', 'type'),
-    'indexes' => array(
-      'type' => array('type'),
-    ),
-  );
-
   $schema['block_custom'] = array(
     'description' => 'Stores contents of custom-made blocks.',
     'fields' => array(
@@ -230,42 +202,6 @@ function block_update_7000() {
     ->execute();
 }
 
-
-/**
- * Add the block_node_type table.
- */
-function block_update_7001() {
-  $schema['block_node_type'] = array(
-    'description' => 'Sets up display criteria for blocks based on content types',
-    'fields' => array(
-      'module' => array(
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => TRUE,
-        'description' => "The block's origin module, from {block}.module.",
-      ),
-      'delta' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'description' => "The block's unique delta within module, from {block}.delta.",
-      ),
-      'type' => array(
-        'type' => 'varchar',
-        'length' => 32,
-        'not null' => TRUE,
-        'description' => "The machine-readable name of this type from {node_type}.type.",
-      ),
-    ),
-    'primary key' => array('module', 'delta', 'type'),
-    'indexes' => array(
-      'type' => array('type'),
-    ),
-  );
-
-  db_create_table('block_node_type', $schema['block_node_type']);
-}
-
 /**
  * Rename {blocks} table to {block}, {blocks_roles} to {block_role} and
  * {boxes} to {block_custom}.
diff --git a/modules/block/block.js b/modules/block/block.js
index 69514ac4776431af39a853d313cc4a75650e6a15..ccfb475424b591a7df146b57fae2228050d113be 100644
--- a/modules/block/block.js
+++ b/modules/block/block.js
@@ -1,4 +1,4 @@
-// $Id: block.js,v 1.14 2010/02/15 19:06:21 dries Exp $
+// $Id: block.js,v 1.15 2010/04/09 12:24:53 dries Exp $
 (function ($) {
 
 /**
@@ -6,14 +6,14 @@
  */
 Drupal.behaviors.blockSettingsSummary = {
   attach: function (context) {
-    // The setSummary method required for this behavior is not available
+    // The drupalSetSummary method required for this behavior is not available
     // on the Blocks administration page, so we need to make sure this
-    // behavior is processed only if setSummary is defined.
-    if (typeof jQuery.fn.setSummary == 'undefined') {
+    // behavior is processed only if drupalSetSummary is defined.
+    if (typeof jQuery.fn.drupalSetSummary == 'undefined') {
       return;
     }
 
-    $('fieldset#edit-path', context).setSummary(function (context) {
+    $('fieldset#edit-path', context).drupalSetSummary(function (context) {
       if (!$('textarea[name="pages"]', context).val()) {
         return Drupal.t('Not restricted');
       }
@@ -22,7 +22,7 @@ Drupal.behaviors.blockSettingsSummary = {
       }
     });
 
-    $('fieldset#edit-node-type', context).setSummary(function (context) {
+    $('fieldset#edit-node-type', context).drupalSetSummary(function (context) {
       var vals = [];
       $('input[type="checkbox"]:checked', context).each(function () {
         vals.push($.trim($(this).next('label').text()));
@@ -33,7 +33,7 @@ Drupal.behaviors.blockSettingsSummary = {
       return vals.join(', ');
     });
 
-    $('fieldset#edit-role', context).setSummary(function (context) {
+    $('fieldset#edit-role', context).drupalSetSummary(function (context) {
       var vals = [];
       $('input[type="checkbox"]:checked', context).each(function () {
         vals.push($.trim($(this).next('label').text()));
@@ -44,7 +44,7 @@ Drupal.behaviors.blockSettingsSummary = {
       return vals.join(', ');
     });
 
-    $('fieldset#edit-user', context).setSummary(function (context) {
+    $('fieldset#edit-user', context).drupalSetSummary(function (context) {
       var $radio = $('input[name="custom"]:checked', context);
       if ($radio.val() == 0) {
         return Drupal.t('Not customizable');
diff --git a/modules/block/block.module b/modules/block/block.module
index 6bbc7d2a56ff8448f5cc9dab690f16745461c00a..b6abb66b02355d8861d35a5d8035f6ac8157a3aa 100644
--- a/modules/block/block.module
+++ b/modules/block/block.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: block.module,v 1.414 2010/03/12 05:14:14 webchick Exp $
+// $Id: block.module,v 1.419 2010/04/26 14:10:40 dries Exp $
 
 /**
  * @file
@@ -317,80 +317,100 @@ function _block_rehash($theme = NULL) {
   global $theme_key;
 
   drupal_theme_initialize();
-
   if (!isset($theme)) {
     // If theme is not specifically set, rehash for the current theme.
     $theme = $theme_key;
   }
-
-  $old_blocks = array();
-  $result = db_query("SELECT * FROM {block} WHERE theme = :theme", array(':theme' => $theme));
-  foreach ($result as $old_block) {
-    $old_block = is_object($old_block) ? get_object_vars($old_block) : $old_block;
-    $old_blocks[$old_block['module']][$old_block['delta']] = $old_block;
-  }
-
-  $blocks = array();
-  // Valid region names for the theme.
   $regions = system_region_list($theme);
 
+  // These are the blocks the function will return.
+  $blocks = array();
+  // These are the blocks defined by code and modified by the database.
+  $current_blocks = array();
+  // These are {block}.bid values to be kept.
+  $bids = array();
+  $or = db_or();
+  // Gather the blocks defined by modules.
   foreach (module_implements('block_info') as $module) {
     $module_blocks = module_invoke($module, 'block_info');
-    if ($module_blocks) {
-      foreach ($module_blocks as $delta => $block) {
-        if (empty($old_blocks[$module][$delta])) {
-          // If it's a new block, add identifiers.
-          $block['module'] = $module;
-          $block['delta']  = $delta;
-          $block['theme']  = $theme;
-          if (!isset($block['pages'])) {
-            // {block}.pages is type 'text', so it cannot have a
-            // default value, and not null, so we need to provide
-            // value if the module did not.
-            $block['pages']  = '';
-          }
-          // Add defaults and save it into the database.
-          drupal_write_record('block', $block);
-          // Set region to none if not enabled.
-          $block['region'] = $block['status'] ? $block['region'] : BLOCK_REGION_NONE;
-          // Add to the list of blocks we return.
-          $blocks[] = $block;
-        }
-        else {
-          // If it's an existing block, database settings should overwrite
-          // the code. But aside from 'info' everything that's definable in
-          // code is stored in the database and we do not store 'info', so we
-          // do not need to update the database here.
-          // Add 'info' to this block.
-          $old_blocks[$module][$delta]['info'] = $block['info'];
-          // If the region name does not exist, disable the block and assign it to none.
-          if (!empty($old_blocks[$module][$delta]['region']) && !isset($regions[$old_blocks[$module][$delta]['region']])) {
-            drupal_set_message(t('The block %info was assigned to the invalid region %region and has been disabled.', array('%info' => $old_blocks[$module][$delta]['info'], '%region' => $old_blocks[$module][$delta]['region'])), 'warning');
-            $old_blocks[$module][$delta]['status'] = 0;
-            $old_blocks[$module][$delta]['region'] = BLOCK_REGION_NONE;
-          }
-          else {
-            $old_blocks[$module][$delta]['region'] = $old_blocks[$module][$delta]['status'] ? $old_blocks[$module][$delta]['region'] : BLOCK_REGION_NONE;
-          }
-          // Add this block to the list of blocks we return.
-          $blocks[] = $old_blocks[$module][$delta];
-          // Remove this block from the list of blocks to be deleted.
-          unset($old_blocks[$module][$delta]);
-        }
-      }
+    foreach ($module_blocks as $delta => $block) {
+      // Compile a condition to retrieve this block from the database.
+      $condition = db_and()
+        ->condition('module', $module)
+        ->condition('delta', $delta);
+      $or->condition($condition);
+      // Add identifiers.
+      $block['module'] = $module;
+      $block['delta']  = $delta;
+      $block['theme']  = $theme;
+      $current_blocks[$module][$delta] = $block;
     }
   }
-
-  // Remove blocks that are no longer defined by the code from the database.
-  foreach ($old_blocks as $module => $old_module_blocks) {
-    foreach ($old_module_blocks as $delta => $block) {
-      db_delete('block')
-        ->condition('module', $module)
-        ->condition('delta', $delta)
-        ->condition('theme', $theme)
-        ->execute();
+  // Save the blocks defined in code for alter context.
+  $code_blocks = $current_blocks;
+  $database_blocks = db_select('block', 'b')
+    ->fields('b')
+    ->condition($or)
+    ->condition('theme', $theme)
+    ->execute();
+  foreach ($database_blocks as $block) {
+    // Preserve info which is not in the database.
+    $block->info = $current_blocks[$block->module][$block->delta]['info'];
+    // Blocks stored in the database override the blocks defined in code.
+    $current_blocks[$block->module][$block->delta] = get_object_vars($block);
+    // Preserve this block.
+    $bids[$block->bid] = $block->bid;
+  }
+  drupal_alter('block_info', $current_blocks, $theme, $code_blocks);
+  foreach ($current_blocks as $module => $module_blocks) {
+    foreach ($module_blocks as $delta => $block) {
+      if (!isset($block['pages'])) {
+        // {block}.pages is type 'text', so it cannot have a
+        // default value, and not null, so we need to provide
+        // value if the module did not.
+        $block['pages']  = '';
+      }
+      // Make sure weight is set.
+      if (!isset($block['weight'])) {
+        $block['weight'] = 0;
+      }
+      if (!empty($block['region']) && $block['region'] != BLOCK_REGION_NONE && !isset($regions[$block['region']])) {
+        drupal_set_message(t('The block %info was assigned to the invalid region %region and has been disabled.', array('%info' => $block['info'], '%region' => $block['region'])), 'warning');
+        // Disabled modules are moved into the BLOCK_REGION_NONE later so no
+        // need to move the bock to another region.
+        $block['status'] = 0;
+      }
+      // Set region to none if not enabled and make sure status is set.
+      if (empty($block['status'])) {
+        $block['status'] = 0;
+        $block['region'] = BLOCK_REGION_NONE;
+      }
+      // There is no point saving disabled blocks. Still, we need to save them
+      // beecause the 'title' attribute is saved to the {blocks} table.
+      if (isset($block['bid'])) {
+        // If the block has a bid property, it comes from the database and
+        // the record needs to be updated, so set the primary key to 'bid'
+        // before passing to drupal_write_record().
+        $primary_keys = array('bid');
+        // Remove a block from the list of blocks to keep if it became disabled.
+        unset($bids[$block['bid']]);
+      }
+      else {
+        $primary_keys = array();
+      }
+      drupal_write_record('block', $block, $primary_keys);
+      // Add to the list of blocks we return.
+      $blocks[] = $block;
     }
   }
+  if ($bids) {
+    // Remove disabled that are no longer defined by the code from the
+    // database.
+    db_delete('block')
+      ->condition('bid', $bids, 'NOT IN')
+      ->condition('theme', $theme)
+      ->execute();
+  }
   return $blocks;
 }
 
@@ -438,7 +458,6 @@ function block_custom_block_form($edit = array()) {
     '#description' => t('The content of the block as shown to the user.'),
     '#required' => TRUE,
     '#weight' => -17,
-    '#access' => filter_access(filter_format_load($edit['format'])),
   );
 
   return $form;
@@ -625,7 +644,7 @@ function _block_load_blocks() {
 
   $block_info = $result->fetchAllAssoc('bid');
   // Allow modules to modify the block list.
-  drupal_alter('block_info', $block_info);
+  drupal_alter('block_list', $block_info);
 
   $blocks = array();
   foreach ($block_info as $block) {
@@ -635,12 +654,12 @@ function _block_load_blocks() {
 }
 
 /**
- * Implements hook_block_info_alter().
+ * Implements hook_block_list_alter().
  *
- * Check the page, user role, content type and user specific visibilty settings.
+ * Check the page, user role and user specific visibilty settings.
  * Remove the block if the visibility conditions are not met.
  */
-function block_block_info_alter(&$blocks) {
+function block_block_list_alter(&$blocks) {
   global $user, $theme_key;
 
   // Build an array of roles for each block.
@@ -650,15 +669,8 @@ function block_block_info_alter(&$blocks) {
     $block_roles[$record->module][$record->delta][] = $record->rid;
   }
 
-  // Build an array of node types for each block.
-  $block_node_types = array();
-  $result = db_query('SELECT module, delta, type FROM {block_node_type}');
-  foreach ($result as $record) {
-    $block_node_types[$record->module][$record->delta][] = $record->type;
-  }
-
   foreach ($blocks as $key => $block) {
-    if ($block->theme != $theme_key || $block->status != 1) {
+    if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) {
       // This block was added by a contrib module, leave it in the list.
       continue;
     }
@@ -672,34 +684,6 @@ function block_block_info_alter(&$blocks) {
       continue;
     }
 
-    // If a block has no node types associated, it is displayed for every type.
-    // For blocks with node types associated, if the node type does not match
-    // the settings from this block, remove it from the block list.
-    if (isset($block_node_types[$block->module][$block->delta])) {
-      $node = menu_get_object();
-      if (!empty($node)) {
-        // This is a node or node edit page.
-        if (!in_array($node->type, $block_node_types[$block->module][$block->delta])) {
-          // This block should not be displayed for this node type.
-          unset($blocks[$key]);
-          continue;
-        }
-      }
-      elseif (arg(0) == 'node' && arg(1) == 'add' && in_array(arg(2), array_keys(node_type_get_types()))) {
-        // This is a node creation page
-        if (!in_array(arg(2), $block_node_types[$block->module][$block->delta])) {
-          // This block should not be displayed for this node type.
-          unset($blocks[$key]);
-          continue;
-        }
-      }
-      else {
-        // This is not a node page, remove the block.
-        unset($blocks[$key]);
-        continue;
-      }
-    }
-
     // Use the user's block visibility setting, if necessary.
     if ($block->custom != 0) {
       if ($user->uid && isset($user->block[$block->module][$block->delta])) {
@@ -719,12 +703,16 @@ function block_block_info_alter(&$blocks) {
 
     // Match path if necessary.
     if ($block->pages) {
+      // Convert path to lowercase. This allows comparison of the same path
+      // with different case. Ex: /Page, /page, /PAGE.
+      $pages = drupal_strtolower($block->pages);
       if ($block->visibility < 2) {
-        $path = drupal_get_path_alias($_GET['q']);
-        // Compare with the internal and path alias (if any).
-        $page_match = drupal_match_path($path, $block->pages);
+        // Convert the Drupal path to lowercase
+        $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
+        // Compare the lowercase internal and lowercase path alias (if any).
+        $page_match = drupal_match_path($path, $pages);
         if ($path != $_GET['q']) {
-          $page_match = $page_match || drupal_match_path($_GET['q'], $block->pages);
+          $page_match = $page_match || drupal_match_path($_GET['q'], $pages);
         }
         // When $block->visibility has a value of 0, the block is displayed on
         // all pages except those listed in $block->pages. When set to 1, it
@@ -884,6 +872,9 @@ function template_preprocess_block(&$variables) {
   $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->region;
   $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;
   $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module . '__' . $variables['block']->delta;
+
+  // Create a valid HTML ID and make sure it is unique.
+  $variables['block_html_id'] = drupal_html_id('block-' . $variables['block']->module . '-' . $variables['block']->delta);
 }
 
 /**
diff --git a/modules/block/block.test b/modules/block/block.test
index 12cfc6dc2ca56f63591b53d98c324b233114f7fe..595e6aa8fe14f8cd3a1ec0465e8f3675d9aba501 100644
--- a/modules/block/block.test
+++ b/modules/block/block.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: block.test,v 1.47 2010/03/12 05:14:14 webchick Exp $
+// $Id: block.test,v 1.52 2010/04/26 14:10:40 dries Exp $
 
 /**
  * @file
@@ -8,6 +8,7 @@
 
 class BlockTestCase extends DrupalWebTestCase {
   protected $regions;
+  protected $admin_user;
 
   public static function getInfo() {
     return array(
@@ -23,13 +24,12 @@ class BlockTestCase extends DrupalWebTestCase {
     // Create and log in an administrative user having access to the Full HTML
     // text format.
     $full_html_format = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchObject();
-    $admin_user = $this->drupalCreateUser(array(
+    $this->admin_user = $this->drupalCreateUser(array(
       'administer blocks',
       filter_permission_name($full_html_format),
       'access administration pages',
-      'access dashboard',
     ));
-    $this->drupalLogin($admin_user);
+    $this->drupalLogin($this->admin_user);
 
     // Define the existing regions
     $this->regions = array();
@@ -58,7 +58,7 @@ class BlockTestCase extends DrupalWebTestCase {
     foreach ($themes as $key => $theme) {
       if ($theme->status) {
         foreach ($theme->info['regions_hidden'] as $hidden_region) {
-          $elements = $this->xpath('//select[@id="edit-regions-' . $key . '"]//option[@value="' . $hidden_region . '"]');
+          $elements = $this->xpath('//select[@id=:id]//option[@value=:value]', array(':id' => 'edit-regions-' . $key, ':value' => $hidden_region));
           $this->assertFalse(isset($elements[0]), t('The hidden region @region is not available for @theme.', array('@region' => $hidden_region, '@theme' => $key)));
         }
       }
@@ -132,7 +132,7 @@ class BlockTestCase extends DrupalWebTestCase {
     $block_admin = $this->drupalCreateUser(array('administer blocks'));
     $this->drupalLogin($block_admin);
     $this->drupalGet('admin/structure/block/manage/block/' . $bid . '/configure');
-    $this->assertNoText(t('Block body'));
+    $this->assertFieldByXPath("//textarea[@name='body[value]' and @disabled='disabled']", t('This field has been disabled because you do not have sufficient permissions to edit it.'), t('Body field contains denied message'));
     $this->drupalPost('admin/structure/block/manage/block/' . $bid . '/configure', array(), t('Save block'));
     $this->assertNoText(t('Ensure that each block description is unique.'));
 
@@ -178,6 +178,9 @@ class BlockTestCase extends DrupalWebTestCase {
     $this->drupalGet('user');
     $this->assertNoText($title, t('Block was not displayed according to block visibility rules.'));
 
+    $this->drupalGet('USER/' . $this->admin_user->uid);
+    $this->assertNoText($title, t('Block was not displayed according to block visibility rules regardless of path case.'));
+
     // Confirm that the block is not displayed to anonymous users.
     $this->drupalLogout();
     $this->drupalGet('');
@@ -220,7 +223,7 @@ class BlockTestCase extends DrupalWebTestCase {
     $this->assertNoText(t($block['title']), t('Block no longer appears on page.'));
 
     // Confirm that the regions xpath is not availble
-    $xpath = '//div[@id="block-block-' . $bid . '"]/*';
+    $xpath = $this->buildXPathQuery('//div[@id=:id]/*', array(':id' => 'block-block-' . $bid));
     $this->assertNoFieldByXPath($xpath, FALSE, t('Custom block found in no regions.'));
 
     // For convenience of developers, put the navigation block back.
@@ -252,7 +255,10 @@ class BlockTestCase extends DrupalWebTestCase {
     $this->assertText(t($block['title']), t('Block successfully being displayed on the page.'));
 
     // Confirm that the custom block was found at the proper region.
-    $xpath = '//div[@class="' . $region['class'] . '"]//div[@id="block-' . $block['module'] . '-' . $block['delta'] . '"]/*';
+    $xpath = $this->buildXPathQuery('//div[@class=:region-class]//div[@id=:block-id]/*', array(
+      ':region-class' => $region['class'],
+      ':block-id' => 'block-' . $block['module'] . '-' . $block['delta'],
+    ));
     $this->assertFieldByXPath($xpath, FALSE, t('Custom block found in %region_name region.', array('%region_name' => $region['name'])));
   }
 }
@@ -543,3 +549,41 @@ class BlockCacheTestCase extends DrupalWebTestCase {
     }
   }
 }
+
+/**
+ * Test block HTML id validity.
+ */
+class BlockHTMLIdTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Block HTML id',
+      'description' => 'Test block HTML id validity.',
+      'group' => 'Block',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('block_test');
+
+    // Create an admin user, log in and enable test blocks.
+    $this->admin_user = $this->drupalCreateUser(array('administer blocks', 'access administration pages'));
+    $this->drupalLogin($this->admin_user);
+
+    // Enable our test block.
+    $edit['block_test_test_html_id[region]'] = 'sidebar_first';
+    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+
+    // Make sure the block has some content so it will appear
+    $current_content = $this->randomName();
+    variable_set('block_test_content', $current_content);
+  }
+
+  /**
+   * Test valid HTML id.
+   */
+  function testHTMLId() {
+    $this->drupalGet('');
+    $this->assertRaw('block-block-test-test-html-id', t('HTML id for test block is valid.'));
+  }
+}
diff --git a/modules/block/block.tpl.php b/modules/block/block.tpl.php
index c4b71e9ca701505b5a8aadc541b52df1003cd190..270479d4904aa3d72f2ffafda8abc79622ca740b 100644
--- a/modules/block/block.tpl.php
+++ b/modules/block/block.tpl.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: block.tpl.php,v 1.9 2010/01/30 07:59:24 dries Exp $
+// $Id: block.tpl.php,v 1.10 2010/04/26 14:10:40 dries Exp $
 
 /**
  * @file
@@ -35,13 +35,14 @@
  * - $is_front: Flags true when presented in the front page.
  * - $logged_in: Flags true when the current user is a logged-in member.
  * - $is_admin: Flags true when the current user is an administrator.
+ * - $block_html_id: A valid HTML ID and guaranteed unique.
  *
  * @see template_preprocess()
  * @see template_preprocess_block()
  * @see template_process()
  */
 ?>
-<div id="block-<?php print $block->module . '-' . $block->delta; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
+<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
 
   <?php print render($title_prefix); ?>
 <?php if ($block->subject): ?>
diff --git a/modules/block/tests/block_test.info b/modules/block/tests/block_test.info
index eeb025aa2204fb97eed960b7615180b38033d388..5deccb00c29d4ffed8c11224ef51274a1902fd18 100644
--- a/modules/block/tests/block_test.info
+++ b/modules/block/tests/block_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = block_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/block/tests/block_test.module b/modules/block/tests/block_test.module
index 904e1bfda0597edafa48c6ef1926f5c1846c0909..5f2ae785e876eb489264aa7d32bb4bc801618d74 100644
--- a/modules/block/tests/block_test.module
+++ b/modules/block/tests/block_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: block_test.module,v 1.1 2010/02/28 17:28:38 dries Exp $
+// $Id: block_test.module,v 1.2 2010/04/26 14:10:40 dries Exp $
 
 /**
  * @file
@@ -13,6 +13,10 @@ function block_test_block_info() {
   $blocks['test_cache'] = array(
     'info' => t('Test block caching'),
   );
+
+  $blocks['test_html_id'] = array(
+    'info' => t('Test block html id'),
+  );
   return $blocks;
 }
 
diff --git a/modules/blog/blog.info b/modules/blog/blog.info
index 8706193444f7dc32fa16554ea8ee4c62e3a120d7..a9f3bf2818671011ae0e20445aaf0783e78ac32b 100644
--- a/modules/blog/blog.info
+++ b/modules/blog/blog.info
@@ -9,8 +9,8 @@ files[] = blog.module
 files[] = blog.pages.inc
 files[] = blog.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/blog/blog.module b/modules/blog/blog.module
index 50605f5f6a45d36ae68314c7897ca271f3daa472..132c554e4038acf535825cf6a3ff084c9ef51220 100644
--- a/modules/blog/blog.module
+++ b/modules/blog/blog.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: blog.module,v 1.352 2010/03/18 06:26:36 dries Exp $
+// $Id: blog.module,v 1.353 2010/04/17 12:46:32 dries Exp $
 
 /**
  * @file
@@ -79,7 +79,7 @@ function blog_view($node, $view_mode) {
  */
 function blog_node_view($node, $view_mode = 'full') {
   if ($view_mode != 'rss') {
-    if ($node->type == 'blog' && arg(0) != 'blog' || arg(1) != $node->uid) {
+    if ($node->type == 'blog' && (arg(0) != 'blog' || arg(1) != $node->uid)) {
       $links['blog_usernames_blog'] = array(
         'title' => t("!username's blog", array('!username' => format_username($node))),
         'href' => "blog/$node->uid",
diff --git a/modules/book/book.admin.inc b/modules/book/book.admin.inc
index 8ed72165b820aee6c46dd0718de305b1bdbdfe0d..ffdcf881298dcc990de318ca77636311affd597f 100644
--- a/modules/book/book.admin.inc
+++ b/modules/book/book.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: book.admin.inc,v 1.32 2010/02/25 09:44:51 dries Exp $
+// $Id: book.admin.inc,v 1.33 2010/04/13 15:23:02 dries Exp $
 
 /**
  * @file
@@ -212,10 +212,14 @@ function _book_admin_table_tree($tree, &$form) {
 }
 
 /**
- * Theme function for the book administration page form.
+ * Returns HTML for a book administration form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
- * @ingroup themeable
  * @see book_admin_table()
+ * @ingroup themeable
  */
 function theme_book_admin_table($variables) {
   $form = $variables['form'];
diff --git a/modules/book/book.info b/modules/book/book.info
index 01bd231f85dbf7fbebfcadc6065f4cc89dc28066..72fd0f306a07c97ae6c98b63816e7441c3e34575 100644
--- a/modules/book/book.info
+++ b/modules/book/book.info
@@ -11,8 +11,8 @@ files[] = book.install
 files[] = book.test
 configure = admin/content/book/settings
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/book/book.js b/modules/book/book.js
index e4dfdfa116e2e132e418e5863b27e2a13c383cf9..e978c8b5e856f7f3318b85c8acf2ae4c31527012 100644
--- a/modules/book/book.js
+++ b/modules/book/book.js
@@ -1,10 +1,10 @@
-// $Id: book.js,v 1.6 2009/04/27 20:19:35 webchick Exp $
+// $Id: book.js,v 1.7 2010/04/16 13:55:06 dries Exp $
 
 (function ($) {
 
 Drupal.behaviors.bookFieldsetSummaries = {
   attach: function (context) {
-    $('fieldset#edit-book', context).setSummary(function (context) {
+    $('fieldset#edit-book', context).drupalSetSummary(function (context) {
       var val = $('#edit-book-bid').val();
 
       if (val === '0') {
diff --git a/modules/book/book.module b/modules/book/book.module
index d60ab1868b7d2ef1c3a3f87d98ff5115c17a25e0..2f6dfc0ba85f44753edbd4fbe40da1d1533458a9 100644
--- a/modules/book/book.module
+++ b/modules/book/book.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: book.module,v 1.537 2010/01/30 04:23:46 webchick Exp $
+// $Id: book.module,v 1.539 2010/04/13 15:23:02 dries Exp $
 
 /**
  * @file
@@ -338,7 +338,11 @@ function book_block_save($delta = '', $edit = array()) {
 }
 
 /**
- * Generate the HTML output for a link to a book title when used as a block title.
+ * Returns HTML for a link to a book title when used as a block title.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - link: An array containing title, href and options for the link.
  *
  * @ingroup themeable
  */
@@ -745,7 +749,11 @@ function book_children($book_link) {
     }
   }
 
-  return $children ? drupal_render(menu_tree_output($children)) : '';
+  if ($children) {
+    $elements = menu_tree_output($children);
+    return drupal_render($elements);
+  }
+  return '';
 }
 
 /**
diff --git a/modules/book/book.test b/modules/book/book.test
index add2dbf8a88f7131e84d083bca146e5b9d6c0fcb..530f536da3dbb3b72e1305c6a94dbbc007b750be 100644
--- a/modules/book/book.test
+++ b/modules/book/book.test
@@ -1,9 +1,14 @@
 <?php
-// $Id: book.test,v 1.22 2010/01/09 21:54:00 webchick Exp $
+// $Id: book.test,v 1.24 2010/04/22 23:31:23 webchick Exp $
 
 class BookTestCase extends DrupalWebTestCase {
   protected $book;
-
+  // $book_author is a user with permission to author a book.
+  protected $book_author;
+  // $web_user is a user with permission to view a book 
+  // and access the printer-friendly version.
+  protected $web_user;
+  
   public static function getInfo() {
     return array(
       'name' => 'Book functionality',
@@ -14,18 +19,18 @@ class BookTestCase extends DrupalWebTestCase {
 
   function setUp() {
     parent::setUp('book');
+    
+    // Create users.
+    $this->book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
+    $this->web_user = $this->drupalCreateUser(array('access printer-friendly version'));
   }
-
+  
   /**
-   * Test book functionality through node interfaces.
+   * Create a new book with a page hierarchy.
    */
-  function testBook() {
-    // Create users.
-    $book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
-    $web_user = $this->drupalCreateUser(array('access printer-friendly version'));
-
+  function createBook() {
     // Create new book.
-    $this->drupalLogin($book_author);
+    $this->drupalLogin($this->book_author);
 
     $this->book = $this->createBookNode('new');
     $book = $this->book;
@@ -47,7 +52,19 @@ class BookTestCase extends DrupalWebTestCase {
     $nodes[] = $this->createBookNode($book->nid); // Node 4.
 
     $this->drupalLogout();
-    $this->drupalLogin($web_user);
+    
+    return $nodes;
+  }
+
+  /**
+   * Test book functionality through node interfaces.
+   */
+  function testBook() {
+    // Create new book.
+    $nodes = $this->createBook();
+    $book = $this->book;
+    
+    $this->drupalLogin($this->web_user);
 
     // Check that book pages display along with the correct outlines and
     // previous/next links.
@@ -61,14 +78,14 @@ class BookTestCase extends DrupalWebTestCase {
     $this->drupalLogout();
 
     // Create a second book, and move an existing book page into it.
-    $this->drupalLogin($book_author);
+    $this->drupalLogin($this->book_author);
     $other_book = $this->createBookNode('new');
     $node = $this->createBookNode($book->nid);
     $edit = array('book[bid]' => $other_book->nid);
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
 
     $this->drupalLogout();
-    $this->drupalLogin($web_user);
+    $this->drupalLogin($this->web_user);
 
     // Check that the nodes in the second book are displayed correctly.
     // First we must set $this->book to the second book, so that the
@@ -129,7 +146,7 @@ class BookTestCase extends DrupalWebTestCase {
     }
 
     // Fetch links in the current breadcrumb.
-    $links = $this->xpath("//div[@class='breadcrumb']/a");
+    $links = $this->xpath('//div[@class="breadcrumb"]/a');
     $got_breadcrumb = array();
     foreach ($links as $link) {
       $got_breadcrumb[] = (string) $link['href'];
@@ -194,6 +211,40 @@ class BookTestCase extends DrupalWebTestCase {
 
     return $node;
   }
+  
+  /**
+   * Tests book export ("printer-friendly version") functionality.
+   */
+  function testBookExport() {
+    // Create a book.
+    $nodes = $this->createBook();
+    
+    // Login as web user and view printer-friendly version.
+    $this->drupalLogin($this->web_user);
+    $this->drupalGet('node/' . $this->book->nid);
+    $this->clickLink(t('Printer-friendly version'));
+    
+    // Make sure each part of the book is there.
+    foreach ($nodes as $node) {
+      $this->assertText($node->title, t('Node title found in printer friendly version.'));
+      $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), t('Node body found in printer friendly version.'));
+    }
+    
+    // Make sure we can't export an unsupported format.
+    $this->drupalGet('book/export/foobar/' . $this->book->nid);
+    $this->assertResponse('404', t('Unsupported export format returned "not found".'));    
+    
+    // Make sure an anonymous user cannot view printer-friendly version.
+    $this->drupalLogout();
+    
+    // Load the book and verify there is no printer-friendly version link.
+    $this->drupalGet('node/' . $this->book->nid);
+    $this->assertNoLink(t('Printer-friendly version'), t('Anonymous user is not shown link to printer-friendly version.'));
+    
+    // Try getting the URL directly, and verify it fails.
+    $this->drupalGet('book/export/html/' . $this->book->nid);
+    $this->assertResponse('403', t('Anonymous user properly forbidden.'));
+  }
 }
 
 class BookBlockTestCase extends DrupalWebTestCase {
diff --git a/modules/color/color.info b/modules/color/color.info
index bee452f7c7ca8e7479debcc8bb03317c48b536b2..0319923caca2ff7da3ddb6efddbefc54441a2b40 100644
--- a/modules/color/color.info
+++ b/modules/color/color.info
@@ -9,8 +9,8 @@ files[] = color.module
 files[] = color.install
 files[] = color.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/color/color.install b/modules/color/color.install
index 4bb3e4cb49a2ae75be870a800911d3c0f0b68ccf..37614f8532fe00d783c0f42fe74e811a4550c82c 100644
--- a/modules/color/color.install
+++ b/modules/color/color.install
@@ -1,5 +1,5 @@
 <?php
-/* $Id: color.install,v 1.6 2010/01/09 23:03:21 webchick Exp $ */
+/* $Id: color.install,v 1.7 2010/04/04 13:18:18 dries Exp $ */
 
 /**
  * @file
@@ -13,27 +13,27 @@ function color_requirements($phase) {
     // Check for the PHP GD library.
     if (function_exists('imagegd2')) {
       $info = gd_info();
-      $requirements['gd'] = array(
+      $requirements['color_gd'] = array(
         'value' => $info['GD Version'],
       );
 
       // Check for PNG support.
       if (function_exists('imagecreatefrompng')) {
-        $requirements['gd']['severity'] = REQUIREMENT_OK;
+        $requirements['color_gd']['severity'] = REQUIREMENT_OK;
       }
       else {
-        $requirements['gd']['severity'] = REQUIREMENT_ERROR;
-        $requirements['gd']['description'] = t('The GD library for PHP is enabled, but was compiled without PNG support. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/ref.image.php'));
+        $requirements['color_gd']['severity'] = REQUIREMENT_ERROR;
+        $requirements['color_gd']['description'] = t('The GD library for PHP is enabled, but was compiled without PNG support. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/ref.image.php'));
       }
     }
     else {
-      $requirements['gd'] = array(
+      $requirements['color_gd'] = array(
         'value' => t('Not installed'),
         'severity' => REQUIREMENT_ERROR,
-        'description' => t('The GD library for PHP is missing or outdated. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/ref.image.php')),
+        'description' => t('The GD library for PHP is missing or outdated. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/book.image.php')),
       );
     }
-    $requirements['gd']['title'] = t('GD library');
+    $requirements['color_gd']['title'] = t('GD library PNG support');
   }
 
   return $requirements;
diff --git a/modules/color/color.js b/modules/color/color.js
index 1968e18071bb2b03c7a0a2643cbaaf5740f5880f..16515efe498172d916a50d4bf376e94ae4177fea 100644
--- a/modules/color/color.js
+++ b/modules/color/color.js
@@ -1,8 +1,9 @@
-// $Id: color.js,v 1.15 2009/09/20 19:14:40 dries Exp $
+// $Id: color.js,v 1.18 2010/04/22 05:18:21 webchick Exp $
 (function ($) {
 
 Drupal.behaviors.color = {
   attach: function (context, settings) {
+    var i, j, colors, field_name;
     // This behavior attaches by ID, so is only valid once on a page.
     var form = $('#system-theme-settings .color-form', context).once('color');
     if (form.length == 0) {
@@ -24,11 +25,23 @@ Drupal.behaviors.color = {
     }
 
     // Build a preview.
-    $('#preview').once('color').append('<div id="gradient"></div>');
-    var gradient = $('#preview #gradient');
-    var h = parseInt(gradient.css('height'), 10) / 10;
-    for (i = 0; i < h; ++i) {
-      gradient.append('<div class="gradient-line"></div>');
+    var height = [];
+    var width = [];
+    // Loop through all defined gradients.
+    for (i in settings.gradients) {
+      // Add element to display the gradient.
+      $('#preview').once('color').append('<div id="gradient-' + i + '"></div>');
+      var gradient = $('#preview #gradient-' + i);
+      // Add height of current gradient to the list (divided by 10).
+      height.push(parseInt(gradient.css('height'), 10) / 10);
+      // Add width of current gradient to the list (divided by 10).
+      width.push(parseInt(gradient.css('width'), 10) / 10);
+      // Add rows (or columns for horizontal gradients).
+      // Each gradient line should have a height (or width for horizontal
+      // gradients) of 10px (because we divided the height/width by 10 above).
+      for (j = 0; j < (settings.gradients[i]['direction'] == 'vertical' ? height[i] : width[i]); ++j) {
+        gradient.append('<div class="gradient-line"></div>');
+      }
     }
 
     // Fix preview background in IE6.
@@ -39,13 +52,14 @@ Drupal.behaviors.color = {
       e.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image.substring(5, image.length - 2) + "')";
     }
 
-    // Set up colorscheme selector.
+    // Set up colorScheme selector.
     $('#edit-scheme', form).change(function () {
-      var colors = this.options[this.selectedIndex].value;
-      if (colors != '') {
-        colors = colors.split(',');
-        for (i in colors) {
-          callback(inputs[i], colors[i], false, true);
+      var schemes = settings.color.schemes, colorScheme = this.options[this.selectedIndex].value;
+      if (colorScheme != '' && schemes[colorScheme]) {
+        // Get colors of active scheme.
+        colors = schemes[colorScheme];
+        for (field_name in colors) {
+          callback($('#edit-palette-' + field_name), colors[field_name], false, true);
         }
         preview();
       }
@@ -55,31 +69,7 @@ Drupal.behaviors.color = {
      * Render the preview.
      */
     function preview() {
-      // Solid background.
-      $('#preview', form).css('backgroundColor', inputs[0].value);
-
-      // Text preview
-      $('#text', form).css('color', inputs[4].value);
-      $('#text a, #text h2', form).css('color', inputs[1].value);
-
-      // Set up gradient.
-      var top = farb.unpack(inputs[2].value);
-      var bottom = farb.unpack(inputs[3].value);
-      if (top && bottom) {
-        var delta = [];
-        for (i in top) {
-          delta[i] = (bottom[i] - top[i]) / h;
-        }
-        var accum = top;
-
-        // Render gradient lines.
-        $('#gradient > div', form).each(function () {
-          for (i in accum) {
-            accum[i] += delta[i];
-          }
-          this.style.backgroundColor = farb.pack(accum);
-        });
-      }
+      Drupal.color.callback(context, settings, form, farb, height, width);
     }
 
     /**
@@ -132,7 +122,8 @@ Drupal.behaviors.color = {
     /**
      * Callback for Farbtastic when a new color is chosen.
      */
-    function callback(input, color, propagate, colorscheme) {
+    function callback(input, color, propagate, colorScheme) {
+      var matched;
       // Set background/foreground colors.
       $(input).css({
         backgroundColor: color,
@@ -140,20 +131,20 @@ Drupal.behaviors.color = {
       });
 
       // Change input value.
-      if (input.value && input.value != color) {
-        input.value = color;
+      if ($(input).val() && $(input).val() != color) {
+        $(input).val(color);
 
         // Update locked values.
         if (propagate) {
-          var i = input.i;
+          i = input.i;
           for (j = i + 1; ; ++j) {
             if (!locks[j - 1] || $(locks[j - 1]).is('.unlocked')) break;
-            var matched = shift_color(color, reference[input.key], reference[inputs[j].key]);
+            matched = shift_color(color, reference[input.key], reference[inputs[j].key]);
             callback(inputs[j], matched, false);
           }
           for (j = i - 1; ; --j) {
             if (!locks[j] || $(locks[j]).is('.unlocked')) break;
-            var matched = shift_color(color, reference[input.key], reference[inputs[j].key]);
+            matched = shift_color(color, reference[input.key], reference[inputs[j].key]);
             callback(inputs[j], matched, false);
           }
 
@@ -161,12 +152,11 @@ Drupal.behaviors.color = {
           preview();
         }
 
-        // Reset colorscheme selector.
-        if (!colorscheme) {
+        // Reset colorScheme selector.
+        if (!colorScheme) {
           resetScheme();
         }
       }
-
     }
 
     /**
diff --git a/modules/color/color.module b/modules/color/color.module
index 0dc7e800a3b0b65e9ab08849c4a15306e102cacb..8282c3a75189cb3410d07465a76fab714eedd23e 100644
--- a/modules/color/color.module
+++ b/modules/color/color.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: color.module,v 1.81 2010/01/30 07:59:24 dries Exp $
+// $Id: color.module,v 1.85 2010/04/22 23:25:32 dries Exp $
 
 /**
  * Implements hook_help().
@@ -115,10 +115,17 @@ function _color_page_alter(&$vars) {
  * Retrieve the color.module info for a particular theme.
  */
 function color_get_info($theme) {
+  static $theme_info = array();
+
+  if (isset($theme_info[$theme])) {
+    return $theme_info[$theme];
+  }
+
   $path = drupal_get_path('theme', $theme);
   $file = DRUPAL_ROOT . '/' . $path . '/color/color.inc';
   if ($path && file_exists($file)) {
     include $file;
+    $theme_info[$theme] = $info;
     return $info;
   }
 }
@@ -128,12 +135,8 @@ function color_get_info($theme) {
  */
 function color_get_palette($theme, $default = FALSE) {
   // Fetch and expand default palette.
-  $fields = array('base', 'link', 'top', 'bottom', 'text');
   $info = color_get_info($theme);
-  $keys = array_keys($info['schemes']);
-  foreach (explode(',', array_shift($keys)) as $k => $scheme) {
-    $palette[$fields[$k]] = $scheme;
-  }
+  $palette = $info['schemes']['default']['colors'];
 
   // Load variable.
   return $default ? $palette : variable_get('color_' . $theme . '_palette', $palette);
@@ -146,18 +149,39 @@ function color_scheme_form($complete_form, &$form_state, $theme) {
   $base = drupal_get_path('module', 'color');
   $info = color_get_info($theme);
 
+  $info['schemes'][''] = array('title' => t('Custom'), 'colors' => array());
+  $color_sets = array();
+  $schemes = array();
+  foreach ($info['schemes'] as $key => $scheme) {
+    $color_sets[$key] = $scheme['title'];
+    $schemes[$key] = $scheme['colors'];
+    $schemes[$key] += $info['schemes']['default']['colors'];
+  }
+
   // See if we're using a predefined scheme.
-  $current = implode(',', variable_get('color_' . $theme . '_palette', array()));
   // Note: we use the original theme when the default scheme is chosen.
-  $current = isset($info['schemes'][$current]) ? $current : ($current == '' ? reset($info['schemes']) : '');
+  $current_scheme = variable_get('color_' . $theme . '_palette', array());
+  foreach ($schemes as $key => $scheme) {
+    if ($current_scheme == $scheme) {
+      $scheme_name = $key;
+      break;
+    }
+  }
+  if (empty($scheme_name)) {
+    if (empty($current_scheme)) {
+      $scheme_name = 'default';
+    }
+    else {
+      $scheme_name = '';
+    }
+  }
 
   // Add scheme selector.
-  $info['schemes'][''] = t('Custom');
   $form['scheme'] = array(
     '#type' => 'select',
     '#title' => t('Color set'),
-    '#options' => $info['schemes'],
-    '#default_value' => $current,
+    '#options' => $color_sets,
+    '#default_value' => $scheme_name,
     '#attached' => array(
       // Add Farbtastic color picker.
       'library' => array(
@@ -172,7 +196,11 @@ function color_scheme_form($complete_form, &$form_state, $theme) {
         $base . '/color.js',
         array(
           'data' => array(
-            'color' => array('reference' => color_get_palette($theme, TRUE)),
+            'color' => array(
+              'reference' => color_get_palette($theme, TRUE),
+              'schemes' => $schemes,
+            ),
+            'gradients' => $info['gradients'],
           ),
           'type' => 'setting',
         ),
@@ -182,21 +210,17 @@ function color_scheme_form($complete_form, &$form_state, $theme) {
 
   // Add palette fields.
   $palette = color_get_palette($theme);
-  $names = array(
-    'base' => t('Base color'),
-    'link' => t('Link color'),
-    'top' => t('Header top'),
-    'bottom' => t('Header bottom'),
-    'text' => t('Text color'),
-  );
+  $names = $info['fields'];
   $form['palette']['#tree'] = TRUE;
   foreach ($palette as $name => $value) {
-    $form['palette'][$name] = array(
-      '#type' => 'textfield',
-      '#title' => $names[$name],
-      '#default_value' => $value,
-      '#size' => 8,
-    );
+    if (isset($names[$name])) {
+      $form['palette'][$name] = array(
+        '#type' => 'textfield',
+        '#title' => $names[$name],
+        '#default_value' => $value,
+        '#size' => 8,
+      );
+    }
   }
   $form['theme'] = array('#type' => 'value', '#value' => $theme);
   $form['info'] = array('#type' => 'value', '#value' => $info);
@@ -205,7 +229,11 @@ function color_scheme_form($complete_form, &$form_state, $theme) {
 }
 
 /**
- * Theme the color form.
+ * Returns HTML for a theme's color form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -216,7 +244,11 @@ function theme_color_scheme_form($variables) {
   $info = $form['info']['#value'];
   $path = drupal_get_path('theme', $theme) . '/';
   drupal_add_css($path . $info['preview_css']);
-
+  
+  $preview_js_path = isset($info['preview_js']) ? $path . $info['preview_js'] : drupal_get_path('module', 'color') . '/' . 'preview.js';
+  // Add the JS at a weight below color.js.
+  drupal_add_js($preview_js_path, array('weight' => JS_DEFAULT - 1));
+    
   $output  = '';
   $output .= '<div class="color-form clearfix">';
   // Color schemes
@@ -230,7 +262,9 @@ function theme_color_scheme_form($variables) {
   // Preview
   $output .= drupal_render_children($form);
   $output .= '<h2>' . t('Preview') . '</h2>';
-  $output .= '<div id="preview"><div id="text"><h2>Lorem ipsum dolor</h2><p>Sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud <a href="#">exercitation ullamco</a> laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p></div><div id="img" style="background-image: url(' . base_path() . $path . $info['preview_image'] . ')"></div></div>';
+  // Attempt to load preview HTML if the theme provides it.
+  $preview_html_path = DRUPAL_ROOT . '/' . (isset($info['preview_html']) ? drupal_get_path('theme', $theme) . '/' . $info['preview_html'] : drupal_get_path('module', 'color') . '/preview.html');
+  $output .= file_get_contents($preview_html_path);
   // Close the wrapper div.
   $output .= '</div>';
 
@@ -251,10 +285,12 @@ function color_scheme_form_submit($form, &$form_state) {
   // Resolve palette.
   $palette = $form_state['values']['palette'];
   if ($form_state['values']['scheme'] != '') {
-    $scheme = explode(',', $form_state['values']['scheme']);
-    foreach ($palette as $k => $color) {
-      $palette[$k] = array_shift($scheme);
+    foreach ($palette as $key => $color) {
+      if (isset($info['schemes'][$form_state['values']['scheme']]['colors'][$key])) {
+        $palette[$key] = $info['schemes'][$form_state['values']['scheme']]['colors'][$key];
+      }
     }
+    $palette += $info['schemes']['default']['colors'];
   }
 
   // Make sure enough memory is available, if PHP's memory limit is compiled in.
@@ -450,7 +486,6 @@ function _color_save_stylesheet($file, $style, &$paths) {
  * Render images that match a given palette.
  */
 function _color_render_images($theme, &$info, &$paths, $palette) {
-
   // Prepare template image.
   $source = $paths['source'] . '/' . $info['base_image'];
   $source = imagecreatefrompng($source);
@@ -466,10 +501,23 @@ function _color_render_images($theme, &$info, &$paths, $palette) {
     imagefilledrectangle($target, $fill[0], $fill[1], $fill[0] + $fill[2], $fill[1] + $fill[3], _color_gd($target, $palette[$color]));
   }
 
-  // Render gradient.
-  for ($y = 0; $y < $info['gradient'][3]; ++$y) {
-    $color = _color_blend($target, $palette['top'], $palette['bottom'], $y / ($info['gradient'][3] - 1));
-    imagefilledrectangle($target, $info['gradient'][0], $info['gradient'][1] + $y, $info['gradient'][0] + $info['gradient'][2], $info['gradient'][1] + $y + 1, $color);
+  // Render gradients.
+  foreach ($info['gradients'] as $gradient) {
+    // Get direction of the gradient.
+    if (isset($gradient['direction']) && $gradient['direction'] == 'horizontal') {
+      // Horizontal gradient.
+      for ($x = 0; $x < $gradient['dimension'][2]; $x++) {
+        $color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $x / ($gradient['dimension'][2] - 1));
+        imagefilledrectangle($target, ($gradient['dimension'][0] + $x), $gradient['dimension'][1], ($gradient['dimension'][0] + $x + 1), ($gradient['dimension'][1] + $gradient['dimension'][3]), $color);
+      }
+    }
+    else {
+      // Vertical gradient.
+      for ($y = 0; $y < $gradient['dimension'][3]; $y++) {
+        $color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $y / ($gradient['dimension'][3] - 1));
+        imagefilledrectangle($target, $gradient['dimension'][0], $gradient['dimension'][1] + $y, $gradient['dimension'][0] + $gradient['dimension'][2], $gradient['dimension'][1] + $y + 1, $color);
+      }
+    }
   }
 
   // Blend over template.
diff --git a/modules/color/color.test b/modules/color/color.test
index 65044497106f51d1ad6f38cd0bfa043ac604d083..c4344b2266b4e8d9705424dd0432577e11460cf1 100644
--- a/modules/color/color.test
+++ b/modules/color/color.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: color.test,v 1.2 2009/12/01 22:30:30 dries Exp $
+// $Id: color.test,v 1.4 2010/04/11 18:33:43 dries Exp $
 
 /**
  * @file
@@ -37,14 +37,25 @@ class ColorTestCase extends DrupalWebTestCase {
     $edit['palette[link]'] = '#123456';
     $this->drupalPost('admin/appearance/settings/garland', $edit, t('Save configuration'));
 
-    global $theme_key;
     $this->drupalGet('<front>');
-    $stylesheets = variable_get('color_' . $theme_key . '_stylesheets', array());
+    $stylesheets = variable_get('color_garland_stylesheets', array());
     $this->assertPattern('|' . file_create_url($stylesheets[0]) . '|', 'Make sure the color stylesheet is included in the content.');
 
     $stylesheet_content = join("\n", file($stylesheets[0]));
     $matched = preg_match('/(.*color: #123456.*)/i', $stylesheet_content, $matches);
     $this->assertTrue($matched == 1, 'Make sure the color we changed is in the color stylesheet.');
+
+    $this->drupalGet('admin/appearance/settings/garland');
+    $this->assertResponse(200);
+    $edit['scheme'] = 'greenbeam';
+    $this->drupalPost('admin/appearance/settings/garland', $edit, t('Save configuration'));
+
+    $this->drupalGet('<front>');
+    $stylesheets = variable_get('color_garland_stylesheets', array());
+    $stylesheet_content = join("\n", file($stylesheets[0]));
+    $matched = preg_match('/(.*color: #0c7a00.*)/i', $stylesheet_content, $matches);
+    $this->assertTrue($matched == 1, 'Make sure the color we changed is in the color stylesheet.');
+
   }
 
 }
diff --git a/modules/color/preview.html b/modules/color/preview.html
new file mode 100644
index 0000000000000000000000000000000000000000..e25b7add726984eff7e9363ee38ba5fdfb17ad8d
--- /dev/null
+++ b/modules/color/preview.html
@@ -0,0 +1,7 @@
+<div id="preview">
+  <div id="text">
+    <h2>Lorem ipsum dolor</h2>
+    <p>Sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud <a href="#">exercitation ullamco</a> laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+  </div>
+  <div id="img"></div>
+</div>
\ No newline at end of file
diff --git a/modules/color/preview.js b/modules/color/preview.js
new file mode 100644
index 0000000000000000000000000000000000000000..f90181e646790f2c5bc9b027544b94fcb77f9c9e
--- /dev/null
+++ b/modules/color/preview.js
@@ -0,0 +1,35 @@
+// $Id: preview.js,v 1.1 2010/04/22 05:18:21 webchick Exp $
+
+(function ($) {
+  Drupal.color = {
+    callback: function(context, settings, form, farb, height, width) {  
+      // Solid background.
+      $('#preview', form).css('backgroundColor', $('#palette input[name="palette[base]"]', form).val());
+      
+      // Text preview
+      $('#text', form).css('color', $('#palette input[name="palette[text]"]', form).val());
+      $('#text a, #text h2', form).css('color', $('#palette input[name="palette[link]"]', form).val());
+      
+      // Set up gradients if there are some.
+      var color_start, color_end;
+      for (i in settings.gradients) {
+        color_start = farb.unpack($('#palette input[name="palette[' + settings.gradients[i]['colors'][0] + ']"]', form).val());
+        color_end = farb.unpack($('#palette input[name="palette[' + settings.gradients[i]['colors'][1] + ']"]', form).val());
+        if (color_start && color_end) {
+          var delta = [];
+          for (j in color_start) {
+            delta[j] = (color_end[j] - color_start[j]) / (settings.gradients[i]['vertical'] ? height[i] : width[i]);
+          }
+          var accum = color_start;
+          // Render gradient lines.
+          $('#gradient-' + i + ' > div', form).each(function () {
+            for (j in accum) {
+              accum[j] += delta[j];
+            }
+            this.style.backgroundColor = farb.pack(accum);
+          });
+        }
+      }
+    }
+  };
+})(jQuery);
\ No newline at end of file
diff --git a/modules/comment/comment-node-form.js b/modules/comment/comment-node-form.js
index 667e9f1e127bad4de02bf0b1fc48721e85227b9a..2401cfdb910992cba276a57682613b9d638d295b 100644
--- a/modules/comment/comment-node-form.js
+++ b/modules/comment/comment-node-form.js
@@ -1,14 +1,14 @@
-// $Id: comment-node-form.js,v 1.4 2009/12/02 15:09:16 dries Exp $
+// $Id: comment-node-form.js,v 1.5 2010/04/16 13:55:06 dries Exp $
 
 (function ($) {
 
 Drupal.behaviors.commentFieldsetSummaries = {
   attach: function (context) {
-    $('fieldset#edit-comment-settings', context).setSummary(function (context) {
+    $('fieldset#edit-comment-settings', context).drupalSetSummary(function (context) {
       return Drupal.checkPlain($('input:checked', context).next('label').text());
     });
     // Provide the summary for the node type form.
-    $('fieldset#edit-comment', context).setSummary(function(context) {
+    $('fieldset#edit-comment', context).drupalSetSummary(function(context) {
       var vals = [];
 
       // Default comment setting.
diff --git a/modules/comment/comment.admin.inc b/modules/comment/comment.admin.inc
index 77deb5dca18ca8d674fee15c101a2f898b1750b9..5434cd341b22102b51961b953ec833caf6f924e5 100644
--- a/modules/comment/comment.admin.inc
+++ b/modules/comment/comment.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: comment.admin.inc,v 1.45 2010/03/07 07:15:28 webchick Exp $
+// $Id: comment.admin.inc,v 1.46 2010/03/31 11:49:50 dries Exp $
 
 /**
  * @file
@@ -162,7 +162,6 @@ function comment_admin_overview_submit($form, &$form_state) {
 
   if ($operation == 'delete') {
     comment_delete_multiple($cids);
-    cache_clear_all();
   }
   else {
     foreach ($cids as $cid => $value) {
@@ -179,6 +178,7 @@ function comment_admin_overview_submit($form, &$form_state) {
   }
   drupal_set_message(t('The update has been performed.'));
   $form_state['redirect'] = 'admin/content/comment';
+  cache_clear_all();
 }
 
 /**
diff --git a/modules/comment/comment.info b/modules/comment/comment.info
index 2c9227fa293a1e8228c1ad3ef4b9c623e38c019b..e70dd7bf4c52753bacb3e2fb979a244a92b05c48 100644
--- a/modules/comment/comment.info
+++ b/modules/comment/comment.info
@@ -12,8 +12,8 @@ files[] = comment.test
 files[] = comment.tokens.inc
 configure = admin/content/comment
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/comment/comment.install b/modules/comment/comment.install
index d25dd4f4b57134bc095ada5fd36697c54b74cde3..f5939a1aa8cb445667c437f1646b4f29f9db6b94 100644
--- a/modules/comment/comment.install
+++ b/modules/comment/comment.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: comment.install,v 1.62 2010/03/07 23:27:14 dries Exp $
+// $Id: comment.install,v 1.66 2010/04/17 12:54:02 dries Exp $
 
 /**
  * @file
@@ -36,7 +36,7 @@ function comment_enable() {
   // Insert records into the node_comment_statistics for nodes that are missing.
   $query = db_select('node', 'n');
   $query->leftJoin('node_comment_statistics', 'ncs', 'ncs.nid = n.nid');
-  $query->addField('n', 'changed', 'last_comment_timestamp');
+  $query->addField('n', 'created', 'last_comment_timestamp');
   $query->addField('n', 'uid', 'last_comment_uid');
   $query->addField('n', 'nid');
   $query->addExpression('0', 'comment_count');
@@ -55,7 +55,7 @@ function comment_enable() {
     $field = array(
       'field_name' => 'comment_body',
       'type' => 'text_long',
-      'object_types' => array('comment'),
+      'entity_types' => array('comment'),
     );
     field_create_field($field);
   }
@@ -69,7 +69,7 @@ function comment_enable() {
   //   and a comment_field_attach_create_bundle() function should be added to
   //   handle the creation of the comment body field instance.
   foreach (node_type_get_types() as $type => $info) {
-    if (!field_info_instance('comment', 'comment_body', 'comment_node_' . $info->type)) {
+    if (!isset($info->is_new) && !isset($info->disabled) && !field_info_instance('comment', 'comment_body', 'comment_node_' . $info->type)) {
       _comment_body_field_instance_create($info);
     }
   }
@@ -269,7 +269,7 @@ function comment_update_7012() {
   $field = array(
     'field_name' => 'comment_body',
     'type' => 'text_long',
-    'object_types' => array('comment'),
+    'entity_types' => array('comment'),
   );
   field_create_field($field);
 
@@ -277,7 +277,7 @@ function comment_update_7012() {
   $body_instance = array(
     'field_name' => 'comment_body',
     'label' => 'Comment',
-    'object_type' => 'comment',
+    'entity_type' => 'comment',
     'settings' => array('text_processing' => 1),
     // Hide field label by default.
     'display' => array(
diff --git a/modules/comment/comment.module b/modules/comment/comment.module
index e332bb2466c08fe0678de852aa3dd27bd358f6ea..b761ce8051e168c16b98729361fbb6b6ea355f34 100644
--- a/modules/comment/comment.module
+++ b/modules/comment/comment.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: comment.module,v 1.853 2010/03/12 15:56:29 dries Exp $
+// $Id: comment.module,v 1.871 2010/04/26 14:26:46 dries Exp $
 
 /**
  * @file
@@ -100,7 +100,7 @@ function comment_entity_info() {
       'uri callback' => 'comment_uri',
       'fieldable' => TRUE,
       'controller class' => 'CommentController',
-      'object keys' => array(
+      'entity keys' => array(
         'id' => 'cid',
         'bundle' => 'node_type',
       ),
@@ -123,8 +123,7 @@ function comment_entity_info() {
         // of local tasks. Note that the paths use a different placeholder name
         // and thus a different menu loader callback, so that Field UI page
         // callbacks get a comment bundle name from the node type in the URL.
-        // @see comment_node_type_load()
-        // @see comment_menu_alter()
+        // See comment_node_type_load() and comment_menu_alter().
         'path' => 'admin/structure/types/manage/%comment_node_type/comment',
         'bundle argument' => 4,
         'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment',
@@ -287,6 +286,21 @@ function comment_menu() {
   return $items;
 }
 
+/**
+ * Implements hook_menu_alter().
+ */
+function comment_menu_alter(&$items) {
+  // Add comments to the description for admin/content.
+  $items['admin/content']['description'] = "Administer content and comments";
+
+  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
+  // See comment_entity_info().
+  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields';
+  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3;
+  $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display';
+  $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4;
+}
+
 /**
  * Returns a menu title which includes the number of unapproved comments.
  */
@@ -344,7 +358,7 @@ function _comment_body_field_instance_create($info) {
   $instance = array(
     'field_name' => 'comment_body',
     'label' => 'Comment',
-    'object_type' => 'comment',
+    'entity_type' => 'comment',
     'bundle' => 'comment_node_' . $info->type,
     'settings' => array('text_processing' => 1),
     'required' => TRUE,
@@ -554,10 +568,8 @@ function comment_new_page_count($num_comments, $new_replies, $node) {
 }
 
 /**
- * Returns a formatted list of recent comments to be displayed in the comment block.
+ * Returns HTML for a list of recent comments to be displayed in the comment block.
  *
- * @return
- *   The comment list HTML.
  * @ingroup themeable
  */
 function theme_comment_block() {
@@ -992,6 +1004,7 @@ function comment_links($comment, $node) {
           'title' => t('approve'),
           'href' => "comment/$comment->cid/approve",
           'html' => TRUE,
+          'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")),
         );
       }
     }
@@ -1230,16 +1243,20 @@ function comment_node_prepare($node) {
  * Implements hook_node_insert().
  */
 function comment_node_insert($node) {
-  db_insert('node_comment_statistics')
-    ->fields(array(
-      'nid' => $node->nid,
-      'cid' => 0,
-      'last_comment_timestamp' => $node->changed,
-      'last_comment_name' => NULL,
-      'last_comment_uid' => $node->uid,
-      'comment_count' => 0,
-    ))
-    ->execute();
+  // Allow bulk updates and inserts to temporarily disable the
+  // maintenance of the {node_comment_statistics} table.
+  if (variable_get('comment_maintain_node_statistics', TRUE)) {
+    db_insert('node_comment_statistics')
+      ->fields(array(
+        'nid' => $node->nid,
+        'cid' => 0,
+        'last_comment_timestamp' => $node->changed,
+        'last_comment_name' => NULL,
+        'last_comment_uid' => $node->uid,
+        'comment_count' => 0,
+      ))
+      ->execute();
+  }
 }
 
 /**
@@ -1277,13 +1294,20 @@ function comment_update_index() {
 
 /**
  * Implements hook_node_search_result().
+ *
+ * Formats a comment count string and returns it, for display with search
+ * results.
  */
 function comment_node_search_result($node) {
+  // Do not make a string if comments are hidden. 
   if ($node->comment != COMMENT_NODE_HIDDEN) {
     $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
-    return format_plural($comments, '1 comment', '@count comments');
+    // Do not make a string if comments are closed and there are currently
+    // zero comments.
+    if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) {
+      return format_plural($comments, '1 comment', '@count comments');
+    }
   }
-  return '';
 }
 
 /**
@@ -1395,17 +1419,23 @@ function comment_save($comment) {
         ))
         ->condition('cid', $comment->cid)
         ->execute();
+
+      // Update the {node_comment_statistics} table prior to executing hooks.
+       _comment_update_node_statistics($comment->nid);
+
       field_attach_update('comment', $comment);
       // Allow modules to respond to the updating of a comment.
       module_invoke_all('comment_update', $comment);
       entity_invoke('update', 'comment', $comment);
-      // Add an entry to the watchdog log.
-      watchdog('content', 'Comment: updated %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
     }
     else {
       // Add the comment to database. This next section builds the thread field.
       // Also see the documentation for comment_view().
-      if ($comment->pid == 0) {
+      if (!empty($comment->thread)) {
+        // Allow calling code to set thread itself.
+        $thread = $comment->thread;
+      }
+      elseif ($comment->pid == 0) {
         // This is a comment with no parent comment (depth 0): we start
         // by retrieving the maximum thread level.
         $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
@@ -1478,24 +1508,22 @@ function comment_save($comment) {
       // saved node to be propagated to the slave.
       db_ignore_slave();
 
+      // Update the {node_comment_statistics} table prior to executing hooks.
+      _comment_update_node_statistics($comment->nid);
+
       field_attach_insert('comment', $comment);
 
       // Tell the other modules a new comment has been submitted.
       module_invoke_all('comment_insert', $comment);
       entity_invoke('insert', 'comment', $comment);
-      // Add an entry to the watchdog log.
-      watchdog('content', 'Comment: added %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
     }
-    _comment_update_node_statistics($comment->nid);
-    // Clear the cache so an anonymous user can see his comment being added.
-    cache_clear_all();
-
     if ($comment->status == COMMENT_PUBLISHED) {
       module_invoke_all('comment_publish', $comment);
     }
   }
   catch (Exception $e) {
     $transaction->rollback('comment', $e->getMessage(), array(), WATCHDOG_ERROR);
+    throw $e;
   }
 
 }
@@ -1579,14 +1607,13 @@ class CommentController extends DrupalDefaultEntityController {
     $query->addField('n', 'type', 'node_type');
     $query->innerJoin('users', 'u', 'base.uid = u.uid');
     $query->addField('u', 'name', 'registered_name');
-    $query->fields('u', array('uid', 'signature', 'picture', 'data'));
+    $query->fields('u', array('uid', 'signature', 'picture'));
     return $query;
   }
 
   protected function attachLoad(&$comments, $revision_id = FALSE) {
     // Setup standard comment properties.
     foreach ($comments as $key => $comment) {
-      $comment = drupal_unpack($comment);
       $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
       $comment->new = node_mark($comment->nid, $comment->changed);
       $comment->node_type = 'comment_node_' . $comment->node_type;
@@ -1664,7 +1691,7 @@ function comment_get_display_ordinal($cid, $node_type) {
   else {
     // For threaded comments, the c.thread column is used for ordering. We can
     // use the vancode for comparison, but must remove the trailing slash.
-    // @see comment_view_multiple().
+    // See comment_view_multiple().
     $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))');
   }
 
@@ -1701,9 +1728,10 @@ function comment_edit_page($comment) {
 /**
  * Generate the basic commenting form, for appending to a node or display on a separate page.
  *
- * @ingroup forms
  * @see comment_form_validate()
  * @see comment_form_submit()
+ *
+ * @ingroup forms
  */
 function comment_form($form, &$form_state, $comment) {
   global $user;
@@ -1757,15 +1785,16 @@ function comment_form($form, &$form_state, $comment) {
       '#weight' => -2,
     );
   }
-
-  // Sets the author form elements above the subject.
-  $form['author'] = array(
-    '#weight' => -2,
-  );
+  else {
+    // Sets the author form elements above the subject.
+    $form['author'] = array(
+      '#weight' => -2,
+    );
+  }
 
   // Prepare default values for form elements.
   if ($is_admin) {
-    $author = ($comment->uid && $comment->name ? $comment->name : $comment->registered_name);
+    $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
     $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
     $date = (!empty($comment->date) ? $comment->date : format_date($comment->changed, 'custom', 'Y-m-d H:i O'));
   }
@@ -1884,7 +1913,7 @@ function comment_form($form, &$form_state, $comment) {
   // already previewing the submission. However, if there are form errors,
   // we hide the save button no matter what, so that optional form elements
   // (e.g., captchas) can be updated.
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
@@ -1985,7 +2014,7 @@ function comment_form_validate($form, &$form_state) {
     if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
       form_set_error('date', t('You have to specify a valid date.'));
     }
-    if ($form_state['values']['name'] && !$account = user_load_by_name($form_state['values']['name'])) {
+    if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account = user_load_by_name($form_state['values']['name'])) {
       form_set_error('name', t('You have to specify a valid author.'));
     }
   }
@@ -2085,11 +2114,15 @@ function comment_form_submit($form, &$form_state) {
   if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
     // Save the anonymous user information to a cookie for reuse.
     if (!$comment->uid) {
-      user_cookie_save($form_state['values']);
+      user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
     }
 
     comment_save($comment);
     $form_state['values']['cid'] = $comment->cid;
+
+    // Add an entry to the watchdog log.
+    watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
+
     // Explain the approval queue if necessary.
     if ($comment->status == COMMENT_NOT_PUBLISHED) {
       if (!user_access('administer comments')) {
@@ -2116,6 +2149,9 @@ function comment_form_submit($form, &$form_state) {
   }
   unset($form_state['rebuild']);
   $form_state['redirect'] = $redirect;
+  // Clear the block and page caches so that anonymous users see the comment
+  // they have posted.
+  cache_clear_all();
 }
 
 /**
@@ -2135,8 +2171,10 @@ function template_preprocess_comment(&$variables) {
   $variables['new']       = !empty($comment->new) ? t('new') : '';
   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
   $variables['signature'] = $comment->signature;
-  $variables['title']     = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
-  $variables['permalink'] = l('#', 'comment/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
+
+  $uri = entity_uri('comment', $comment);
+  $variables['title']     = l($comment->subject, $uri['path'], $uri['options']);
+  $variables['permalink'] = l('#', $uri['path'], $uri['options']);
 
   // Preprocess fields.
   field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
@@ -2177,7 +2215,7 @@ function template_preprocess_comment(&$variables) {
 }
 
 /**
- * Theme a "you can't post comments" notice.
+ * Returns HTML for a "you can't post comments" notice.
  *
  * @param $variables
  *   An associative array containing:
@@ -2267,6 +2305,12 @@ function _comment_per_page() {
  * - comment_count: the total number of approved/published comments on this node.
  */
 function _comment_update_node_statistics($nid) {
+  // Allow bulk updates and inserts to temporarily disable the
+  // maintenance of the {node_comment_statistics} table.
+  if (!variable_get('comment_maintain_node_statistics', TRUE)) {
+    return;
+  }
+
   $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
     ':nid' => $nid,
     ':status' => COMMENT_PUBLISHED,
@@ -2480,6 +2524,7 @@ function comment_unpublish_by_keyword_action_submit($form, $form_state) {
  */
 function comment_save_action($comment) {
   comment_save($comment);
+  cache_clear_all();
   watchdog('action', 'Saved comment %title', array('%title' => $comment->subject));
 }
 
@@ -2503,21 +2548,6 @@ function comment_ranking() {
   );
 }
 
-/**
- * Implements hook_menu_alter().
- */
-function comment_menu_alter(&$items) {
-  // Add comments to the description for admin/content.
-  $items['admin/content']['description'] = "Administer content and comments";
-
-  // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
-  // @see comment_entity_info()
-  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields';
-  $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3;
-  $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display';
-  $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4;
-}
-
 /**
  * Implements hook_rdf_mapping().
  */
diff --git a/modules/comment/comment.pages.inc b/modules/comment/comment.pages.inc
index cba73b0f74d64720ffd0aec43c8cba74cc5c3575..100f92331cfedde0364f528672358d8777a7bbdd 100644
--- a/modules/comment/comment.pages.inc
+++ b/modules/comment/comment.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: comment.pages.inc,v 1.36 2010/02/22 15:38:52 webchick Exp $
+// $Id: comment.pages.inc,v 1.38 2010/04/13 15:13:41 dries Exp $
 
 /**
  * @file
@@ -61,7 +61,6 @@ function comment_reply($node, $pid = NULL) {
             drupal_goto("node/$node->nid");
           }
           // Display the parent comment
-          $comment = drupal_unpack($comment);
           $comment->node_type = 'comment_node_' . $node->type;
           field_attach_load('comment', array($comment->cid => $comment));
           $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
@@ -107,6 +106,9 @@ function comment_reply($node, $pid = NULL) {
  *   A comment identifier.
  */
 function comment_approve($cid) {
+  if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], "comment/$cid/approve")) {
+    return MENU_ACCESS_DENIED;
+  }
   if ($comment = comment_load($cid)) {
     $comment->status = COMMENT_PUBLISHED;
     comment_save($comment);
diff --git a/modules/comment/comment.test b/modules/comment/comment.test
index 0c13f39160307c3ab218761898f7145208746425..ecab2569dbdfa43046da261aff1dd97f00d6219a 100644
--- a/modules/comment/comment.test
+++ b/modules/comment/comment.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: comment.test,v 1.71 2010/03/07 23:14:20 webchick Exp $
+// $Id: comment.test,v 1.77 2010/04/22 10:12:25 webchick Exp $
 
 class CommentHelperCase extends DrupalWebTestCase {
   protected $admin_user;
@@ -530,11 +530,18 @@ class CommentAnonymous extends CommentHelperCase {
     $this->assertFalse($this->commentExists($anonymous_comment3), t('Anonymous comment with contact info (required) not found.'));
 
     // Post comment with contact info (required).
-    $anonymous_comment3 = $this->postComment($this->node, $this->randomName(), $this->randomName(), array('mail' => 'tester@simpletest.org'));
+    $author_name = $this->randomName();
+    $author_mail = $this->randomName() . '@example.com';
+    $anonymous_comment3 = $this->postComment($this->node, $this->randomName(), $this->randomName(), array('name' => $author_name, 'mail' => $author_mail));
     $this->assertTrue($this->commentExists($anonymous_comment3), t('Anonymous comment with contact info (required) found.'));
 
-    // Unpublish comment.
+    // Make sure the user data appears correctly when editing the comment.
     $this->drupalLogin($this->admin_user);
+    $this->drupalGet('comment/' . $anonymous_comment3->id . '/edit');
+    $this->assertRaw($author_name, t("The anonymous user's name is correct when editing the comment."));
+    $this->assertRaw($author_mail, t("The anonymous user's e-mail address is correct when editing the comment."));
+
+    // Unpublish comment.
     $this->performCommentOperation($anonymous_comment3, 'unpublish');
 
     $this->drupalGet('admin/content/comment/approval');
@@ -762,7 +769,7 @@ class CommentPagerTest extends CommentHelperCase {
       $expected_cids[] = $comments[$key]->id;
     }
 
-    $comment_anchors = $this->xpath("//a[starts-with(@id,'comment-')]");
+    $comment_anchors = $this->xpath('//a[starts-with(@id,"comment-")]');
     $result_order = array();
     foreach ($comment_anchors as $anchor) {
       $result_order[] = substr($anchor['id'], 8);
@@ -954,6 +961,10 @@ class CommentApprovalTest extends CommentHelperCase {
 
     // Approve comment.
     $this->drupalLogin($this->admin_user);
+    $this->drupalGet('comment/1/approve');
+    $this->assertResponse(403, t('Forged comment approval was denied.'));
+    $this->drupalGet('comment/1/approve', array('query' => array('token' => 'forged')));
+    $this->assertResponse(403, t('Forged comment approval was denied.'));
     $this->drupalGet('node/' . $this->node->nid);
     $this->clickLink(t('approve'));
     $this->drupalLogout();
@@ -1130,13 +1141,13 @@ class CommentRdfaTestCase extends CommentHelperCase {
 
     // Tests number of comments in teaser view.
     $this->drupalGet('node');
-    $comment_count_teaser = $this->xpath("//div[contains(@typeof, 'sioc:Item')]//li[contains(@class, 'comment_comments')]/a[contains(@property, 'sioc:num_replies') and contains(@content, '2') and @datatype='xsd:integer']");
+    $comment_count_teaser = $this->xpath('//div[contains(@typeof, "sioc:Item")]//li[contains(@class, "comment_comments")]/a[contains(@property, "sioc:num_replies") and contains(@content, "2") and @datatype="xsd:integer"]');
     $this->assertTrue(!empty($comment_count_teaser), t('RDFa markup for the number of comments found on teaser view.'));
 
     // Tests number of comments in full node view.
     $this->drupalGet('node/' . $this->node1->nid);
     $node_url = url('node/' . $this->node1->nid);
-    $comment_count_teaser = $this->xpath("/html/head/meta[@about='$node_url' and @property='sioc:num_replies' and @content='2' and @datatype='xsd:integer']");
+    $comment_count_teaser = $this->xpath('/html/head/meta[@about=:node-url and @property="sioc:num_replies" and @content="2" and @datatype="xsd:integer"]', array(':node-url' => $node_url));
     $this->assertTrue(!empty($comment_count_teaser), t('RDFa markup for the number of comments found on full node view.'));
   }
 
@@ -1176,10 +1187,10 @@ class CommentRdfaTestCase extends CommentHelperCase {
     // Tests comment #2 as anonymous user.
     $this->_testBasicCommentRdfaMarkup($comment2, $anonymous_user);
     // Tests the RDFa markup for the homepage (specific to anonymous comments).
-    $comment_homepage = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//span[@rel='sioc:has_creator']/a[contains(@class, 'username') and @typeof='sioc:User' and @property='foaf:name' and @href='http://example.org/' and contains(@rel, 'foaf:page')]");
+    $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @href="http://example.org/" and contains(@rel, "foaf:page")]');
     $this->assertTrue(!empty($comment_homepage), t('RDFa markup for the homepage of anonymous user found.'));
     // There should be no about attribute on anonymous comments.
-    $comment_homepage = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//span[@rel='sioc:has_creator']/a[@about]");
+    $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[@about]');
     $this->assertTrue(empty($comment_homepage), t('No about attribute is present on anonymous user comment.'));
 
     // Tests comment #2 as logged in user.
@@ -1187,13 +1198,37 @@ class CommentRdfaTestCase extends CommentHelperCase {
     $this->drupalGet('node/' . $this->node2->nid);
     $this->_testBasicCommentRdfaMarkup($comment2, $anonymous_user);
     // Tests the RDFa markup for the homepage (specific to anonymous comments).
-    $comment_homepage = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//span[@rel='sioc:has_creator']/a[contains(@class, 'username') and @typeof='sioc:User' and @property='foaf:name' and @href='http://example.org/' and contains(@rel, 'foaf:page')]");
-    $this->assertTrue(!empty($comment_homepage), t('RDFa markup for the homepage of anonymous user found.'));
+    $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @href="http://example.org/" and contains(@rel, "foaf:page")]');
+    $this->assertTrue(!empty($comment_homepage), t("RDFa markup for the homepage of anonymous user found."));
     // There should be no about attribute on anonymous comments.
-    $comment_homepage = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//span[@rel='sioc:has_creator']/a[@about]");
-    $this->assertTrue(empty($comment_homepage), t('No about attribute is present on anonymous user comment.'));
+    $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[@about]');
+    $this->assertTrue(empty($comment_homepage), t("No about attribute is present on anonymous user comment."));
   }
 
+  /**
+   * Test RDF comment replies.
+   */
+  function testCommentReplyOfRdfaMarkup() {
+    // Posts comment #1 as a registered user.
+    $this->drupalLogin($this->web_user);
+    $comments[] = $this->postComment($this->node1, $this->randomName(), $this->randomName());
+
+    // Tests the reply_of relationship of a first level comment.
+    $result = $this->xpath("id('comments')//div[@class='comment' and position()=0]//span[@rel='sioc:reply_of' and @resource=:node]", array(':node' => url("node/{$this->node1->nid}")));
+    $this->assertEqual(1, count($result), t('RDFa markup referring to the node is present.'));
+    $result = $this->xpath("id('comments')//div[@class='comment' and position()=0]//span[@rel='sioc:reply_of' and @resource=:comment]", array(':comment' => url('comment/1#comment-1')));
+    $this->assertFalse($result, t('No RDFa markup referring to the comment itself is present.'));
+
+    // Posts a reply to the first comment.
+    $this->drupalGet('comment/reply/' . $this->node1->nid . '/' . $comments[0]->id);
+    $comments[] = $this->postComment(NULL, $this->randomName(), $this->randomName(), TRUE);
+
+    // Tests the reply_of relationship of a second level comment.
+    $result = $this->xpath("id('comments')//div[@class='comment' and position()=1]//span[@rel='sioc:reply_of' and @resource=:node]", array(':node' => url("node/{$this->node1->nid}")));
+    $this->assertEqual(1, count($result), t('RDFa markup referring to the node is present.'));
+    $result = $this->xpath("id('comments')//div[@class='comment' and position()=1]//span[@rel='sioc:reply_of' and @resource=:comment]", array(':comment' => url('comment/1#comment-1')));
+    $this->assertEqual(1, count($result), t('RDFa markup referring to the parent comment is present.'));
+  }
   /**
    * Helper function for testCommentRdfaMarkup().
    *
@@ -1205,18 +1240,18 @@ class CommentRdfaTestCase extends CommentHelperCase {
    *   An array containing information about an anonymous user.
    */
   function _testBasicCommentRdfaMarkup($comment, $account = array()) {
-    $comment_container = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]");
-    $this->assertTrue(!empty($comment_container), t('Comment RDF type for comment found.'));
-    $comment_title = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//h3[@property='dc:title']");
-    $this->assertEqual((string)$comment_title[0]->a, $comment->subject, t('RDFa markup for the comment title found.'));
-    $comment_date = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//*[contains(@property, 'dc:date') and contains(@property, 'dc:created')]");
-    $this->assertTrue(!empty($comment_date), t('RDFa markup for the date of the comment found.'));
+    $comment_container = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]');
+    $this->assertTrue(!empty($comment_container), t("Comment RDF type for comment found."));
+    $comment_title = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//h3[@property="dc:title"]');
+    $this->assertEqual((string)$comment_title[0]->a, $comment->subject, t("RDFa markup for the comment title found."));
+    $comment_date = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//*[contains(@property, "dc:date") and contains(@property, "dc:created")]');
+    $this->assertTrue(!empty($comment_date), t("RDFa markup for the date of the comment found."));
     // The author tag can be either a or span
-    $comment_author = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//span[@rel='sioc:has_creator']/*[contains(@class, 'username') and @typeof='sioc:User' and @property='foaf:name']");
-    $name = empty($account['name']) ? $this->web_user->name : $account['name'] . ' (not verified)';
-    $this->assertEqual((string)$comment_author[0], $name, t('RDFa markup for the comment author found.'));
-    $comment_body = $this->xpath("//div[contains(@class, 'comment') and contains(@typeof, 'sioct:Comment')]//div[@class='content']//div[contains(@class, 'comment-body')]//div[@property='content:encoded']");
-    $this->assertEqual((string)$comment_body[0]->p, $comment->comment, t('RDFa markup for the comment body found.'));
+    $comment_author = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/*[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name"]');
+    $name = empty($account["name"]) ? $this->web_user->name : $account["name"] . " (not verified)";
+    $this->assertEqual((string)$comment_author[0], $name, t("RDFa markup for the comment author found."));
+    $comment_body = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//div[@class="content"]//div[contains(@class, "comment-body")]//div[@property="content:encoded"]');
+    $this->assertEqual((string)$comment_body[0]->p, $comment->comment, t("RDFa markup for the comment body found."));
   }
 }
 
@@ -1259,3 +1294,103 @@ class CommentContentRebuild extends CommentHelperCase {
     $this->assertFalse(isset($built_content['test_property']), t('Comment content was emptied before being built.'));
   }
 }
+
+/**
+ * Test comment token replacement in strings.
+ */
+class CommentTokenReplaceTestCase extends CommentHelperCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Comment token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check comment token replacement.',
+      'group' => 'Comment',
+    );
+  }
+
+  /**
+   * Creates a comment, then tests the tokens generated from it.
+   */
+  function testCommentTokenReplacement() {
+    global $language;
+    $url_options = array(
+      'absolute' => TRUE,
+      'language' => $language,
+    );
+
+    $this->drupalLogin($this->admin_user);
+
+    // Set comment variables.
+    $this->setCommentSubject(TRUE);
+
+    // Create a node and a comment.
+    $node = $this->drupalCreateNode(array('type' => 'article'));
+    $parent_comment = $this->postComment($node, $this->randomName(), $this->randomName(), TRUE);
+
+    // Post a reply to the comment.
+    $this->drupalGet('comment/reply/' . $node->nid . '/' . $parent_comment->id);
+    $child_comment = $this->postComment(NULL, $this->randomName(), $this->randomName());
+    $comment = comment_load($child_comment->id);
+    $comment->homepage = 'http://example.org/';
+
+    // Add HTML to ensure that sanitation of some fields tested directly.
+    $comment->subject = '<blink>Blinking Comment</blink>';
+    $instance = field_info_instance('comment', 'body', 'comment_body');
+
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[comment:cid]'] = $comment->cid;
+    $tests['[comment:pid]'] = $comment->pid;
+    $tests['[comment:nid]'] = $comment->nid;
+    $tests['[comment:uid]'] = $comment->uid;
+    $tests['[comment:hostname]'] = check_plain($comment->hostname);
+    $tests['[comment:name]'] = filter_xss($comment->name);
+    $tests['[comment:mail]'] = check_plain($this->admin_user->mail);
+    $tests['[comment:homepage]'] = filter_xss_bad_protocol($comment->homepage);
+    $tests['[comment:title]'] = filter_xss($comment->subject);
+    $tests['[comment:body]'] = _text_sanitize($instance, LANGUAGE_NONE, $comment->comment_body[LANGUAGE_NONE][0], 'value');
+    $tests['[comment:url]'] = url('comment/' . $comment->cid, $url_options + array('fragment' => 'comment-' . $comment->cid));
+    $tests['[comment:edit-url]'] = url('comment/' . $comment->cid . '/edit', $url_options);
+    $tests['[comment:created:since]'] = format_interval(REQUEST_TIME - $comment->created, 2, $language->language);
+    $tests['[comment:changed:since]'] = format_interval(REQUEST_TIME - $comment->changed, 2, $language->language);
+    $tests['[comment:parent:title]'] = check_plain($parent_comment->subject);
+    $tests['[comment:node:title]'] = check_plain($node->title);
+    $tests['[comment:author:name]'] = check_plain($this->admin_user->name);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('comment' => $comment), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized comment token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[comment:hostname]'] = $comment->hostname;
+    $tests['[comment:name]'] = $comment->name;
+    $tests['[comment:mail]'] = $this->admin_user->mail;
+    $tests['[comment:homepage]'] = $comment->homepage;
+    $tests['[comment:title]'] = $comment->subject;
+    $tests['[comment:body]'] = $comment->comment_body[LANGUAGE_NONE][0]['value'];
+    $tests['[comment:parent:title]'] = $parent_comment->subject;
+    $tests['[comment:node:title]'] = $node->title;
+    $tests['[comment:author:name]'] = $this->admin_user->name;
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('comment' => $comment), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized comment token %token replaced.', array('%token' => $input)));
+    }
+
+    // Load node so comment_count gets computed.
+    $node = node_load($node->nid);
+
+    // Generate comment tokens for the node (it has 2 comments, both new).
+    $tests = array();
+    $tests['[node:comment-count]'] = 2;
+    $tests['[node:comment-count-new]'] = 2;
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('node' => $node), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Node comment token %token replaced.', array('%token' => $input)));
+    }
+  }
+}
diff --git a/modules/comment/comment.tokens.inc b/modules/comment/comment.tokens.inc
index 848ec0faf88d80336753822f842e7fdbda76d264..dfd9cd9fe97c3b234adebbea6d40f7ecb1d29ea6 100644
--- a/modules/comment/comment.tokens.inc
+++ b/modules/comment/comment.tokens.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: comment.tokens.inc,v 1.10 2010/01/09 21:54:00 webchick Exp $
+// $Id: comment.tokens.inc,v 1.11 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -82,6 +82,11 @@ function comment_token_info() {
     'description' => t("The date the comment was posted."),
     'type' => 'date',
   );
+  $comment['changed'] = array(
+    'name' => t("Date changed"),
+    'description' => t("The date the comment was most recently updated."),
+    'type' => 'date',
+  );
   $comment['parent'] = array(
     'name' => t("Parent"),
     'description' => t("The comment's parent, if comment threading is active."),
@@ -133,6 +138,10 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
           $replacements[$original] = $comment->cid;
           break;
 
+        case 'pid':
+          $replacements[$original] = $comment->pid;
+          break;
+
         case 'nid':
           $replacements[$original] = $comment->nid;
           break;
@@ -141,10 +150,6 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
           $replacements[$original] = $comment->uid;
           break;
 
-        case 'pid':
-          $replacements[$original] = $comment->pid;
-          break;
-
         // Poster identity information for comments
         case 'hostname':
           $replacements[$original] = $sanitize ? check_plain($comment->hostname) : $comment->hostname;
@@ -175,7 +180,9 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
           break;
 
         case 'body':
-          $replacements[$original] = $sanitize ? check_markup($comment->comment, $comment->format, '', TRUE) : $comment->comment;
+          $item = $comment->comment_body[LANGUAGE_NONE][0];
+          $instance = field_info_instance('comment', 'body', 'comment_body');
+          $replacements[$original] = $sanitize ? _text_sanitize($instance, LANGUAGE_NONE, $item, 'value') : $item['value'];
           break;
 
         // Comment related URLs.
@@ -185,6 +192,7 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
           break;
 
         case 'edit-url':
+          $url_options['fragment'] = NULL;
           $replacements[$original] = url('comment/' . $comment->cid . '/edit', $url_options);
           break;
 
@@ -226,6 +234,10 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
       $replacements += token_generate('date', $date_tokens, array('date' => $comment->created), $options);
     }
 
+    if ($date_tokens = token_find_with_prefix($tokens, 'changed')) {
+      $replacements += token_generate('date', $date_tokens, array('date' => $comment->changed), $options);
+    }
+
     if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) {
       $replacements += token_generate('comment', $parent_tokens, array('comment' => $parent), $options);
     }
diff --git a/modules/contact/contact.admin.inc b/modules/contact/contact.admin.inc
index d3b0e015a5e27d40d07f4806fbbdee26de0043fb..477a20ce457692b27146eb3253829cf242ea0c1c 100644
--- a/modules/contact/contact.admin.inc
+++ b/modules/contact/contact.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: contact.admin.inc,v 1.23 2010/02/26 18:35:35 dries Exp $
+// $Id: contact.admin.inc,v 1.24 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -108,10 +108,7 @@ function contact_category_edit_form($form, &$form_state, array $category = array
     '#type' => 'value',
     '#value' => $category['cid'],
   );
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
diff --git a/modules/contact/contact.info b/modules/contact/contact.info
index cec42a2aa9f13d5721bc4687831da6b48c89bcf6..ce836de9f462e647e5062c293b1f7664c2e67a5e 100644
--- a/modules/contact/contact.info
+++ b/modules/contact/contact.info
@@ -11,8 +11,8 @@ files[] = contact.install
 files[] = contact.test
 configure = admin/structure/contact
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/contact/contact.install b/modules/contact/contact.install
index f7cc0677e98d46e89e8545e9034cc341724762c8..62fbfb1318b0704771d9cfe32a1f17dd7805cf5a 100644
--- a/modules/contact/contact.install
+++ b/modules/contact/contact.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: contact.install,v 1.23 2010/01/30 00:08:34 webchick Exp $
+// $Id: contact.install,v 1.24 2010/04/20 07:46:33 webchick Exp $
 
 /**
  * @file
@@ -89,6 +89,18 @@ function contact_uninstall() {
   variable_del('contact_threshold_window');
 }
 
+/**
+ * Implements hook_update_dependencies().
+ */
+function contact_update_dependencies() {
+  // Contact update 7002 calls the new user_role_grant_permissions() and
+  // therefore needs to run after user_update_7006();
+  $dependencies['contact'][7002] = array(
+    'user' => 7006,
+  );
+  return $dependencies;
+}
+
 /**
  * @defgroup updates-6.x-to-7.x Contact updates from 6.x to 7.x
  * @{
diff --git a/modules/contact/contact.module b/modules/contact/contact.module
index 45a1e8103c7906ee38df290a7928de626c9d1631..5beceaf3420dcce48c7854cdcff43353bdfe6789 100644
--- a/modules/contact/contact.module
+++ b/modules/contact/contact.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: contact.module,v 1.146 2010/03/20 14:55:06 dries Exp $
+// $Id: contact.module,v 1.147 2010/04/13 15:13:41 dries Exp $
 
 /**
  * @file
@@ -138,7 +138,7 @@ function _contact_personal_tab_access(stdClass $account) {
 
   // If the requested user has disabled their contact form, or this preference
   // has not yet been saved, do not allow users to contact them.
-  if (empty($account->contact)) {
+  if (empty($account->data['contact'])) {
     return FALSE;
   }
 
@@ -227,7 +227,7 @@ function contact_form_user_profile_form_alter(&$form, &$form_state) {
     $form['contact']['contact'] = array(
       '#type' => 'checkbox',
       '#title' => t('Personal contact form'),
-      '#default_value' => !empty($account->contact) ? $account->contact : FALSE,
+      '#default_value' => !empty($account->data['contact']) ? $account->data['contact'] : FALSE,
       '#description' => t('Allow other users to contact you via a <a href="@url">personal contact form</a> which keeps your e-mail address hidden. Note that some privileged users such as site administrators are still able to contact you even if you choose to disable this feature.', array('@url' => url("user/$account->uid/contact"))),
     );
   }
diff --git a/modules/contact/contact.pages.inc b/modules/contact/contact.pages.inc
index 6363dc5135603eb1dc3f0dbff60e4fc8841f0fb4..13720218777c835a714f88c8dc9242e4c8ee7fb4 100644
--- a/modules/contact/contact.pages.inc
+++ b/modules/contact/contact.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: contact.pages.inc,v 1.41 2010/02/17 08:48:18 dries Exp $
+// $Id: contact.pages.inc,v 1.43 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -101,10 +101,7 @@ function contact_site_form($form, &$form_state) {
     '#title' => t('Send yourself a copy.'),
     '#access' => $user->uid,
   );
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Send message'),
@@ -139,7 +136,7 @@ function contact_site_form_submit($form, &$form_state) {
 
   // Save the anonymous user information to a cookie for reuse.
   if (!$user->uid) {
-    user_cookie_save($values);
+    user_cookie_save(array_intersect_key($values, array_flip(array('name', 'mail'))));
   }
 
   // Get the to and from e-mail addresses.
@@ -235,10 +232,7 @@ function contact_personal_form($form, &$form_state, $recipient) {
     '#title' => t('Send yourself a copy.'),
     '#access' => $user->uid,
   );
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Send message'),
@@ -272,7 +266,7 @@ function contact_personal_form_submit($form, &$form_state) {
 
   // Save the anonymous user information to a cookie for reuse.
   if (!$user->uid) {
-    user_cookie_save($values);
+    user_cookie_save(array_intersect_key($values, array_flip(array('name', 'mail'))));
   }
 
   // Get the to and from e-mail addresses.
diff --git a/modules/contact/contact.test b/modules/contact/contact.test
index f1c2128494a51c97c6721c87cf2873b639b0c13c..311c7560534b7fbb38d1061dffc7ae102d92f3db 100644
--- a/modules/contact/contact.test
+++ b/modules/contact/contact.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: contact.test,v 1.42 2010/02/17 04:39:49 webchick Exp $
+// $Id: contact.test,v 1.43 2010/03/22 17:52:58 webchick Exp $
 
 /**
  * Test the sitewide contact form.
@@ -166,15 +166,15 @@ class ContactSitewideTestCase extends DrupalWebTestCase {
     $this->drupalLogin($admin_user);
 
     // Set up three categories, 2 with an auto-reply and one without.
-    $foo_autoreply = $this->randomString(40);
-    $bar_autoreply = $this->randomString(40);
+    $foo_autoreply = $this->randomName(40);
+    $bar_autoreply = $this->randomName(40);
     $this->addCategory('foo', 'foo@example.com', $foo_autoreply, FALSE);
     $this->addCategory('bar', 'bar@example.com', $bar_autoreply, FALSE);
     $this->addCategory('no_autoreply', 'bar@example.com', '', FALSE);
 
     // Test the auto-reply for category 'foo'.
     $email = $this->randomName(32) . '@example.com';
-    $subject = $this->randomString(64);
+    $subject = $this->randomName(64);
     $this->submitContact($this->randomName(16), $email, $subject, 2, $this->randomString(128));
 
     // We are testing the auto-reply, so there should be one e-mail going to the sender.
diff --git a/modules/contextual/contextual.info b/modules/contextual/contextual.info
index fdd0d0004edeb49370959218202b5d8105a78f79..09308f775fcd84822679616a04e057822d80b3ac 100644
--- a/modules/contextual/contextual.info
+++ b/modules/contextual/contextual.info
@@ -6,8 +6,8 @@ version = VERSION
 core = 7.x
 files[] = contextual.module
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/contextual/contextual.js b/modules/contextual/contextual.js
index 187056b90e25121e0a5f6c5eabe36f0c50568eed..f5abcff3aecb7bc7f527b20a241353951d80ef1e 100644
--- a/modules/contextual/contextual.js
+++ b/modules/contextual/contextual.js
@@ -1,4 +1,4 @@
-// $Id: contextual.js,v 1.4 2009/12/14 10:38:19 dries Exp $
+// $Id: contextual.js,v 1.5 2010/03/26 18:59:54 dries Exp $
 (function ($) {
 
 Drupal.contextualLinks = Drupal.contextualLinks || {};
@@ -14,7 +14,7 @@ Drupal.behaviors.contextualLinks = {
       var $links = $wrapper.find('ul.contextual-links');
       var $trigger = $('<a class="contextual-links-trigger" href="#" />').text(Drupal.t('Configure')).click(
         function () {
-          $wrapper.find('ul.contextual-links').stop(true, true).slideToggle(100);
+          $links.stop(true, true).slideToggle(100);
           $wrapper.toggleClass('contextual-links-active');
           return false;
         }
@@ -27,7 +27,7 @@ Drupal.behaviors.contextualLinks = {
       // Hide the contextual links when user rolls out of the .contextual-links-region.
       $region.bind('mouseleave', Drupal.contextualLinks.mouseleave);
       // Prepend the trigger.
-      $links.end().prepend($trigger);
+      $wrapper.prepend($trigger);
     });
   }
 };
@@ -36,7 +36,7 @@ Drupal.behaviors.contextualLinks = {
  * Disables outline for the region contextual links are associated with.
  */
 Drupal.contextualLinks.mouseleave = function () {
-  $(this).closest('.contextual-links-region')
+  $(this)
     .find('.contextual-links-active').removeClass('contextual-links-active')
     .find('ul.contextual-links').hide();
 };
diff --git a/modules/dashboard/dashboard.css b/modules/dashboard/dashboard.css
index aa1bf1d71d3b3af20d7221cc93388e43cf2db1b5..07fdb00602302c91fc868ae5c028322c0737a126 100644
--- a/modules/dashboard/dashboard.css
+++ b/modules/dashboard/dashboard.css
@@ -1,4 +1,4 @@
-/* $Id: dashboard.css,v 1.11 2010/03/20 14:45:05 dries Exp $ */
+/* $Id: dashboard.css,v 1.12 2010/03/28 11:29:27 dries Exp $ */
 
 #dashboard div.dashboard-region {
   float: left;
@@ -66,7 +66,7 @@
   margin: 5px;
 }
 
-#dashboard #disabled-blocks .section {
+#dashboard #disabled-blocks .region {
   background-color: #E0E0D8;
   border: #ccc 1px solid;
   padding: 10px;
@@ -77,12 +77,6 @@
   padding: 5px 0;
 }
 
-#dashboard div.dragging {
-  background: #0074BD;
-  color: #fff;
-  width: 30%;
-}
-
 #dashboard #disabled-blocks h2 {
   display: inline;
   font-weight: normal;
@@ -94,7 +88,7 @@
   color: #fff;
 }
 
-#dashboard #disabled-blocks .block:hover {
+#dashboard.customize-inactive #disabled-blocks .block:hover {
   background: #0074BD;
 }
 
@@ -112,12 +106,12 @@
   padding: 0 17px;
 }
 
-#dashboard #disabled-blocks .block:hover h2 {
+#dashboard.customize-inactive #disabled-blocks .block:hover h2 {
   background: #0074BD url(../../misc/draggable.png) no-repeat 0px -39px;
   color: #fff;
 }
 
-#dashboard .dashboard-region .ui-sortable .block:hover h2 {
+#dashboard.customize-inactive .dashboard-region .ui-sortable .block:hover h2 {
   background: #0074BD url(../../misc/draggable.png) no-repeat;
   background-position: 3px -36px;
   color: #fff;
diff --git a/modules/dashboard/dashboard.info b/modules/dashboard/dashboard.info
index 712e37541c7b913e46945dfb13295b02861b838d..ac82e93228b307a7bc42e9e5750b364125c3a1c9 100644
--- a/modules/dashboard/dashboard.info
+++ b/modules/dashboard/dashboard.info
@@ -8,8 +8,8 @@ files[] = dashboard.module
 dependencies[] = block
 configure = admin/dashboard/customize
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/dashboard/dashboard.js b/modules/dashboard/dashboard.js
index 6155264566477816936b95322bf681aad3c72aaa..af41205a3b884874dbb1e21733109ee681cfcb72 100644
--- a/modules/dashboard/dashboard.js
+++ b/modules/dashboard/dashboard.js
@@ -1,4 +1,4 @@
-// $Id: dashboard.js,v 1.8 2010/02/21 10:47:25 dries Exp $
+// $Id: dashboard.js,v 1.9 2010/03/28 11:29:27 dries Exp $
 (function ($) {
 
 /**
@@ -44,7 +44,7 @@ Drupal.behaviors.dashboard = {
    * Enter "customize" mode by displaying disabled blocks.
    */
   enterCustomizeMode: function () {
-    $('#dashboard').addClass('customize-mode');
+    $('#dashboard').addClass('customize-mode customize-inactive');
     Drupal.behaviors.dashboard.addPlaceholders();
     // Hide the customize link
     $('#dashboard .customize .action-links').hide();
@@ -56,7 +56,7 @@ Drupal.behaviors.dashboard = {
    * Exit "customize" mode by simply forcing a page refresh.
    */
   exitCustomizeMode: function () {
-    $('#dashboard').removeClass('customize-mode');
+    $('#dashboard').removeClass('customize-mode customize-inactive');
     Drupal.behaviors.dashboard.addPlaceholders();
     location.href = Drupal.settings.dashboard.dashboard;
   },
@@ -75,11 +75,12 @@ Drupal.behaviors.dashboard = {
       cursor: 'move',
       cursorAt: {top:0},
       dropOnEmpty: true,
-      items: '>div.block, div.disabled-block',
-      opacity: 1,
-      helper: 'block-dragging',
+      items: '> div.block, > div.disabled-block',
       placeholder: 'block-placeholder clearfix',
+      tolerance: 'pointer',
       start: Drupal.behaviors.dashboard.start,
+      over: Drupal.behaviors.dashboard.over,
+      sort: Drupal.behaviors.dashboard.sort,
       update: Drupal.behaviors.dashboard.update
     });
   },
@@ -95,6 +96,7 @@ Drupal.behaviors.dashboard = {
    *  An object containing information about the item that is being dragged.
    */
   start: function (event, ui) {
+    $('#dashboard').removeClass('customize-inactive');
     var item = $(ui.item);
 
     // If the block is already in disabled state, don't do anything.
@@ -103,6 +105,48 @@ Drupal.behaviors.dashboard = {
     }
   },
 
+  /**
+   * While dragging, adapt block's width to the width of the region it is moved
+   * into.
+   *
+   * This function is called on the jQuery UI Sortable "over" event.
+   *
+   * @param event
+   *  The event that triggered this callback.
+   * @param ui
+   *  An object containing information about the item that is being dragged.
+   */
+  over: function (event, ui) {
+    var item = $(ui.item);
+
+    // If the block is in disabled state, remove width.
+    if ($(this).closest('#disabled-blocks').length) {
+      item.css('width', '');
+    }
+    else {
+      item.css('width', $(this).width());
+    }
+  },
+
+  /**
+   * While dragging, adapt block's position to stay connected with the position
+   * of the mouse pointer.
+   *
+   * This function is called on the jQuery UI Sortable "sort" event.
+   *
+   * @param event
+   *  The event that triggered this callback.
+   * @param ui
+   *  An object containing information about the item that is being dragged.
+   */
+  sort: function (event, ui) {
+    var item = $(ui.item);
+
+    if (event.pageX > ui.offset.left + item.width()) {
+      item.css('left', event.pageX);
+    }
+  },
+
   /**
    * Send block order to the server, and expand previously disabled blocks.
    *
@@ -114,6 +158,7 @@ Drupal.behaviors.dashboard = {
    *   An object containing information about the item that was just dropped.
    */
   update: function (event, ui) {
+    $('#dashboard').addClass('customize-inactive');
     var item = $(ui.item);
 
     // If the user dragged a disabled block, load the block contents.
diff --git a/modules/dashboard/dashboard.module b/modules/dashboard/dashboard.module
index 63c833d404647c458bd19774405322d50d689b43..25318e1cceed62063bd36c77b3a752136316cd1d 100644
--- a/modules/dashboard/dashboard.module
+++ b/modules/dashboard/dashboard.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: dashboard.module,v 1.23 2010/03/01 13:12:45 dries Exp $
+// $Id: dashboard.module,v 1.27 2010/04/22 09:12:35 webchick Exp $
 
 /**
  * Implements hook_help().
@@ -29,7 +29,7 @@ function dashboard_menu() {
     'title' => 'Dashboard',
     'description' => 'View and customize your dashboard',
     'page callback' => 'dashboard_admin',
-    'access arguments' => array('access dashboard'),
+    'access arguments' => array('access administration pages'),
     // Make this appear first, so for example, in admin menus, it shows up on
     // the top corner of the window as a convinient "home link".
     'weight' => -100,
@@ -39,7 +39,7 @@ function dashboard_menu() {
     'description' => 'View and customize your dashboard',
     'page callback' => 'dashboard_admin',
     'page arguments' => array(TRUE),
-    'access arguments' => array('access dashboard'),
+    'access arguments' => array('access administration pages'),
     'type' => MENU_CALLBACK,
   );
   $items['admin/dashboard/drawer'] = array(
@@ -68,19 +68,18 @@ function dashboard_menu_alter(&$items) {
   // Make the dashboard the default local task on /admin.
   $items['admin']['title'] = 'Dashboard';
   $items['admin']['page callback'] = 'dashboard_admin';
-  $items['admin']['access arguments'] = array('access dashboard');
   $items['admin/dashboard']['type'] = MENU_DEFAULT_LOCAL_TASK;
   $items['admin/by-task']['type'] = MENU_LOCAL_TASK;
 }
 
 /**
- * Implements hook_block_info_alter().
+ * Implements hook_block_list_alter().
  *
  * Skip rendering dashboard blocks when not on the dashboard page itself. This
  * prevents expensive dashboard blocks from causing performance issues on pages
  * where they will never be displayed.
  */
-function dashboard_block_info_alter(&$blocks) {
+function dashboard_block_list_alter(&$blocks) {
   if (!dashboard_is_visible()) {
     foreach ($blocks as $key => $block) {
       if (in_array($block->region, dashboard_regions())) {
@@ -154,18 +153,6 @@ function dashboard_page_build(&$page) {
   }
 }
 
-/**
- * Implements hook_permission().
- */
-function dashboard_permission() {
-  return array(
-    'access dashboard' => array(
-      'title' => t('View the administrative dashboard'),
-      'description' => t('Note: modifying the dashboard requires the !administer_blocks permission.', array('!administer_blocks' => l(t('Administer blocks'), 'admin/people/permissions', array('fragment' => 'module-block')))),
-    ),
-  );
-}
-
 /**
  * Implements hook_system_info_alter().
  *
@@ -379,13 +366,12 @@ function dashboard_update() {
 }
 
 /**
- * Theme the entire dashboard.
+ * Returns HTML for the entire dashboard.
  *
  * @param $variables
- *   - element: An associative array containing the properties of the dashboard region
- *              element. Properties used: #dashboard_region, #children
- * @return
- *   A string representing the themed dashboard.
+ *   An associative array containing:
+ *   - element: A render element containing the properties of the dashboard
+ *     region element, #dashboard_region and #children.
  *
  * @ingroup themeable
  */
@@ -396,15 +382,11 @@ function theme_dashboard($variables) {
 }
 
 /**
- * Theme the page containing the dashboard.
+ * Returns HTML for the non-customizable part of the dashboard page.
  *
  * @param $variables
  *   An associative array containing:
- *   - elements: An associative array containing the properties of the element.
- *     Properties used: #message
- * @return
- *   A themed HTML string representing the non-customizable part of the
- *   dashboard page.
+ *   - element: A render element containing a #message.
  *
  * @ingroup themeable
  */
@@ -415,13 +397,12 @@ function theme_dashboard_admin($variables) {
 }
 
 /**
- * Theme a generic dashboard region.
+ * Returns HTML for a generic dashboard region.
  *
  * @param $variables
- *   - element: An associative array containing the properties of the dashboard region
- *              element. Properties used: #dashboard_region, #children
- * @return
- *   A string representing the themed dashboard region.
+ *   An associative array containing:
+ *   - element: A render element containing the properties of the dashboard
+ *     region element, #dashboard_region and #children.
  *
  * @ingroup themeable
  */
@@ -438,20 +419,18 @@ function theme_dashboard_region($variables) {
 }
 
 /**
- * Theme a set of disabled blocks, for display in dashboard customization mode.
+ * Returns HTML for a set of disabled blocks, for display in dashboard customization mode.
  *
  * @param $variables
+ *   An associative array containing:
  *   - blocks: An array of block objects from _block_rehash().
- * @return
- *   A string representing the disabled blocks region of the dashboard
- *   customization page.
  *
  * @ingroup themeable
  */
 function theme_dashboard_disabled_blocks($variables) {
   extract($variables);
   $output = '<div class="canvas-content"><p>' . t('Drag and drop these blocks to the columns below. Changes are automatically saved.') . '</p>';
-  $output .= '<div id="disabled-blocks"><div class="section region disabled-blocks clearfix">';
+  $output .= '<div id="disabled-blocks"><div class="region disabled-blocks clearfix">';
   foreach ($blocks as $block) {
     $output .= theme('dashboard_disabled_block', array('block' => $block));
   }
@@ -460,12 +439,11 @@ function theme_dashboard_disabled_blocks($variables) {
 }
 
 /**
- * Theme a disabled block, for display in dashboard customization mode.
+ * Returns HTML for a disabled block, for display in dashboard customization mode.
  *
  * @param $variables
+ *   An associative array containing:
  *   - block: A block object from _block_rehash().
- * @return
- *   A string representing the disabled block.
  *
  * @ingroup themeable
  */
diff --git a/modules/dblog/dblog.admin.inc b/modules/dblog/dblog.admin.inc
index f0301999b891929bc398abdcd6989105a03e888d..6d3524d74e1c5a673be430bb9bf94151601e8e50 100644
--- a/modules/dblog/dblog.admin.inc
+++ b/modules/dblog/dblog.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: dblog.admin.inc,v 1.36 2010/03/20 14:55:55 dries Exp $
+// $Id: dblog.admin.inc,v 1.39 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -8,6 +8,9 @@
 
 /**
  * Menu callback; displays a listing of log messages.
+ *
+ * Messages are truncated at 56 chars. Full-length message could be viewed at
+ * the message details page.
  */
 function dblog_overview() {
   $filter = dblog_build_filter_query();
@@ -65,7 +68,7 @@ function dblog_overview() {
         $icons[$dblog->severity],
         t($dblog->type),
         format_date($dblog->timestamp, 'short'),
-        l(truncate_utf8(_dblog_format_message($dblog), 56, TRUE, TRUE), 'admin/reports/event/' . $dblog->wid, array('html' => TRUE)),
+        theme('dblog_message', array('event' => $dblog, 'link' => TRUE)),
         theme('username', array('account' => $dblog)),
         $dblog->link,
       ),
@@ -87,8 +90,12 @@ function dblog_overview() {
 }
 
 /**
- * Menu callback; generic function to display a page of the most frequent
- * dblog events of a specified type.
+ * Menu callback; generic function to display a page of the most frequent events.
+ *
+ * Messages are not truncated because events from this page have no detail view.
+ *
+ * @param $type
+ *   type of dblog events to display.
  */
 function dblog_top($type) {
 
@@ -114,7 +121,7 @@ function dblog_top($type) {
 
   $rows = array();
   foreach ($result as $dblog) {
-    $rows[] = array($dblog->count, truncate_utf8(_dblog_format_message($dblog), 56, TRUE, TRUE));
+    $rows[] = array($dblog->count, theme('dblog_message', array('event' => $dblog)));
   }
 
   $build['dblog_top_table']  = array(
@@ -158,7 +165,7 @@ function dblog_event($id) {
       ),
       array(
         array('data' => t('Message'), 'header' => TRUE),
-        _dblog_format_message($dblog),
+        theme('dblog_message', array('event' => $dblog)),
       ),
       array(
         array('data' => t('Severity'), 'header' => TRUE),
@@ -244,23 +251,37 @@ function dblog_filters() {
 }
 
 /**
- * Formats a log message for display.
+ * Returns HTML for a log message.
  *
- * @param $dblog
- *   An object with at least the message and variables properties
+ * @param $variables
+ *   An associative array containing:
+ *   - event: An object with at least the message and variables properties.
+ *   - link: (optional) Format message as link, event->wid is required.
+ *
+ * @ingroup themeable
  */
-function _dblog_format_message($dblog) {
-  // Legacy messages and user specified text
-  if ($dblog->variables === 'N;') {
-    return $dblog->message;
-  }
-  // Message to translate with injected variables
-  else {
-    return t($dblog->message, unserialize($dblog->variables));
+function theme_dblog_message($variables) {
+  $output = '';
+  $event = $variables['event'];
+  // Check for required properties.
+  if (isset($event->message) && isset($event->variables)) {
+    // Messages without variables or user specified text.
+    if ($event->variables === 'N;') {
+      $output = $event->message;
+    }
+    // Message to translate with injected variables.
+    else {
+      $output = t($event->message, unserialize($event->variables));
+    }
+    if ($variables['link'] && isset($event->wid)) {
+      // Truncate message to 56 chars.
+      $output = truncate_utf8(filter_xss($output, array()), 56, TRUE, TRUE);
+      $output = l($output, 'admin/reports/event/' . $event->wid, array('html' => TRUE));
+    }
   }
+  return $output;
 }
 
-
 /**
  * Return form for dblog administration filters.
  *
@@ -291,8 +312,8 @@ function dblog_filter_form($form) {
   }
 
   $form['filters']['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions', 'container-inline')),
+    '#type' => 'actions',
+    '#attributes' => array('class' => array('container-inline')),
   );
   $form['filters']['actions']['submit'] = array(
     '#type' => 'submit',
diff --git a/modules/dblog/dblog.info b/modules/dblog/dblog.info
index 9261a3a99eacc3da78db341f2141d4007e610635..be2bfd20e8a417e08d485455b6249e95ffbda347 100644
--- a/modules/dblog/dblog.info
+++ b/modules/dblog/dblog.info
@@ -9,8 +9,8 @@ files[] = dblog.admin.inc
 files[] = dblog.install
 files[] = dblog.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/dblog/dblog.module b/modules/dblog/dblog.module
index cd93497515fa8a22091163caa388d59b916fbc30..e3a84395c2531136ce494a5b7ea5ca8df05eb21d 100644
--- a/modules/dblog/dblog.module
+++ b/modules/dblog/dblog.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: dblog.module,v 1.51 2010/02/26 15:25:43 dries Exp $
+// $Id: dblog.module,v 1.52 2010/03/27 14:24:14 dries Exp $
 
 /**
  * @file
@@ -144,3 +144,15 @@ function dblog_form_system_logging_settings_alter(&$form, $form_state) {
   );
   $form['actions']['#weight'] = 1;
 }
+
+/**
+ * Implements hook_theme().
+ */
+function dblog_theme() {
+  return array(
+    'dblog_message' => array(
+      'variables' => array('event' => NULL, 'link' => FALSE),
+      'file' => 'dblog.admin.inc',
+    ),
+  );
+}
diff --git a/modules/dblog/dblog.test b/modules/dblog/dblog.test
index 7e849e1026535f5c1807992801b6891f9745a948..f9048f0e80524603b3a62cd649e98013d88cccba 100644
--- a/modules/dblog/dblog.test
+++ b/modules/dblog/dblog.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: dblog.test,v 1.35 2010/03/11 21:23:06 dries Exp $
+// $Id: dblog.test,v 1.36 2010/03/27 14:24:14 dries Exp $
 
 class DBLogTestCase extends DrupalWebTestCase {
   protected $big_user;
@@ -74,9 +74,7 @@ class DBLogTestCase extends DrupalWebTestCase {
     $this->assertTrue($count > $row_limit, t('Dblog row count of @count exceeds row limit of @limit', array('@count' => $count, '@limit' => $row_limit)));
 
     // Run cron job.
-    $this->drupalGet('admin/reports/status/run-cron');
-    $this->assertResponse(200);
-    $this->assertText(t('Cron ran successfully'), t('Cron ran successfully'));
+    $this->cronRun();
     // Verify dblog row count equals row limit plus one because cron adds a record after it runs.
     $count = db_query('SELECT COUNT(wid) FROM {watchdog}')->fetchField();
     $this->assertTrue($count == $row_limit + 1, t('Dblog row count of @count equals row limit of @limit plus one', array('@count' => $count, '@limit' => $row_limit)));
@@ -108,9 +106,9 @@ class DBLogTestCase extends DrupalWebTestCase {
       'ip'          => ip_address(),
       'timestamp'   => REQUEST_TIME,
       );
-    $message = 'Log entry added to test the dblog row limit.';
+    $message = 'Log entry added to test the dblog row limit. Entry #';
     for ($i = 0; $i < $count; $i++) {
-      $log['message'] = $this->randomString();
+      $log['message'] = $message . $i;
       dblog_watchdog($log);
     }
   }
@@ -181,7 +179,7 @@ class DBLogTestCase extends DrupalWebTestCase {
    */
   private function doUser() {
     // Set user variables.
-    $name = $this->randomName(4);
+    $name = $this->randomName();
     $pass = user_password();
     // Add user using form to generate add user event (which is not triggered by drupalCreateUser).
     $edit = array();
@@ -223,11 +221,38 @@ class DBLogTestCase extends DrupalWebTestCase {
     // Default display includes name and email address; if too long then email is replaced by three periods.
     $this->assertLogMessage(t('New user: %name (%email).', array('%name' => $name, '%email' => $user->mail)), t('DBLog event was recorded: [add user]'));
     // Login user.
-    $this->assertLogMessage(t('Session opened for %name', array('%name' => $name)), t('DBLog event was recorded: [login user]'));
+    $this->assertLogMessage(t('Session opened for %name.', array('%name' => $name)), t('DBLog event was recorded: [login user]'));
     // Logout user.
-    $this->assertLogMessage(t('Session closed for %name', array('%name' => $name)), t('DBLog event was recorded: [logout user]'));
+    $this->assertLogMessage(t('Session closed for %name.', array('%name' => $name)), t('DBLog event was recorded: [logout user]'));
     // Delete user.
-    $this->assertLogMessage(t('Deleted user: %name', array('%name' => $name)), t('DBLog event was recorded: [delete user]'));
+    $message = t('Deleted user: %name %email.', array('%name' => $name, '%email' => '<' . $user->mail . '>'));
+    $message_text = truncate_utf8(filter_xss($message, array()), 56, TRUE, TRUE);
+    // Verify full message on details page.
+    $link = FALSE;
+    if ($links = $this->xpath('//a[text()="' . html_entity_decode($message_text) . '"]')) {
+      // Found link with the message text.
+      $links = array_shift($links);
+      foreach ($links->attributes() as $attr => $value) {
+        if ($attr == 'href') {
+          // Extract link to details page.
+          $link = drupal_substr($value, strpos($value, 'admin/reports/event/'));
+          $this->drupalGet($link);
+          // Check for full message text on the details page.
+          $this->assertRaw($message, t('DBLog event details was found: [delete user]'));
+          break;
+        }
+      }
+    }
+    $this->assertTrue($link, t('DBLog event was recorded: [delete user]'));
+    // Visit random URL (to generate page not found event).
+    $not_found_url = $this->randomName(60);
+    $this->drupalGet($not_found_url);
+    $this->assertResponse(404);
+    // View dblog page-not-found report page.
+    $this->drupalGet('admin/reports/page-not-found');
+    $this->assertResponse(200);
+    // Check that full-length url displayed.
+    $this->assertText($not_found_url, t('DBLog event was recorded: [page not found]'));
   }
 
   /**
@@ -537,15 +562,20 @@ class DBLogTestCase extends DrupalWebTestCase {
   /**
    * Assert messages appear on the log overview screen.
    *
+   * This function should be used only for admin/reports/dblog page, because it
+   * check for the message link text truncated to 56 characters. Other dblog
+   * pages have no detail links so contains a full message text.
+   *
    * @param $log_message
    *   The message to check.
    * @param $message
    *   The message to pass to simpletest.
    */
   protected function assertLogMessage($log_message, $message) {
-    // Truncate at 56 characters to compare with dblog's HTML output.
-    // @todo: Check the database instead for the exact error string.
-    $this->assertRaw(truncate_utf8($log_message, 56, TRUE, TRUE), $message);
+    $message_text = truncate_utf8(filter_xss($log_message, array()), 56, TRUE, TRUE);
+    // After filter_xss() HTML entities should be converted to their characters
+    // because assertLink() uses this string in xpath() to query DOM.
+    $this->assertLink(html_entity_decode($message_text), 0, $message);
   }
 }
 
diff --git a/modules/field/field.api.php b/modules/field/field.api.php
index 0b2d8b565023bdefeec1dd90cec30752952dffdb..382247e395129d3ebc9949285d9cb22c19fd143f 100644
--- a/modules/field/field.api.php
+++ b/modules/field/field.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.api.php,v 1.66 2010/03/04 18:28:29 webchick Exp $
+// $Id: field.api.php,v 1.75 2010/04/24 07:19:09 dries Exp $
 
 /**
  * @ingroup field_fieldable_type
@@ -19,9 +19,10 @@
  * field_attach_extra_weight() to retrieve the user-defined weight when
  * inserting the component.
  *
- * @return @todo
- *   An array of 'pseudo-field' components. The keys are the name of the element
- *   as it appears in the form structure. The values are arrays with the
+ * @return
+ *   A nested array of 'pseudo-field' components. Each list is nested within the
+ *   field bundle to which those components apply. The keys are the name of the
+ *   element as it appears in the form structure. The values are arrays with the
  *   following key/value pairs:
  *   - label: The human readable name of the component.
  *   - description: A short description of the component contents.
@@ -34,32 +35,28 @@
 function hook_field_extra_fields() {
   $extra = array();
 
-  if ($type = node_type_get_type($bundle)) {
+  foreach (node_type_get_types() as $bundle) {
     if ($type->has_title) {
-      $extra['title'] = array(
+      $extra['node'][$bundle]['title'] = array(
         'label' => $type->title_label,
         'description' => t('Node module element.'),
         'weight' => -5,
       );
     }
-    if ($bundle == 'poll' && module_exists('poll')) {
-      $extra['title'] = array(
-        'label' => t('Poll title'),
-        'description' => t('Poll module title.'),
-        'weight' => -5,
-      );
-      $extra['choice_wrapper'] = array(
-        'label' => t('Poll choices'),
-        'description' => t('Poll module choices.'),
-        'weight' => -4,
-      );
-      $extra['settings'] = array(
-        'label' => t('Poll settings'),
-        'description' => t('Poll module settings.'),
-        'weight' => -3,
-      );
-    }
   }
+  if (module_exists('poll')) {
+    $extra['node']['poll']['choice_wrapper'] = array(
+      'label' => t('Poll choices'),
+      'description' => t('Poll module choices.'),
+      'weight' => -4,
+    );
+    $extra['node']['poll']['settings'] = array(
+      'label' => t('Poll settings'),
+      'description' => t('Poll module settings.'),
+      'weight' => -3,
+    );
+  }
+
   return $extra;
 }
 
@@ -74,8 +71,10 @@ function hook_field_extra_fields() {
 function hook_field_extra_fields_alter(&$info) {
   // Force node title to always be at the top of the list
   // by default.
-  if (isset($info['title'])) {
-    $info['title']['weight'] = -20;
+  foreach (node_type_get_types() as $bundle) {
+    if (isset($info['node'][$bundle]['title'])) {
+      $info['node'][$bundle]['title']['weight'] = -20;
+    }
   }
 
 }
@@ -94,26 +93,26 @@ function hook_field_extra_fields_alter(&$info) {
  * can be attached to a fieldable entity. hook_field_info() defines the basic
  * properties of a field type, and a variety of other field hooks are called by
  * the Field Attach API to perform field-type-specific actions.
- * @see hook_field_info().
- * @see hook_field_info_alter().
- * @see hook_field_schema().
- * @see hook_field_load().
- * @see hook_field_validate().
- * @see hook_field_presave().
- * @see hook_field_insert().
- * @see hook_field_update().
- * @see hook_field_delete().
- * @see hook_field_delete_revision().
- * @see hook_field_prepare_view().
- * @see hook_field_is_empty().
+ * @see hook_field_info()
+ * @see hook_field_info_alter()
+ * @see hook_field_schema()
+ * @see hook_field_load()
+ * @see hook_field_validate()
+ * @see hook_field_presave()
+ * @see hook_field_insert()
+ * @see hook_field_update()
+ * @see hook_field_delete()
+ * @see hook_field_delete_revision()
+ * @see hook_field_prepare_view()
+ * @see hook_field_is_empty()
  *
  * The Field Types API also defines two kinds of pluggable handlers: widgets
  * and formatters, which specify how the field appears in edit forms and in
  * displayed entities. Widgets and formatters can be implemented by a field-type
  * module for it's own field types, or by a third-party module to extend the
  * behavior of existing field types.
- * @see hook_field_widget_info().
- * @see hook_field_formatter_info().
+ * @see hook_field_widget_info()
+ * @see hook_field_formatter_info()
  *
  * A third kind of pluggable handlers, storage backends, is defined by the
  * @link field_storage Field Storage API @endlink.
@@ -541,9 +540,9 @@ function hook_field_is_empty($item, $field) {
  * Widgets are Form API elements with additional processing capabilities.
  * Widget hooks are typically called by the Field Attach API during the
  * creation of the field form structure with field_attach_form().
- * @see hook_field_widget_info_alter().
- * @see hook_field_widget_form().
- * @see hook_field_widget_error().
+ * @see hook_field_widget_info_alter()
+ * @see hook_field_widget_form()
+ * @see hook_field_widget_error()
  *
  * @return
  *   An array describing the widget types implemented by the module.
@@ -659,7 +658,7 @@ function hook_field_widget_info_alter(&$info) {
  *   The order of this item in the array of subelements (0, 1, 2, etc).
  * @param $element
  *   A form element array containing basic properties for the widget:
- *   - #object_type: The name of the entity the field is attached to.
+ *   - #entity_type: The name of the entity the field is attached to.
  *   - #bundle: The name of the field bundle the field is contained in.
  *   - #field_name: The name of the field.
  *   - #language: The language the field is being edited in.
@@ -713,10 +712,10 @@ function hook_field_widget_error($element, $error, $form, &$form_state) {
  * called by the Field Attach API field_attach_prepare_view() and
  * field_attach_view() functions.
  *
- * @see hook_field_formatter_info().
- * @see hook_field_formatter_info_alter().
- * @see hook_field_formatter_view().
- * @see hook_field_formatter_prepare_view().
+ * @see hook_field_formatter_info()
+ * @see hook_field_formatter_info_alter()
+ * @see hook_field_formatter_view()
+ * @see hook_field_formatter_prepare_view()
  *
  * @return
  *   An array describing the formatter types implemented by the module.
@@ -1023,7 +1022,7 @@ function hook_field_attach_update($entity_type, $entity) {
  *   values.
  * @param $context
  *   An associative array containing:
- *   - obj_type: The type of $entity; e.g. 'node' or 'user'.
+ *   - entity_type: The type of $entity; e.g. 'node' or 'user'.
  *   - object: The entity with fields to render.
  *   - element: The structured array containing the values ready for rendering.
  */
@@ -1050,6 +1049,33 @@ function hook_field_attach_delete($entity_type, $entity) {
 function hook_field_attach_delete_revision($entity_type, $entity) {
 }
 
+/**
+ * Act on field_purge_data.
+ *
+ * This hook is invoked in field_purge_data() and allows modules to act on
+ * purging data from a single field pseudo-entity.  For example, if a module
+ * relates data in the field with its own data, it may purge its own data
+ * during this process as well.
+ *
+ * @param $entity_type
+ *   The type of $entity; e.g. 'node' or 'user'.
+ * @param $entity
+ *   The pseudo-entity whose field data is being purged.
+ * @param $field
+ *   The (possibly deleted) field whose data is being purged.
+ * @param $instance
+ *   The deleted field instance whose data is being purged.
+ *
+ * @see @link field_purge Field API bulk data deletion @endlink
+ * @see field_purge_data()
+ */
+function hook_field_attach_purge($entity_type, $entity, $field, $instance) {
+  // find the corresponding data in mymodule and purge it
+  if($entity_type == 'node' && $field->field_name == 'my_field_name') {
+    mymodule_remove_mydata($entity->nid);
+  }
+}
+
 /**
  * Act on field_attach_view.
  *
@@ -1059,14 +1085,46 @@ function hook_field_attach_delete_revision($entity_type, $entity) {
  *   The structured content array tree for all of $entity's fields.
  * @param $context
  *   An associative array containing:
- *   - obj_type: The type of $entity; e.g. 'node' or 'user'.
+ *   - entity_type: The type of $entity; e.g. 'node' or 'user'.
  *   - object: The entity with fields to render.
  *   - view_mode: View mode, e.g. 'full', 'teaser'...
- *   - langcode: The language in which the field values will be displayed.
  */
 function hook_field_attach_view_alter(&$output, $context) {
 }
 
+/**
+ * Act on field_language().
+ *
+ * This hook is invoked to alter the array of display languages for the given
+ * entity.
+ *
+ * @param $display_language
+ *   A reference to an array of language codes keyed by field name.
+ * @param $context
+ *   An associative array containing:
+ *   - entity_type: The type of the entity to be displayed.
+ *   - entity: The entity with fields to render.
+ *   - langcode: The language code $entity has to be displayed in.
+ */
+function hook_field_language_alter(&$display_language, $context) {
+}
+
+/**
+ * Act on field_available_languages().
+ *
+ * This hook is invoked to alter the array of available languages for the given
+ * field.
+ *
+ * @param &$languages
+ *   A reference to an array of language codes to be made available.
+ * @param $context
+ *   An associative array containing:
+ *   - entity_type: The type of the entity the field is attached to.
+ *   - field: A field data structure.
+ */
+function hook_field_available_languages_alter(&$languages, $context) {
+}
+
 /**
  * Act on field_attach_create_bundle.
  *
@@ -1192,23 +1250,28 @@ function hook_field_storage_details_alter(&$details, $field) {
 /**
  * Load field data for a set of entities.
  *
+ * Modules implementing this hook should load field values and add them to
+ * objects in $entities. Fields with no values should be added as empty
+ * arrays.
+ *
  * @param $entity_type
- *   The entity type of entity, such as 'node' or 'user'.
+ *   The type of entity, such as 'node' or 'user'.
  * @param $entities
- *   The array of entities for which to load data, keyed by entity id.
+ *   The array of entity objects to add fields to, keyed by entity ID.
  * @param $age
- *   FIELD_LOAD_CURRENT to load the most recent revision for all
- *   fields, or FIELD_LOAD_REVISION to load the version indicated by
- *   each entity.
+ *   FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
+ *   FIELD_LOAD_REVISION to load the version indicated by each entity.
  * @param $fields
  *   An array listing the fields to be loaded. The keys of the array are field
- *   ids, the values of the array are the entity ids (or revision ids,
- *   depending on the $age parameter) to be loaded for each field.
- * @return
- *   Loaded field values are added to $entities. Fields with no values should be
- *   set as an empty array.
+ *   IDs, and the values of the array are the entity IDs (or revision IDs,
+ *   depending on the $age parameter) to add each field to.
+ * @param $options
+ *   An associative array of additional options, with the following keys:
+ *   - 'deleted': If TRUE, deleted fields should be loaded as well as
+ *     non-deleted fields. If unset or FALSE, only non-deleted fields should be
+ *     loaded.
  */
-function hook_field_storage_load($entity_type, $entities, $age, $fields) {
+function hook_field_storage_load($entity_type, $entities, $age, $fields, $options) {
 }
 
 /**
@@ -1314,25 +1377,32 @@ function hook_field_storage_delete_instance($instance) {
  *
  * This lets 3rd party modules override, mirror, shard, or otherwise store a
  * subset of fields in a different way than the current storage engine.
- * Possible use cases include: per-bundle storage, per-combo-field storage...
+ * Possible use cases include per-bundle storage, per-combo-field storage, etc.
+ *
+ * Modules implementing this hook should load field values and add them to
+ * objects in $entities. Fields with no values should be added as empty
+ * arrays. In addition, fields loaded should be added as keys to $skip_fields.
  *
  * @param $entity_type
- *   The type of entity for which to load fields; e.g. 'node' or 'user'.
+ *   The type of entity, such as 'node' or 'user'.
  * @param $entities
- *   An array of entities for which to load fields, keyed by entity id.
+ *   The array of entity objects to add fields to, keyed by entity ID.
  * @param $age
  *   FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
  *   FIELD_LOAD_REVISION to load the version indicated by each entity.
  * @param $skip_fields
- *   An array keyed by field ids whose data has already been loaded and
- *   therefore should not be loaded again. The values associated to these keys
- *   are not specified.
- * @return
- *   - Loaded field values are added to $entities. Fields with no values should
- *     be set as an empty array.
- *   - Loaded field ids are set as keys in $skip_fields.
- */
-function hook_field_storage_pre_load($entity_type, $entities, $age, &$skip_fields) {
+ *   An array keyed by field IDs whose data has already been loaded and
+ *   therefore should not be loaded again. Add a key to this array to indicate
+ *   that your module has already loaded a field.
+ * @param $options
+ *   An associative array of additional options, with the following keys:
+ *   - 'field_id': The field ID that should be loaded. If unset, all fields
+ *     should be loaded.
+ *   - 'deleted': If TRUE, deleted fields should be loaded as well as
+ *     non-deleted fields. If unset or FALSE, only non-deleted fields should be
+ *     loaded.
+ */
+function hook_field_storage_pre_load($entity_type, $entities, $age, &$skip_fields, $options) {
 }
 
 /**
@@ -1557,12 +1627,12 @@ function hook_field_delete_field($field) {
  * This hook is invoked after the instance record is saved and so it cannot
  * modify the instance itself.
  *
- * TODO: Not implemented.
- *
  * @param $instance
- *   The instance just updated.
+ *   The instance as it is post-update.
+ * @param $prior_$instance
+ *   The instance as it was pre-update.
  */
-function hook_field_update_instance($instance) {
+function hook_field_update_instance($instance, $prior_instance) {
 }
 
 /**
@@ -1594,6 +1664,105 @@ function hook_field_read_field($field) {
 function hook_field_read_instance($instance) {
 }
 
+/**
+ * Acts when a field record is being purged.
+ *
+ * In field_purge_field(), after the field configuration has been
+ * removed from the database, the field storage module has had a chance to
+ * run its hook_field_storage_purge_field(), and the field info cache
+ * has been cleared, this hook is invoked on all modules to allow them to
+ * respond to the field being purged.
+ *
+ * @param $field
+ *   The field being purged.
+ */
+function hook_field_purge_field($field) {
+  db_delete('my_module_field_info')
+    ->condition('id', $field['id'])
+    ->execute();
+}
+
+/**
+ * Acts when a field instance is being purged.
+ *
+ * In field_purge_instance(), after the field instance has been
+ * removed from the database, the field storage module has had a chance to
+ * run its hook_field_storage_purge_instance(), and the field info cache
+ * has been cleared, this hook is invoked on all modules to allow them to
+ * respond to the field instance being purged.
+ *
+ * @param $instance
+ *   The instance being purged.
+ */
+function hook_field_purge_field_instance($instance) {
+  db_delete('my_module_field_instance_info')
+    ->condition('id', $instance['id'])
+    ->execute();
+}
+
+/**
+ * Remove field storage information when a field record is purged.
+ *
+ * Called from field_purge_field() to allow the field storage module
+ * to remove field information when a field is being purged.
+ *
+ * @param $field
+ *   The field being purged.
+ */
+function hook_field_storage_purge_field($field) {
+  $table_name = _field_sql_storage_tablename($field);
+  $revision_name = _field_sql_storage_revision_tablename($field);
+  db_drop_table($table_name);
+  db_drop_table($revision_name);
+}
+
+/**
+ * Remove field storage information when a field instance is purged.
+ *
+ * Called from field_purge_instance() to allow the field storage module
+ * to remove field instance information when a field instance is being
+ * purged.
+ *
+ * @param $instance
+ *   The instance being purged.
+ */
+function hook_field_storage_purge_field_instance($instance) {
+  db_delete('my_module_field_instance_info')
+    ->condition('id', $instance['id'])
+    ->execute();
+}
+
+/**
+ * Remove field storage information when field data is purged.
+ *
+ * Called from field_purge_data() to allow the field storage
+ * module to delete field data information.
+ *
+ * @param $entity_type
+ *   The type of $entity; e.g. 'node' or 'user'.
+ * @param $entity
+ *   The pseudo-entity whose field data to delete.
+ * @param $field
+ *   The (possibly deleted) field whose data is being purged.
+ * @param $instance
+ *   The deleted field instance whose data is being purged.
+ */
+function hook_field_storage_purge($entity_type, $entity, $field, $instance) {
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+  $etid = _field_sql_storage_etid($entity_type);
+
+  $table_name = _field_sql_storage_tablename($field);
+  $revision_name = _field_sql_storage_revision_tablename($field);
+  db_delete($table_name)
+    ->condition('etid', $etid)
+    ->condition('entity_id', $id)
+    ->execute();
+  db_delete($revision_name)
+    ->condition('etid', $etid)
+    ->condition('entity_id', $id)
+    ->execute();
+}
+
 /**
  * @} End of "ingroup field_crud"
  */
diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc
index 28f40b625cfe63ead4d5e97d84e2e38fe7c11fcb..ab4bf81d44063022015dfd169a6bd30219ddce3e 100644
--- a/modules/field/field.attach.inc
+++ b/modules/field/field.attach.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.attach.inc,v 1.78 2010/02/13 21:05:31 dries Exp $
+// $Id: field.attach.inc,v 1.86 2010/04/04 12:48:18 dries Exp $
 
 /**
  * @file
@@ -80,46 +80,53 @@ define('FIELD_STORAGE_INSERT', 'insert');
  * @{
  * Operate on Field API data attached to Drupal entities.
  *
- * Field Attach API functions load, store, generate Form API
- * structures, display, and perform a variety of other functions for
- * field data connected to individual entities.
- *
- * Field Attach API functions generally take $entity_type and $entity
- * arguments along with additional function-specific arguments.
- * $entity_type is the type of the fieldable entity, such as 'node' or
- * 'user', and $entity is the entity itself. An individual entity's
- * bundle, if any, is read from the entity's bundle key property
- * identified by hook_fieldable_info() for $entity_type.
- *
- * Fieldable types call Field Attach API functions during their own
- * API calls; for example, node_load() calls field_attach_load(). A
- * fieldable type is not required to use all of the Field Attach
- * API functions.
- *
- * Most Field Attach API functions define a corresponding hook
- * function that allows any module to act on Field Attach operations
- * for any entity after the operation is complete, and access or
- * modify all the field, form, or display data for that entity and
- * operation. For example, field_attach_view() invokes
+ * Field Attach API functions load, store, display, generate Form API
+ * structures, and perform a variety of other functions for field data attached
+ * to individual entities.
+ *
+ * Field Attach API functions generally take $entity_type and $entity arguments
+ * along with additional function-specific arguments. $entity_type is the type
+ * of the fieldable entity, such as 'node' or 'user', and $entity is the entity
+ * itself.
+ *
+ * hook_entity_info() is the central place for entity types to define if and
+ * how Field API should operate on their entity objects. Notably, the
+ * 'fieldable' property needs to be set to TRUE.
+ *
+ * The Field Attach API uses the concept of bundles: the set of fields for a
+ * given entity is defined on a per-bundle basis. The collection of bundles for
+ * an entity type is defined its hook_entity_info() implementation. For
+ * instance, node_entity_info() exposes each node type as its own bundle. This
+ * means that the set of fields of a node is determined by the node type. The
+ * Field API reads the bundle name for a given entity from a particular
+ * property of the entity object, and hook_entity_info() defines which property
+ * to use. For instance, node_entity_info() specifies:
+ * @code $info['entity keys']['bundle'] = 'type'@endcode
+ * This indicates that for a particular node object, the bundle name can be
+ * found in $node->type. This property can be omitted if the entity type only
+ * exposes a single bundle (all entities of this type have the same collection
+ * of fields). This is the case for the 'user' entity type.
+ *
+ * Most Field Attach API functions define a corresponding hook function that
+ * allows any module to act on Field Attach operations for any entity after the
+ * operation is complete, and access or modify all the field, form, or display
+ * data for that entity and operation. For example, field_attach_view() invokes
  * hook_field_attach_view_alter(). These all-module hooks are distinct from
- * those of the Field Types API, such as hook_field_load(), that are
- * only invoked for the module that defines a specific field type.
- *
- * field_attach_load(), field_attach_insert(), and
- * field_attach_update() also define pre-operation hooks,
- * e.g. hook_field_attach_pre_load(). These hooks run before the
- * corresponding Field Storage API and Field Type API operations.
- * They allow modules to define additional storage locations
- * (e.g. denormalizing, mirroring) for field data on a per-field
- * basis. They also allow modules to take over field storage
- * completely by instructing other implementations of the same hook
- * and the Field Storage API itself not to operate on specified
- * fields.
- *
- * The pre-operation hooks do not make the Field Storage API
- * irrelevant. The Field Storage API is essentially the "fallback
- * mechanism" for any fields that aren't being intercepted explicitly
- * by pre-operation hooks.
+ * those of the Field Types API, such as hook_field_load(), that are only
+ * invoked for the module that defines a specific field type.
+ *
+ * field_attach_load(), field_attach_insert(), and field_attach_update() also
+ * define pre-operation hooks, e.g. hook_field_attach_pre_load(). These hooks
+ * run before the corresponding Field Storage API and Field Type API
+ * operations. They allow modules to define additional storage locations (e.g.
+ * denormalizing, mirroring) for field data on a per-field basis. They also
+ * allow modules to take over field storage completely by instructing other
+ * implementations of the same hook and the Field Storage API itself not to
+ * operate on specified fields.
+ *
+ * The pre-operation hooks do not make the Field Storage API irrelevant. The
+ * Field Storage API is essentially the "fallback mechanism" for any fields
+ * that aren't being intercepted explicitly by pre-operation hooks.
  */
 
 /**
@@ -168,6 +175,9 @@ define('FIELD_STORAGE_INSERT', 'insert');
  *  - 'deleted': If TRUE, the function will operate on deleted fields
  *    as well as non-deleted fields. If unset or FALSE, only
  *    non-deleted fields are operated on.
+ *  - 'language': A language code or an array of language codes keyed by field
+ *    name. It will be used to narrow down to a single value the available
+ *    languages to act on.
  */
 function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
   // Merge default options.
@@ -178,54 +188,40 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
   );
   $options += $default_options;
 
-  // Iterate through the entity's field instances.
-  $return = array();
+  // Determine the list of instances to iterate on.
   list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+  $instances = _field_invoke_get_instances($entity_type, $bundle, $options);
 
-  if ($options['deleted']) {
-    $instances = field_read_instances(array('object_type' => $entity_type, 'bundle' => $bundle), array('include_deleted' => $options['deleted']));
-  }
-  else {
-    $instances = field_info_instances($entity_type, $bundle);
-  }
-
+  // Iterate through the instances and collect results.
+  $return = array();
   foreach ($instances as $instance) {
     $field_name = $instance['field_name'];
-
-    // When in 'single field' mode, only act on the specified field.
-    if ((!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) && (!isset($options['field_name']) || $options['field_name'] == $field_name)) {
-      $field = field_info_field($field_name);
-      $field_translations = array();
-      $suggested_languages = empty($options['language']) ? NULL : array($options['language']);
-
-      // Initialize field translations according to the available languages.
-      foreach (field_multilingual_available_languages($entity_type, $field, $suggested_languages) as $langcode) {
-        $field_translations[$langcode] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
-      }
-
-      // Invoke the field hook and collect results.
-      $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
-      if (function_exists($function)) {
-        // Iterate over all the field translations.
-        foreach ($field_translations as $langcode => $items) {
-          $result = $function($entity_type, $entity, $field, $instance, $langcode, $items, $a, $b);
-          if (isset($result)) {
-            // For hooks with array results, we merge results together.
-            // For hooks with scalar results, we collect results in an array.
-            if (is_array($result)) {
-              $return = array_merge($return, $result);
-            }
-            else {
-              $return[] = $result;
-            }
+    $field = field_info_field($field_name);
+    $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
+    if (function_exists($function)) {
+      // Determine the list of languages to iterate on.
+      $available_languages = field_available_languages($entity_type, $field);
+      $languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
+
+      foreach ($languages as $langcode) {
+        $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
+        $result = $function($entity_type, $entity, $field, $instance, $langcode, $items, $a, $b);
+        if (isset($result)) {
+          // For hooks with array results, we merge results together.
+          // For hooks with scalar results, we collect results in an array.
+          if (is_array($result)) {
+            $return = array_merge($return, $result);
           }
-
-          // Populate $items back in the field values, but avoid replacing missing
-          // fields with an empty array (those are not equivalent on update).
-          if ($items !== array() || isset($entity->{$field_name}[$langcode])) {
-            $entity->{$field_name}[$langcode] = $items;
+          else {
+            $return[] = $result;
           }
         }
+
+        // Populate $items back in the field values, but avoid replacing missing
+        // fields with an empty array (those are not equivalent on update).
+        if ($items !== array() || isset($entity->{$field_name}[$langcode])) {
+          $entity->{$field_name}[$langcode] = $items;
+        }
       }
     }
   }
@@ -271,6 +267,10 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
  *  - 'deleted': If TRUE, the function will operate on deleted fields
  *    as well as non-deleted fields. If unset or FALSE, only
  *    non-deleted fields are operated on.
+ *  - 'language': A language code or an array of language codes keyed by field
+ *    name. It will be used to narrow down to a single value the available
+ *    languages to act on.
+ *
  * @return
  *   An array of returned values keyed by entity id.
  */
@@ -298,31 +298,30 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b =
   // is deleted, so we reference field data via the
   // $entity->$field_name property.
   foreach ($entities as $entity) {
+    // Determine the list of instances to iterate on.
     list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
-
-    if ($options['deleted']) {
-      $instances = field_read_field(array('bundle' => $bundle, array('include_deleted' => $options['deleted'])));
-    }
-    else {
-      $instances = field_info_instances($entity_type, $bundle);
-    }
+    $instances = _field_invoke_get_instances($entity_type, $bundle, $options);
 
     foreach ($instances as $instance) {
       $field_id = $instance['field_id'];
       $field_name = $instance['field_name'];
-      // When in 'single field' mode, only act on the specified field.
-      if ((empty($options['field_id']) || $options['field_id'] == $field_id) && (empty($options['field_name']) || $options['field_name'] == $field_name)) {
+      $field = field_info_field_by_id($field_id);
+      $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
+      if (function_exists($function)) {
         // Add the field to the list of fields to invoke the hook on.
         if (!isset($fields[$field_id])) {
-          $fields[$field_id] = field_info_field_by_id($field_id);
+          $fields[$field_id] = $field;
         }
         // Group the corresponding instances and entities.
         $grouped_instances[$field_id][$id] = $instance;
         $grouped_entities[$field_id][$id] = $entities[$id];
         // Extract the field values into a separate variable, easily accessed
         // by hook implementations.
-        $suggested_languages = empty($options['language']) ? NULL : array($options['language']);
-        foreach (field_multilingual_available_languages($entity_type, $fields[$field_id], $suggested_languages) as $langcode) {
+        // Unless a language suggestion is provided we iterate on all the
+        // available languages.
+        $available_languages = field_available_languages($entity_type, $field);
+        $languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
+        foreach ($languages as $langcode) {
           $grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
         }
       }
@@ -335,21 +334,19 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b =
   foreach ($fields as $field_id => $field) {
     $field_name = $field['field_name'];
     $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
-    if (function_exists($function)) {
-      // Iterate over all the field translations.
-      foreach ($grouped_items[$field_id] as $langcode => $items) {
-        $results = $function($entity_type, $grouped_entities[$field_id], $field, $grouped_instances[$field_id], $langcode, $grouped_items[$field_id][$langcode], $a, $b);
-        if (isset($results)) {
-          // Collect results by entity.
-          // For hooks with array results, we merge results together.
-          // For hooks with scalar results, we collect results in an array.
-          foreach ($results as $id => $result) {
-            if (is_array($result)) {
-              $return[$id] = array_merge($return[$id], $result);
-            }
-            else {
-              $return[$id][] = $result;
-            }
+    // Iterate over all the field translations.
+    foreach ($grouped_items[$field_id] as $langcode => $items) {
+      $results = $function($entity_type, $grouped_entities[$field_id], $field, $grouped_instances[$field_id], $langcode, $grouped_items[$field_id][$langcode], $a, $b);
+      if (isset($results)) {
+        // Collect results by entity.
+        // For hooks with array results, we merge results together.
+        // For hooks with scalar results, we collect results in an array.
+        foreach ($results as $id => $result) {
+          if (is_array($result)) {
+            $return[$id] = array_merge($return[$id], $result);
+          }
+          else {
+            $return[$id][] = $result;
           }
         }
       }
@@ -376,7 +373,7 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b =
  * Use _field_invoke() to invoke the field type implementation,
  * hook_field_[op]().
  *
- * @see _field_invoke().
+ * @see _field_invoke()
  */
 function _field_invoke_default($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
   $options['default'] = TRUE;
@@ -390,13 +387,66 @@ function _field_invoke_default($op, $entity_type, $entity, &$a = NULL, &$b = NUL
  * Use _field_invoke_multiple() to invoke the field type implementation,
  * hook_field_[op]().
  *
- * @see _field_invoke_multiple().
+ * @see _field_invoke_multiple()
  */
 function _field_invoke_multiple_default($op, $entity_type, $entities, &$a = NULL, &$b = NULL, $options = array()) {
   $options['default'] = TRUE;
   return _field_invoke_multiple($op, $entity_type, $entities, $a, $b, $options);
 }
 
+/**
+ * Helper for _field_invoke(): retrieves a list of instances to operate on.
+ *
+ * @param $entity_type
+ *   The entity type.
+ * @param $bundle
+ *   The bundle name.
+ * @param $options
+ *   An associative array of options, as provided to _field_invoke(). Only the
+ *   following keys are considered :
+ *   - deleted
+ *   - field_name
+ *   - field_id
+ *   See _field_invoke() for details.
+ *
+ * @return
+ *   The array of selected instance definitions.
+ */
+function _field_invoke_get_instances($entity_type, $bundle, $options) {
+  if ($options['deleted']) {
+    // Deleted fields are not included in field_info_instances(), and need to
+    // be fetched from the database with field_read_instances().
+    $params = array('entity_type' => $entity_type, 'bundle' => $bundle);
+    if (isset($options['field_id'])) {
+      // Single-field mode by field id: field_read_instances() does the filtering.
+      // Single-field mode by field name is not compatible with the 'deleted'
+      // option.
+      $params['field_id'] = $options['field_id'];
+    }
+    $instances = field_read_instances($params, array('include_deleted' => TRUE));
+  }
+  elseif (isset($options['field_name'])) {
+    // Single-field mode by field name: field_info_instance() does the
+    // filtering.
+    $instances = array(field_info_instance($entity_type, $options['field_name'], $bundle));
+  }
+  else {
+    $instances = field_info_instances($entity_type, $bundle);
+    if (isset($options['field_id'])) {
+      // Single-field mode by field id: we need to loop on each instance to
+      // find the right one.
+      foreach ($instances as $instance) {
+        if ($instance['field_id'] == $options['field_id']) {
+          $instances = array($instance);
+          break;
+        }
+      }
+    }
+  }
+
+  return $instances;
+}
+
 /**
  * Add form elements for all fields for an entity to a form structure.
  *
@@ -473,6 +523,10 @@ function _field_invoke_multiple_default($op, $entity_type, $entities, &$a = NULL
  * ),
  * @endcode
  *
+ * field_attach_load() is automatically called by the default entity controller
+ * class, and thus, in most cases, doesn't need to be explicitly called by the
+ * entity type module.
+ *
  * @param $entity_type
  *   The type of $entity; e.g. 'node' or 'user'.
  * @param $entity
@@ -492,7 +546,7 @@ function _field_invoke_multiple_default($op, $entity_type, $entities, &$a = NULL
  */
 function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL) {
   // If no language is provided use the default site language.
-  $options = array('language' => field_multilingual_valid_language($langcode));
+  $options = array('language' => field_valid_language($langcode));
   $form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options);
 
   // Add custom weight handling.
@@ -510,15 +564,18 @@ function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcod
 }
 
 /**
- * Load all fields for the most current version of each of a set of
- * entities of a single entity type.
+ * Loads fields for the current revisions of a group of entities.
+ *
+ * Loads all fields for each entity object in a group of a single entity type.
+ * The loaded field values are added directly to the entity objects.
  *
  * @param $entity_type
- *   The type of $entity; e.g. 'node' or 'user'.
+ *   The type of $entity; e.g., 'node' or 'user'.
  * @param $entities
- *   An array of entities for which to load fields, keyed by entity id.
+ *   An array of entities for which to load fields, keyed by entity ID.
  *   Each entity needs to have its 'bundle', 'id' and (if applicable)
- *   'revision' keys filled.
+ *   'revision' keys filled in. The function adds the loaded field data
+ *   directly in the entity objects of the $entities array.
  * @param $age
  *   FIELD_LOAD_CURRENT to load the most recent revision for all
  *   fields, or FIELD_LOAD_REVISION to load the version indicated by
@@ -526,15 +583,13 @@ function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcod
  *   field_attach_load_revision() instead of passing FIELD_LOAD_REVISION.
  * @param $options
  *   An associative array of additional options, with the following keys:
- *  - 'field_id': The field id that should be loaded, instead of
- *    loading all fields, for each entity. Note that returned entities
- *    may contain data for other fields, for example if they are read
- *    from a cache.
- *  - 'deleted': If TRUE, the function will operate on deleted fields
- *    as well as non-deleted fields. If unset or FALSE, only
- *    non-deleted fields are operated on.
- * @return
- *   Loaded field values are added to $entities.
+ *   - 'field_id': The field ID that should be loaded, instead of
+ *     loading all fields, for each entity. Note that returned entities
+ *     may contain data for other fields, for example if they are read
+ *     from a cache.
+ *   - 'deleted': If TRUE, the function will operate on deleted fields
+ *     as well as non-deleted fields. If unset or FALSE, only
+ *     non-deleted fields are operated on.
  */
 function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $options = array()) {
   $load_current = $age == FIELD_LOAD_CURRENT;
@@ -546,9 +601,9 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $
   $options += $default_options;
 
   $info = entity_get_info($entity_type);
-  // Only the most current revision of non-deleted fields for
-  // cacheable fieldable types can be cached.
-  $cache_read = $load_current && $info['cacheable'] && empty($options['deleted']);
+  // Only the most current revision of non-deleted fields for cacheable entity
+  // types can be cached.
+  $cache_read = $load_current && $info['field cache'] && empty($options['deleted']);
   // In addition, do not write to the cache when loading a single field.
   $cache_write = $cache_read && !isset($options['field_id']);
 
@@ -597,30 +652,25 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $
       $function($entity_type, $queried_entities, $age, $skip_fields, $options);
     }
 
+    $instances = array();
+
     // Collect the storage backends used by the remaining fields in the entities.
     $storages = array();
     foreach ($queried_entities as $entity) {
       list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
-      if ($options['deleted']) {
-        $instances = field_read_instances(array('object_type' => $entity_type, 'bundle' => $bundle), array('include_deleted' => $options['deleted']));
-      }
-      else {
-        $instances = field_info_instances($entity_type, $bundle);
-      }
+      $instances = _field_invoke_get_instances($entity_type, $bundle, $options);
 
       foreach ($instances as $instance) {
-        if (!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) {
-          $field_name = $instance['field_name'];
-          $field_id = $instance['field_id'];
-          // Make sure all fields are present at least as empty arrays.
-          if (!isset($queried_entities[$id]->{$field_name})) {
-            $queried_entities[$id]->{$field_name} = array();
-          }
-          // Collect the storage backend if the field has not been loaded yet.
-          if (!isset($skip_fields[$field_id])) {
-            $field = field_info_field_by_id($field_id);
-            $storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid;
-          }
+        $field_name = $instance['field_name'];
+        $field_id = $instance['field_id'];
+        // Make sure all fields are present at least as empty arrays.
+        if (!isset($queried_entities[$id]->{$field_name})) {
+          $queried_entities[$id]->{$field_name} = array();
+        }
+        // Collect the storage backend if the field has not been loaded yet.
+        if (!isset($skip_fields[$field_id])) {
+          $field = field_info_field_by_id($field_id);
+          $storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid;
         }
       }
     }
@@ -655,27 +705,25 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $
 }
 
 /**
- * Load all fields for a previous version of each of a set of
- * entities of a single entity type.
+ * Load all fields for previous versions of a group of entities.
+ *
+ * Loading different versions of the same entities is not supported, and should
+ * be done by separate calls to the function.
  *
- * Loading different versions of the same entities is not supported,
- * and should be done by separate calls to the function.
+ * field_attach_load_revision() is automatically called by the default entity
+ * controller class, and thus, in most cases, doesn't need to be explicitly
+ * called by the entity type module.
  *
  * @param $entity_type
  *   The type of $entity; e.g. 'node' or 'user'.
  * @param $entities
- *   An array of entities for which to load fields, keyed by entity id.
- *   Each entity needs to have its 'bundle', 'id' and (if applicable)
- *   'revision' keys filled.
+ *   An array of entities for which to load fields, keyed by entity ID. Each
+ *   entity needs to have its 'bundle', 'id' and (if applicable) 'revision'
+ *   keys filled. The function adds the loaded field data directly in the
+ *   entity objects of the $entities array.
  * @param $options
- *   An associative array of additional options, with the following keys:
- *   - 'field_name':  The field name that should be loaded, instead of
- *     loading all fields, for each entity. Note that returned entities
- *     may contain data for other fields, for example if they are read
- *     from a cache.
- * @return
- *   On return, the entities in $entities are modified by having the
- *   appropriate set of fields added.
+ *   An associative array of additional options. See field_attach_load() for
+ *   details.
  */
 function field_attach_load_revision($entity_type, $entities, $options = array()) {
   return field_attach_load($entity_type, $entities, FIELD_LOAD_REVISION, $options);
@@ -834,7 +882,7 @@ function field_attach_insert($entity_type, $entity) {
   _field_invoke_default('insert', $entity_type, $entity);
   _field_invoke('insert', $entity_type, $entity);
 
-  list($id, $vid, $bundle, $cacheable) = entity_extract_ids($entity_type, $entity);
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
 
   // Let any module insert field data before the storage engine, accumulating
   // saved fields along the way.
@@ -867,9 +915,7 @@ function field_attach_insert($entity_type, $entity) {
   // Let other modules act on inserting the entity.
   module_invoke_all('field_attach_insert', $entity_type, $entity);
 
-  if ($cacheable) {
-    cache_clear_all("field:$entity_type:$id", 'cache_field');
-  }
+  $entity_info = entity_get_info($entity_type);
 }
 
 /**
@@ -883,7 +929,7 @@ function field_attach_insert($entity_type, $entity) {
 function field_attach_update($entity_type, $entity) {
   _field_invoke('update', $entity_type, $entity);
 
-  list($id, $vid, $bundle, $cacheable) = entity_extract_ids($entity_type, $entity);
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
 
   // Let any module update field data before the storage engine, accumulating
   // saved fields along the way.
@@ -920,7 +966,8 @@ function field_attach_update($entity_type, $entity) {
   // Let other modules act on updating the entity.
   module_invoke_all('field_attach_update', $entity_type, $entity);
 
-  if ($cacheable) {
+  $entity_info = entity_get_info($entity_type);
+  if ($entity_info['field cache']) {
     cache_clear_all("field:$entity_type:$id", 'cache_field');
   }
 }
@@ -937,7 +984,7 @@ function field_attach_update($entity_type, $entity) {
 function field_attach_delete($entity_type, $entity) {
   _field_invoke('delete', $entity_type, $entity);
 
-  list($id, $vid, $bundle, $cacheable) = entity_extract_ids($entity_type, $entity);
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
 
   // Collect the storage backends used by the fields in the entities.
   $storages = array();
@@ -956,7 +1003,8 @@ function field_attach_delete($entity_type, $entity) {
   // Let other modules act on deleting the entity.
   module_invoke_all('field_attach_delete', $entity_type, $entity);
 
-  if ($cacheable) {
+  $entity_info = entity_get_info($entity_type);
+  if ($entity_info['field cache']) {
     cache_clear_all("field:$entity_type:$id", 'cache_field');
   }
 }
@@ -973,7 +1021,7 @@ function field_attach_delete($entity_type, $entity) {
 function field_attach_delete_revision($entity_type, $entity) {
   _field_invoke('delete_revision', $entity_type, $entity);
 
-  list($id, $vid, $bundle, $cacheable) = entity_extract_ids($entity_type, $entity);
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
 
   // Collect the storage backends used by the fields in the entities.
   $storages = array();
@@ -1174,7 +1222,7 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode = 'full')
  *     '#title' => the label of the field instance,
  *     '#label_display' => the label display mode,
  *     '#object' => the fieldable entity being displayed,
- *     '#object_type' => the type of the entity being displayed,
+ *     '#entity_type' => the type of the entity being displayed,
  *     '#language' => the language of the field values being displayed,
  *     '#view_mode' => the view mode,
  *     '#field_name' => the name of the field,
@@ -1200,9 +1248,12 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode = 'full')
  *   A renderable array for the field values.
  */
 function field_attach_view($entity_type, $entity, $view_mode = 'full', $langcode = NULL) {
-  // Invoke field_default_view(). If no language is provided, use the current
-  // UI language.
-  $options = array('language' => field_multilingual_valid_language($langcode, FALSE));
+  // Determine the actual language to display for each field, given the
+  // languages available in the field data.
+  $display_language = field_language($entity_type, $entity, NULL, $langcode);
+  $options = array('language' => $display_language);
+
+  // Invoke field_default_view().
   $null = NULL;
   $output = _field_invoke_default('view', $entity_type, $entity, $view_mode, $null, $options);
 
@@ -1216,10 +1267,9 @@ function field_attach_view($entity_type, $entity, $view_mode = 'full', $langcode
 
   // Let other modules alter the renderable array.
   $context = array(
-    'obj_type' => $entity_type,
-    'object' => $entity,
+    'entity_type' => $entity_type,
+    'entity' => $entity,
     'view_mode' => $view_mode,
-    'langcode' => $langcode,
   );
   drupal_alter('field_attach_view', $output, $context);
 
@@ -1262,8 +1312,8 @@ function field_attach_preprocess($entity_type, $entity, $element, &$variables) {
 
   // Let other modules make changes to the $variables array.
   $context = array(
-    'obj_type' => $entity_type,
-    'object' => $entity,
+    'entity_type' => $entity_type,
+    'entity' => $entity,
     'element' => $element,
   );
   drupal_alter('field_attach_preprocess', $variables, $context);
@@ -1321,7 +1371,7 @@ function field_attach_create_bundle($entity_type, $bundle) {
 function field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
   db_update('field_config_instance')
     ->fields(array('bundle' => $bundle_new))
-    ->condition('object_type', $entity_type)
+    ->condition('entity_type', $entity_type)
     ->condition('bundle', $bundle_old)
     ->execute();
 
diff --git a/modules/field/field.crud.inc b/modules/field/field.crud.inc
index 0d339712f787c16e280d9820ff07b3ea3dc23ed1..071b58aa259c04bcd2c1236bda417b869f8d70b8 100644
--- a/modules/field/field.crud.inc
+++ b/modules/field/field.crud.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.crud.inc,v 1.53 2010/03/03 07:56:18 dries Exp $
+// $Id: field.crud.inc,v 1.57 2010/04/24 07:19:09 dries Exp $
 
 /**
  * @file
@@ -45,7 +45,7 @@
  * - type (string)
  *     The type of the field, such as 'text' or 'image'. Field types
  *     are defined by modules that implement hook_field_info().
- * - object_types (array)
+ * - entity_types (array)
  *     The array of entity types that can hold instances of this field. If
  *     empty or not specified, the field can have instances in any entity type.
  * - cardinality (integer)
@@ -108,7 +108,7 @@
  *     It is populated automatically by field_create_instance().
  * - field_name (string)
  *     The name of the field attached to the bundle by this instance.
- * - object_type (string)
+ * - entity_type (string)
  *     The name of the entity type the instance is attached to.
  * - bundle (string)
  *     The name of the bundle that the field is attached to.
@@ -262,13 +262,13 @@ function field_create_field($field) {
   // collisions with existing entity properties, but some is better
   // than none.
   foreach (entity_get_info() as $type => $info) {
-    if (in_array($field['field_name'], $info['object keys'])) {
+    if (in_array($field['field_name'], $info['entity keys'])) {
       throw new FieldException(t('Attempt to create field name %name which is reserved by entity type %type.', array('%name' => $field['field_name'], '%type' => $type)));
     }
   }
 
   $field += array(
-    'object_types' => array(),
+    'entity_types' => array(),
     'cardinality' => 1,
     'translatable' => FALSE,
     'locked' => FALSE,
@@ -403,8 +403,8 @@ function field_update_field($field) {
   if ($field['type'] != $prior_field['type']) {
     throw new FieldException("Cannot change an existing field's type.");
   }
-  if ($field['object_types'] != $prior_field['object_types']) {
-    throw new FieldException("Cannot change an existing field's object_types property.");
+  if ($field['entity_types'] != $prior_field['entity_types']) {
+    throw new FieldException("Cannot change an existing field's entity_types property.");
   }
   if ($field['storage']['type'] != $prior_field['storage']['type']) {
     throw new FieldException("Cannot change an existing field's storage type.");
@@ -588,7 +588,7 @@ function field_delete_field($field_name) {
  * Creates an instance of a field, binding it to a bundle.
  *
  * @param $instance
- *   A field instance definition array. The field_name, object_type and
+ *   A field instance definition array. The field_name, entity_type and
  *   bundle properties are required. Other properties, if omitted,
  *   will be given the following default values:
  *   - label: the field name
@@ -620,15 +620,15 @@ function field_create_instance($instance) {
     throw new FieldException(t("Attempt to create an instance of a field @field_name that doesn't exist or is currently inactive.", array('@field_name' => $instance['field_name'])));
   }
   // Check that the required properties exists.
-  if (empty($instance['object_type'])) {
+  if (empty($instance['entity_type'])) {
     throw new FieldException(t('Attempt to create an instance of field @field_name without an object type.', array('@field_name' => $instance['field_name'])));
   }
   if (empty($instance['bundle'])) {
     throw new FieldException(t('Attempt to create an instance of field @field_name without a bundle.', array('@field_name' => $instance['field_name'])));
   }
   // Check that the field can be attached to this entity type.
-  if (!empty($field['object_types']) && !in_array($instance['object_type'], $field['object_types'])) {
-    throw new FieldException(t('Attempt to create an instance of field @field_name on forbidden object type @obj_type.', array('@field_name' => $instance['field_name'], '@obj_type' => $instance['object_type'])));
+  if (!empty($field['entity_types']) && !in_array($instance['entity_type'], $field['entity_types'])) {
+    throw new FieldException(t('Attempt to create an instance of field @field_name on forbidden object type @entity_type.', array('@field_name' => $instance['field_name'], '@entity_type' => $instance['entity_type'])));
   }
 
   // Set the field id.
@@ -647,7 +647,7 @@ function field_create_instance($instance) {
   // Ensure the field instance is unique within the bundle.
   // We only check for instances of active fields, since adding an instance of
   // a disabled field is not supported.
-  $prior_instance = field_read_instance($instance['object_type'], $instance['field_name'], $instance['bundle']);
+  $prior_instance = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
   if (!empty($prior_instance)) {
     $message = t('Attempt to create an instance of field @field_name on bundle @bundle that already has an instance of that field.', array('@field_name' => $instance['field_name'], '@bundle' => $instance['bundle']));
     throw new FieldException($message);
@@ -688,7 +688,7 @@ function field_update_instance($instance) {
 
   // Check that the field instance exists (even if it is inactive, since we
   // want to be able to replace inactive widgets with new ones).
-  $prior_instance = field_read_instance($instance['object_type'], $instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE));
+  $prior_instance = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE));
   if (empty($prior_instance)) {
     throw new FieldException("Attempt to update a field instance that doesn't exist.");
   }
@@ -700,6 +700,8 @@ function field_update_instance($instance) {
 
   // Clear caches.
   field_cache_clear();
+
+  module_invoke_all('field_update_instance', $instance, $prior_instance);
 }
 
 /**
@@ -764,12 +766,12 @@ function _field_write_instance($instance, $update = FALSE) {
   // not have its own column and is not automatically populated when the
   // instance is read.
   $data = $instance;
-  unset($data['id'], $data['field_id'], $data['field_name'], $data['bundle'], $data['deleted']);
+  unset($data['id'], $data['field_id'], $data['field_name'], $data['entity_type'], $data['bundle'], $data['deleted']);
 
   $record = array(
     'field_id' => $instance['field_id'],
     'field_name' => $instance['field_name'],
-    'object_type' => $instance['object_type'],
+    'entity_type' => $instance['entity_type'],
     'bundle' => $instance['bundle'],
     'data' => $data,
     'deleted' => $instance['deleted'],
@@ -810,7 +812,7 @@ function _field_write_instance($instance, $update = FALSE) {
  *   An instance structure, or FALSE.
  */
 function field_read_instance($entity_type, $field_name, $bundle, $include_additional = array()) {
-  $instances = field_read_instances(array('object_type' => $entity_type, 'field_name' => $field_name, 'bundle' => $bundle), $include_additional);
+  $instances = field_read_instances(array('entity_type' => $entity_type, 'field_name' => $field_name, 'bundle' => $bundle), $include_additional);
   return $instances ? current($instances) : FALSE;
 }
 
@@ -858,13 +860,13 @@ function field_read_instances($params = array(), $include_additional = array())
   foreach ($results as $record) {
     // Filter out instances on unknown entity types (for instance because the
     // module exposing them was disabled).
-    $entity_info = entity_get_info($record['object_type']);
+    $entity_info = entity_get_info($record['entity_type']);
     if ($include_inactive || $entity_info) {
       $instance = unserialize($record['data']);
       $instance['id'] = $record['id'];
       $instance['field_id'] = $record['field_id'];
       $instance['field_name'] = $record['field_name'];
-      $instance['object_type'] = $record['object_type'];
+      $instance['entity_type'] = $record['entity_type'];
       $instance['bundle'] = $record['bundle'];
       $instance['deleted'] = $record['deleted'];
 
@@ -886,7 +888,7 @@ function field_delete_instance($instance) {
   db_update('field_config_instance')
     ->fields(array('deleted' => 1))
     ->condition('field_name', $instance['field_name'])
-    ->condition('object_type', $instance['object_type'])
+    ->condition('entity_type', $instance['entity_type'])
     ->condition('bundle', $instance['bundle'])
     ->execute();
 
@@ -1038,7 +1040,7 @@ function field_purge_batch($batch_size) {
  * @param $entity_type
  *   The type of $entity; e.g. 'node' or 'user'.
  * @param $entity
- *   The pseudo-entity whose field data to delete.
+ *   The pseudo-entity whose field data is being purged.
  * @param $field
  *   The (possibly deleted) field whose data is being purged.
  * @param $instance
diff --git a/modules/field/field.default.inc b/modules/field/field.default.inc
index 25beb421c33da131159d2018638e07a51201b197..3371ee833986d0204b98fa0b7b460ed8d4bf644c 100644
--- a/modules/field/field.default.inc
+++ b/modules/field/field.default.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.default.inc,v 1.33 2010/02/12 05:38:09 webchick Exp $
+// $Id: field.default.inc,v 1.34 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * @file
@@ -203,7 +203,7 @@ function field_default_view($entity_type, $entity, $field, $instance, $langcode,
           '#field_name' => $field['field_name'],
           '#field_type' => $field['type'],
           '#field_translatable' => $field['translatable'],
-          '#object_type' => $entity_type,
+          '#entity_type' => $entity_type,
           '#bundle' => $bundle,
           '#object' => $entity,
           '#items' => $items,
diff --git a/modules/field/field.form.inc b/modules/field/field.form.inc
index 63b9a84f4da3c32847956e113d822db806d309ce..cbc39e05d12d3f7bfd1a37d3927214c998e2e974 100644
--- a/modules/field/field.form.inc
+++ b/modules/field/field.form.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.form.inc,v 1.43 2010/03/13 06:55:50 dries Exp $
+// $Id: field.form.inc,v 1.47 2010/04/13 15:23:02 dries Exp $
 
 /**
  * @file
@@ -50,7 +50,7 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode,
       $function = $instance['widget']['module'] . '_field_widget_form';
       if (function_exists($function)) {
         $element = array(
-          '#object_type' => $instance['object_type'],
+          '#entity_type' => $instance['entity_type'],
           '#bundle' => $instance['bundle'],
           '#field_name' => $field_name,
           '#language' => $langcode,
@@ -131,11 +131,6 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode,
  * - drag-n-drop value reordering
  */
 function field_multiple_value_form($field, $instance, $langcode, $items, &$form, &$form_state) {
-  // This form has its own multistep persistance.
-  if ($form_state['rebuild']) {
-    $form_state['input'] = array();
-  }
-
   $field_name = $field['field_name'];
 
   // Determine the number of widgets to display.
@@ -158,7 +153,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form,
 
   $title = check_plain(t($instance['label']));
   $description = field_filter_xss(t($instance['description']));
-  $wrapper_id = drupal_html_class($field_name) . '-add-more-wrapper';
+  $wrapper_id = drupal_html_id($field_name . '-add-more-wrapper');
   $field_elements = array();
 
   $function = $instance['widget']['module'] . '_field_widget_form';
@@ -166,7 +161,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form,
     for ($delta = 0; $delta <= $max; $delta++) {
       $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
       $element = array(
-        '#object_type' => $instance['object_type'],
+        '#entity_type' => $instance['entity_type'],
         '#bundle' => $instance['bundle'],
         '#field_name' => $field_name,
         '#language' => $langcode,
@@ -236,17 +231,23 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form,
 }
 
 /**
- * Theme an individual form element.
+ * Returns HTML for an individual form element.
  *
  * Combine multiple values into a table with drag-n-drop reordering.
  * TODO : convert to a template.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the form element.
+ *
+ * @ingroup themeable
  */
 function theme_field_multiple_value_form($variables) {
   $element = $variables['element'];
   $output = '';
 
   if ($element['#cardinality'] > 1 || $element['#cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
-    $table_id = $element['#field_name'] . '_values';
+    $table_id = drupal_html_id($element['#field_name'] . '_values');
     $order_class = $element['#field_name'] . '-delta-order';
     $required = !empty($element['#required']) ? '<span class="form-required" title="' . t('This field is required. ') . '">*</span>' : '';
 
diff --git a/modules/field/field.info b/modules/field/field.info
index 0e6026f39ad46ce276b1281c746c47b7a4be8c4c..9f3e2453cef6ef15df76d813e3fb7953bfefad28 100644
--- a/modules/field/field.info
+++ b/modules/field/field.info
@@ -16,8 +16,8 @@ files[] = tests/field.test
 dependencies[] = field_sql_storage
 required = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/field.info.inc b/modules/field/field.info.inc
index f5daa19b54668f2a7842ba83ea5a49888f5855ff..5e870e3defc2fbea34f1f76d45620f0485a058d8 100644
--- a/modules/field/field.info.inc
+++ b/modules/field/field.info.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.info.inc,v 1.42 2010/02/27 22:11:11 webchick Exp $
+// $Id: field.info.inc,v 1.44 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * @file
@@ -220,13 +220,13 @@ function _field_info_collate_fields($reset = FALSE) {
     foreach ($definitions['instances'] as $instance) {
       $field = $info['fields'][$instance['field_name']];
       $instance = _field_info_prepare_instance($instance, $field);
-      $info['instances'][$instance['object_type']][$instance['bundle']][$instance['field_name']] = $instance;
+      $info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance;
       // Enrich field definitions with the list of bundles where they have
       // instances. NOTE: Deleted fields in $info['field_ids'] are not
       // enriched because all of their instances are deleted, too, and
       // are thus not in $definitions['instances'].
-      $info['fields'][$instance['field_name']]['bundles'][$instance['object_type']][] = $instance['bundle'];
-      $info['field_ids'][$instance['field_id']]['bundles'][$instance['object_type']][] = $instance['bundle'];
+      $info['fields'][$instance['field_name']]['bundles'][$instance['entity_type']][] = $instance['bundle'];
+      $info['field_ids'][$instance['field_id']]['bundles'][$instance['entity_type']][] = $instance['bundle'];
     }
   }
 
@@ -291,7 +291,7 @@ function _field_info_prepare_instance($instance, $field) {
   }
 
   // Fallback to 'full' display settings for unspecified view modes.
-  $entity_info = entity_get_info($instance['object_type']);
+  $entity_info = entity_get_info($instance['entity_type']);
   foreach ($entity_info['view modes'] as $view_mode => $info) {
     if (!isset($instance['display'][$view_mode])) {
       $instance['display'][$view_mode] = $instance['display']['full'];
@@ -535,7 +535,7 @@ function field_info_fields() {
  *   additional element 'bundles', whose value is an array of all the bundles
  *   this field belongs to.
  *
- * @see field_info_field_by_id().
+ * @see field_info_field_by_id()
  */
 function field_info_field($field_name) {
   $info = _field_info_collate_fields();
@@ -556,7 +556,7 @@ function field_info_field($field_name) {
  *   additional element 'bundles', whose value is an array of all the bundles
  *   this field belongs to.
  *
- * @see field_info_field().
+ * @see field_info_field()
  */
 function field_info_field_by_id($field_id) {
   $info = _field_info_collate_fields();
diff --git a/modules/field/field.install b/modules/field/field.install
index 37b01998ef41dcba609ec19ebc6b627991888389..3ed731ee4a53dd3bfae4fb72d2d3cd23898852ae 100644
--- a/modules/field/field.install
+++ b/modules/field/field.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.install,v 1.17 2009/12/04 16:49:46 dries Exp $
+// $Id: field.install,v 1.18 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * @file
@@ -130,7 +130,7 @@ function field_schema() {
         'not null' => TRUE,
         'default' => ''
       ),
-      'object_type'       => array(
+      'entity_type'       => array(
         'type' => 'varchar',
         'length' => 32,
         'not null' => TRUE,
@@ -158,7 +158,7 @@ function field_schema() {
     'primary key' => array('id'),
     'indexes' => array(
       // Used by field_delete_instance().
-      'field_name_bundle' => array('field_name', 'object_type', 'bundle'),
+      'field_name_bundle' => array('field_name', 'entity_type', 'bundle'),
       // Used by field_read_instances().
       'deleted' => array('deleted'),
     ),
diff --git a/modules/field/field.module b/modules/field/field.module
index 95d9663c56092214749682fbf12bbd6b06b7a50b..6186a626b46e3ad70cd8cf1226b4ba830c424ec3 100644
--- a/modules/field/field.module
+++ b/modules/field/field.module
@@ -1,10 +1,18 @@
 <?php
-// $Id: field.module,v 1.68 2010/03/12 19:51:40 dries Exp $
+// $Id: field.module,v 1.72 2010/04/13 15:23:02 dries Exp $
 /**
  * @file
  * Attach custom data fields to Drupal entities.
  */
 
+/**
+ * Base class for all exceptions thrown by Field API functions.
+ *
+ * This class has no functionality of its own other than allowing all
+ * Field API exceptions to be caught by a single catch block.
+ */
+class FieldException extends Exception {}
+
 /*
  * Load all public Field API functions. Drupal currently has no
  * mechanism for auto-loading core APIs, so we have to load them on
@@ -115,14 +123,6 @@ define('FIELD_QUERY_COMPLETE', 'FIELD_QUERY_COMPLETE');
  * @} End of "Field query flags".
  */
 
-/**
- * Base class for all exceptions thrown by Field API functions.
- *
- * This class has no functionality of its own other than allowing all
- * Field API exceptions to be caught by a single catch block.
- */
-class FieldException extends Exception {}
-
 /**
  * Exception class thrown by hook_field_update_forbid().
  */
@@ -496,10 +496,8 @@ function field_view_value($entity_type, $entity, $field_name, $item, $display =
   $output = array();
 
   if ($field = field_info_field($field_name)) {
-    $langcode = field_multilingual_valid_language($langcode, FALSE);
-
     // Determine the langcode that will be used by language fallback.
-    $langcode = current(field_multilingual_available_languages($entity_type, $field, array($langcode)));
+    $langcode = field_language($entity_type, $entity, $field_name, $langcode);
 
     // Push the item as the single value for the field, and defer to
     // field_view_field() to build the render array for the whole field.
@@ -588,7 +586,10 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
 
     // Hook invocations are done through the _field_invoke() functions in
     // 'single field' mode, to reuse the language fallback logic.
-    $options = array('field_name' => $field_name, 'language' => field_multilingual_valid_language($langcode, FALSE));
+    // Determine the actual language to display for the field, given the
+    // languages available in the field data.
+    $display_language = field_language($entity_type, $entity, $field_name, $langcode);
+    $options = array('field_name' => $field_name, 'language' => $display_language);
     $null = NULL;
 
     // Invoke prepare_view steps if needed.
@@ -604,13 +605,12 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
     // Build the renderable array.
     $result = _field_invoke_default('view', $entity_type, $entity, $display, $null, $options);
 
-    // Invoke hook_field_attach_view_alter() to tet other modules alter the
+    // Invoke hook_field_attach_view_alter() to let other modules alter the
     // renderable array, as in a full field_attach_view() execution.
     $context = array(
-      'obj_type' => $entity_type,
-      'object' => $entity,
+      'entity_type' => $entity_type,
+      'entity' => $entity,
       'view_mode' => '_custom',
-      'langcode' => $langcode,
     );
     drupal_alter('field_attach_view', $result, $context);
 
@@ -623,6 +623,27 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
   return $output;
 }
 
+/**
+ * Returns the field items in the language they currently would be displayed.
+ *
+ * @param $entity_type
+ *   The type of $entity.
+ * @param $entity
+ *   The entity containing the data to be displayed.
+ * @param $field_name
+ *   The field to be displayed.
+ * @param $langcode
+ *   (optional) The language code $entity->{$field_name} has to be displayed in.
+ *   Defaults to the current language.
+ *
+ * @return
+ *   An array of field items keyed by delta if available, FALSE otherwise.
+ */
+function field_get_items($entity_type, $entity, $field_name, $langcode = NULL) {
+  $langcode = field_language($entity_type, $entity, $field_name, $langcode);
+  return isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : FALSE;
+}
+
 /**
  * Determine whether a field has any data.
  *
@@ -769,7 +790,7 @@ function template_process_field(&$variables, $hook) {
  */
 
 /**
- * Returns a themed field.
+ * Returns HTML for a field.
  *
  * This is the default theme implementation to display the value of a field.
  * Theme developers who are comfortable with overriding theme functions may do
@@ -810,6 +831,18 @@ function template_process_field(&$variables, $hook) {
  * the exact performance impact depends on the server configuration and the
  * details of the website.
  *
+ * @param $variables
+ *   An associative array containing:
+ *   - label_hidden: A boolean indicating to show or hide the field label.
+ *   - title_attributes: A string containing the attributes for the title.
+ *   - label: The label for the field.
+ *   - content_attributes: A string containing the attaributes for the content's
+ *     div.
+ *   - items: An array of field items.
+ *   - item_attributes: An array of attributes for each item.
+ *   - classes: A string containing the classes for the wrapping div.
+ *   - attributes: A string containing the attributes for the wrapping div.
+ *
  * @see template_preprocess_field()
  * @see template_process_field()
  * @see field.tpl.php
diff --git a/modules/field/field.multilingual.inc b/modules/field/field.multilingual.inc
index f2f25bb9d10ddcd3b22973b6d17d1ab3ef290317..3d868010a31681210f1d96c275e0dc2e5fcd0da1 100644
--- a/modules/field/field.multilingual.inc
+++ b/modules/field/field.multilingual.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.multilingual.inc,v 1.9 2010/03/07 07:44:18 webchick Exp $
+// $Id: field.multilingual.inc,v 1.10 2010/03/25 11:46:20 dries Exp $
 
 /**
  * @file
@@ -14,74 +14,111 @@ function field_multilingual_settings_changed() {
 }
 
 /**
- * Collect the available languages for the given entity type and field.
+ * Collects the available languages for the given entity type and field.
  *
- * If an entity has a translation handler and the given field is translatable,
- * a (not necessarily strict) subset of the current enabled languages will be
- * returned, otherwise only LANGUAGE_NONE will be returned. Since the
- * default value for a 'translatable' entity property is FALSE, we ensure that
- * only entities that are able to handle translations actually get translatable
- * fields.
+ * If the given field has language support enabled, an array of available
+ * languages will be returned, otherwise only LANGUAGE_NONE will be returned.
+ * Since the default value for a 'translatable' entity property is FALSE, we
+ * ensure that only entities that are able to handle translations actually get
+ * translatable fields.
  *
  * @param $entity_type
  *   The type of the entity the field is attached to, e.g. 'node' or 'user'.
  * @param $field
  *   A field structure.
- * @param $suggested_languages
- *   An array of language preferences which will be intersected with the enabled
- *   languages.
+ *
  * @return
  *   An array of valid language codes.
  */
-function field_multilingual_available_languages($entity_type, $field, $suggested_languages = NULL) {
+function field_available_languages($entity_type, $field) {
   $field_languages = &drupal_static(__FUNCTION__, array());
   $field_name = $field['field_name'];
 
-  if (!isset($field_languages[$field_name]) || !empty($suggested_languages)) {
-    $translation_handlers = field_multilingual_check_translation_handlers($entity_type);
-
-    if ($translation_handlers && $field['translatable']) {
-      // The returned languages are a subset of the intersection of enabled ones
-      // and suggested ones.
-      $available_languages = field_multilingual_content_languages();
-      $languages = !empty($suggested_languages) ? $available_languages = array_intersect($available_languages, $suggested_languages) : $available_languages;
-
-      foreach (module_implements('field_languages') as $module) {
-        $function = $module . '_field_languages';
-        $function($entity_type, $field, $languages);
-      }
-      // Accept only available languages.
-      $result = array_values(array_intersect($available_languages, $languages));
-      // Do not cache suggested values as they might alter the general result.
-      if (empty($suggested_languages)) {
-        $field_languages[$field_name] = $result;
-      }
+  if (!isset($field_languages[$entity_type][$field_name])) {
+    // If the field has language support enabled we retrieve an (alterable) list
+    // of enabled languages, otherwise we return just LANGUAGE_NONE.
+    if (field_is_translatable($entity_type, $field)) {
+      $languages = field_content_languages();
+      // Let other modules alter the available languages.
+      $context = array('entity_type' => $entity_type, 'field' => $field);
+      drupal_alter('field_available_languages', $languages, $context);
+      $field_languages[$entity_type][$field_name] = $languages;
     }
     else {
-      $result = $field_languages[$field_name] = array(LANGUAGE_NONE);
+      $field_languages[$entity_type][$field_name] = array(LANGUAGE_NONE);
     }
   }
-  else {
-    $result = $field_languages[$field_name];
+
+  return $field_languages[$entity_type][$field_name];
+}
+
+/**
+ * Process the given language suggestion based on the available languages.
+ *
+ * If a non-empty language suggestion is provided it must appear among the
+ * available languages, otherwise it will be ignored.
+ *
+ * @param $available_languages
+ *   An array of valid language codes.
+ * @param $language_suggestion
+ *   A language code or an array of language codes keyed by field name.
+ * @param $field_name
+ *   The name of the field being processed.
+ *
+ * @return
+ *   An array of valid language codes.
+ */
+function _field_language_suggestion($available_languages, $language_suggestion, $field_name) {
+  // Handle possible language suggestions.
+  if (!empty($language_suggestion)) {
+    // We might have an array of language suggestions keyed by field name.
+    if (is_array($language_suggestion) && isset($language_suggestion[$field_name])) {
+      $language_suggestion = $language_suggestion[$field_name];
+    }
+
+    // If we have a language suggestion and the suggested language is available,
+    // we return only it.
+    if (in_array($language_suggestion, $available_languages)) {
+      $available_languages = array($language_suggestion);
+    }
   }
 
-  return $result;
+  return $available_languages;
 }
 
 /**
- * Return available content languages.
+ * Returns available content languages.
  *
  * The languages that may be associated to fields include LANGUAGE_NONE.
  *
  * @return
  *   An array of language codes.
  */
-function field_multilingual_content_languages() {
-  return array_keys(language_list() + array(LANGUAGE_NONE => NULL));
+function field_content_languages() {
+  $languages = language_list('enabled');
+  return array_keys($languages[1] + array(LANGUAGE_NONE => NULL));
 }
 
 /**
- * Check if a module is registered as a translation handler for a given entity.
+ * Checks whether a field has language support.
+ *
+ * A field has language support enabled if its 'translatable' property is set to
+ * TRUE, and its entity type has at least one translation handler registered.
+ *
+ * @param $entity_type
+ *   The type of the entity the field is attached to.
+ * @param $field
+ *   A field data structure.
+ *
+ * @return
+ *   TRUE if the field can be translated.
+ */
+function field_is_translatable($entity_type, $field) {
+  return $field['translatable'] && field_has_translation_handler($entity_type);
+}
+
+/**
+ * Checks if a module is registered as a translation handler for a given entity.
  *
  * If no handler is passed, simply check if there is any translation handler
  * enabled for the given entity type.
@@ -89,16 +126,18 @@ function field_multilingual_content_languages() {
  * @param $entity_type
  *   The type of the entity whose fields are to be translated.
  * @param $handler
- *   The name of the handler to be checked.
+ *   (optional) The name of the handler to be checked. Defaults to NULL.
  *
  * @return
- *   TRUE, if the handler is allowed to manage field translations.
+ *   TRUE, if the given handler is allowed to manage field translations. If no
+ *   handler is passed, TRUE means there is at least one registered translation
+ *   handler.
  */
-function field_multilingual_check_translation_handlers($entity_type, $handler = NULL) {
+function field_has_translation_handler($entity_type, $handler = NULL) {
   $entity_info = entity_get_info($entity_type);
 
   if (isset($handler)) {
-    return isset($entity_info['translation'][$handler]) && !empty($entity_info['translation'][$handler]);
+    return !empty($entity_info['translation'][$handler]);
   }
   elseif (isset($entity_info['translation'])) {
     foreach ($entity_info['translation'] as $handler_info) {
@@ -113,7 +152,7 @@ function field_multilingual_check_translation_handlers($entity_type, $handler =
 }
 
 /**
- * Helper function to ensure that a given language code is valid.
+ * Ensures that a given language code is valid.
  *
  * Checks whether the given language is one of the enabled languages. Otherwise,
  * it returns the current, global language; or the site's default language, if
@@ -127,16 +166,76 @@ function field_multilingual_check_translation_handlers($entity_type, $handler =
  * @return
  *   A valid language code.
  */
-function field_multilingual_valid_language($langcode, $default = TRUE) {
-  $enabled_languages = field_multilingual_content_languages();
+function field_valid_language($langcode, $default = TRUE) {
+  $enabled_languages = field_content_languages();
   if (in_array($langcode, $enabled_languages)) {
     return $langcode;
   }
   global $language_content;
-  $langcode = $default ? language_default('language') : $language_content->language;
-  if (in_array($langcode, $enabled_languages)) {
-    return $langcode;
+  return $default ? language_default('language') : $language_content->language;
+}
+
+/**
+ * Returns the display language for the fields attached to the given entity.
+ *
+ * The actual language for each given field is determined based on the requested
+ * language and the actual data available in the fields themselves.
+ * If there is no registered translation handler for the given entity type, the
+ * display language to be used is just LANGUAGE_NONE, as no other language code
+ * is allowed by field_available_languages().
+ * If translation handlers are found, we let modules provide alternative display
+ * languages for fields not having the requested language available.
+ * Core language fallback rules are provided by locale_field_language_fallback()
+ * which is called by locale_field_language_alter().
+ *
+ * @param $entity_type
+ *   The type of $entity.
+ * @param $entity
+ *   The entity to be displayed.
+ * @param $field_name
+ *   (optional) The name of the field to be displayed. Defaults to NULL. If
+ *   no value is specified, the display languages for every field attached to
+ *   the given entity will be returned.
+ * @param $langcode
+ *   (optional) The language code $entity has to be displayed in. Defaults to
+ *   NULL. If no value is given the current language will be used.
+ *
+ * @return
+ *   A language code if a field name is specified, an array of language codes
+ *   keyed by field name otherwise.
+ */
+function field_language($entity_type, $entity, $field_name = NULL, $langcode = NULL) {
+  $display_languages = &drupal_static(__FUNCTION__, array());
+  list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
+  $langcode = field_valid_language($langcode, FALSE);
+
+  if (!isset($display_languages[$entity_type][$id][$langcode])) {
+    $display_language = array();
+
+    // By default display language is set to LANGUAGE_NONE. It is up to
+    // translation handlers to implement language fallback rules.
+    foreach (field_info_instances($entity_type, $bundle) as $instance) {
+      $display_language[$instance['field_name']] = LANGUAGE_NONE;
+    }
+
+    if (field_has_translation_handler($entity_type)) {
+      $context = array(
+        'entity_type' => $entity_type,
+        'entity' => $entity,
+        'language' => $langcode,
+      );
+      drupal_alter('field_language', $display_language, $context);
+    }
+
+    $display_languages[$entity_type][$id][$langcode] = $display_language;
+  }
+
+  $display_language = $display_languages[$entity_type][$id][$langcode];
+
+  // Single-field mode.
+  if (isset($field_name)) {
+    return isset($display_language[$field_name]) ? $display_language[$field_name] : FALSE;
   }
-  // @todo Throw a more specific exception.
-  throw new FieldException('No valid content language could be found.');
+
+  return $display_language;
 }
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.info b/modules/field/modules/field_sql_storage/field_sql_storage.info
index e21718f9ee479b53221443139c5742dd7d974890..60f6a00c2903ad4637a067b48a6cf6b3043095d6 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.info
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.info
@@ -9,8 +9,8 @@ files[] = field_sql_storage.install
 files[] = field_sql_storage.test
 required = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module
index 6e077b204cc114b4f9b8456e8fc49f010a6cbe2a..4ce68ad922b4eba47f92142eef07d50d9ca69a27 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.module
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_sql_storage.module,v 1.42 2010/02/17 05:24:53 webchick Exp $
+// $Id: field_sql_storage.module,v 1.44 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * @file
@@ -311,7 +311,7 @@ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fi
       ->fields('t')
       ->condition('etid', $etid)
       ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
-      ->condition('language', field_multilingual_available_languages($entity_type, $field), 'IN')
+      ->condition('language', field_available_languages($entity_type, $field), 'IN')
       ->orderBy('delta');
 
     if (empty($options['deleted'])) {
@@ -356,7 +356,7 @@ function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fiel
     $table_name = _field_sql_storage_tablename($field);
     $revision_name = _field_sql_storage_revision_tablename($field);
 
-    $all_languages = field_multilingual_available_languages($entity_type, $field);
+    $all_languages = field_available_languages($entity_type, $field);
     $field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
 
     // Delete and insert, rather than update, in case a value was added.
@@ -577,7 +577,7 @@ function field_sql_storage_field_storage_query($field_id, $conditions, $options)
       // If querying all revisions and the entity type has revisions, we need
       // to key the results by revision_ids.
       $entity_type = entity_get_info($row->type);
-      $id = ($load_current || empty($entity_type['object keys']['revision'])) ? $row->entity_id : $row->revision_id;
+      $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id;
 
       if (!isset($return[$row->type][$id])) {
         $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
@@ -623,7 +623,7 @@ function field_sql_storage_field_storage_delete_revision($entity_type, $entity,
  * This function simply marks for deletion all data associated with the field.
  */
 function field_sql_storage_field_storage_delete_instance($instance) {
-  $etid = _field_sql_storage_etid($instance['object_type']);
+  $etid = _field_sql_storage_etid($instance['entity_type']);
   $field = field_info_field($instance['field_name']);
   $table_name = _field_sql_storage_tablename($field);
   $revision_name = _field_sql_storage_revision_tablename($field);
@@ -645,7 +645,7 @@ function field_sql_storage_field_storage_delete_instance($instance) {
 function field_sql_storage_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
   $etid = _field_sql_storage_etid($entity_type);
   // We need to account for deleted or inactive fields and instances.
-  $instances = field_read_instances(array('object_type' => $entity_type, 'bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
+  $instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
   foreach ($instances as $instance) {
     $field = field_info_field_by_id($instance['field_id']);
     if ($field['storage']['type'] == 'field_sql_storage') {
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.test b/modules/field/modules/field_sql_storage/field_sql_storage.test
index a09e3f5e18d3f1559fd4e4c5c648cb4c63415ba3..552b95891835c0879b17e426170bd1e782f56a79 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.test
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_sql_storage.test,v 1.18 2010/02/27 07:52:03 dries Exp $
+// $Id: field_sql_storage.test,v 1.19 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * @file
@@ -28,7 +28,7 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
     $this->field = field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle'
     );
     $this->instance = field_create_instance($this->instance);
@@ -303,7 +303,7 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
     // Create a decimal 5.2 field and add some data.
     $field = array('field_name' => 'decimal52', 'type' => 'number_decimal', 'settings' => array('precision' => 5, 'scale' => 2));
     $field = field_create_field($field);
-    $instance = array('field_name' => 'decimal52', 'object_type' => 'test_entity', 'bundle' => 'test_bundle');
+    $instance = array('field_name' => 'decimal52', 'entity_type' => 'test_entity', 'bundle' => 'test_bundle');
     $instance = field_create_instance($instance);
     $entity = field_test_create_stub_entity(0, 0, $instance['bundle']);
     $entity->decimal52[LANGUAGE_NONE][0]['value'] = '1.235';
@@ -329,7 +329,7 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
     $field_name = 'testfield';
     $field = array('field_name' => $field_name, 'type' => 'text');
     $field = field_create_field($field);
-    $instance = array('field_name' => $field_name, 'object_type' => 'test_entity', 'bundle' => 'test_bundle');
+    $instance = array('field_name' => $field_name, 'entity_type' => 'test_entity', 'bundle' => 'test_bundle');
     $instance = field_create_instance($instance);
     $tables = array(_field_sql_storage_tablename($field), _field_sql_storage_revision_tablename($field));
 
@@ -374,7 +374,7 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
 
     // Retrieve the field and instance with field_info so the storage details are attached.
     $field = field_info_field($this->field['field_name']);
-    $instance = field_info_instance($this->instance['object_type'], $this->instance['field_name'], $this->instance['bundle']);
+    $instance = field_info_instance($this->instance['entity_type'], $this->instance['field_name'], $this->instance['bundle']);
 
     // The storage details are indexed by a storage engine type.
     $this->assertTrue(array_key_exists('sql', $field['storage']['details']), t('The storage type is SQL.'));
diff --git a/modules/field/modules/list/list.info b/modules/field/modules/list/list.info
index de0850c4abd7b23279b868a36fe7c61069ef4fcd..9f0b8adab24ee464b1934ebb38fc8c442ff14e27 100644
--- a/modules/field/modules/list/list.info
+++ b/modules/field/modules/list/list.info
@@ -8,8 +8,8 @@ files[]=list.module
 files[]=tests/list.test
 required = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/modules/list/list.module b/modules/field/modules/list/list.module
index f675286fcaacda6770741cd543467a3c195c1959..2a17b44f4ec7c1494a295126e904143120c8dca8 100644
--- a/modules/field/modules/list/list.module
+++ b/modules/field/modules/list/list.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: list.module,v 1.27 2010/02/15 15:46:23 dries Exp $
+// $Id: list.module,v 1.30 2010/03/27 12:49:32 dries Exp $
 
 /**
  * @file
@@ -120,6 +120,33 @@ function list_field_settings_form($field, $instance, $has_data) {
     '#access' => empty($settings['allowed_values_function']),
   );
 
+  if ($field['type'] == 'list_boolean') {
+    $values = list_extract_allowed_values($settings['allowed_values']);
+    $off_value = array_shift($values);
+    $on_value = array_shift($values);
+    $form['allowed_values'] = array(
+      '#type' => 'markup',
+      '#description' => '',
+      '#input' => TRUE,
+      '#value_callback' => 'list_boolean_allowed_values_callback',
+      '#access' => empty($settings['allowed_values_function']),
+    );
+    $form['allowed_values']['on'] = array(
+      '#type' => 'textfield',
+      '#title' => t('On value'),
+      '#default_value' => $on_value,
+      '#required' => FALSE,
+      '#description' => t('If left empty, "1" will be used.'),
+    );
+    $form['allowed_values']['off'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Off value'),
+      '#default_value' => $off_value,
+      '#required' => FALSE,
+      '#description' => t('If left empty, "0" will be used.'),
+    );
+  }
+
   // Alter the description for allowed values depending on the widget type.
   if ($instance['widget']['type'] == 'options_onoff') {
     $form['allowed_values']['#description'] .= '<p>' . t("For a 'single on/off checkbox' widget, define the 'off' value first, then the 'on' value in the <strong>Allowed values</strong> section. Note that the checkbox will be labeled with the label of the 'on' value.") . '</p>';
@@ -164,15 +191,18 @@ function list_allowed_values_setting_validate($element, &$form_state) {
       form_error($element, t('Allowed values list: keys must be integers.'));
       break;
     }
-    elseif ($field_type == 'list_boolean' && !in_array($key, array('0', '1'))) {
-      form_error($element, t('Allowed values list: keys must be either 0 or 1.'));
-      break;
-    }
   }
+}
 
-  // Check that boolean fields get two values.
-  if ($field_type == 'list_boolean' && count($values) != 2) {
-    form_error($element, t('Allowed values list: two values are required.'));
+/**
+* Form element #value_callback: assembles the allowed values for 'boolean' fields.
+*/
+function list_boolean_allowed_values_callback($element, $edit = FALSE) {
+  if ($edit !== FALSE) {
+    $on = $edit['on'];
+    $off = $edit['off'];
+    $edit = "0|$off\n1|$on";
+    return $edit;
   }
 }
 
@@ -292,7 +322,7 @@ function list_field_is_empty($item, $field) {
  * The List module does not implement widgets of its own, but reuses the
  * widgets defined in options.module.
  *
- * @see list_options_list().
+ * @see list_options_list()
  */
 function list_field_widget_info_alter(&$info) {
   $widgets = array(
@@ -344,7 +374,7 @@ function list_field_formatter_view($entity_type, $entity, $field, $instance, $la
         }
         else {
           // If no match was found in allowed values, fall back to the key.
-          $output = field_filter_xss($value);
+          $output = field_filter_xss($item['value']);
         }
         $element[$delta] = array('#markup' => $output);
       }
diff --git a/modules/field/modules/list/tests/list.test b/modules/field/modules/list/tests/list.test
index 89bea99fdcecc41c1f73a9df515d73a8ac0e0762..30db6ee31be4c57e1fb8a1184d96c3f57a447378 100644
--- a/modules/field/modules/list/tests/list.test
+++ b/modules/field/modules/list/tests/list.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: list.test,v 1.3 2010/02/27 07:52:03 dries Exp $
+// $Id: list.test,v 1.5 2010/03/27 12:49:32 dries Exp $
 
 /**
  * @file
@@ -34,7 +34,7 @@ class ListFieldTestCase extends FieldTestCase {
 
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_buttons',
@@ -82,7 +82,7 @@ class ListFieldTestCase extends FieldTestCase {
     $this->field = field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_buttons',
@@ -154,18 +154,6 @@ class ListFieldUITestCase extends FieldTestCase {
     $edit = array($element_name => "1|one\n" . $this->randomName(256) . "|two");
     $this->drupalPost($admin_path, $edit, t('Save settings'));
     $this->assertText("each key must be a string at most 255 characters long", t('Form validation failed.'));
-
-    // Test 'List (boolean)' field type.
-    $admin_path = $this->createListFieldAndEdit('list_boolean');
-    // Check that invalid option keys are rejected.
-    $edit = array($element_name => "1|one\n2|two");
-    $this->drupalPost($admin_path, $edit, t('Save settings'));
-    $this->assertText("keys must be either 0 or 1", t('Form validation failed.'));
-
-    //Check that missing option causes failure.
-    $edit = array($element_name => "1|one");
-    $this->drupalPost($admin_path, $edit, t('Save settings'));
-    $this->assertText("two values are required", t('Form validation failed.'));
   }
 
   /**
@@ -184,7 +172,7 @@ class ListFieldUITestCase extends FieldTestCase {
     field_create_field($field);
     $instance = array(
       'field_name' => $field_name,
-      'object_type' => 'node',
+      'entity_type' => 'node',
       'bundle' => $this->type,
     );
     field_create_instance($instance);
diff --git a/modules/field/modules/list/tests/list_test.info b/modules/field/modules/list/tests/list_test.info
index 5930385081d261a804d798d0466753e1ba9e47e8..cb448199ca64f6eca213bc0a20e5abccfd044496 100644
--- a/modules/field/modules/list/tests/list_test.info
+++ b/modules/field/modules/list/tests/list_test.info
@@ -7,8 +7,8 @@ files[] = list_test.module
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/modules/number/number.info b/modules/field/modules/number/number.info
index 7323b0fe0eb9597f03007446d475b0d48621b5d4..65fee6a9428f3526c2a0b0a72a0f0ca7891692cc 100644
--- a/modules/field/modules/number/number.info
+++ b/modules/field/modules/number/number.info
@@ -7,8 +7,8 @@ core = 7.x
 files[]=number.module
 required = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/modules/number/number.module b/modules/field/modules/number/number.module
index 3e5e7401312bb375cc94322d1f3297ab60daca8d..454e2b5016e35b297492fe68578c0598933946dd 100644
--- a/modules/field/modules/number/number.module
+++ b/modules/field/modules/number/number.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: number.module,v 1.37 2010/02/11 17:44:47 webchick Exp $
+// $Id: number.module,v 1.38 2010/04/23 05:48:13 webchick Exp $
 
 /**
  * @file
@@ -352,7 +352,7 @@ function number_field_widget_validate($element, &$form_state) {
         $message = t('Only numbers and the decimal separator (@separator) allowed in %field.', array('%field' => t($instance['label']), '@separator' => $field['settings']['decimal_separator']));
         break;
 
-      case 'integer';
+      case 'integer':
         $regexp = '@[^-0-9]@';
         $message = t('Only numbers are allowed in %field.', array('%field' => t($instance['label'])));
         break;
diff --git a/modules/field/modules/options/options.info b/modules/field/modules/options/options.info
index 129d0a92bfe855ded07f3fa064642a36756e4c73..423355b2b43c50ba612f765138cbf7a9c67ce273 100644
--- a/modules/field/modules/options/options.info
+++ b/modules/field/modules/options/options.info
@@ -8,8 +8,8 @@ files[]=options.module
 files[]=options.test
 required = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/modules/options/options.module b/modules/field/modules/options/options.module
index 2a8d382ba4c9b7f4efad579348f2a4785d80fe2a..73f8932caee663ab5614643dc20bf323223119fe 100644
--- a/modules/field/modules/options/options.module
+++ b/modules/field/modules/options/options.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: options.module,v 1.25 2010/02/11 15:42:14 webchick Exp $
+// $Id: options.module,v 1.26 2010/04/13 15:23:02 dries Exp $
 
 /**
  * @file
@@ -366,8 +366,15 @@ function options_field_widget_error($element, $error, $form, &$form_state) {
 }
 
 /**
- *  Theme the label for the empty value for options that are not required.
- *  The default theme will display N/A for a radio list and blank for a select.
+ * Returns HTML for the label for the empty value for options that are not required.
+ *
+ * The default theme will display N/A for a radio list and blank for a select.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - instance: An array representing the widget requesting the options.
+ *
+ * @ingroup themeable
  */
 function theme_options_none($variables) {
   $instance = $variables['instance'];
diff --git a/modules/field/modules/options/options.test b/modules/field/modules/options/options.test
index dae8e60c12a434435362b02a6c9d09835744038c..42b6da57b197b01e951e2e1928a549a071e7ead8 100644
--- a/modules/field/modules/options/options.test
+++ b/modules/field/modules/options/options.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: options.test,v 1.10 2009/12/14 20:18:55 dries Exp $
+// $Id: options.test,v 1.13 2010/04/07 17:30:43 dries Exp $
 
 class OptionsWidgetsTestCase extends FieldTestCase {
   public static function getInfo() {
@@ -61,7 +61,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     // Create an instance of the 'single value' field.
     $instance = array(
       'field_name' => $this->card_1['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_buttons',
@@ -118,7 +118,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     // Create an instance of the 'multiple values' field.
     $instance = array(
       'field_name' => $this->card_2['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_buttons',
@@ -135,7 +135,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
 
     // Display form: with no field data, nothing is checked.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertNoFieldChecked("edit-card-2-$langcode--0");
+    $this->assertNoFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertNoFieldChecked("edit-card-2-$langcode-2");
     $this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', t('Option text was properly filtered.'));
@@ -151,7 +151,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
 
     // Display form: check that the right options are selected.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFieldChecked("edit-card-2-$langcode--0");
+    $this->assertFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertFieldChecked("edit-card-2-$langcode-2");
 
@@ -166,7 +166,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
 
     // Display form: check that the right options are selected.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFieldChecked("edit-card-2-$langcode--0");
+    $this->assertFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertNoFieldChecked("edit-card-2-$langcode-2");
 
@@ -205,7 +205,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     // Create an instance of the 'single value' field.
     $instance = array(
       'field_name' => $this->card_1['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_select',
@@ -247,7 +247,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     $instance['required'] = TRUE;
     field_update_instance($instance);
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFalse($this->xpath('//select[@id="edit-card-1-' . $langcode . '"]//option[@value=""]'), t('A required select list does not have an empty key.'));
+    $this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-1-' . $langcode)), t('A required select list does not have an empty key.'));
 
     // We do not have to test that a required select list with one option is
     // auto-selected because the browser does it for us.
@@ -292,7 +292,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     // Create an instance of the 'multiple values' field.
     $instance = array(
       'field_name' => $this->card_2['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_select',
@@ -363,7 +363,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     $instance['required'] = TRUE;
     field_update_instance($instance);
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFalse($this->xpath('//select[@id="edit-card-2-' . $langcode . '"]//option[@value=""]'), t('A required select list does not have an empty key.'));
+    $this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-2-' . $langcode)), t('A required select list does not have an empty key.'));
 
     // We do not have to test that a required select list with one option is
     // auto-selected because the browser does it for us.
@@ -409,7 +409,7 @@ class OptionsWidgetsTestCase extends FieldTestCase {
     // Create an instance of the 'boolean' field.
     $instance = array(
       'field_name' => $this->bool['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_onoff',
diff --git a/modules/field/modules/text/text.info b/modules/field/modules/text/text.info
index ecb334c855e724d962751e22ceee3dd4d5586eeb..a6c4aea7f772b8ff52fe0539049e58c0e9283c50 100644
--- a/modules/field/modules/text/text.info
+++ b/modules/field/modules/text/text.info
@@ -8,8 +8,8 @@ files[] = text.module
 files[] = text.test
 required = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/modules/text/text.module b/modules/field/modules/text/text.module
index d41fa6d26b7c94630ff11b299f669b0a10cd7984..3d41bffca93493448ba1b6ee5daf517849cecc58 100644
--- a/modules/field/modules/text/text.module
+++ b/modules/field/modules/text/text.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: text.module,v 1.50 2010/03/08 03:59:25 webchick Exp $
+// $Id: text.module,v 1.53 2010/04/13 15:16:27 dries Exp $
 
 /**
  * @file
@@ -122,8 +122,8 @@ function text_field_settings_form($field, $instance, $has_data) {
     '#type' => 'textfield',
     '#title' => t('Maximum length'),
     '#default_value' => $settings['max_length'],
-    '#required' => FALSE,
-    '#description' => t('The maximum length of the field in characters. Leave blank for an unlimited size.'),
+    '#required' => TRUE,
+    '#description' => t('The maximum length of the field in characters.'),
     '#element_validate' => array('_element_validate_integer_positive'),
     // @todo: If $has_data, add a validate handler that only allows
     // max_length to increase.
@@ -199,7 +199,8 @@ function text_field_validate($entity_type, $entity, $field, $instance, $langcode
  * Where possible, generate the sanitized version of each field early so that
  * it is cached in the field cache. This avoids looking up from the filter cache
  * separately.
- * @see text_field_formatter_view().
+ *
+ * @see text_field_formatter_view()
  */
 function text_field_load($entity_type, $entities, $field, $instances, $langcode, &$items) {
   foreach ($entities as $id => $entity) {
@@ -327,7 +328,7 @@ function _text_sanitize($instance, $langcode, $item, $column) {
   if (isset($item["safe_$column"])) {
     return $item["safe_$column"];
   }
-  return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], $langcode, TRUE) : check_plain($item[$column]);
+  return $instance['settings']['text_processing'] ? check_markup($item[$column], $item['format'], $langcode) : check_plain($item[$column]);
 }
 
 /**
diff --git a/modules/field/modules/text/text.test b/modules/field/modules/text/text.test
index feec2e7541699904d15e675c029ab7425651229a..bc87bc4c77bf52494fd2f612ebf98966c0afe668 100644
--- a/modules/field/modules/text/text.test
+++ b/modules/field/modules/text/text.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: text.test,v 1.20 2010/03/07 23:14:20 webchick Exp $
+// $Id: text.test,v 1.22 2010/04/10 10:01:15 dries Exp $
 
 class TextFieldTestCase extends DrupalWebTestCase {
   protected $instance;
@@ -40,7 +40,7 @@ class TextFieldTestCase extends DrupalWebTestCase {
     field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'text_textfield',
@@ -86,7 +86,7 @@ class TextFieldTestCase extends DrupalWebTestCase {
     field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->randomName() . '_label',
       'settings' => array(
@@ -140,7 +140,7 @@ class TextFieldTestCase extends DrupalWebTestCase {
     field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->randomName() . '_label',
       'settings' => array(
@@ -309,11 +309,11 @@ class TextSummaryTestCase extends DrupalWebTestCase {
     $expected_lb = array(
       "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
       "",
-      "<p />",
-      "<p />",
-      "<p />",
-      "<p />",
-      "<p />",
+      "<p></p>",
+      "<p></p>",
+      "<p></p>",
+      "<p></p>",
+      "<p></p>",
       "<p>\nHi</p>",
       "<p>\nHi</p>",
       "<p>\nHi</p>",
diff --git a/modules/field/tests/field.test b/modules/field/tests/field.test
index 29a9e3362f92818ea01ac422bf31f8142fdcaa31..a9b4bd21f1a530dcb3f9a7459b3507f4dc9c1479 100644
--- a/modules/field/tests/field.test
+++ b/modules/field/tests/field.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: field.test,v 1.21 2010/03/12 19:51:40 dries Exp $
+// $Id: field.test,v 1.27 2010/04/11 18:33:43 dries Exp $
 
 /**
  * @file
@@ -16,10 +16,14 @@ class FieldTestCase extends DrupalWebTestCase {
    * Set the default field storage backend for fields created during tests.
    */
   function setUp() {
-    // Call parent::setUp().
-    // @see http://www.php.net/manual/en/function.call-user-func-array.php#73105
-    $args = func_get_args();
-    call_user_func_array(array($this, 'parent::setUp'), $args);
+    // Since this is a base class for many test cases, support the same
+    // flexibility that DrupalWebTestCase::setUp() has for the modules to be
+    // passed in as either an array or a variable number of string arguments.
+    $modules = func_get_args();
+    if (isset($modules[0]) && is_array($modules[0])) {
+      $modules = $modules[0];
+    }
+    parent::setUp($modules);
     // Set default storage backend.
     variable_set('field_storage_default', $this->default_storage);
   }
@@ -69,8 +73,17 @@ class FieldTestCase extends DrupalWebTestCase {
 }
 
 class FieldAttachTestCase extends FieldTestCase {
-  function setUp() {
-    parent::setUp('field_test');
+  function setUp($modules = array()) {
+    // Since this is a base class for many test cases, support the same
+    // flexibility that DrupalWebTestCase::setUp() has for the modules to be
+    // passed in as either an array or a variable number of string arguments.
+    if (!is_array($modules)) {
+      $modules = func_get_args();
+    }
+    if (!in_array('field_test', $modules)) {
+      $modules[] = 'field_test';
+    }
+    parent::setUp($modules);
 
     $this->field_name = drupal_strtolower($this->randomName() . '_field_name');
     $this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4);
@@ -78,7 +91,7 @@ class FieldAttachTestCase extends FieldTestCase {
     $this->field_id = $this->field['id'];
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->randomName() . '_label',
       'description' => $this->randomName() . '_description',
@@ -207,7 +220,7 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
       foreach ($field_bundles_map[$i] as $bundle) {
         $instance = array(
           'field_name' => $field_names[$i],
-          'object_type' => 'test_entity',
+          'entity_type' => 'test_entity',
           'bundle' => $bundles[$bundle],
           'settings' => array(
             // Configure the instance so that we test hook_field_load()
@@ -278,7 +291,7 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
       field_create_field($field);
       $instance = array(
         'field_name' => $field['field_name'],
-        'object_type' => 'test_entity',
+        'entity_type' => 'test_entity',
         'bundle' => 'test_bundle',
       );
       field_create_instance($instance);
@@ -319,13 +332,13 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
     $field = field_create_field($field);
     $instance = array(
       'field_name' => $field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
     );
     field_create_instance($instance);
 
     $field = field_info_field($instance['field_name']);
-    $instance = field_info_instance($instance['object_type'], $instance['field_name'], $instance['bundle']);
+    $instance = field_info_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
 
     // The storage details are indexed by a storage engine type.
     $this->assertTrue(array_key_exists('drupal_variables', $field['storage']['details']), t('The storage type is Drupal variables.'));
@@ -570,7 +583,7 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
     field_create_field($field);
     $instance = array(
       'field_name' => $field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => $this->instance['bundle'],
       'label' => $this->randomName() . '_label',
       'description' => $this->randomName() . '_description',
@@ -626,10 +639,10 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
 
     // Create instances of both fields on the second entity type.
     $instance = $this->instance;
-    $instance['object_type'] = 'test_cacheable_entity';
+    $instance['entity_type'] = 'test_cacheable_entity';
     field_create_instance($instance);
     $instance2 = $this->instance2;
-    $instance2['object_type'] = 'test_cacheable_entity';
+    $instance2['entity_type'] = 'test_cacheable_entity';
     field_create_instance($instance2);
 
     // Unconditional count query returns 0.
@@ -990,7 +1003,7 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase {
     $entity_type = 'test_cacheable_entity';
     $cid = "field:$entity_type:{$entity_init->ftid}";
     $instance = $this->instance;
-    $instance['object_type'] = $entity_type;
+    $instance['entity_type'] = $entity_type;
     field_create_instance($instance);
 
     // Check that no initial cache entry is present.
@@ -1247,7 +1260,7 @@ class FieldInfoTestCase extends FieldTestCase {
     // Create an instance, verify that it shows up
     $instance = array(
       'field_name' => $field['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->randomName(),
       'description' => $this->randomName(),
@@ -1306,7 +1319,7 @@ class FieldInfoTestCase extends FieldTestCase {
     field_create_field($field_definition);
     $instance_definition = array(
       'field_name' => $field_definition['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
     );
     field_create_instance($instance_definition);
@@ -1330,7 +1343,7 @@ class FieldInfoTestCase extends FieldTestCase {
     field_cache_clear();
 
     // Read the instance back.
-    $instance = field_info_instance($instance_definition['object_type'], $instance_definition['field_name'], $instance_definition['bundle']);
+    $instance = field_info_instance($instance_definition['entity_type'], $instance_definition['field_name'], $instance_definition['bundle']);
 
     // Check that all expected instance settings are in place.
     $field_type = field_info_field_types($field_definition['type']);
@@ -1363,7 +1376,7 @@ class FieldInfoTestCase extends FieldTestCase {
     field_create_field($field_definition);
     $instance_definition = array(
       'field_name' => 'field',
-      'object_type' => 'comment',
+      'entity_type' => 'comment',
       'bundle' => 'comment_node_article',
     );
     field_create_instance($instance_definition);
@@ -1415,7 +1428,7 @@ class FieldFormTestCase extends FieldTestCase {
     $this->field_unlimited = array('field_name' => drupal_strtolower($this->randomName()), 'type' => 'test_field', 'cardinality' => FIELD_CARDINALITY_UNLIMITED);
 
     $this->instance = array(
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->randomName() . '_label',
       'description' => $this->randomName() . '_description',
@@ -1557,23 +1570,21 @@ class FieldFormTestCase extends FieldTestCase {
       do {
         $weight = mt_rand(-$delta_range, $delta_range);
       } while (in_array($weight, $weights));
-      $weights[] = $weight;
       $value = mt_rand(1, 127);
       $edit["$this->field_name[$langcode][$delta][value]"] = $value;
       $edit["$this->field_name[$langcode][$delta][_weight]"] = $weight;
       // We'll need three slightly different formats to check the values.
-      $values[$weight] = $value;
+      $values[$delta] = $value;
+      $weights[$delta] = $weight;
       $field_values[$weight]['value'] = (string)$value;
       $pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*";
     }
 
     // Press 'add more' button -> 4 widgets
     $this->drupalPost(NULL, $edit, t('Add another item'));
-    ksort($values);
-    $values = array_values($values);
     for ($delta = 0; $delta <= $delta_range; $delta++) {
       $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
-      $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "Widget $delta has the right weight");
+      $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
     }
     ksort($pattern);
     $pattern = implode('.*', array_values($pattern));
@@ -1627,12 +1638,12 @@ class FieldFormTestCase extends FieldTestCase {
       do {
         $weight = mt_rand(-$delta_range, $delta_range);
       } while (in_array($weight, $weights));
-      $weights[] = $weight;
       $value = mt_rand(1, 127);
       $edit["$this->field_name[$langcode][$delta][value]"] = $value;
       $edit["$this->field_name[$langcode][$delta][_weight]"] = $weight;
       // We'll need three slightly different formats to check the values.
-      $values[$weight] = $value;
+      $values[$delta] = $value;
+      $weights[$delta] = $weight;
       $field_values[$weight]['value'] = (string)$value;
       $pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*";
     }
@@ -1641,11 +1652,9 @@ class FieldFormTestCase extends FieldTestCase {
     $commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more');
     $this->content = $commands[1]['data'];
 
-    ksort($values);
-    $values = array_values($values);
     for ($delta = 0; $delta <= $delta_range; $delta++) {
       $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
-      $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "Widget $delta has the right weight");
+      $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
     }
     ksort($pattern);
     $pattern = implode('.*', array_values($pattern));
@@ -1715,7 +1724,7 @@ class FieldFormTestCase extends FieldTestCase {
     $field_name_no_access = $field_no_access['field_name'];
     $instance_no_access = array(
       'field_name' => $field_name_no_access,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'default_value' => array(0 => array('value' => 99)),
     );
@@ -1779,7 +1788,7 @@ class FieldDisplayAPITestCase extends FieldTestCase {
     );
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->label,
       'display' => array(
@@ -2177,7 +2186,7 @@ class FieldCrudTestCase extends FieldTestCase {
     // Create instances for each.
     $this->instance_definition = array(
       'field_name' => $this->field['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'test_field_widget',
@@ -2275,7 +2284,7 @@ class FieldCrudTestCase extends FieldTestCase {
     // Create a decimal 5.2 field.
     $field = array('field_name' => 'decimal53', 'type' => 'number_decimal', 'cardinality' => 3, 'settings' => array('precision' => 5, 'scale' => 2));
     $field = field_create_field($field);
-    $instance = array('field_name' => 'decimal53', 'object_type' => 'test_entity', 'bundle' => 'test_bundle');
+    $instance = array('field_name' => 'decimal53', 'entity_type' => 'test_entity', 'bundle' => 'test_bundle');
     $instance = field_create_instance($instance);
 
     // Update it to a deciaml 5.3 field.
@@ -2406,7 +2415,7 @@ class FieldInstanceCrudTestCase extends FieldTestCase {
     field_create_field($this->field);
     $this->instance_definition = array(
       'field_name' => $this->field['field_name'],
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
     );
   }
@@ -2468,7 +2477,7 @@ class FieldInstanceCrudTestCase extends FieldTestCase {
     $field_restricted = array(
       'field_name' => drupal_strtolower($this->randomName()),
       'type' => 'test_field',
-      'object_types' => array('test_cacheable_entity'),
+      'entity_types' => array('test_cacheable_entity'),
     );
     field_create_field($field_restricted);
 
@@ -2477,7 +2486,7 @@ class FieldInstanceCrudTestCase extends FieldTestCase {
     try {
       $instance = $this->instance_definition;
       $instance['field_name'] = $field_restricted['field_name'];
-      $instance['object_type'] = 'test_cacheable_entity';
+      $instance['entity_type'] = 'test_cacheable_entity';
       field_create_instance($instance);
       $this->pass(t('Can create an instance on an entity type allowed by the field.'));
     }
@@ -2608,8 +2617,7 @@ class FieldInstanceCrudTestCase extends FieldTestCase {
  * Unit test class for the multilanguage fields logic.
  *
  * The following tests will check the multilanguage logic of _field_invoke() and
- * that only the correct values are returned by
- * field_multilingual_available_languages().
+ * that only the correct values are returned by field_available_languages().
  */
 class FieldTranslationsTestCase extends FieldTestCase {
   public static function getInfo() {
@@ -2625,37 +2633,21 @@ class FieldTranslationsTestCase extends FieldTestCase {
 
     $this->field_name = drupal_strtolower($this->randomName() . '_field_name');
 
-    $this->obj_type = 'test_entity';
+    $this->entity_type = 'test_entity';
 
     $field = array(
       'field_name' => $this->field_name,
       'type' => 'test_field',
       'cardinality' => 4,
       'translatable' => TRUE,
-      'settings' => array(
-        'test_hook_in' => FALSE,
-      ),
     );
     field_create_field($field);
     $this->field = field_read_field($this->field_name);
 
     $instance = array(
       'field_name' => $this->field_name,
-      'object_type' => $this->obj_type,
+      'entity_type' => $this->entity_type,
       'bundle' => 'test_bundle',
-      'label' => $this->randomName() . '_label',
-      'description' => $this->randomName() . '_description',
-      'weight' => mt_rand(0, 127),
-      'settings' => array(
-        'test_instance_setting' => $this->randomName(),
-      ),
-      'widget' => array(
-        'type' => 'test_field_widget',
-        'label' => 'Test Field',
-        'settings' => array(
-          'test_widget_setting' => $this->randomName(),
-        ),
-      ),
     );
     field_create_instance($instance);
     $this->instance = field_read_instance('test_entity', $this->field_name, 'test_bundle');
@@ -2667,64 +2659,34 @@ class FieldTranslationsTestCase extends FieldTestCase {
   }
 
   /**
-   * Ensure that only valid values are returned by field_multilingual_available_languages().
+   * Ensures that only valid values are returned by field_available_languages().
    */
   function testFieldAvailableLanguages() {
     // Test 'translatable' fieldable info.
     field_test_entity_info_translatable('test_entity', FALSE);
     $field = $this->field;
     $field['field_name'] .= '_untranslatable';
-    $langcode = language_default();
-    $suggested_languages = array($langcode->language);
-    $available_languages = field_multilingual_available_languages($this->obj_type, $field, $suggested_languages);
-    $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('Untranslatable entity: suggested language ignored.'));
 
     // Enable field translations for the entity.
     field_test_entity_info_translatable('test_entity', TRUE);
 
     // Test hook_field_languages() invocation on a translatable field.
-    $this->field['settings']['test_hook_in'] = TRUE;
-    $enabled_languages = array_keys(language_list());
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field);
-    $this->assertTrue(in_array(LANGUAGE_NONE, $available_languages), t('%language is an available language.', array('%language' => LANGUAGE_NONE)));
+    variable_set('field_test_field_available_languages_alter', TRUE);
+    $enabled_languages = field_content_languages();
+    $available_languages = field_available_languages($this->entity_type, $this->field);
     foreach ($available_languages as $delta => $langcode) {
-      if ($langcode != LANGUAGE_NONE) {
+      if ($langcode != 'xx' && $langcode != 'en') {
         $this->assertTrue(in_array($langcode, $enabled_languages), t('%language is an enabled language.', array('%language' => $langcode)));
       }
     }
-    $this->assertFalse(in_array('xx', $available_languages), t('No invalid language was made available.'));
-    $this->assertTrue(count($available_languages) == count($enabled_languages), t('An enabled language was successfully made unavailable.'));
+    $this->assertTrue(in_array('xx', $available_languages), t('%language was made available.', array('%language' => 'xx')));
+    $this->assertFalse(in_array('en', $available_languages), t('%language was made unavailable.', array('%language' => 'en')));
 
-    // Test field_multilingual_available_languages() behavior for untranslatable fields.
+    // Test field_available_languages() behavior for untranslatable fields.
     $this->field['translatable'] = FALSE;
     $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name');
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field);
-    $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only neutral language is available.'));
-
-    // Test language suggestions.
-    $this->field['settings']['test_hook_in'] = FALSE;
-    $this->field['translatable'] = TRUE;
-    $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name');
-    $suggested_languages = array();
-    $lang_count = mt_rand(1, count($enabled_languages) - 1);
-    for ($i = 0; $i < $lang_count; ++$i) {
-      do {
-        $langcode = $enabled_languages[mt_rand(0, $lang_count)];
-      }
-      while (in_array($langcode, $suggested_languages));
-      $suggested_languages[] = $langcode;
-    }
-
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages);
-    $this->assertEqual(count($available_languages), count($suggested_languages), t('Suggested languages were successfully made available.'));
-    foreach ($available_languages as $langcode) {
-      $this->assertTrue(in_array($langcode, $available_languages), t('Suggested language %language is available.', array('%language' => $langcode)));
-    }
-
-    $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name');
-    $suggested_languages = array('xx');
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages);
-    $this->assertTrue(empty($available_languages), t('An invalid suggested language was not made available.'));
+    $available_languages = field_available_languages($this->entity_type, $this->field);
+    $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only LANGUAGE_NONE is available.'));
   }
 
   /**
@@ -2735,10 +2697,10 @@ class FieldTranslationsTestCase extends FieldTestCase {
     $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
 
     // Populate some extra languages to check if _field_invoke() correctly uses
-    // the result of field_multilingual_available_languages().
+    // the result of field_available_languages().
     $values = array();
     $extra_languages = mt_rand(1, 4);
-    $languages = $available_languages = field_multilingual_available_languages($this->obj_type, $this->field);
+    $languages = $available_languages = field_available_languages($this->entity_type, $this->field);
     for ($i = 0; $i < $extra_languages; ++$i) {
       $languages[] = $this->randomString(2);
     }
@@ -2769,14 +2731,14 @@ class FieldTranslationsTestCase extends FieldTestCase {
     $entities = array();
     $entity_type = 'test_entity';
     $entity_count = mt_rand(1, 5);
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field);
+    $available_languages = field_available_languages($this->entity_type, $this->field);
 
     for ($id = 1; $id <= $entity_count; ++$id) {
       $entity = field_test_create_stub_entity($id, $id, $this->instance['bundle']);
       $languages = $available_languages;
 
       // Populate some extra languages to check whether _field_invoke()
-      // correctly uses the result of field_multilingual_available_languages().
+      // correctly uses the result of field_available_languages().
       $extra_languages = mt_rand(1, 4);
       for ($i = 0; $i < $extra_languages; ++$i) {
         $languages[] = $this->randomString(2);
@@ -2819,7 +2781,7 @@ class FieldTranslationsTestCase extends FieldTestCase {
     $entity_type = 'test_entity';
     $entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']);
     $field_translations = array();
-    $available_languages = field_multilingual_available_languages($entity_type, $this->field);
+    $available_languages = field_available_languages($entity_type, $this->field);
     $this->assertTrue(count($available_languages) > 1, t('Field is translatable.'));
     foreach ($available_languages as $langcode) {
       $field_translations[$langcode] = $this->_generateTestFieldValues($this->field['cardinality']);
@@ -2840,6 +2802,82 @@ class FieldTranslationsTestCase extends FieldTestCase {
       $this->assertTrue($result, t('%language translation correctly handled.', array('%language' => $langcode)));
     }
   }
+
+  /**
+   * Tests display language logic for translatable fields.
+   */
+  function testFieldDisplayLanguage() {
+    $field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $entity_type = 'test_entity';
+
+    // We need an additional field here to properly test display language
+    // suggestions.
+    $field = array(
+      'field_name' => $field_name,
+      'type' => 'test_field',
+      'cardinality' => 2,
+      'translatable' => TRUE,
+    );
+    field_create_field($field);
+
+    $instance = array(
+      'field_name' => $field['field_name'],
+      'entity_type' => $entity_type,
+      'bundle' => 'test_bundle',
+    );
+    field_create_instance($instance);
+
+    $entity = field_test_create_stub_entity(1, 1, $this->instance['bundle']);
+    list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+    $instances = field_info_instances($entity_type, $bundle);
+
+    $enabled_languages = field_content_languages();
+    $languages = array();
+
+    // Generate field translations for languages different from the first
+    // enabled.
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      do {
+        // Index 0 is reserved for the requested language, this way we ensure
+        // that no field is actually populated with it.
+        $langcode = $enabled_languages[mt_rand(1, count($enabled_languages) - 1)];
+      }
+      while (isset($languages[$langcode]));
+      $languages[$langcode] = TRUE;
+      $entity->{$field_name}[$langcode] = $this->_generateTestFieldValues($field['cardinality']);
+    }
+
+    // Test multiple-fields display languages for untranslatable entities.
+    field_test_entity_info_translatable($entity_type, FALSE);
+    drupal_static_reset('field_language');
+    $requested_language = $enabled_languages[0];
+    $display_language = field_language($entity_type, $entity, NULL, $requested_language);
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $this->assertTrue($display_language[$field_name] == LANGUAGE_NONE, t('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => LANGUAGE_NONE)));
+    }
+
+    // Test multiple-fields display languages for translatable entities.
+    field_test_entity_info_translatable($entity_type, TRUE);
+    drupal_static_reset('field_language');
+    $display_language = field_language($entity_type, $entity, NULL, $requested_language);
+
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $langcode = $display_language[$field_name];
+      // As the requested language was not assinged to any field, if the
+      // returned language is defined for the current field, core fallback rules
+      // were successfully applied.
+      $this->assertTrue(isset($entity->{$field_name}[$langcode]) && $langcode != $requested_language, t('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
+    }
+
+    // Test single-field display language.
+    drupal_static_reset('field_language');
+    $langcode = field_language($entity_type, $entity, $this->field_name, $requested_language);
+    $this->assertTrue(isset($entity->{$this->field_name}[$langcode]) && $langcode != $requested_language, t('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
+  }
 }
 
 /**
@@ -2913,7 +2951,7 @@ class FieldBulkDeleteTestCase extends FieldTestCase {
       foreach ($this->fields as $field) {
         $instance = array(
           'field_name' => $field['field_name'],
-          'object_type' => $this->entity_type,
+          'entity_type' => $this->entity_type,
           'bundle' => $bundle,
           'widget' => array(
             'type' => 'test_field_widget',
diff --git a/modules/field/tests/field_test.entity.inc b/modules/field/tests/field_test.entity.inc
index 069f0ad2d141851e6e5b03f95f89814341e2f1c7..77203a6deed5455392f27761fb2d21b3a5f81012 100644
--- a/modules/field/tests/field_test.entity.inc
+++ b/modules/field/tests/field_test.entity.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_test.entity.inc,v 1.4 2010/02/12 05:38:10 webchick Exp $
+// $Id: field_test.entity.inc,v 1.7 2010/03/27 18:41:14 dries Exp $
 
 /**
  * @file
@@ -23,26 +23,26 @@ function field_test_entity_info() {
   return array(
     'test_entity' => array(
       'name' => t('Test Entity'),
-      'object keys' => array(
+      'fieldable' => TRUE,
+      'field cache' => FALSE,
+      'entity keys' => array(
         'id' => 'ftid',
         'revision' => 'ftvid',
         'bundle' => 'fttype',
       ),
-      'cacheable' => FALSE,
       'bundles' => $bundles,
-      'fieldable' => TRUE,
       'view modes' => $test_entity_modes,
     ),
     // This entity type doesn't get form handling for now...
     'test_cacheable_entity' => array(
       'name' => t('Test Entity, cacheable'),
-      'object keys' => array(
+      'fieldable' => TRUE,
+      'field cache' => TRUE,
+      'entity keys' => array(
         'id' => 'ftid',
         'revision' => 'ftvid',
         'bundle' => 'fttype',
       ),
-      'fieldable' => TRUE,
-      'cacheable' => TRUE,
       'bundles' => $bundles,
       'view modes' => $test_entity_modes,
     ),
@@ -63,6 +63,19 @@ function field_test_entity_info_alter(&$entity_info) {
   }
 }
 
+/**
+ * Helper function to enable entity translations.
+ */
+function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
+  drupal_static_reset('field_has_translation_handler');
+  $stored_value = &drupal_static(__FUNCTION__, array());
+  if (isset($entity_type)) {
+    $stored_value[$entity_type] = $translatable;
+    entity_info_cache_clear();
+  }
+  return $stored_value;
+}
+
 /**
  * Creates a new bundle for test_entity entities.
  *
diff --git a/modules/field/tests/field_test.info b/modules/field/tests/field_test.info
index 34467b2d04b4ad320cc9e5db688003af8d38543c..325e8abb49872266d42896cea80ca58ff4899ac8 100644
--- a/modules/field/tests/field_test.info
+++ b/modules/field/tests/field_test.info
@@ -11,8 +11,8 @@ files[] = field_test.install
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field/tests/field_test.module b/modules/field/tests/field_test.module
index 07351fe0178fcdab17a1dada944085561a4cc94c..7cd2e3266b907c25933a3d6c94534f5599e06d71 100644
--- a/modules/field/tests/field_test.module
+++ b/modules/field/tests/field_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_test.module,v 1.5 2010/02/11 17:44:47 webchick Exp $
+// $Id: field_test.module,v 1.6 2010/03/25 11:46:20 dries Exp $
 
 /**
  * @file
@@ -87,27 +87,23 @@ function field_test_field_test_op_multiple($entity_type, $entities, $field, $ins
 }
 
 /**
- * Implements hook_field_languages().
+ * Implements hook_field_available_languages_alter().
  */
-function field_test_field_languages($entity_type, $field, &$languages) {
-  if ($field['settings']['test_hook_in']) {
+function field_test_field_available_languages_alter(&$languages, $context) {
+  if (variable_get('field_test_field_available_languages_alter', FALSE)) {
     // Add an unavailable language.
     $languages[] = 'xx';
     // Remove an available language.
-    unset($languages[0]);
+    $index = array_search('en', $languages);
+    unset($languages[$index]);
   }
 }
 
 /**
- * Helper function to enable entity translations.
+ * Implements hook_field_language_alter().
  */
-function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
-  $stored_value = &drupal_static(__FUNCTION__, array());
-  if (isset($entity_type)) {
-    $stored_value[$entity_type] = $translatable;
-    entity_info_cache_clear();
-  }
-  return $stored_value;
+function field_test_field_language_alter(&$display_language, $context) {
+  locale_field_language_fallback($display_language, $context['entity'], $context['language']);
 }
 
 /**
diff --git a/modules/field/tests/field_test.storage.inc b/modules/field/tests/field_test.storage.inc
index 53baa78950047f5b78ad1e8428e0495110148294..7ef92101965d42bdae9ace3d86013afcc3b28d66 100644
--- a/modules/field/tests/field_test.storage.inc
+++ b/modules/field/tests/field_test.storage.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_test.storage.inc,v 1.4 2010/02/12 05:38:10 webchick Exp $
+// $Id: field_test.storage.inc,v 1.6 2010/03/27 05:52:49 webchick Exp $
 
 /**
  * @file
@@ -95,7 +95,7 @@ function field_test_field_storage_load($entity_type, $entities, $age, $fields, $
     foreach ($field_data[$sub_table] as $row) {
       if ($row->type == $entity_type && (!$row->deleted || $options['deleted'])) {
         if (($load_current && in_array($row->entity_id, $ids)) || (!$load_current && in_array($row->revision_id, $ids))) {
-          if (in_array($row->language, field_multilingual_available_languages($entity_type, $field))) {
+          if (in_array($row->language, field_available_languages($entity_type, $field))) {
             if (!isset($delta_count[$row->entity_id][$row->language])) {
               $delta_count[$row->entity_id][$row->language] = 0;
             }
@@ -127,7 +127,7 @@ function field_test_field_storage_write($entity_type, $entity, $op, $fields) {
     $field_name = $field['field_name'];
     $field_data = &$data[$field_id];
 
-    $all_languages = field_multilingual_available_languages($entity_type, $field);
+    $all_languages = field_available_languages($entity_type, $field);
     $field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
 
     // Delete and insert, rather than update, in case a value was added.
@@ -323,7 +323,7 @@ function field_test_field_storage_query($field_id, $conditions, $count, &$cursor
           // If querying all revisions and the entity type has revisions, we need
           // to key the results by revision_ids.
           $entity_type = entity_get_info($row->type);
-          $id = ($load_current || empty($entity_type['object keys']['revision'])) ? $row->entity_id : $row->revision_id;
+          $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id;
 
           if (!isset($return[$row->type][$id])) {
             $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
diff --git a/modules/field/theme/field.tpl.php b/modules/field/theme/field.tpl.php
index e4d33ac62db11782c4acee692a6da0ab4d0f9d1e..56502f065edf6804b0390eb8e02956631f598f11 100644
--- a/modules/field/theme/field.tpl.php
+++ b/modules/field/theme/field.tpl.php
@@ -1,12 +1,12 @@
 <?php
-// $Id: field.tpl.php,v 1.12 2010/02/12 05:38:10 webchick Exp $
+// $Id: field.tpl.php,v 1.13 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file field.tpl.php
  * Default template implementation to display the value of a field.
  *
  * This file is not used and is here as a starting point for customization only.
- * @see theme_field().
+ * @see theme_field()
  *
  * Available variables:
  * - $items: An array of field values. Use render() to output them.
diff --git a/modules/field_ui/field_ui.admin.inc b/modules/field_ui/field_ui.admin.inc
index 5a2f3c7a6075eda392709769ea1f7f7616583846..f8070016a88b8188a9cb58203be8ad816f6957e8 100644
--- a/modules/field_ui/field_ui.admin.inc
+++ b/modules/field_ui/field_ui.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_ui.admin.inc,v 1.45 2010/03/03 08:01:42 dries Exp $
+// $Id: field_ui.admin.inc,v 1.49 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -90,7 +90,7 @@ function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle
 
   $form += array(
     '#tree' => TRUE,
-    '#object_type' => $entity_type,
+    '#entity_type' => $entity_type,
     '#bundle' => $bundle,
     '#fields' => array_keys($instances),
     '#extra' => array_keys($extra),
@@ -274,7 +274,8 @@ function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle
     $form['#field_rows'][] = $name;
   }
 
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
   return $form;
 }
 
@@ -289,9 +290,9 @@ function template_preprocess_field_ui_field_overview_form(&$vars) {
   drupal_add_js(drupal_get_path('module', 'field_ui') . '/field_ui.js');
   // Add settings for the update selects behavior.
   $js_fields = array();
-  foreach (field_ui_existing_field_options($form['#object_type'], $form['#bundle']) as $field_name => $fields) {
+  foreach (field_ui_existing_field_options($form['#entity_type'], $form['#bundle']) as $field_name => $fields) {
     $field = field_info_field($field_name);
-    $instance = field_info_instance($form['#object_type'], $field_name, $form['#bundle']);
+    $instance = field_info_instance($form['#entity_type'], $field_name, $form['#bundle']);
     $js_fields[$field_name] = array('label' => $instance['label'], 'type' => $field['type'], 'widget' => $instance['widget']['type']);
   }
   drupal_add_js(array('fieldWidgetTypes' => field_ui_widget_type_options(), 'fields' => $js_fields), 'setting');
@@ -459,7 +460,7 @@ function _field_ui_field_overview_form_validate_add_existing($form, &$form_state
  */
 function field_ui_field_overview_form_submit($form, &$form_state) {
   $form_values = $form_state['values'];
-  $entity_type = $form['#object_type'];
+  $entity_type = $form['#entity_type'];
   $bundle = $form['#bundle'];
   $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
 
@@ -497,7 +498,7 @@ function field_ui_field_overview_form_submit($form, &$form_state) {
     );
     $instance = array(
       'field_name' => $field['field_name'],
-      'object_type' => $entity_type,
+      'entity_type' => $entity_type,
       'bundle' => $bundle,
       'label' => $values['label'],
       'widget' => array(
@@ -532,7 +533,7 @@ function field_ui_field_overview_form_submit($form, &$form_state) {
     else {
       $instance = array(
         'field_name' => $field['field_name'],
-        'object_type' => $entity_type,
+        'entity_type' => $entity_type,
         'bundle' => $bundle,
         'label' => $values['label'],
         'widget' => array(
@@ -580,7 +581,7 @@ function field_ui_display_overview_form($form, &$form_state, $entity_type, $bund
 
   $form += array(
     '#tree' => TRUE,
-    '#object_type' => $entity_type,
+    '#entity_type' => $entity_type,
     '#bundle' => $bundle,
     '#fields' => array_keys($instances),
     '#contexts' => $view_modes_selector,
@@ -624,7 +625,8 @@ function field_ui_display_overview_form($form, &$form_state, $entity_type, $bund
     }
   }
 
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
   return $form;
 }
 
@@ -635,8 +637,8 @@ function template_preprocess_field_ui_display_overview_form(&$vars) {
   $form = &$vars['form'];
 
   $contexts_selector = $form['#contexts'];
-  $view_modes = field_ui_view_modes_tabs($form['#object_type'], $contexts_selector);
-  $entity_info = entity_get_info($form['#object_type']);
+  $view_modes = field_ui_view_modes_tabs($form['#entity_type'], $contexts_selector);
+  $entity_info = entity_get_info($form['#entity_type']);
   $view_modes_info = $entity_info['view modes'];
   $vars['contexts'] = array();
   foreach ($view_modes as $view_mode) {
@@ -655,6 +657,7 @@ function template_preprocess_field_ui_display_overview_form(&$vars) {
     $row = new stdClass();
     foreach (element_children($element) as $child) {
       if (array_key_exists('label', $element[$child])) {
+        $row->{$child} = new stdClass();
         $row->{$child}->label = drupal_render($element[$child]['label']);
         $row->{$child}->type = drupal_render($element[$child]['type']);
       }
@@ -677,7 +680,7 @@ function field_ui_display_overview_form_submit($form, &$form_state) {
   $form_values = $form_state['values'];
   foreach ($form_values as $key => $values) {
     if (in_array($key, $form['#fields'])) {
-      $instance = field_info_instance($form['#object_type'], $key, $form['#bundle']);
+      $instance = field_info_instance($form['#entity_type'], $key, $form['#bundle']);
       foreach ($instance['display'] as $view_mode => $display) {
         if (isset($values[$view_mode])) {
           $instance['display'][$view_mode] = array_merge($instance['display'][$view_mode], $values[$view_mode]);
@@ -793,7 +796,7 @@ function field_ui_existing_field_options($entity_type, $bundle) {
           // - field that cannot be added to the entity type.
           if (empty($field['locked'])
             && !field_info_instance($entity_type, $field['field_name'], $bundle)
-            && (empty($field['object_types']) || in_array($entity_type, $field['object_types']))) {
+            && (empty($field['entity_types']) || in_array($entity_type, $field['entity_types']))) {
             $text = t('@type: @field (@label)', array(
               '@type' => $field_types[$field['type']]['label'],
               '@label' => t($instance['label']), '@field' => $instance['field_name'],
@@ -812,15 +815,10 @@ function field_ui_existing_field_options($entity_type, $bundle) {
 /**
  * Menu callback; presents the field settings edit page.
  */
-function field_ui_field_settings_form($form, &$form_state, $entity_type, $bundle, $field) {
-  $bundle = field_extract_bundle($entity_type, $bundle);
-  $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
-
-  // When a field is first created, we have to get data from the db.
-  if (!isset($instance['label'])) {
-    $instance = field_read_instance($field['field_name'], $bundle);
-    $field = field_read_field($field['field_name']);
-  }
+function field_ui_field_settings_form($form, &$form_state, $instance) {
+  $bundle = $instance['bundle'];
+  $entity_type = $instance['entity_type'];
+  $field = field_info_field($instance['field_name']);
 
   drupal_set_title($instance['label']);
 
@@ -860,10 +858,10 @@ function field_ui_field_settings_form($form, &$form_state, $entity_type, $bundle
       '#markup' => t('%field has no field settings.', array('%field' => $instance['label'])),
     );
   }
-  $form['#object_type'] = $entity_type;
+  $form['#entity_type'] = $entity_type;
   $form['#bundle'] = $bundle;
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save field settings'));
   return $form;
 }
@@ -878,7 +876,7 @@ function field_ui_field_settings_form_submit($form, &$form_state) {
   // Merge incoming form values into the existing field.
   $field = field_info_field($field_values['field_name']);
 
-  $entity_type = $form['#object_type'];
+  $entity_type = $form['#entity_type'];
   $bundle = $form['#bundle'];
   $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
 
@@ -900,9 +898,10 @@ function field_ui_field_settings_form_submit($form, &$form_state) {
 /**
  * Menu callback; select a widget for the field.
  */
-function field_ui_widget_type_form($form, &$form_state, $entity_type, $bundle, $field) {
-  $bundle = field_extract_bundle($entity_type, $bundle);
-  $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
+function field_ui_widget_type_form($form, &$form_state, $instance) {
+  $bundle = $instance['bundle'];
+  $entity_type = $instance['entity_type'];
+  $field = field_info_field($instance['field_name']);
 
   drupal_set_title($instance['label']);
 
@@ -925,7 +924,8 @@ function field_ui_widget_type_form($form, &$form_state, $entity_type, $bundle, $
   );
 
   $form['#instance'] = $instance;
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Continue'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Continue'));
 
   $form['#validate'] = array();
   $form['#submit'] = array('field_ui_widget_type_form_submit');
@@ -940,7 +940,7 @@ function field_ui_widget_type_form_submit($form, &$form_state) {
   $form_values = $form_state['values'];
   $instance = $form['#instance'];
   $bundle = $instance['bundle'];
-  $entity_type = $instance['object_type'];
+  $entity_type = $instance['entity_type'];
 
   // Set the right module information.
   $widget_type = field_info_widget_types($form_values['widget_type']);
@@ -962,12 +962,14 @@ function field_ui_widget_type_form_submit($form, &$form_state) {
 /**
  * Menu callback; present a form for removing a field from a content type.
  */
-function field_ui_field_delete_form($form, &$form_state, $entity_type, $bundle, $field) {
-  $bundle = field_extract_bundle($entity_type, $bundle);
-  $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
+function field_ui_field_delete_form($form, &$form_state, $instance) {
+  $bundle = $instance['bundle'];
+  $entity_type = $instance['entity_type'];
+  $field = field_info_field($instance['field_name']);
+
   $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
 
-  $form['object_type'] = array('#type' => 'value', '#value' => $entity_type);
+  $form['entity_type'] = array('#type' => 'value', '#value' => $entity_type);
   $form['bundle'] = array('#type' => 'value', '#value' => $bundle);
   $form['field_name'] = array('#type' => 'value', '#value' => $field['field_name']);
 
@@ -994,7 +996,7 @@ function field_ui_field_delete_form_submit($form, &$form_state) {
   $form_values = $form_state['values'];
   $field_name = $form_values['field_name'];
   $bundle = $form_values['bundle'];
-  $entity_type = $form_values['object_type'];
+  $entity_type = $form_values['entity_type'];
 
   $field = field_info_field($field_name);
   $instance = field_info_instance($entity_type, $field_name, $bundle);
@@ -1020,9 +1022,12 @@ function field_ui_field_delete_form_submit($form, &$form_state) {
 /**
  * Menu callback; presents the field instance edit page.
  */
-function field_ui_field_edit_form($form, &$form_state, $entity_type, $bundle, $field) {
-  $bundle = field_extract_bundle($entity_type, $bundle);
-  $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
+function field_ui_field_edit_form($form, &$form_state, $instance) {
+  $bundle = $instance['bundle'];
+  $entity_type = $instance['entity_type'];
+  $field = field_info_field($instance['field_name']);
+
+  drupal_set_title($instance['label']);
 
   $form['#field'] = $field;
 
@@ -1037,9 +1042,6 @@ function field_ui_field_edit_form($form, &$form_state, $entity_type, $bundle, $f
   $widget_type = field_info_widget_types($instance['widget']['type']);
   $bundles = field_info_bundles();
 
-  $title = isset($instance['label']) ? $instance['label'] : $instance['field_name'];
-  drupal_set_title(check_plain($title));
-
   // Create a form structure for the instance values.
   $form['instance'] = array(
     '#tree' => TRUE,
@@ -1054,7 +1056,7 @@ function field_ui_field_edit_form($form, &$form_state, $entity_type, $bundle, $f
     '#type' => 'value',
     '#value' => $instance['field_name'],
   );
-  $form['instance']['object_type'] = array(
+  $form['instance']['entity_type'] = array(
     '#type' => 'value',
     '#value' => $entity_type,
   );
@@ -1161,7 +1163,8 @@ function field_ui_field_edit_form($form, &$form_state, $entity_type, $bundle, $f
     $form['field']['settings'] = $additions;
   }
 
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));
   return $form;
 }
 
@@ -1262,13 +1265,13 @@ function field_ui_field_edit_form_submit($form, &$form_state) {
   $instance['default_value'] = $items ? $items : NULL;
 
   // Update the instance settings.
-  $instance_source = field_info_instance($instance['object_type'], $instance['field_name'], $instance['bundle']);
+  $instance_source = field_info_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
   $instance = array_merge($instance_source, $instance);
   field_update_instance($instance);
 
   drupal_set_message(t('Saved %label configuration.', array('%label' => $instance['label'])));
 
-  $form_state['redirect'] = field_ui_next_destination($instance['object_type'], $instance['bundle']);
+  $form_state['redirect'] = field_ui_next_destination($instance['entity_type'], $instance['bundle']);
 }
 
 /**
diff --git a/modules/field_ui/field_ui.api.php b/modules/field_ui/field_ui.api.php
index b14a8f826021605b9f2279587d290af176029003..04eb99cc1f07df6ca1c992586f60ef3ad7e23f22 100644
--- a/modules/field_ui/field_ui.api.php
+++ b/modules/field_ui/field_ui.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_ui.api.php,v 1.4 2010/01/09 23:15:26 webchick Exp $
+// $Id: field_ui.api.php,v 1.5 2010/04/26 14:40:47 dries Exp $
 
 /**
  * @file
@@ -142,6 +142,43 @@ function hook_field_widget_settings_form($field, $instance) {
 function hook_field_formatter_settings_form($formatter, $settings, $field, $instance) {
 }
 
+/**
+ * Provide information on view mode tabs for an entity type.
+ *
+ * @param $entity_type
+ *   The type of entity to return tabs for.
+ *
+ * @return
+ *   An array whose keys are internal-use tab names, and whose values are
+ *   arrays of tab information, with the following elements:
+ *   - 'title': Human-readable title of the tab.
+ *   - 'view modes': Array of view modes for this entity type that should
+ *     be displayed on this tab.
+ *
+ * @see field_ui_view_modes_tabs()
+ */
+function hook_field_ui_view_modes_tabs($entity_type) {
+  $modes = array(
+    'basic' => array(
+      'title' => t('Basic'),
+      'view modes' => array('teaser', 'full'),
+    ),
+    'rss' => array(
+      'title' => t('RSS'),
+      'view modes' => array('rss'),
+    ),
+    'print' => array(
+      'title' => t('Print'),
+      'view modes' => array('print'),
+    ),
+    'search' => array(
+      'title' => t('Search'),
+      'view modes' => array('search_index', 'search_result'),
+    ),
+  );
+  return $modes;
+}
+
 /**
  * @} End of "ingroup field_ui_field_type"
  */
diff --git a/modules/field_ui/field_ui.info b/modules/field_ui/field_ui.info
index ad06101bb6e3ae0afefd23ad9a3d21be637e23a4..b2331b32a5f7c3729d908468d9bcfae8323ea7ee 100644
--- a/modules/field_ui/field_ui.info
+++ b/modules/field_ui/field_ui.info
@@ -8,8 +8,8 @@ files[] = field_ui.module
 files[] = field_ui.admin.inc
 files[] = field_ui.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/field_ui/field_ui.module b/modules/field_ui/field_ui.module
index adcaacf88d4e8882aec01609455629b33fed99bf..c7c28615714f95a91b0e969184a3cb4739ad60af 100644
--- a/modules/field_ui/field_ui.module
+++ b/modules/field_ui/field_ui.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_ui.module,v 1.26 2010/02/22 16:29:34 webchick Exp $
+// $Id: field_ui.module,v 1.29 2010/04/26 14:40:47 dries Exp $
 
 /**
  * @file
@@ -71,12 +71,29 @@ function field_ui_menu() {
     if ($info['fieldable']) {
       foreach ($info['bundles'] as $bundle_name => $bundle_info) {
         if (isset($bundle_info['admin'])) {
-          // Extract informations from the bundle description.
+          // Extract path information from the bundle.
           $path = $bundle_info['admin']['path'];
-          $bundle_arg = isset($bundle_info['admin']['bundle argument']) ? $bundle_info['admin']['bundle argument'] : $bundle_name;
-          $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
+          // Different bundles can appear on the same path (e.g. %node_type and
+          // %comment_node_type). To allow field_ui_menu_load() to extract the
+          // actual bundle object from the translated menu router path
+          // arguments, we need to identify the argument position of the bundle
+          // name string ('bundle argument') and pass that position to the menu
+          // loader. The position needs to be casted into a string; otherwise it
+          // would be replaced with the bundle name string.
+          if (isset($bundle_info['admin']['bundle argument'])) {
+            $bundle_arg = $bundle_info['admin']['bundle argument'];
+            $bundle_pos = (string) $bundle_arg;
+          }
+          else {
+            $bundle_arg = $bundle_name;
+            $bundle_pos = '0';
+          }
+          // This is the position of the %field_ui_menu placeholder in the
+          // items below.
           $field_position = count(explode('/', $path)) + 1;
 
+          $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
+
           $items["$path/fields"] = array(
             'title' => 'Manage fields',
             'page callback' => 'drupal_get_form',
@@ -86,33 +103,45 @@ function field_ui_menu() {
             'file' => 'field_ui.admin.inc',
           ) + $access;
           $items["$path/fields/%field_ui_menu"] = array(
+            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+            'title callback' => 'field_ui_menu_title',
+            'title arguments' => array($field_position),
             'page callback' => 'drupal_get_form',
-            'page arguments' => array('field_ui_field_edit_form', $entity_type, $bundle_arg, $field_position),
+            'page arguments' => array('field_ui_field_edit_form', $field_position),
             'type' => MENU_LOCAL_TASK,
             'file' => 'field_ui.admin.inc',
           ) + $access;
           $items["$path/fields/%field_ui_menu/edit"] = array(
+            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+            'title' => 'Edit',
             'page callback' => 'drupal_get_form',
-            'page arguments' => array('field_ui_field_edit_form', $entity_type, $bundle_arg, $field_position),
+            'page arguments' => array('field_ui_field_edit_form', $field_position),
             'type' => MENU_DEFAULT_LOCAL_TASK,
             'file' => 'field_ui.admin.inc',
           ) + $access;
           $items["$path/fields/%field_ui_menu/field-settings"] = array(
+            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+            'title' => 'Field settings',
             'page callback' => 'drupal_get_form',
-            'page arguments' => array('field_ui_field_settings_form', $entity_type, $bundle_arg, $field_position),
+            'page arguments' => array('field_ui_field_settings_form', $field_position),
             'type' => MENU_LOCAL_TASK,
             'file' => 'field_ui.admin.inc',
           ) + $access;
           $items["$path/fields/%field_ui_menu/widget-type"] = array(
+            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+            'title' => 'Widget type',
             'page callback' => 'drupal_get_form',
-            'page arguments' => array('field_ui_widget_type_form', $entity_type, $bundle_arg, $field_position),
+            'page arguments' => array('field_ui_widget_type_form', $field_position),
             'type' => MENU_LOCAL_TASK,
             'file' => 'field_ui.admin.inc',
           ) + $access;
           $items["$path/fields/%field_ui_menu/delete"] = array(
+            'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+            'title' => 'Delete',
             'page callback' => 'drupal_get_form',
-            'page arguments' => array('field_ui_field_delete_form', $entity_type, $bundle_arg, $field_position),
+            'page arguments' => array('field_ui_field_delete_form', $field_position),
             'type' => MENU_LOCAL_TASK,
+            'weight' => 10,
             'file' => 'field_ui.admin.inc',
           ) + $access;
 
@@ -143,15 +172,52 @@ function field_ui_menu() {
 }
 
 /**
- * Menu loader; Load a field based on its name.
+ * Menu loader; Load a field instance based on field and bundle name.
+ *
+ * @param $field_name
+ *   The name of the field, as contained in the path.
+ * @param $entity_type
+ *   The name of the entity.
+ * @param $bundle_name
+ *   The name of the bundle, as contained in the path.
+ * @param $bundle_pos
+ *   The position of $bundle_name in $map.
+ * @param $map
+ *   The translated menu router path argument map.
  */
-function field_ui_menu_load($field_name) {
+function field_ui_menu_load($field_name, $entity_type, $bundle_name, $bundle_pos, $map) {
+  // Extract the actual bundle name from the translated argument map.
+  // The menu router path to manage fields of an entity can be shared among
+  // multiple bundles. For example:
+  // - admin/structure/types/manage/%node_type/fields/%field_ui_menu
+  // - admin/structure/types/manage/%comment_node_type/fields/%field_ui_menu
+  // The menu system will automatically load the correct bundle depending on the
+  // actual path arguments, but this menu loader function only receives the node
+  // type string as $bundle_name, which is not the bundle name for comments.
+  // We therefore leverage the dynamically translated $map provided by the menu
+  // system to retrieve the actual bundle and bundle name for the current path.
+  if ($bundle_pos > 0) {
+    $bundle = $map[$bundle_pos];
+    $bundle_name = field_extract_bundle($entity_type, $bundle);
+  }
+  // Check whether the field exists at all.
   if ($field = field_info_field($field_name)) {
-    return $field;
+    // Only return the field if a field instance exists for the given entity
+    // type and bundle.
+    if ($instance = field_info_instance($entity_type, $field_name, $bundle_name)) {
+      return $instance;
+    }
   }
   return FALSE;
 }
 
+/**
+ * Menu title callback.
+ */
+function field_ui_menu_title($instance) {
+  return t($instance['label']);
+}
+
 /**
  * Implements hook_theme().
  */
@@ -171,7 +237,28 @@ function field_ui_theme() {
 }
 
 /**
- * Group available view modes on tabs on the 'Manage display' page.
+ * Returns information about tab groups for view modes on an entity type.
+ *
+ * On the 'Manage display' page, an administrator can manage the display of
+ * fields for each view mode for an entity type. The view modes are organized
+ * into tabs. This function returns the list of entity types to display on
+ * each tab, as well as the titles of the tabs.
+ *
+ * @param $entity_type
+ *   The type of entity to return tab information for.
+ * @param $tab_selector
+ *   If not NULL, return only information for this particular tab; if NULL,
+ *   return all tab information.
+ *
+ * @return
+ *   Array of information about the tabs to display. The keys are internal-use
+ *   tab names and the values are arrays of tab information, with the following
+ *   elements:
+ *   - 'title': Human-readable title of the tab.
+ *   - 'view modes': Array of view modes for this entity type that should
+ *     be displayed on this tab.
+ *
+ * @see hook_field_ui_view_modes_tabs()
  *
  * @todo Remove this completely and use vertical tabs?
  */
@@ -205,23 +292,6 @@ function field_ui_view_modes_tabs($entity_type, $tab_selector = NULL) {
 
 /**
  * Implements hook_field_ui_view_modes_tabs() on behalf of other core modules.
- *
- * @return
- *   An array describing the view modes defined by the module, grouped by tabs.
- *
- * A module can add its render modes to a tab defined by another module.
- * Expected format:
- * @code
- *   array(
- *     'tab1' => array(
- *       'title' => t('The human-readable title of the tab'),
- *       'view modes' => array('mymodule_mode1', 'mymodule_mode2'),
- *     ),
- *     'tab2' => array(
- *       // ...
- *     ),
- *   );
- * @endcode
  */
 function field_ui_field_ui_view_modes_tabs() {
   $modes = array(
@@ -300,7 +370,7 @@ function field_ui_inactive_instances($entity_type, $bundle_name = NULL) {
     $inactive = array();
     $params = array();
   }
-  $params['object_type'] = $entity_type;
+  $params['entity_type'] = $entity_type;
 
   $active_instances = field_info_instances($entity_type);
   $all_instances = field_read_instances($params, array('include_inactive' => TRUE));
diff --git a/modules/field_ui/field_ui.test b/modules/field_ui/field_ui.test
index c0fbaf21d0a11f4c30a37866de6458e0d2a60409..d5601635241604ad5a6b92d6578994c55ce4e1d9 100644
--- a/modules/field_ui/field_ui.test
+++ b/modules/field_ui/field_ui.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: field_ui.test,v 1.12 2010/03/03 08:01:42 dries Exp $
+// $Id: field_ui.test,v 1.15 2010/04/07 17:30:43 dries Exp $
 
 /**
  * @file
@@ -92,8 +92,9 @@ class FieldUITestCase extends DrupalWebTestCase {
     // Assert the field appears in the "add existing field" section for
     // different entity types; e.g. if a field was added in a node entity, it
     // should also appear in the 'taxonomy term' entity.
-    $this->drupalGet('admin/structure/taxonomy/1/fields');
-    $this->assertTrue($this->xpath('//select[@id="edit--add-existing-field-field-name"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.'));
+    $vocabulary = taxonomy_vocabulary_load(1);
+    $this->drupalGet('admin/structure/taxonomy/' . $vocabulary->machine_name . '/fields');
+    $this->assertTrue($this->xpath('//select[@name="_add_existing_field[field_name]"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.'));
   }
 
   /**
@@ -178,7 +179,7 @@ class FieldUITestCase extends DrupalWebTestCase {
     field_create_field($field);
     $instance = array(
       'field_name' => $field_name,
-      'object_type' => 'node',
+      'entity_type' => 'node',
       'bundle' => $this->type,
     );
     field_create_instance($instance);
diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc
index 6612eae04fd00bae14efcd669d6cf858e27ae446..1a30a9d7a0d41351d5cdacbb883f946e536be017 100644
--- a/modules/file/file.field.inc
+++ b/modules/file/file.field.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: file.field.inc,v 1.24 2010/03/11 22:44:44 dries Exp $
+// $Id: file.field.inc,v 1.26 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -246,7 +246,7 @@ function file_field_prepare_view($entity_type, $entities, $field, $instances, $l
 /**
  * Implements hook_field_presave().
  */
-function file_field_presave($obj_type, $object, $field, $instance, $langcode, &$items) {
+function file_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
   // Make sure that each file which will be saved with this object has a
   // permanent status, so that it will not be removed when temporary files are
   // cleaned up.
@@ -696,7 +696,13 @@ function file_field_widget_process_multiple($element, &$form_state, $form) {
 }
 
 /**
- * Theme an individual file upload widget.
+ * Returns HTML for an individual file upload widget.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the widget.
+ *
+ * @ingroup themeable
  */
 function theme_file_widget($variables) {
   $element = $variables['element'];
@@ -715,7 +721,13 @@ function theme_file_widget($variables) {
 }
 
 /**
- * Theme a group of file upload widgets.
+ * Returns HTML for a group of file upload widgets.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the widgets.
+ *
+ * @ingroup themeable
  */
 function theme_file_widget_multiple($variables) {
   $element = $variables['element'];
@@ -800,7 +812,7 @@ function theme_file_widget_multiple($variables) {
 }
 
 /**
- * Generate help text based on upload validators.
+ * Returns HTML for help text based on file upload validators.
  *
  * @param $variables
  *   An associative array containing:
@@ -809,8 +821,7 @@ function theme_file_widget_multiple($variables) {
  *   - upload_validators: An array of upload validators as used in
  *     $element['#upload_validators'].
  *
- * @return
- *   A string suitable for a file field description.
+ * @ingroup themeable
  */
 function theme_file_upload_help($variables) {
   $description = $variables['description'];
@@ -882,7 +893,13 @@ function file_field_formatter_view($entity_type, $entity, $field, $instance, $la
 }
 
 /**
- * Theme function for the 'table' formatter.
+ * Returns HTML for a file attachments table.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - items: An array of file attachments.
+ *
+ * @ingroup themeable
  */
 function theme_file_formatter_table($variables) {
   $header = array(t('Attachment'), t('Size'));
diff --git a/modules/file/file.info b/modules/file/file.info
index 24a34ae64d767a6f5b614123cfb0928315301670..5e8edc90e2b068257588814de83ad4b77d407856 100644
--- a/modules/file/file.info
+++ b/modules/file/file.info
@@ -9,8 +9,8 @@ files[] = file.field.inc
 files[] = file.install
 files[] = tests/file.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/file/file.js b/modules/file/file.js
index f943105ca49c48f16ee8eab89ceaee88a1b33f23..837e15c4c67e8ea1a4aa1471b8dd2dd79d6df7e6 100644
--- a/modules/file/file.js
+++ b/modules/file/file.js
@@ -1,4 +1,4 @@
-// $Id: file.js,v 1.1 2009/08/29 12:52:32 dries Exp $
+// $Id: file.js,v 1.2 2010/04/21 07:40:37 webchick Exp $
 
 /**
  * @file
@@ -9,16 +9,16 @@
  * prevents separate file fields from accidentally uploading files).
  */
 
-(function($) {
+(function ($) {
 
 /**
  * Attach behaviors to managed file element upload fields.
  */
 Drupal.behaviors.fileValidateAutoAttach = {
-  attach: function(context) {
+  attach: function (context) {
     $('div.form-managed-file input.form-file[accept]', context).bind('change', Drupal.file.validateExtension);
   },
-  detach: function(context) {
+  detach: function (context) {
     $('div.form-managed-file input.form-file[accept]', context).unbind('change', Drupal.file.validateExtension);
   }
 };
@@ -27,11 +27,11 @@ Drupal.behaviors.fileValidateAutoAttach = {
  * Attach behaviors to the file upload and remove buttons.
  */
 Drupal.behaviors.fileButtons = {
-  attach: function(context) {
+  attach: function (context) {
     $('input.form-submit', context).bind('mousedown', Drupal.file.disableFields);
     $('div.form-managed-file input.form-submit', context).bind('mousedown', Drupal.file.progressBar);
   },
-  unattach: function(context) {
+  unattach: function (context) {
     $('input.form-submit', context).unbind('mousedown', Drupal.file.disableFields);
     $('div.form-managed-file input.form-submit', context).unbind('mousedown', Drupal.file.progressBar);
   }
@@ -41,10 +41,10 @@ Drupal.behaviors.fileButtons = {
  * Attach behaviors to links within managed file elements.
  */
 Drupal.behaviors.filePreviewLinks = {
-  attach: function(context) {
+  attach: function (context) {
     $('div.form-managed-file .file a, .file-widget .file a', context).bind('click',Drupal.file.openInNewWindow);
   },
-  detach: function(context){
+  detach: function (context){
     $('div.form-managed-file .file a, .file-widget .file a', context).unbind('click', Drupal.file.openInNewWindow);
   }
 };
@@ -56,7 +56,7 @@ Drupal.file = Drupal.file || {
   /**
    * Client-side file input validation based on the HTML "accept" attribute.
    */
-  validateExtension: function(event) {
+  validateExtension: function (event) {
     // Remove any previous errors.
     $('.file-upload-js-error').remove();
 
@@ -78,7 +78,7 @@ Drupal.file = Drupal.file || {
   /**
    * Prevent file uploads when using buttons not intended to upload.
    */
-  disableFields: function(event){
+  disableFields: function (event){
     var clickedButton = this;
 
     // Only disable upload fields for AJAX buttons.
@@ -101,14 +101,14 @@ Drupal.file = Drupal.file || {
     // excuted before any timeout functions will be called, so this effectively
     // re-enables the file fields after other processing is complete even though
     // it is only a 1 second timeout.
-    setTimeout(function(){
+    setTimeout(function (){
       $disabledFields.attr('disabled', '');
     }, 1000);
   },
   /**
    * Add progress bar support if possible.
    */
-  progressBar: function(event) {
+  progressBar: function (event) {
     var clickedButton = this;
     var $progressId = $(clickedButton).parents('div.form-managed-file').find('input.file-progress');
     if ($progressId.size()) {
@@ -118,19 +118,19 @@ Drupal.file = Drupal.file || {
       $progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
 
       // Restore the original name after the upload begins.
-      setTimeout(function() {
+      setTimeout(function () {
         $progressId.attr('name', originalName);
       }, 1000);
     }
     // Show the progress bar if the upload takes longer than half a second.
-    setTimeout(function() {
+    setTimeout(function () {
       $(clickedButton).parents('div.form-managed-file').find('div.ajax-progress-bar').slideDown();
     }, 500);
   },
   /**
    * Open links to files within forms in a new window.
    */
-  openInNewWindow: function(event) {
+  openInNewWindow: function (event) {
     $(this).attr('target', '_blank');
     window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');
     return false;
diff --git a/modules/file/file.module b/modules/file/file.module
index f61c1595299488c2e138a67d5254c1d61890a45f..b8f78b7775c6fb476a957ca99ad6aa7bff3d7943 100644
--- a/modules/file/file.module
+++ b/modules/file/file.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: file.module,v 1.20 2010/03/13 06:55:50 dries Exp $
+// $Id: file.module,v 1.25 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -239,7 +239,7 @@ function file_ajax_upload() {
 
   // This call recreates the form relying solely on the form_state that the
   // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
+  $form = drupal_rebuild_form($form_id, $form_state, $form);
 
   // Retrieve the element to be rendered.
   foreach ($form_parents as $parent) {
@@ -323,7 +323,7 @@ function file_progress_implementation() {
  * Implements hook_file_references().
  */
 function file_file_references($file) {
-  $count = file_get_file_reference_count($file);
+  $count = file_get_file_reference_count($file, NULL, 'file');
   return $count ? array('file' => $count) : NULL;
 }
 
@@ -385,12 +385,12 @@ function file_managed_file_process($element, &$form_state, $form) {
     '#weight' => -5,
   );
 
-  // Because the output of this field changes depending on the button clicked,
-  // we need to ask FAPI immediately if the remove button was clicked.
-  // It's not good that we call this private function, but
-  // $form_state['clicked_button'] is only available after this #process
-  // callback is finished.
-  if (_form_button_was_clicked($element['remove_button'], $form_state)) {
+  // @todo It is not good to call these private functions. This should be
+  //   refactored so that the file deletion happens during a submit handler,
+  //   and form changes affected by that (such as toggling the upload and remove
+  //   buttons) happens during the 2nd run of this function that is triggered by
+  //   a form rebuild: http://drupal.org/node/736298.
+  if (_form_button_was_clicked($element['remove_button'], $form_state) || _form_element_triggered_scripted_submission($element['remove_button'], $form_state)) {
     // If it's a temporary file we can safely remove it immediately, otherwise
     // it's up to the implementing module to clean up files that are in use.
     if ($element['#file'] && $element['#file']->status == 0) {
@@ -607,7 +607,13 @@ function file_managed_file_save_upload($element) {
 }
 
 /**
- * Theme a managed file element.
+ * Returns HTML for a managed file element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the file.
+ *
+ * @ingroup themeable
  */
 function theme_file_managed_file($variables) {
   $element = $variables['element'];
@@ -621,11 +627,13 @@ function theme_file_managed_file($variables) {
 }
 
 /**
- * Output a link to a file.
+ * Returns HTML for a link to a file.
  *
  * @param $variables
  *   An associative array containing:
  *   - file: A file object to which the link will be created.
+ *
+ * @ingroup themeable
  */
 function theme_file_link($variables) {
   $file = $variables['file'];
@@ -654,11 +662,13 @@ function theme_file_link($variables) {
 }
 
 /**
- * Return an image with an appropriate icon for the given file.
+ * Returns HTML for an image with an appropriate icon for the given file.
  *
  * @param $variables
  *   An associative array containing:
  *   - file: A file object for which to make an icon.
+ *
+ * @ingroup themeable
  */
 function theme_file_icon($variables) {
   $file = $variables['file'];
@@ -883,7 +893,7 @@ function file_icon_map($file) {
  * @return
  *   An integer value.
  */
-function file_get_file_reference_count($file, $field = NULL, $field_type = 'file') {
+function file_get_file_reference_count($file, $field = NULL, $field_type = NULL) {
   // Determine the collection of fields to check.
   if (isset($field)) {
     // Support $field as 'field name'.
@@ -915,7 +925,7 @@ function file_get_file_reference_count($file, $field = NULL, $field_type = 'file
         if (isset($file->file_field_type) && isset($file->file_field_id)) {
           if ($file->file_field_type == $entity_type) {
             $info = entity_get_info($entity_type);
-            $id = $types[$entity_type]['object keys']['id'];
+            $id = $types[$entity_type]['entity keys']['id'];
             foreach ($type_references as $reference) {
               if ($file->file_field_id == $reference->$id) {
                 $reference_count--;
diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test
index a776754384c644636605d579edb8591ea8e2032d..8e5955883dc07310ef17c966d43a083b22ee37ab 100644
--- a/modules/file/tests/file.test
+++ b/modules/file/tests/file.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: file.test,v 1.12 2010/02/27 07:52:03 dries Exp $
+// $Id: file.test,v 1.14 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -57,7 +57,7 @@ class FileFieldTestCase extends DrupalWebTestCase {
 
     $instance = array(
       'field_name' => $field['field_name'],
-      'object_type' => 'node',
+      'entity_type' => 'node',
       'label' => $name,
       'bundle' => $type_name,
       'required' => !empty($instance_settings['required']),
@@ -568,3 +568,79 @@ class FileFieldPathTestCase extends FileFieldTestCase {
     $this->assertTrue($result, $message);
   }
 }
+
+/**
+ * Test file token replacement in strings.
+ */
+class FileTokenReplaceTestCase extends FileFieldTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'File token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check file token replacement.',
+      'group' => 'File',
+    );
+  }
+
+  /**
+   * Creates a file, then tests the tokens generated from it.
+   */
+  function testFileTokenReplacement() {
+    global $language;
+    $url_options = array(
+      'absolute' => TRUE,
+      'language' => $language,
+    );
+
+    // Create file field.
+    $type_name = 'article';
+    $field_name = 'field_' . strtolower($this->randomName());
+    $this->createFileField($field_name, $type_name);
+    $field = field_info_field($field_name);
+    $instance = field_info_instance('node', $field_name, $type_name);
+
+    $test_file = $this->getTestFile('text');
+
+    // Create a new node with the uploaded file.
+    $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+
+    // Load the node and the file.
+    $node = node_load($nid);
+    $file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+    $file->description = 'File description.';
+
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[file:fid]'] = $file->fid;
+    $tests['[file:uid]'] = $file->uid;
+    $tests['[file:name]'] = check_plain($file->filename);
+    $tests['[file:description]'] = filter_xss($file->description);
+    $tests['[file:path]'] = filter_xss($file->uri);
+    $tests['[file:mime]'] = filter_xss($file->filemime);
+    $tests['[file:size]'] = format_size($file->filesize);
+    $tests['[file:url]'] = url(file_create_url($file->uri), $url_options);
+    $tests['[file:timestamp]'] = format_date($file->timestamp, 'medium', '', NULL, $language->language);
+    $tests['[file:timestamp:short]'] = format_date($file->timestamp, 'short', '', NULL, $language->language);
+    $tests['[file:owner]'] = $this->admin_user->name;
+    $tests['[file:owner:uid]'] = $this->admin_user->uid;
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('file' => $file), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized file token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[file:name]'] = $file->filename;
+    $tests['[file:description]'] = $file->description;
+    $tests['[file:path]'] = $file->uri;
+    $tests['[file:mime]'] = $file->filemime;
+    $tests['[file:size]'] = format_size($file->filesize);
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('file' => $file), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized file token %token replaced.', array('%token' => $input)));
+    }
+  }
+}
diff --git a/modules/file/tests/file_module_test.info b/modules/file/tests/file_module_test.info
index c15fdcad36f6fc2859e9763a14fc3ff5a7db2458..45a8f9930c6cb9f6be2ac7bc0694f43908cc9057 100644
--- a/modules/file/tests/file_module_test.info
+++ b/modules/file/tests/file_module_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = file_module_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/filter/filter.admin.inc b/modules/filter/filter.admin.inc
index 3f4fd76cd693a8b2bcbad1a71c809259ca65b14c..59a1200ae032157be90970da1e10e32561584aa8 100644
--- a/modules/filter/filter.admin.inc
+++ b/modules/filter/filter.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: filter.admin.inc,v 1.58 2010/03/06 19:40:21 dries Exp $
+// $Id: filter.admin.inc,v 1.61 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -28,7 +28,7 @@ function filter_admin_overview($form) {
     }
     else {
       $form['formats'][$id]['name'] = array('#markup' => check_plain($format->name));
-      $roles = filter_get_roles_by_format($format);
+      $roles = array_map('check_plain', filter_get_roles_by_format($format));
       $roles_markup = $roles ? implode(', ', $roles) : t('No roles may use this format');
     }
     $form['formats'][$id]['roles'] = array('#markup' => $roles_markup);
@@ -36,7 +36,7 @@ function filter_admin_overview($form) {
     $form['formats'][$id]['delete'] = array('#type' => 'link', '#title' => t('delete'), '#href' => 'admin/config/content/formats/' . $id . '/delete', '#access' => !$form['formats'][$id]['#is_fallback']);
     $form['formats'][$id]['weight'] = array('#type' => 'weight', '#default_value' => $format->weight);
   }
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save changes'));
   return $form;
 }
@@ -56,7 +56,11 @@ function filter_admin_overview_submit($form, &$form_state) {
 }
 
 /**
- * Theme the text format administration overview form.
+ * Returns HTML for the text format administration overview form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -123,7 +127,7 @@ function filter_admin_format_form($form, &$form_state, $format) {
   $form['roles'] = array(
     '#type' => 'checkboxes',
     '#title' => t('Roles'),
-    '#options' => user_roles(),
+    '#options' => array_map('check_plain', user_roles()),
     '#disabled' => $is_fallback,
   );
   if ($is_fallback) {
@@ -226,14 +230,18 @@ function filter_admin_format_form($form, &$form_state, $format) {
   if (!empty($format->format)) {
     $form['format'] = array('#type' => 'value', '#value' => $format->format);
   }
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
 
   return $form;
 }
 
 /**
- * Theme text format filter order form elements as tabledrag.
+ * Returns HTML for a text format's filter order form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the form.
  *
  * @ingroup themeable
  */
diff --git a/modules/filter/filter.admin.js b/modules/filter/filter.admin.js
index d1005c8762322206364a392fc96ecfd419aee7ed..bb90755c687b17aad7623326311ad4dafd58158c 100644
--- a/modules/filter/filter.admin.js
+++ b/modules/filter/filter.admin.js
@@ -1,46 +1,6 @@
-// $Id: filter.admin.js,v 1.2 2010/03/12 22:31:37 webchick Exp $
-
+// $Id: filter.admin.js,v 1.4 2010/04/16 13:55:06 dries Exp $
 (function ($) {
 
-/**
- * Shows the vertical tab pane.
- */
-Drupal.verticalTab.prototype.tabShow = function () {
-  // Display the tab.
-  this.item.show();
-  // Update .first marker for items. We need recurse from parent to retain the
-  // actual DOM element order as jQuery implements sortOrder, but not as public
-  // method.
-  this.item.parent().children('.vertical-tab-button').removeClass('first')
-    .filter(':visible:first').addClass('first');
-  // Display the fieldset.
-  this.fieldset.removeClass('filter-settings-hidden').show();
-  // Focus this tab.
-  this.focus();
-  return this;
-};
-
-/**
- * Hides the vertical tab pane.
- */
-Drupal.verticalTab.prototype.tabHide = function () {
-  // Hide this tab.
-  this.item.hide();
-  // Update .first marker for items. We need recurse from parent to retain the
-  // actual DOM element order as jQuery implements sortOrder, but not as public
-  // method.
-  this.item.parent().children('.vertical-tab-button').removeClass('first')
-    .filter(':visible:first').addClass('first');
-  // Hide the fieldset.
-  this.fieldset.addClass('filter-settings-hidden').hide();
-  // Focus the first visible tab (if there is one).
-  var $firstTab = this.fieldset.siblings('.vertical-tabs-pane:not(.filter-settings-hidden):first');
-  if ($firstTab.length) {
-    $firstTab.data('verticalTab').focus();
-  }
-  return this;
-};
-
 Drupal.behaviors.filterStatus = {
   attach: function (context, settings) {
     $('#filters-status-wrapper input.form-checkbox', context).once('filter-status', function () {
@@ -71,7 +31,7 @@ Drupal.behaviors.filterStatus = {
 
       // Attach summary for configurable filters (only for screen-readers).
       if (tab) {
-        tab.fieldset.setSummary(function (tabContext) {
+        tab.fieldset.drupalSetSummary(function (tabContext) {
           return $checkbox.is(':checked') ? Drupal.t('Enabled') : Drupal.t('Disabled');
         });
       }
diff --git a/modules/filter/filter.api.php b/modules/filter/filter.api.php
index 0e6ef5d9bfc3293ab3d326b7a2c5e021178a04b8..a9e95771c8ea7deee83ec40cf08555d1da542fd4 100644
--- a/modules/filter/filter.api.php
+++ b/modules/filter/filter.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: filter.api.php,v 1.18 2010/03/08 05:21:23 webchick Exp $
+// $Id: filter.api.php,v 1.19 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -208,8 +208,8 @@ function hook_filter_info_alter(&$info) {
  * @param $format
  *   The format object of the format being updated.
  *
- * @see hook_filter_format_update().
- * @see hook_filter_format_delete().
+ * @see hook_filter_format_update()
+ * @see hook_filter_format_delete()
  */
 function hook_filter_format_insert($format) {
   mymodule_cache_rebuild();
@@ -225,8 +225,8 @@ function hook_filter_format_insert($format) {
  * @param $format
  *   The format object of the format being updated.
  *
- * @see hook_filter_format_insert().
- * @see hook_filter_format_delete().
+ * @see hook_filter_format_insert()
+ * @see hook_filter_format_delete()
  */
 function hook_filter_format_update($format) {
   mymodule_cache_rebuild();
@@ -245,8 +245,8 @@ function hook_filter_format_update($format) {
  *   The format object of the site's fallback format, which is always available
  *   to all users.
  *
- * @see hook_filter_format_insert().
- * @see hook_filter_format_update().
+ * @see hook_filter_format_insert()
+ * @see hook_filter_format_update()
  */
 function hook_filter_format_delete($format, $fallback) {
   // Replace the deleted format with the fallback format.
diff --git a/modules/filter/filter.info b/modules/filter/filter.info
index 9b2943c4b1ed0734faabe2ce76c8cb2a606e8645..067d223b93ed3574f9e1adf26b8362b8759c336c 100644
--- a/modules/filter/filter.info
+++ b/modules/filter/filter.info
@@ -12,8 +12,8 @@ files[] = filter.test
 required = TRUE
 configure = admin/config/content/formats
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/filter/filter.install b/modules/filter/filter.install
index e295574e0f5b379f62dbbd67fe2d6e693d39765f..c4cf99fd76066beda04d51a6989997901be61909 100644
--- a/modules/filter/filter.install
+++ b/modules/filter/filter.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: filter.install,v 1.35 2010/03/12 14:31:01 dries Exp $
+// $Id: filter.install,v 1.36 2010/04/22 05:44:41 webchick Exp $
 
 /**
  * @file
@@ -220,6 +220,13 @@ function filter_update_7002() {
  * Remove hardcoded numeric deltas from all filters in core.
  */
 function filter_update_7003() {
+  // Duplicates the filter table since core cannot take care of the potential
+  // contributed module filters.
+  db_rename_table('filter', 'd6_upgrade_filter');
+  // Creates the Drupal 7 filter table.
+  $schema = filter_schema();
+  db_create_table('filter', $schema['filter']);
+
   // Get an array of the renamed filter deltas, organized by module.
   $renamed_deltas = array(
     'filter' => array(
@@ -234,33 +241,28 @@ function filter_update_7003() {
     ),
   );
 
-  // The unique key on (filter, module, delta) is not necessary anymore,
-  // as filter_update_7004() will add a primary key on (filter, name).
-  db_drop_unique_key('filter', 'fmd');
-
-  // Rename field 'delta' to 'name'.
-  db_change_field('filter', 'delta', 'name',
-    array(
-      'type' => 'varchar',
-      'length' => 32,
-      'not null' => TRUE,
-      'default' => '',
-      'description' => 'Name of the filter being referenced.',
-    ),
-    array(
-      'indexes' => array(
-        'list' => array('weight', 'module', 'name'),
-      ),
-    )
-  );
-
-  // Loop through each filter and make changes to the core filter table.
+  // Loop through each filter and make changes to the core filter table by
+  // each record from the old to the new table.
   foreach ($renamed_deltas as $module => $deltas) {
-    foreach ($deltas as $old_delta => $new_delta) {
-      db_update('filter')
-        ->fields(array('name' => $new_delta))
+    foreach ($deltas as $old_delta => $new_name) {
+      $query = db_select('d6_upgrade_filter');
+      $query->fields('d6_upgrade_filter', array('format', 'weight'));
+      $query->condition('module', $module);
+      $query->condition('delta', $old_delta);
+      $result = $query->execute();
+      foreach ($result as $record) {
+        db_insert('filter')
+          ->fields(array(
+            'format' => $record->format,
+            'module' => $module,
+            'name' => $new_name,
+            'weight' => $record->weight,
+          ))
+          ->execute();
+      }
+      db_delete('d6_upgrade_filter')
         ->condition('module', $module)
-        ->condition('name', $old_delta)
+        ->condition('delta', $old_delta)
         ->execute();
     }
   }
@@ -268,33 +270,8 @@ function filter_update_7003() {
 
 /**
  * Move filter settings storage into {filter} table.
- *
- * - Remove {filter}.fid.
- * - Add (format, name) as primary key for {filter}.
- * - Add {filter}.status.
- * - Add {filter}.settings.
  */
 function filter_update_7004() {
-  db_drop_field('filter', 'fid');
-  db_add_primary_key('filter', array('format', 'name'));
-  db_add_field('filter', 'status',
-    array(
-      'type' => 'int',
-      'not null' => TRUE,
-      'default' => 0,
-      'description' => 'Filter enabled status. (1 = enabled, 0 = disabled)',
-    )
-  );
-  db_add_field('filter', 'settings',
-    array(
-      'type' => 'text',
-      'not null' => FALSE,
-      'size' => 'big',
-      'serialize' => TRUE,
-      'description' => 'A serialized array of name value pairs that store the filter settings for the specific format.',
-    )
-  );
-
   // Enable all existing filters ({filter} contained only enabled previously).
   db_update('filter')
     ->fields(array('status' => '1'))
diff --git a/modules/filter/filter.module b/modules/filter/filter.module
index 3c09243e0574de80284f58b8a88aa6591ad3ad0a..bcd8605ae036a5486e25bfa7eecae45a450f68c8 100644
--- a/modules/filter/filter.module
+++ b/modules/filter/filter.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: filter.module,v 1.322 2010/03/08 03:59:25 webchick Exp $
+// $Id: filter.module,v 1.328 2010/04/24 14:53:59 dries Exp $
 
 /**
  * @file
@@ -290,7 +290,7 @@ function filter_admin_format_title($format) {
 function filter_permission() {
   $perms['administer filters'] = array(
     'title' => t('Administer text formats and filters'),
-    'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+    'restrict access' => TRUE,
   );
 
   // Generate permissions for each text format. Warn the administrator that any
@@ -668,6 +668,8 @@ function filter_list_format($format_id) {
  *   Boolean whether to cache the filtered output in the {cache_filter} table.
  *   The caller may set this to FALSE when the output is already cached
  *   elsewhere to avoid duplicate cache lookups and storage.
+ *
+ * @ingroup sanitization
  */
 function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) {
   if (empty($format_id)) {
@@ -849,6 +851,38 @@ function filter_process_format($element) {
     '#weight' => 0,
   );
 
+  // Lastly, disallow editing of this field if the user is not allowed to use
+  // the stored and preselected text format. But only, if that format actually
+  // exists.
+  $all_formats = filter_formats();
+  if (!isset($formats[$element['#format']]) && isset($all_formats[$element['#format']])) {
+    // Overload default values into #value to make them unalterable.
+    $element['value']['#value'] = $element['value']['#default_value'];
+    $element['format']['format']['#value'] = $element['format']['format']['#default_value'];
+
+    // Prepend #pre_render callback to replace field value with user notice
+    // prior to rendering.
+    if (!isset($element['value']['#pre_render'])) {
+      $element['value']['#pre_render'] = array();
+    }
+    array_unshift($element['value']['#pre_render'], 'filter_form_access_denied');
+
+    // Cosmetic adjustments.
+    if (isset($element['value']['#rows'])) {
+      $element['value']['#rows'] = 3;
+    }
+    $element['value']['#disabled'] = TRUE;
+    $element['value']['#resizable'] = FALSE;
+
+    // Hide the text format selector and any other child element (such as text
+    // field's summary).
+    foreach (element_children($element) as $key) {
+      if ($key != 'value') {
+        $element[$key]['#access'] = FALSE;
+      }
+    }
+  }
+
   return $element;
 }
 
@@ -885,15 +919,27 @@ function filter_form_after_build($element, &$form_state) {
 }
 
 /**
- * Render a text format-enabled form element.
+ * #pre_render callback for #type 'text_format' to hide field value from prying eyes.
+ *
+ * To not break form processing and previews if a user does not have access to a
+ * stored text format, the expanded form elements in filter_process_format() are
+ * forced to take over the stored #default_values for 'value' and 'format'.
+ * However, to prevent the unfiltered, original #value from being displayed to
+ * the user, we replace it with a friendly notice here.
+ *
+ * @see filter_process_format()
+ */
+function filter_form_access_denied($element) {
+  $element['#value'] = t('This field has been disabled because you do not have sufficient permissions to edit it.');
+  return $element;
+}
+
+/**
+ * Returns HTML for a text format-enabled form element.
  *
  * @param $variables
  *   An associative array containing:
- *   - element: An associative array containing the properties of the element.
- *     Properties used: #children, #description
- *
- * @return
- *   A string representing the form element.
+ *   - element: A render element containing #children and #description.
  *
  * @ingroup themeable
  */
@@ -983,7 +1029,7 @@ function _filter_tips($format_id, $long = FALSE) {
  */
 function filter_dom_load($text) {
   // Ignore warnings during HTML soup loading.
-  $dom_document = @DOMDocument::loadHTML('<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $text . '</body></html>');
+  $dom_document = @DOMDocument::loadHTML('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $text . '</body></html>');
 
   return $dom_document;
 }
@@ -1018,7 +1064,7 @@ function filter_dom_serialize($dom_document) {
   foreach ($body_node->childNodes as $child_node) {
     $body_content .= $dom_document->saveXML($child_node);
   }
-  return preg_replace('|<([^>]*)/>|i', '<$1 />', $body_content);
+  return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content);
 }
 
 /**
@@ -1043,7 +1089,7 @@ function filter_dom_serialize($dom_document) {
 function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element, $comment_start = '//', $comment_end = '') {
   foreach ($dom_element->childNodes as $node) {
     if (get_class($node) == 'DOMCdataSection') {
-      // @see drupal_get_js().  This code is more or less duplicated there.
+      // See drupal_get_js().  This code is more or less duplicated there.
       $embed_prefix = "\n<!--{$comment_start}--><![CDATA[{$comment_start} ><!--{$comment_end}\n";
       $embed_suffix = "\n{$comment_start}--><!]]>{$comment_end}\n";
       $fragment = $dom_document->createDocumentFragment();
@@ -1055,7 +1101,7 @@ function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element,
 }
 
 /**
- * Format a link to the more extensive filter tips.
+ * Returns HTML for a link to the more extensive filter tips.
  *
  * @ingroup themeable
  */
@@ -1064,7 +1110,7 @@ function theme_filter_tips_more_info() {
 }
 
 /**
- * Format guidelines for a text format.
+ * Returns HTML for guidelines for a text format.
  *
  * @param $variables
  *   An associative array containing:
diff --git a/modules/filter/filter.pages.inc b/modules/filter/filter.pages.inc
index 1add777fbbdfd1ae1ff3397cca3016e572fc1e76..76ad21d0a5d21b5a778d3be4a4b65f8fb5ba38a9 100644
--- a/modules/filter/filter.pages.inc
+++ b/modules/filter/filter.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: filter.pages.inc,v 1.9 2009/10/13 15:39:41 dries Exp $
+// $Id: filter.pages.inc,v 1.10 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -23,7 +23,7 @@ function filter_tips_long() {
 
 
 /**
- * Render HTML for a set of filter tips.
+ * Returns HTML for a set of filter tips.
  *
  * @param $variables
  *   An associative array containing:
diff --git a/modules/filter/filter.test b/modules/filter/filter.test
index d2990ef0caaf1954f4ec5cc65be3c9f38e41ceee..5c26585023d74666c9768c9ba0d586930bbd1789 100644
--- a/modules/filter/filter.test
+++ b/modules/filter/filter.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: filter.test,v 1.62 2010/03/07 23:14:20 webchick Exp $
+// $Id: filter.test,v 1.66 2010/04/11 18:33:44 dries Exp $
 
 /**
  * Tests for text format and filter CRUD operations.
@@ -238,7 +238,10 @@ class FilterAdminTestCase extends DrupalWebTestCase {
     $result = db_query('SELECT * FROM {cache_filter}')->fetchObject();
     $this->assertFalse($result, t('Cache cleared.'));
 
-    $elements = $this->xpath('//select[@name="filters[' . $first_filter . '][weight]"]/following::select[@name="filters[' . $second_filter . '][weight]"]');
+    $elements = $this->xpath('//select[@name=:first]/following::select[@name=:second]', array(
+      ':first' => 'filters[' . $first_filter . '][weight]',
+      ':second' => 'filters[' . $second_filter . '][weight]',
+    ));
     $this->assertTrue(!empty($elements), t('Order confirmed in admin interface.'));
 
     // Reorder filters.
@@ -249,7 +252,10 @@ class FilterAdminTestCase extends DrupalWebTestCase {
     $this->assertFieldByName('filters[' . $second_filter . '][weight]', 1, t('Order saved successfully.'));
     $this->assertFieldByName('filters[' . $first_filter . '][weight]', 2, t('Order saved successfully.'));
 
-    $elements = $this->xpath('//select[@name="filters[' . $second_filter . '][weight]"]/following::select[@name="filters[' . $first_filter . '][weight]"]');
+    $elements = $this->xpath('//select[@name=:first]/following::select[@name=:second]', array(
+      ':first' => 'filters[' . $second_filter . '][weight]',
+      ':second' => 'filters[' . $first_filter . '][weight]',
+    ));
     $this->assertTrue(!empty($elements), t('Reorder confirmed in admin interface.'));
 
     $result = db_query('SELECT * FROM {filter} WHERE format = :format ORDER BY weight ASC', array(':format' => $filtered));
@@ -389,7 +395,7 @@ class FilterAdminTestCase extends DrupalWebTestCase {
   }
 }
 
-class FilterAccessTestCase extends DrupalWebTestCase {
+class FilterFormatAccessTestCase extends DrupalWebTestCase {
   protected $admin_user;
   protected $web_user;
   protected $allowed_format;
@@ -397,8 +403,8 @@ class FilterAccessTestCase extends DrupalWebTestCase {
 
   public static function getInfo() {
     return array(
-      'name' => 'Filter access functionality',
-      'description' => 'Test the filter access system.',
+      'name' => 'Filter format access',
+      'description' => 'Tests access to text formats.',
       'group' => 'Filter',
     );
   }
@@ -406,8 +412,15 @@ class FilterAccessTestCase extends DrupalWebTestCase {
   function setUp() {
     parent::setUp();
 
+    $this->full_html_format = db_query_range('SELECT * FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Full HTML'))->fetchObject();
+
     // Create two text formats and grant a regular user access to one of them.
-    $this->admin_user = $this->drupalCreateUser(array('administer filters'));
+    $this->admin_user = $this->drupalCreateUser(array(
+      'administer filters',
+      'create page content',
+      'edit any page content',
+      filter_permission_name($this->full_html_format),
+    ));
     $this->drupalLogin($this->admin_user);
     $formats = array();
     for ($i = 0; $i < 2; $i++) {
@@ -418,7 +431,11 @@ class FilterAccessTestCase extends DrupalWebTestCase {
       $formats[] = filter_format_load($format_id);
     }
     list($this->allowed_format, $this->disallowed_format) = $formats;
-    $this->web_user = $this->drupalCreateUser(array('create page content', filter_permission_name($this->allowed_format)));
+
+    $this->web_user = $this->drupalCreateUser(array(
+      'create page content',
+      filter_permission_name($this->allowed_format),
+    ));
   }
 
   function testFormatPermissions() {
@@ -468,6 +485,61 @@ class FilterAccessTestCase extends DrupalWebTestCase {
     $this->assertTrue(in_array(filter_fallback_format(), array_keys(filter_get_formats_by_role($rid))), t('The fallback format appears in the list of allowed formats for any role.'));
   }
 
+  /**
+   * Test editing a page using a disallowed text format.
+   *
+   * Verifies that a regular user is able to edit a page, but is not allowed to
+   * change the fields which use an inaccessible text format.
+   */
+  function testFormatWidgetPermissions() {
+    $langcode = LANGUAGE_NONE;
+    $title_key = "title";
+    $body_value_key = "body[$langcode][0][value]";
+    $body_format_key = "body[$langcode][0][format]";
+
+    // Create node to edit.
+    $this->drupalLogin($this->admin_user);
+    $edit = array();
+    $edit['title'] = $this->randomName(8);
+    $edit[$body_value_key] = $this->randomName(16);
+    $edit[$body_format_key] = $this->full_html_format->format;
+    $this->drupalPost('node/add/page', $edit, t('Save'));
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+
+    // Try to edit with a less privileged user.
+    $this->moderator = $this->drupalCreateUser(array(
+      'edit any page content',
+      'create page content',
+    ));
+    $this->drupalLogin($this->moderator);
+    $this->drupalGet('node/' . $node->nid);
+    $this->clickLink(t('Edit'));
+
+    // Verify that body field is read-only and contains replacement value.
+    $this->assertFieldByXPath("//textarea[@name='$body_value_key' and @disabled='disabled']", t('This field has been disabled because you do not have sufficient permissions to edit it.'), t('Text format access denied message found.'));
+
+    // Verify that title can be changed, but preview displays original body.
+    $new_edit = array();
+    $new_edit['title'] = $this->randomName(8);
+    $this->drupalPost(NULL, $new_edit, t('Preview'));
+    $this->assertText($edit[$body_value_key], t('Old body found in preview.'));
+
+    // Save and verify that only the title was changed.
+    $this->drupalPost(NULL, $new_edit, t('Save'));
+    $this->assertNoText($edit['title'], t('Old title not found.'));
+    $this->assertText($new_edit['title'], t('New title found.'));
+    $this->assertText($edit[$body_value_key], t('Old body found.'));
+
+    // Delete the Full HTML text format.
+    filter_format_delete($this->full_html_format);
+    $this->resetFilterCaches();
+
+    // Verify that body field can be edited and a new format can be selected.
+    $this->drupalGet('node/' . $node->nid . '/edit');
+    $this->assertNoFieldByXPath("//textarea[@name='$body_value_key' and @disabled='disabled']", NULL, t('Text format access denied message not found.'));
+    $this->assertFieldByXPath("//select[@name='$body_format_key']", NULL, t('Text format selector found.'));
+  }
+
   /**
    * Returns the expected HTML for a particular text format selector.
    *
@@ -997,7 +1069,7 @@ class FilterUnitTestCase extends DrupalUnitTestCase {
     $this->assertEqual($f, '<p>text</p>', t('HTML corrector -- tag closing at the end of input.'));
 
     $f = _filter_htmlcorrector('<p>text<p><p>text');
-    $this->assertEqual($f, '<p>text</p><p /><p>text</p>', t('HTML corrector -- tag closing.'));
+    $this->assertEqual($f, '<p>text</p><p></p><p>text</p>', t('HTML corrector -- tag closing.'));
 
     $f = _filter_htmlcorrector("<ul><li>e1<li>e2");
     $this->assertEqual($f, "<ul><li>e1</li><li>e2</li></ul>", t('HTML corrector -- unclosed list tags.'));
@@ -1015,14 +1087,17 @@ class FilterUnitTestCase extends DrupalUnitTestCase {
     $f = _filter_htmlcorrector('<P>test</p>');
     $this->assertEqual($f, '<p>test</p>', t('HTML corrector -- Convert uppercased tags to proper lowercased ones.'));
 
+    $f = _filter_htmlcorrector('test<hr />');
+    $this->assertEqual($f, 'test<hr />', t('HTML corrector -- Let proper XHTML pass through.'));
+
     $f = _filter_htmlcorrector('test<hr/>');
-    $this->assertEqual($f, 'test<hr />', t('HTML corrector -- Let proper XHTML pass thru.'));
+    $this->assertEqual($f, 'test<hr />', t('HTML corrector -- Let proper XHTML pass through, but ensure there is a single space before the closing slash.'));
 
-    $f = _filter_htmlcorrector('test<hr />');
-    $this->assertEqual($f, 'test<hr />', t('HTML corrector -- Let proper XHTML pass thru.'));
+    $f = _filter_htmlcorrector('test<hr    />');
+    $this->assertEqual($f, 'test<hr />', t('HTML corrector -- Let proper XHTML pass through, but ensure there are not too many spaces before the closing slash.'));
 
     $f = _filter_htmlcorrector('<span class="test" />');
-    $this->assertEqual($f, '<span class="test" />', t('HTML corrector -- Let proper XHTML pass thru.'));
+    $this->assertEqual($f, '<span class="test"></span>', t('HTML corrector -- Convert XHTML that is properly formed but that would not be compatible with typical HTML user agents.'));
 
     $f = _filter_htmlcorrector('test1<br class="test">test2');
     $this->assertEqual($f, 'test1<br class="test" />test2', t('HTML corrector -- Automatically close single tags.'));
@@ -1036,6 +1111,12 @@ class FilterUnitTestCase extends DrupalUnitTestCase {
     $f = _filter_htmlcorrector('<img src="http://example.com/test.jpg">test</img>');
     $this->assertEqual($f, '<img src="http://example.com/test.jpg" />test', t('HTML corrector -- Automatically close single tags.'));
 
+    $f = _filter_htmlcorrector('<br></br>');
+    $this->assertEqual($f, '<br />', t("HTML corrector -- Transform empty tags to a single closed tag if the tag's content model is EMPTY."));
+
+    $f = _filter_htmlcorrector('<div></div>');
+    $this->assertEqual($f, '<div></div>', t("HTML corrector -- Do not transform empty tags to a single closed tag if the tag's content model is not EMPTY."));
+
     $f = _filter_htmlcorrector('<p>line1<br/><hr/>line2</p>');
     $this->assertEqual($f, '<p>line1<br /></p><hr />line2', t('HTML corrector -- Move non-inline elements outside of inline containers.'));
 
@@ -1163,7 +1244,8 @@ alert("test")
     $this->assertEqual($f, '&#039;', t('The @function() function correctly filters single quotes.', $replacements));
 
     // Test that the filter is not fooled by different evasion techniques.
-    $f = $function("\xc2\"");
+    // Ignore PHP 5.3+ invalid multibyte sequence warning.
+    $f = @$function("\xc2\"");
     $this->assertEqual($f, '', t('The @function() function correctly filters invalid UTF-8.', $replacements));
   }
 }
diff --git a/modules/forum/forum-topic-list.tpl.php b/modules/forum/forum-topic-list.tpl.php
index cd0198dbd8ee71dacd097edd45b45596275a7076..9c8ce3e4169d3c94e639edf1fbca9b2aa0bc96ed 100644
--- a/modules/forum/forum-topic-list.tpl.php
+++ b/modules/forum/forum-topic-list.tpl.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: forum-topic-list.tpl.php,v 1.8 2009/10/08 07:58:45 webchick Exp $
+// $Id: forum-topic-list.tpl.php,v 1.9 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -7,7 +7,8 @@
  *
  * Available variables:
  * - $header: The table header. This is pre-generated with click-sorting
- *   information. If you need to change this, @see template_preprocess_forum_topic_list().
+ *   information. If you need to change this, see 
+ *   template_preprocess_forum_topic_list().
  * - $pager: The pager to display beneath the table.
  * - $topics: An array of topics to be displayed.
  * - $topic_id: Numeric id for the current forum topic.
diff --git a/modules/forum/forum.admin.inc b/modules/forum/forum.admin.inc
index 5a004f4d39d797b750280c9423c2507f82332cbf..8c1c262dd73a6404640ddf74608d2c8aa1fc6d3b 100644
--- a/modules/forum/forum.admin.inc
+++ b/modules/forum/forum.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: forum.admin.inc,v 1.30 2010/03/10 17:59:00 webchick Exp $
+// $Id: forum.admin.inc,v 1.33 2010/04/24 14:49:13 dries Exp $
 
 /**
  * @file
@@ -55,7 +55,7 @@ function forum_form_forum($form, &$form_state, $edit = array()) {
   );
 
   $form['vid'] = array('#type' => 'hidden', '#value' => variable_get('forum_nav_vocabulary', ''));
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit' ] = array('#type' => 'submit', '#value' => t('Save'));
   if ($edit['tid']) {
     $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
@@ -93,6 +93,8 @@ function forum_form_submit($form, &$form_state) {
       break;
     case SAVED_UPDATED:
       drupal_set_message(t('The @type %term has been updated.', array('%term' => $form_state['values']['name'], '@type' => $type)));
+      // Clear the page and block caches to avoid stale data.
+      cache_clear_all();
       break;
   }
   $form_state['redirect'] = 'admin/structure/forum';
@@ -100,14 +102,15 @@ function forum_form_submit($form, &$form_state) {
 }
 
 /**
- * Theme forum forms.
+ * Returns HTML for a forum form.
  *
  * By default this does not alter the appearance of a form at all,
  * but is provided as a convenience for themers.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An associative array containing the structure of the form.
+ *   - form: A render element representing the form.
+ *
  * @ingroup themeable
  */
 function theme_forum_form($variables) {
@@ -157,7 +160,7 @@ function forum_form_container($form, &$form_state, $edit = array()) {
     '#type' => 'hidden',
     '#value' => variable_get('forum_nav_vocabulary', ''),
   );
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save')
diff --git a/modules/forum/forum.info b/modules/forum/forum.info
index ea755f918379420095f3101aa6396a4ca7c59012..7b7a88c570ee87945908060f6d7a8ea21c4ca56b 100644
--- a/modules/forum/forum.info
+++ b/modules/forum/forum.info
@@ -13,8 +13,8 @@ files[] = forum.install
 files[] = forum.test
 configure = admin/structure/forum
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/forum/forum.install b/modules/forum/forum.install
index 3b3b59dc29ffc4d0d7d940e00d0ff65e385f3cda..49aa336bb83b7f49bc3c809294876ff8cd275ae3 100644
--- a/modules/forum/forum.install
+++ b/modules/forum/forum.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: forum.install,v 1.42 2010/02/11 03:29:22 webchick Exp $
+// $Id: forum.install,v 1.45 2010/04/26 14:32:27 dries Exp $
 
 /**
  * @file
@@ -20,6 +20,9 @@ function forum_install() {
   variable_set('node_options_forum', array('status'));
 }
 
+/**
+ * Implements hook_enable().
+ */
 function forum_enable() {
   // Create the forum vocabulary if it does not exist.
   $vocabulary = taxonomy_vocabulary_load(variable_get('forum_nav_vocabulary', 0));
@@ -55,9 +58,10 @@ function forum_enable() {
 
     $instance = array(
       'field_name' => 'taxonomy_' . $vocabulary->machine_name,
-      'object_type' => 'node',
+      'entity_type' => 'node',
       'label' => $vocabulary->name,
       'bundle' => 'forum',
+      'required' => TRUE,
       'widget' => array(
         'type' => 'options_select',
       ),
@@ -65,6 +69,16 @@ function forum_enable() {
     field_create_instance($instance);
 
     variable_set('forum_nav_vocabulary', $vocabulary->vid);
+
+    // Create a default forum so forum posts can be created.
+    $edit = array(
+      'name' => t('General discussion'),
+      'description' => '',
+      'parent' => array(0),
+      'vid' => $vocabulary->vid,
+    );
+    $term = (object) $edit;
+    taxonomy_term_save($term);
   }
 }
 
diff --git a/modules/forum/forum.module b/modules/forum/forum.module
index d82e7b5382ac9ed719d8ce36fbf95d04a21b0f36..a9088ed08c26d1df2f5bce401dbb5f335b6bb986 100644
--- a/modules/forum/forum.module
+++ b/modules/forum/forum.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: forum.module,v 1.558 2010/03/08 15:40:25 webchick Exp $
+// $Id: forum.module,v 1.564 2010/04/26 14:32:27 dries Exp $
 
 /**
  * @file
@@ -218,6 +218,36 @@ function forum_init() {
   drupal_add_css(drupal_get_path('module', 'forum') . '/forum.css');
 }
 
+/**
+ * Implements hook_entity_info_alter().
+ */
+function forum_entity_info_alter(&$info) {
+  // Take over URI constuction for taxonomy terms that are forums.
+  if ($vid = variable_get('forum_nav_vocabulary', 0)) {
+    // Within hook_entity_info(), we can't invoke entity_load() as that would
+    // cause infinite recursion, so we call taxonomy_vocabulary_get_names()
+    // instead of taxonomy_vocabulary_load(). All we need is the machine name
+    // of $vid, so retrieving and iterating all the vocabulary names is somewhat
+    // inefficient, but entity info is cached across page requests, and an
+    // iteration of all vocabularies once per cache clearing isn't a big deal,
+    // and is done as part of taxonomy_entity_info() anyway.
+    foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
+      if ($vid == $vocabulary->vid) {
+        $info['taxonomy_term']['bundles'][$machine_name]['uri callback'] = 'forum_uri';
+      }
+    }
+  }
+}
+
+/**
+ * Entity URI callback.
+ */
+function forum_uri($forum) {
+  return array(
+    'path' => 'forum/' . $forum->tid,
+  );
+}
+
 /**
  * Check whether a content type can be used in a forum.
  *
@@ -269,8 +299,18 @@ function forum_node_validate($node, $form) {
     if (!empty($node->taxonomy_forums[$langcode])) {
       // Extract the node's proper topic ID.
       $containers = variable_get('forum_containers', array());
-      foreach ($node->taxonomy_forums[$langcode] as $item) {
+      foreach ($node->taxonomy_forums[$langcode] as $delta => $item) {
+        // If no term was selected (e.g. when no terms exist yet), remove the
+        // item.
+        if (empty($item['tid'])) {
+          unset($node->taxonomy_forums[$langcode][$delta]);
+          continue;
+        }
         $term = taxonomy_term_load($item['tid']);
+        if (!$term) {
+          form_set_error('taxonomy_forums', t('Select a forum.'));
+          continue;
+        }
         $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid',0 , 1, array(
           ':tid' => $term->tid,
           ':vid' => $term->vid,
@@ -292,7 +332,8 @@ function forum_node_presave($node) {
   if (_forum_node_check_node_type($node)) {
     // Make sure all fields are set properly:
     $node->icon = !empty($node->icon) ? $node->icon : '';
-    $langcode = array_shift(array_keys($node->taxonomy_forums));
+    reset($node->taxonomy_forums);
+    $langcode = key($node->taxonomy_forums);
     if (!empty($node->taxonomy_forums[$langcode])) {
       $node->forum_tid = $node->taxonomy_forums[$langcode][0]['tid'];
       $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField();
@@ -644,7 +685,7 @@ function forum_block_view($delta = '') {
 /**
 * A #pre_render callback. Lists nodes based on the element's #query property.
 *
-* @see forum_block_view().
+* @see forum_block_view()
 *
 * @return
 *   A renderable array.
@@ -681,18 +722,6 @@ function forum_form($node, $form_state) {
   return $form;
 }
 
-/**
- * Implements hook_url_outbound_alter().
- */
-function forum_url_outbound_alter(&$path, &$options, $original_path) {
-  if (preg_match('!^taxonomy/term/(\d+)!', $path, $matches)) {
-    $term = taxonomy_term_load($matches[1]);
-    if ($term && $term->vocabulary_machine_name == 'forums') {
-      $path = 'forum/' . $matches[1];
-    }
-  }
-}
-
 /**
  * Returns a list of all forums for a given taxonomy id
  *
@@ -835,7 +864,7 @@ function forum_get_topics($tid, $sortby, $forum_per_page) {
     $nids[] = $record->nid;
   }
   if ($nids) {
-    $result = db_query("SELECT n.title, n.nid, n.sticky, n.created, n.uid, n.comment AS comment_mode, ncs.*, f.tid AS forum_tid, u.name, IF (ncs.last_comment_uid != 0, u2.name, ncs.last_comment_name) AS last_comment_name FROM {node} n INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {forum} f ON n.vid = f.vid INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {users} u2 ON ncs.last_comment_uid = u2.uid WHERE n.nid IN (:nids)", array(':nids' => $nids));
+    $result = db_query("SELECT n.title, n.nid, n.type, n.sticky, n.created, n.uid, n.comment AS comment_mode, ncs.*, f.tid AS forum_tid, u.name, IF (ncs.last_comment_uid != 0, u2.name, ncs.last_comment_name) AS last_comment_name FROM {node} n INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {forum} f ON n.vid = f.vid INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {users} u2 ON ncs.last_comment_uid = u2.uid WHERE n.nid IN (:nids)", array(':nids' => $nids));
   }
   else {
     $result = array();
@@ -1046,7 +1075,7 @@ function template_preprocess_forum_topic_list(&$variables) {
       $variables['topics'][$id]->new_url = '';
       if ($topic->new_replies) {
         $variables['topics'][$id]->new_text = format_plural($topic->new_replies, '1 new', '@count new');
-        $variables['topics'][$id]->new_url = url("node/$topic->nid", array('query' => comment_new_page_count($topic->num_comments, $topic->new_replies, $topic), 'fragment' => 'new'));
+        $variables['topics'][$id]->new_url = url("node/$topic->nid", array('query' => comment_new_page_count($topic->comment_count, $topic->new_replies, $topic), 'fragment' => 'new'));
       }
 
     }
diff --git a/modules/forum/forum.test b/modules/forum/forum.test
index 5d640528c877615e53f9d5176b4a30dfc7910591..caaabb9501d9733530e0078b80c43fb9fc82b248 100644
--- a/modules/forum/forum.test
+++ b/modules/forum/forum.test
@@ -1,11 +1,11 @@
 <?php
-// $Id: forum.test,v 1.49 2010/03/18 06:36:39 dries Exp $
+// $Id: forum.test,v 1.55 2010/04/26 14:32:27 dries Exp $
 
 class ForumTestCase extends DrupalWebTestCase {
   protected $admin_user;
-  protected $own_user;
-  protected $any_user;
-  protected $nid_user;
+  protected $edit_own_topics_user;
+  protected $edit_any_topics_user;
+  protected $web_user;
   protected $container;
   protected $forum;
   protected $root_forum;
@@ -26,43 +26,48 @@ class ForumTestCase extends DrupalWebTestCase {
     parent::setUp('taxonomy', 'comment', 'forum');
     // Create users.
     $this->admin_user = $this->drupalCreateUser(array('administer blocks', 'administer forums', 'administer menu', 'administer taxonomy', 'create forum content')); // 'access administration pages'));
-    $this->own_user = $this->drupalCreateUser(array('create forum content', 'edit own forum content', 'delete own forum content'));
-    $this->any_user = $this->drupalCreateUser(array('create forum content', 'edit any forum content', 'delete any forum content', 'access administration pages'));
-    $this->nid_user = $this->drupalCreateUser(array());
+    $this->edit_any_topics_user = $this->drupalCreateUser(array('create forum content', 'edit any forum content', 'delete any forum content', 'access administration pages'));
+    $this->edit_own_topics_user = $this->drupalCreateUser(array('create forum content', 'edit own forum content', 'delete own forum content'));
+    $this->web_user = $this->drupalCreateUser(array());
   }
 
   /**
    * Login users, create forum nodes, and test forum functionality through the admin and user interfaces.
    */
   function testForum() {
+    //Check that the basic forum install creates a default forum topic
+    $this->drupalGet("/forum");
+    // Look for the "General discussion" default forum
+    $this->assertText(t("General discussion"), "Found the default forum at the /forum listing");
+
     // Do the admin tests.
     $this->doAdminTests($this->admin_user);
     // Generate topics to populate the active forum block.
     $this->generateForumTopics($this->forum);
 
-    // Login the nid user to view the forum topics and generate an active forum
-    // topics list.
-    $this->drupalLogin($this->nid_user);
+    // Login an unprivileged user to view the forum topics and generate an
+    // active forum topics list.
+    $this->drupalLogin($this->web_user);
     $this->viewForumTopics($this->nids);
 
-    // Do basic tests for the any forum user.
-    $this->doBasicTests($this->any_user, TRUE);
-
-    // Create another forum node for the any forum user.
-    $node = $this->createForumTopic($this->forum, FALSE);
-
-    // Do basic tests for the own forum user.
-    $this->doBasicTests($this->own_user, FALSE);
-
-    // Verify the own forum user only has access to the forum view node.
-    $this->verifyForums($this->any_user, $node, FALSE, 403);
-    // Create another forum node for the own forum user.
-    $node = $this->createForumTopic($this->forum, FALSE);
-
-    // Login the any forum user.
-    $this->drupalLogin($this->any_user);
-    // Verify the any forum user has access to all the forum nodes.
-    $this->verifyForums($this->own_user, $node, TRUE);
+    // Log in, and do basic tests for a user with permission to edit any forum
+    // content.
+    $this->doBasicTests($this->edit_any_topics_user, TRUE);
+    // Create a forum node authored by this user.
+    $any_topics_user_node = $this->createForumTopic($this->forum, FALSE);
+
+    // Log in, and do basic tests for a user with permission to edit only its
+    // own forum content.
+    $this->doBasicTests($this->edit_own_topics_user, FALSE);
+    // Create a forum node authored by this user.
+    $own_topics_user_node = $this->createForumTopic($this->forum, FALSE);
+    // Verify that this user cannot edit forum content authored by another user.
+    $this->verifyForums($this->edit_any_topics_user, $any_topics_user_node, FALSE, 403);
+
+    // Login a user with permission to edit any forum content.
+    $this->drupalLogin($this->edit_any_topics_user);
+    // Verify that this user can edit forum content authored by another user.
+    $this->verifyForums($this->edit_own_topics_user, $own_topics_user_node, TRUE);
 
     // Verify the topic and post counts on the forum page.
     $this->drupalGet('forum');
@@ -84,11 +89,34 @@ class ForumTestCase extends DrupalWebTestCase {
     $this->assertResponse(200);
 
     // Test editing a forum topic that has a comment.
-    $this->drupalLogin($this->any_user);
+    $this->drupalLogin($this->edit_any_topics_user);
+    $this->drupalGet('forum/' . $this->forum['tid']);
     $this->drupalPost("node/$node->nid/edit", array(), t('Save'));
     $this->assertResponse(200);
   }
 
+  /**
+   * Forum nodes should not be created without choosing forum from select list.
+   */
+  function testAddOrphanTopic() {
+    // Must remove forum topics to test creating orphan topics.
+    $vid = variable_get('forum_nav_vocabulary');
+    $tree = taxonomy_get_tree($vid);
+    foreach($tree as $term) {
+      taxonomy_term_delete($term->tid);
+    }
+
+    // Create an orphan forum item.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalPost('node/add/forum', array('title' => $this->randomName(10), 'body[' . LANGUAGE_NONE .'][0][value]' => $this->randomName(120)), t('Save'));
+
+    $nid_count = db_query('SELECT COUNT(nid) FROM {node}')->fetchField();
+    $this->assertEqual(0, $nid_count, t('A forum node was not created when missing a forum vocabulary.'));
+
+    // Reset the defaults for future tests.
+    module_enable(array('forum'));
+  }
+
   /**
    * Run admin tests on the admin user.
    *
@@ -125,8 +153,18 @@ class ForumTestCase extends DrupalWebTestCase {
     $this->container = $this->editForumTaxonomy();
     // Create forum container.
     $this->container = $this->createForum('container');
+    // Verify "edit container" link exists and functions correctly.
+    $this->drupalGet('admin/structure/forum');
+    $this->clickLink('edit container');
+    $this->assertRaw('Edit container', t('Followed the link to edit the container'));
     // Create forum inside the forum container.
     $this->forum = $this->createForum('forum', $this->container['tid']);
+    // Verify the "edit forum" link exists and functions correctly.
+    $this->drupalGet('admin/structure/forum');
+    $this->clickLink('edit forum');
+    $this->assertRaw('Edit forum', t('Followed the link to edit the forum'));
+    // Navigate back to forum structure page.
+    $this->drupalGet('admin/structure/forum');
     // Create second forum in container.
     $this->delete_forum = $this->createForum('forum', $this->container['tid']);
     // Save forum overview.
@@ -157,7 +195,7 @@ class ForumTestCase extends DrupalWebTestCase {
     );
 
     // Edit the vocabulary.
-    $this->drupalPost('admin/structure/taxonomy/' . $vid . '/edit', $edit, t('Save'));
+    $this->drupalPost('admin/structure/taxonomy/' . $original_settings->machine_name . '/edit', $edit, t('Save'));
     $this->assertResponse(200);
     $this->assertRaw(t('Updated vocabulary %name.', array('%name' => $title)), t('Vocabulary was edited'));
 
@@ -255,9 +293,13 @@ class ForumTestCase extends DrupalWebTestCase {
   /**
    * Create forum topic.
    *
-   * @param array $forum Forum array.
-   * @param boolean $container True if $forum is a container.
-   * @return object Topic node created.
+   * @param array $forum
+   *   Forum array.
+   * @param boolean $container
+   *   True if $forum is a container.
+   *
+   * @return object
+   *   Topic node created.
    */
   function createForumTopic($forum, $container = FALSE) {
     // Generate a random subject/body.
@@ -269,9 +311,10 @@ class ForumTestCase extends DrupalWebTestCase {
       "title" => $title,
       "body[$langcode][0][value]" => $body,
     );
+    $tid = $forum['tid'];
 
     // Create the forum topic, preselecting the forum ID via a URL parameter.
-    $this->drupalPost('node/add/forum/' . $forum['tid'], $edit, t('Save'));
+    $this->drupalPost('node/add/forum/' . $tid, $edit, t('Save'));
 
     $type = t('Forum topic');
     if ($container) {
@@ -280,14 +323,14 @@ class ForumTestCase extends DrupalWebTestCase {
       return;
     }
     else {
-      $this->assertRaw(t('@type %title has been created.', array('%title' => $title, '@type' => $type)), t('Forum topic was created'));
+      $this->assertRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), t('Forum topic was created'));
       $this->assertNoRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), t('No error message was shown'));
     }
 
     // Retrieve node object, ensure that the topic was created and in the proper forum.
     $node = $this->drupalGetNodeByTitle($title);
     $this->assertTrue($node != NULL, t('Node @title was loaded', array('@title' => $title)));
-    $this->assertEqual($node->taxonomy_forums[LANGUAGE_NONE][0]['tid'], $forum['tid'], 'Saved forum topic was in the expected forum');
+    $this->assertEqual($node->taxonomy_forums[LANGUAGE_NONE][0]['tid'], $tid, 'Saved forum topic was in the expected forum');
 
     // View forum topic.
     $this->drupalGet('node/' . $node->nid);
diff --git a/modules/help/help.info b/modules/help/help.info
index 9bc5d6bab9e8e73c4c440b2794f1aa7257b56ae2..c73eda75d8c7070268d71ec8f79bab0ce7bb6eab 100644
--- a/modules/help/help.info
+++ b/modules/help/help.info
@@ -8,8 +8,8 @@ files[] = help.module
 files[] = help.admin.inc
 files[] = help.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/help/help.test b/modules/help/help.test
index 8c5e571f2ed4631b85688c4c87e100f724c5a56b..fc80ca540cab96b00a4a7c322426b129731ba02b 100644
--- a/modules/help/help.test
+++ b/modules/help/help.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: help.test,v 1.18 2010/01/12 06:09:04 webchick Exp $
+// $Id: help.test,v 1.19 2010/04/01 14:52:21 dries Exp $
 
 class HelpTestCase extends DrupalWebTestCase {
   protected $big_user;
@@ -60,7 +60,7 @@ class HelpTestCase extends DrupalWebTestCase {
    *
    * @param integer $response HTTP response code.
    */
-  private function verifyHelp($response = 200) {
+  protected function verifyHelp($response = 200) {
     foreach ($this->modules as $module => $name) {
       // View module help node.
       $this->drupalGet('admin/help/' . $module);
@@ -77,7 +77,7 @@ class HelpTestCase extends DrupalWebTestCase {
    *
    * @return array Enabled modules.
    */
-  private function getModuleList() {
+  protected function getModuleList() {
     $this->modules = array();
     $result = db_query("SELECT name, filename, info FROM {system} WHERE type = 'module' AND status = 1 ORDER BY weight ASC, filename ASC");
     foreach ($result as $module) {
diff --git a/modules/image/image.admin.inc b/modules/image/image.admin.inc
index 508f771fd79734a8332ce4bb0fd30da73cb25f35..d41c86a1a3b698878af58dcab7419541cba1abc1 100644
--- a/modules/image/image.admin.inc
+++ b/modules/image/image.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: image.admin.inc,v 1.17 2010/01/03 21:01:04 webchick Exp $
+// $Id: image.admin.inc,v 1.20 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -83,31 +83,36 @@ function image_style_form($form, &$form_state, $style) {
   $form['effects'] = array(
     '#theme' => 'image_style_effects',
   );
-  foreach ($style['effects'] as $ieid => $effect) {
-    $form['effects'][$ieid]['#weight'] = isset($form_state['input']['effects']) ? $form_state['input']['effects'][$ieid]['weight'] : NULL;
-    $form['effects'][$ieid]['label'] = array(
+  foreach ($style['effects'] as $key => $effect) {
+    $form['effects'][$key]['#weight'] = isset($form_state['input']['effects']) ? $form_state['input']['effects'][$key]['weight'] : NULL;
+    $form['effects'][$key]['label'] = array(
       '#markup' => $effect['label'],
     );
-    $form['effects'][$ieid]['summary'] = array(
+    $form['effects'][$key]['summary'] = array(
       '#markup' => isset($effect['summary theme']) ? theme($effect['summary theme'], array('data' => $effect['data'])) : '',
     );
-    $form['effects'][$ieid]['weight'] = array(
+    $form['effects'][$key]['weight'] = array(
       '#type' => 'weight',
       '#default_value' => $effect['weight'],
       '#access' => $editable,
     );
-    $form['effects'][$ieid]['configure'] = array(
-      '#type' => 'link',
-      '#title' => t('edit'),
-      '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'],
-      '#access' => $editable && isset($effect['form callback']),
-    );
-    $form['effects'][$ieid]['remove'] = array(
-      '#type' => 'link',
-      '#title' => t('delete'),
-      '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'] . '/delete',
-      '#access' => $editable,
-    );
+
+    // Only attempt to display these fields for editable styles as the 'ieid'
+    // key is not set for styles defined in code.
+    if ($editable) {
+      $form['effects'][$key]['configure'] = array(
+        '#type' => 'link',
+        '#title' => t('edit'),
+        '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'],
+        '#access' => $editable && isset($effect['form callback']),
+      );
+      $form['effects'][$key]['remove'] = array(
+        '#type' => 'link',
+        '#title' => t('delete'),
+        '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'] . '/delete',
+        '#access' => $editable,
+      );
+    }
   }
 
   // Build the new image effect addition form and add it to the effect list.
@@ -136,14 +141,15 @@ function image_style_form($form, &$form_state, $style) {
   );
 
   // Show the Override or Submit button for this style.
-  $form['override'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['override'] = array(
     '#type' => 'submit',
     '#value' => t('Override defaults'),
     '#validate' => array(),
     '#submit' => array('image_style_form_override_submit'),
     '#access' => !$editable,
   );
-  $form['submit'] = array(
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Update style'),
     '#access' => $editable,
@@ -388,7 +394,7 @@ function image_effect_form($form, &$form_state, $style, $effect) {
     '#value' => isset($_GET['weight']) ? intval($_GET['weight']) : (isset($effect['weight']) ? $effect['weight'] : count($style['effects'])),
   );
 
-  $form['actions'] = array('#tree' => FALSE, '#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#tree' => FALSE, '#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => isset($effect['ieid']) ? t('Update effect') : t('Add effect'),
@@ -623,7 +629,7 @@ function image_rotate_form($data) {
 }
 
 /**
- * Display the page containing the list of image styles.
+ * Returns HTML for the page containing the list of image styles.
  *
  * @param $variables
  *   An associative array containing:
@@ -674,11 +680,11 @@ function theme_image_style_list($variables) {
 }
 
 /**
- * Theme callback for listing the effects within a specific image style.
+ * Returns HTML for a listing of the effects within a specific image style.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An associative array containing the structure of the effects group.
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -733,7 +739,7 @@ function theme_image_style_effects($variables) {
 }
 
 /**
- * Theme callback for displaying a preview of an image style.
+ * Returns HTML for a preview of an image style.
  *
  * @param $variables
  *   An associative array containing:
@@ -809,11 +815,11 @@ function theme_image_style_preview($variables) {
 }
 
 /**
- * Theme callback for displaying a grid of checkboxes.
+ * Returns HTML for a 3x3 grid of checkboxes for image anchors.
  *
  * @param $variables
  *   An associative array containing:
- *   - element: A Form API element containing radio buttons.
+ *   - element: A render element containing radio buttons.
  *
  * @ingroup themeable
  */
@@ -836,7 +842,7 @@ function theme_image_anchor($variables) {
 }
 
 /**
- * Theme callback for image resize effect summary output.
+ * Returns HTML for a summary of an image resize effect.
  *
  * @param $variables
  *   An associative array containing:
@@ -856,7 +862,7 @@ function theme_image_resize_summary($variables) {
 }
 
 /**
- * Theme callback for image scale effect summary output.
+ * Returns HTML for a summary of an image scale effect.
  *
  * @param $variables
  *   An associative array containing:
@@ -870,7 +876,7 @@ function theme_image_scale_summary($variables) {
 }
 
 /**
- * Theme callback for image crop effect summary output.
+ * Returns HTML for a summary of an image crop effect.
  *
  * @param $variables
  *   An associative array containing:
@@ -883,7 +889,7 @@ function theme_image_crop_summary($variables) {
 }
 
 /**
- * Theme callback for image rotate effect summary output.
+ * Returns HTML for a summary of an image rotate effect.
  *
  * @param $variables
  *   An associative array containing:
diff --git a/modules/image/image.effects.inc b/modules/image/image.effects.inc
index bad378aa9a1795703c64d7764cb50e386336527f..94d92961e906c99e0fd363ef6f6f278ad41a36e8 100644
--- a/modules/image/image.effects.inc
+++ b/modules/image/image.effects.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: image.effects.inc,v 1.3 2009/12/04 16:49:46 dries Exp $
+// $Id: image.effects.inc,v 1.4 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -66,8 +66,10 @@ function image_image_effect_info() {
  *   following items:
  *   - "width": An integer representing the desired width in pixels.
  *   - "height": An integer representing the desired height in pixels.
+ *
  * @return
  *   TRUE on success. FALSE on failure to resize image.
+ *
  * @see image_resize()
  */
 function image_resize_effect(&$image, $data) {
@@ -90,8 +92,10 @@ function image_resize_effect(&$image, $data) {
  *   - "height": An integer representing the desired height in pixels.
  *   - "upscale": A Boolean indicating that the image should be upscalled if
  *     the dimensions are larger than the original image.
+ *
  * @return
  *   TRUE on success. FALSE on failure to scale image.
+ *
  * @see image_scale()
  */
 function image_scale_effect(&$image, $data) {
diff --git a/modules/image/image.field.inc b/modules/image/image.field.inc
index c90033d63d32cb7404cb25670144dd5990ca0327..9c89707cad122e84ae6195d1e71bd85d96b1f424 100644
--- a/modules/image/image.field.inc
+++ b/modules/image/image.field.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: image.field.inc,v 1.18 2010/03/06 07:19:57 dries Exp $
+// $Id: image.field.inc,v 1.20 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -228,8 +228,8 @@ function image_field_prepare_view($entity_type, $entities, $field, $instances, $
 /**
  * Implements hook_field_presave().
  */
-function image_field_presave($obj_type, $object, $field, $instance, $langcode, &$items) {
-  file_field_presave($obj_type, $object, $field, $instance, $langcode, $items);
+function image_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
+  file_field_presave($entity_type, $entity, $field, $instance, $langcode, $items);
 }
 
 /**
@@ -390,7 +390,13 @@ function image_field_widget_process($element, &$form_state, $form) {
 }
 
 /**
- * Theme the display of the image field widget.
+ * Returns HTML for an image field widget.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the image field widget.
+ *
+ * @ingroup themeable
  */
 function theme_image_widget($variables) {
   $element = $variables['element'];
@@ -490,7 +496,15 @@ function image_field_formatter_view($entity_type, $entity, $field, $instance, $l
 }
 
 /**
- * Theme function for image field formatters.
+ * Returns HTML for an image field formatter.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - item: An array of image data.
+ *   - image_style: An optional image style.
+ *   - path: An array containing the link 'path' and link 'options'.
+ *
+ * @ingroup themeable
  */
 function theme_image_formatter($variables) {
   $item = $variables['item'];
diff --git a/modules/image/image.info b/modules/image/image.info
index 0860a3a7ed53821ac26f54017fad01c9928542e7..8e29c076d8dab3be3fbef63b17fe2e2736d3b90d 100644
--- a/modules/image/image.info
+++ b/modules/image/image.info
@@ -13,8 +13,8 @@ files[] = image.install
 files[] = image.test
 configure = admin/config/media/image-styles
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/image/image.install b/modules/image/image.install
index d35ec898a92807bdb4e1e3859ded7956b787d4bc..9bd4b8b93b4ee8433b43f60042a0fd9bc310702a 100644
--- a/modules/image/image.install
+++ b/modules/image/image.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: image.install,v 1.4 2009/12/04 16:49:46 dries Exp $
+// $Id: image.install,v 1.8 2010/04/09 12:07:12 dries Exp $
 
 /**
  * @file
@@ -50,7 +50,7 @@ function image_schema() {
       ),
     ),
     'primary key' => array('isid'),
-    'indexes' => array(
+    'unique keys' => array(
       'name' => array('name'),
     ),
   );
@@ -104,3 +104,125 @@ function image_schema() {
 
   return $schema;
 }
+
+/**
+ * Install the schema for users upgrading from the contributed module.
+ */
+function image_update_7000() {
+  if (!db_table_exists('image_styles')) {
+    $schema = array();
+
+    $schema['cache_image'] = drupal_get_schema_unprocessed('system', 'cache');
+    $schema['cache_image']['description'] = 'Cache table used to store information about image manipulations that are in-progress.';
+
+    $schema['image_styles'] = array(
+      'description' => 'Stores configuration options for image styles.',
+      'fields' => array(
+        'isid' => array(
+          'description' => 'The primary identifier for an image style.',
+          'type' => 'serial',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+        ),
+        'name' => array(
+          'description' => 'The style name.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+        ),
+      ),
+      'primary key' => array('isid'),
+      'unique keys' => array(
+        'name' => array('name'),
+      ),
+    );
+
+    $schema['image_effects'] = array(
+      'description' => 'Stores configuration options for image effects.',
+      'fields' => array(
+        'ieid' => array(
+          'description' => 'The primary identifier for an image effect.',
+          'type' => 'serial',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+        ),
+        'isid' => array(
+          'description' => 'The {image_styles}.isid for an image style.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'weight' => array(
+          'description' => 'The weight of the effect in the style.',
+          'type' => 'int',
+          'unsigned' => FALSE,
+          'not null' => TRUE,
+          'default' => 0,
+        ),
+        'name' => array(
+          'description' => 'The unique name of the effect to be executed.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+        ),
+        'data' => array(
+          'description' => 'The configuration data for the effect.',
+          'type' => 'text',
+          'not null' => TRUE,
+          'size' => 'big',
+          'serialize' => TRUE,
+        ),
+      ),
+      'primary key' => array('ieid'),
+      'indexes' => array(
+        'isid' => array('isid'),
+        'weight' => array('weight'),
+      ),
+      'foreign keys' => array(
+        'isid' => array('image_styles' => 'isid'),
+      ),
+    );
+    db_create_table('cache_image', $schema['cache_image']);
+    db_create_table('image_styles', $schema['image_styles']);
+    db_create_table('image_effect', $schema['image_effects']);
+  }
+}
+
+/**
+ * Implements hook_requirements() to check the PHP GD Library.
+ *
+ * @param $phase
+ */
+function image_requirements($phase) {
+  $requirements = array();
+
+  if ($phase == 'runtime') {
+    // Check for the PHP GD library.
+    if (function_exists('imagegd2')) {
+      $info = gd_info();
+      $requirements['image_gd'] = array(
+        'value' => $info['GD Version'],
+      );
+
+      // Check for filter and rotate support.
+      if (function_exists('imagefilter') && function_exists('imagerotate')) {
+        $requirements['image_gd']['severity'] = REQUIREMENT_OK;
+      }
+      else {
+        $requirements['image_gd']['severity'] = REQUIREMENT_ERROR;
+        $requirements['image_gd']['description'] = t('The GD Library for PHP is enabled, but was compiled without support for functions used by the rotate and desaturate effects. It was probably compiled using the official GD libraries from http://www.libgd.org instead of the GD library bundled with PHP. You should recompile PHP --with-gd using the bundled GD library. See <a href="http://www.php.net/manual/book.image.php">the PHP manual</a>.');
+      }
+    }
+    else {
+      $requirements['image_gd'] = array(
+        'value' => t('Not installed'),
+        'severity' => REQUIREMENT_ERROR,
+        'description' => t('The GD library for PHP is missing or outdated. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/book.image.php')),
+      );
+    }
+    $requirements['image_gd']['title'] = t('GD library rotate and desaturate effects');
+  }
+
+  return $requirements;
+}
diff --git a/modules/image/image.module b/modules/image/image.module
index 573b66e79765faf31ccd9c8b8ff51cd7737578a8..c21b1373028ec13c585923cb85edc00e2cd60007 100644
--- a/modules/image/image.module
+++ b/modules/image/image.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: image.module,v 1.34 2010/03/06 06:31:24 dries Exp $
+// $Id: image.module,v 1.40 2010/04/23 07:54:44 webchick Exp $
 
 /**
  * @file
@@ -50,8 +50,8 @@ function image_help($path, $arg) {
       $output .= '<ul><li>' . t('Based on where it will be used: eg. <em>profile-picture</em>') . '</li>';
       $output .= '<li>' . t('Describing its appearance: eg. <em>square-85x85</em>') . '</li></ul>';
       $output .=  t('After you create an image style, you can add effects: crop, scale, resize, rotate, desaturate, and rotate (other contributed modules provide additional effects). For example, by combining effects as crop, scale, and desaturate, you can create square, grayscale thumbnails.') . '<dd>';
-      $output .= '<dt>' . t ('Attaching images to content as fields') . '</dt>';
-      $output .= '<dd>' . t ("Image module also allows you to attach images to content as fields. To add an image field to a <a href='@content-type'>content type</a>, go to the content type's <em>manage fields</em> page, and add a new field of type <em>Image</em>. Attaching images to content this way allows image styles to be applied and maintained, and also allows you more flexibility when theming.", array('@content-type' => url('admin/structure/types'))) . '</dd>';
+      $output .= '<dt>' . t('Attaching images to content as fields') . '</dt>';
+      $output .= '<dd>' . t("Image module also allows you to attach images to content as fields. To add an image field to a <a href='@content-type'>content type</a>, go to the content type's <em>manage fields</em> page, and add a new field of type <em>Image</em>. Attaching images to content this way allows image styles to be applied and maintained, and also allows you more flexibility when theming.", array('@content-type' => url('admin/structure/types'))) . '</dd>';
       $output .= '</dl>';
       return $output;
     case 'admin/config/media/image-styles':
@@ -353,6 +353,47 @@ function image_image_default_styles() {
   return $styles;
 }
 
+/**
+ * Implements hook_image_style_save().
+ */
+function image_image_style_save($style) {
+  if (isset($style['old_name']) && $style['old_name'] != $style['name']) {
+    $instances = field_read_instances();
+    // Loop through all fields searching for image fields.
+    foreach ($instances as $instance) {
+      if ($instance['widget']['module'] == 'image') {
+        $instance_changed = FALSE;
+        foreach ($instance['display'] as $view_mode => $display) {
+          // Check if the formatter involves an image style.
+          $matches = array();
+          if (preg_match('/__([a-z0-9_]+)/', $display['type'], $matches)) {
+            // Update display information for any instance using the image
+            // style that was just deleted.
+            if ($style['old_name'] == $matches[1]) {
+              $instance['display'][$view_mode]['type'] = str_replace($style['old_name'], $style['name'], $display['type']);
+              $instance_changed = TRUE;
+            }
+          }
+        }
+        if ($instance['widget']['settings']['preview_image_style'] == $style['old_name']) {
+          $instance['widget']['settings']['preview_image_style'] = $style['name'];
+          $instance_changed = TRUE;
+        }
+        if ($instance_changed) {
+          field_update_instance($instance);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_image_style_delete().
+ */
+function image_image_style_delete($style) {
+  image_image_style_save($style);
+}
+
 /**
  * Clear cached versions of a specific file in all styles.
  *
@@ -362,9 +403,9 @@ function image_image_default_styles() {
 function image_path_flush($path) {
   $styles = image_styles();
   foreach ($styles as $style) {
-    $path = image_style_path($style['name'], $path);
-    if (file_exists($path)) {
-      file_unmanaged_delete($path);
+    $image_path = image_style_path($style['name'], $path);
+    if (file_exists($image_path)) {
+      file_unmanaged_delete($image_path);
     }
   }
 }
@@ -394,11 +435,10 @@ function image_styles() {
           $style['name'] = $style_name;
           $style['module'] = $module;
           $style['storage'] = IMAGE_STORAGE_DEFAULT;
-          foreach ($style['effects'] as $ieid => $effect) {
+          foreach ($style['effects'] as $key => $effect) {
             $definition = image_effect_definition_load($effect['name']);
             $effect = array_merge($definition, $effect);
-            $effect['ieid'] = $ieid;
-            $style['effects'][$ieid] = $effect;
+            $style['effects'][$key] = $effect;
           }
           $styles[$style_name] = $style;
         }
@@ -461,6 +501,7 @@ function image_style_load($name = NULL, $isid = NULL, $include = NULL) {
   if (!isset($name) && isset($isid)) {
     foreach ($styles as $name => $database_style) {
       if (isset($database_style['isid']) && $database_style['isid'] == $isid) {
+        $style = $database_style;
         break;
       }
     }
@@ -792,7 +833,7 @@ function image_default_style_save($style) {
   $effects = array();
   foreach ($style['effects'] as $effect) {
     $effect['isid'] = $style['isid'];
-    image_effect_save($effect);
+    $effect = image_effect_save($effect);
     $effects[$effect['ieid']] = $effect;
   }
   $style['effects'] = $effects;
@@ -847,7 +888,7 @@ function image_effect_definitions() {
           $effect['name'] = $name;
           $effect['data'] = isset($effect['data']) ? $effect['data'] : array();
           $effects[$name] = $effect;
-        };
+        }
       }
       uasort($effects, '_image_effect_definitions_sort');
       cache_set("image_effects:$langcode", $effects);
@@ -1010,7 +1051,7 @@ function image_effect_apply($image, $effect) {
 }
 
 /**
- * Return a themed image using a specific image style.
+ * Returns HTML for an image using a specific image style.
  *
  * @param $variables
  *   An associative array containing:
@@ -1025,8 +1066,6 @@ function image_effect_apply($image, $effect) {
  *   - getsize: If set to TRUE, the image's dimension are fetched and added as
  *     width/height attributes.
  *
- * @return
- *   A string containing the image tag.
  * @ingroup themeable
  */
 function theme_image_style($variables) {
diff --git a/modules/image/image.test b/modules/image/image.test
index 45c0196bf6ce287fd931dcd97c4c0a9705b8397f..340c65f8e5749a7d2110f837ef3bb8f32ab49259 100644
--- a/modules/image/image.test
+++ b/modules/image/image.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: image.test,v 1.15 2010/02/27 07:52:03 dries Exp $
+// $Id: image.test,v 1.20 2010/04/22 21:43:59 webchick Exp $
 
 /**
  * @file
@@ -27,6 +27,83 @@
  *   image_filter_keyword()
  */
 
+/**
+ * This class provides methods specifically for testing Image's field handling.
+ */
+class ImageFieldTestCase extends DrupalWebTestCase {
+  protected $admin_user;
+
+  function setUp() {
+    parent::setUp('image');
+    $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles'));
+    $this->drupalLogin($this->admin_user);
+  }
+
+  /**
+   * Create a new image field.
+   *
+   * @param $name
+   *   The name of the new field (all lowercase), exclude the "field_" prefix.
+   * @param $type_name
+   *   The node type that this field will be added to.
+   * @param $field_settings
+   *   A list of field settings that will be added to the defaults.
+   * @param $instance_settings
+   *   A list of instance settings that will be added to the instance defaults.
+   * @param $widget_settings
+   *   A list of widget settings that will be added to the widget defaults.
+   */
+  function createImageField($name, $type_name, $field_settings = array(), $instance_settings = array(), $widget_settings = array()) {
+    $field = array(
+      'field_name' => $name,
+      'type' => 'image',
+      'settings' => array(),
+      'cardinality' => !empty($field_settings['cardinality']) ? $field_settings['cardinality'] : 1,
+    );
+    $field['settings'] = array_merge($field['settings'], $field_settings);
+    field_create_field($field);
+
+    $instance = array(
+      'field_name' => $field['field_name'],
+      'entity_type' => 'node',
+      'label' => $name,
+      'bundle' => $type_name,
+      'required' => !empty($instance_settings['required']),
+      'settings' => array(),
+      'widget' => array(
+        'type' => 'image_image',
+        'settings' => array(),
+      ),
+    );
+    $instance['settings'] = array_merge($instance['settings'], $instance_settings);
+    $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings);
+    return field_create_instance($instance);
+  }
+
+  /**
+   * Upload an image to a node.
+   *
+   * @param $image
+   *   A file object representing the image to upload.
+   * @param $field_name
+   *   Name of the image field the image should be attached to.
+   * @param $type
+   *   The type of node to create.
+   */
+  function uploadNodeImage($image, $field_name, $type) {
+    $edit = array(
+      'title' => $this->randomName(),
+    );
+    $edit['files[' . $field_name . '_' . LANGUAGE_NONE . '_0]'] = realpath($image->uri);
+    $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+
+    // Retrieve ID of the newly created node from the current URL.
+    $matches = array();
+    preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
+    return isset($matches[1]) ? $matches[1] : FALSE;
+  }
+}
+
 /**
  * Tests the functions for generating paths and URLs for image styles.
  */
@@ -246,9 +323,9 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase {
 /**
  * Tests creation, deletion, and editing of image styles and effects.
  */
-class ImageAdminStylesUnitTest extends DrupalWebTestCase {
+class ImageAdminStylesUnitTest extends ImageFieldTestCase {
 
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Image styles and effects UI configuration',
       'description' => 'Tests creation, deletion, and editing of image styles and effects at the UI level.',
@@ -256,17 +333,6 @@ class ImageAdminStylesUnitTest extends DrupalWebTestCase {
     );
   }
 
-  /**
-   * Implementation of setUp().
-   */
-  function setUp() {
-    parent::setUp();
-
-    // Create an administrative user.
-    $this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer image styles'));
-    $this->drupalLogin($this->admin_user);
-  }
-
   /**
    * Given an image style, generate an image.
    */
@@ -470,6 +536,11 @@ class ImageAdminStylesUnitTest extends DrupalWebTestCase {
     $image_path = $this->createSampleImage($style);
     $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path)));
 
+    // Verify that effects attached to a default style do not have an ieid key.
+    foreach ($style['effects'] as $effect) {
+      $this->assertFalse(isset($effect['ieid']), t('The %effect effect does not have an ieid.', array('%effect' => $effect['name'])));
+    }
+
     // Override the default.
     $this->drupalPost($edit_path, array(), t('Override defaults'));
     $this->assertRaw(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $style_name)), t('Default image style may be overridden.'));
@@ -479,6 +550,11 @@ class ImageAdminStylesUnitTest extends DrupalWebTestCase {
     drupal_static_reset('image_styles');
     $style = image_style_load($style_name);
 
+    // Verify that effects attached to the style have an ieid now.
+    foreach ($style['effects'] as $effect) {
+      $this->assertTrue(isset($effect['ieid']), t('The %effect effect has an ieid.', array('%effect' => $effect['name'])));
+    }
+
     // The style should now have 2 effect, the original scale provided by core
     // and the desaturate effect we added in the override.
     $effects = array_values($style['effects']);
@@ -503,82 +579,51 @@ class ImageAdminStylesUnitTest extends DrupalWebTestCase {
     $this->assertEqual($effects[0]['name'], 'image_scale', t('The default effect still exists in the reverted style.'));
     $this->assertFalse(array_key_exists(1, $effects), t('The added effect has been removed in the reverted style.'));
   }
-}
-
-/**
- * This class provides methods specifically for testing Image's field handling.
- */
-class ImageFieldTestCase extends DrupalWebTestCase {
-  protected $admin_user;
-
-  function setUp() {
-    parent::setUp('image');
-    $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles'));
-    $this->drupalLogin($this->admin_user);
-  }
 
   /**
-   * Create a new image field.
-   *
-   * @param $name
-   *   The name of the new field (all lowercase), exclude the "field_" prefix.
-   * @param $type_name
-   *   The node type that this field will be added to.
-   * @param $field_settings
-   *   A list of field settings that will be added to the defaults.
-   * @param $instance_settings
-   *   A list of instance settings that will be added to the instance defaults.
-   * @param $widget_settings
-   *   A list of widget settings that will be added to the widget defaults.
+   * Test deleting a style and choosing a replacement style.
    */
-  function createImageField($name, $type_name, $field_settings = array(), $instance_settings = array(), $widget_settings = array()) {
-    $field = array(
-      'field_name' => $name,
-      'type' => 'image',
-      'settings' => array(),
-      'cardinality' => !empty($field_settings['cardinality']) ? $field_settings['cardinality'] : 1,
-    );
-    $field['settings'] = array_merge($field['settings'], $field_settings);
-    field_create_field($field);
+  function testStyleReplacement() {
+    // Create a new style.
+    $style_name = strtolower($this->randomName(10));
+    image_style_save(array('name' => $style_name));
+    $style_path = 'admin/config/media/image-styles/edit/' . $style_name;
 
-    $instance = array(
-      'field_name' => $field['field_name'],
-      'object_type' => 'node',
-      'label' => $name,
-      'bundle' => $type_name,
-      'required' => !empty($instance_settings['required']),
-      'settings' => array(),
-      'widget' => array(
-        'type' => 'image_image',
-        'settings' => array(),
-      ),
+    // Create an image field that uses the new style.
+    $field_name = strtolower($this->randomName(10));
+    $instance = $this->createImageField($field_name, 'article');
+    $instance['display']['full']['type'] = 'image__' . $style_name;
+    field_update_instance($instance);
+
+    // Create a new node with an image attached.
+    $test_image = current($this->drupalGetTestFiles('image'));
+    $nid = $this->uploadNodeImage($test_image, $field_name, 'article');
+    $node = node_load($nid);
+
+    // Test that image is displayed using newly created style.
+    $this->drupalGet('node/' . $nid);
+    $this->assertRaw(image_style_url($style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri']), t('Image displayed using style @style.', array('@style' => $style_name)));
+
+    // Rename the style and make sure the image field is updated.
+    $new_style_name = strtolower($this->randomName(10));
+    $edit = array(
+      'name' => $new_style_name,
     );
-    $instance['settings'] = array_merge($instance['settings'], $instance_settings);
-    $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings);
-    return field_create_instance($instance);
-  }
+    $this->drupalPost('admin/config/media/image-styles/edit/' . $style_name, $edit, t('Update style'));
+    $this->assertText(t('Changes to the style have been saved.'), t('Style %name was renamed to %new_name.', array('%name' => $style_name, '%new_name' => $new_style_name)));
+    $this->drupalGet('node/' . $nid);
+    $this->assertRaw(image_style_url($new_style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri']), t('Image displayed using style replacement style.'));
 
-  /**
-   * Upload an image to a node.
-   *
-   * @param $image
-   *   A file object representing the image to upload.
-   * @param $field_name
-   *   Name of the image field the image should be attached to.
-   * @param $type
-   *   The type of node to create.
-   */
-  function uploadNodeImage($image, $field_name, $type) {
+    // Delete the style and choose a replacement style.
     $edit = array(
-      'title' => $this->randomString(),
+      'replacement' => 'thumbnail',
     );
-    $edit['files[' . $field_name . '_' . LANGUAGE_NONE . '_0]'] = realpath($image->uri);
-    $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+    $this->drupalPost('admin/config/media/image-styles/delete/' . $new_style_name, $edit, t('Delete'));
+    $message = t('Style %name was deleted.', array('%name' => $new_style_name));
+    $this->assertRaw($message, $message);
 
-    // Retrieve ID of the newly created node from the current URL.
-    $matches = array();
-    preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
-    return isset($matches[1]) ? $matches[1] : FALSE;
+    $this->drupalGet('node/' . $nid);
+    $this->assertRaw(image_style_url('thumbnail', $node->{$field_name}[LANGUAGE_NONE][0]['uri']), t('Image displayed using style replacement style.'));
   }
 }
 
diff --git a/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc
index 1da8b4b18ea2b79308111cb02d58673876b6aca7..437f31b4017337c1910bd3448ef63ca424b84d39 100644
--- a/modules/locale/locale.admin.inc
+++ b/modules/locale/locale.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: locale.admin.inc,v 1.7 2010/03/10 14:23:31 dries Exp $
+// $Id: locale.admin.inc,v 1.12 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -43,7 +43,7 @@ function locale_languages_overview_form() {
     '#options' => $options,
     '#default_value' => language_default('language'),
   );
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
   $form['#theme'] = 'locale_languages_overview_form';
 
@@ -51,14 +51,11 @@ function locale_languages_overview_form() {
 }
 
 /**
- * Theme the language overview form.
+ * Returns HTML for the language overview form.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: @todo: document
- *
- * @return
- *   A themed HTML string representing the form.
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -171,7 +168,7 @@ function locale_languages_predefined_form($form) {
     '#options' => $predefined,
     '#description' => t('Use the <em>Custom language</em> section below if your desired language does not appear in this list.'),
   );
-  $form['language list']['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['language list']['actions'] = array('#type' => 'actions');
   $form['language list']['actions']['submit'] = array('#type' => 'submit', '#value' => t('Add language'));
   return $form;
 }
@@ -186,7 +183,7 @@ function locale_languages_custom_form($form) {
     '#collapsed' => TRUE,
   );
   _locale_languages_common_controls($form['custom language']);
-  $form['custom language']['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['custom language']['actions'] = array('#type' => 'actions');
   $form['custom language']['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Add custom language')
@@ -206,7 +203,7 @@ function locale_languages_custom_form($form) {
 function locale_languages_edit_form($form, &$form_state, $langcode) {
   if ($language = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchObject()) {
     _locale_languages_common_controls($form, $language);
-    $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+    $form['actions'] = array('#type' => 'actions');
     $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Save language')
@@ -339,8 +336,6 @@ function locale_languages_predefined_form_submit($form, &$form_state) {
     batch_set($batch);
   }
 
-  module_invoke_all('multilingual_settings_changed');
-
   $form_state['redirect'] = 'admin/config/regional/language';
   return;
 }
@@ -488,7 +483,7 @@ function locale_languages_configure_form() {
     _locale_languages_configure_form_language_table($form, $type);
   }
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save settings'),
@@ -571,7 +566,11 @@ function _locale_languages_configure_form_language_table(&$form, $type) {
 }
 
 /**
- * Theme the language configure form.
+ * Returns HTML for a language configuration form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -687,7 +686,7 @@ function locale_languages_configure_form_submit($form, &$form_state) {
 /**
  * The URL language provider configuration form.
  */
-function locale_language_providers_url_form() {
+function locale_language_providers_url_form($form, &$form_state) {
   $form = array();
 
   $form['locale_language_negotiation_url_part'] = array(
@@ -701,7 +700,7 @@ function locale_language_providers_url_form() {
     '#description' => t('<em>Path prefix</em>: URLs like http://example.com/de/contact set language to German (de). <em>Domain</em>: URLs like http://de.example.com/contact set the language to German. <strong>Warning: Changing this setting may break incoming URLs. Use with caution on a production site.</strong>'),
   );
 
-  $form['#redirect'] = 'admin/config/regional/language/configure';
+  $form_state['redirect'] = 'admin/config/regional/language/configure';
 
   return system_settings_form($form);
 }
@@ -709,7 +708,7 @@ function locale_language_providers_url_form() {
 /**
  * The URL language provider configuration form.
  */
-function locale_language_providers_session_form() {
+function locale_language_providers_session_form($form, &$form_state) {
   $form = array();
 
   $form['locale_language_negotiation_session_param'] = array(
@@ -719,7 +718,7 @@ function locale_language_providers_session_form() {
     '#description' => t('Name of the request/session parameter used to determine the desired language.'),
   );
 
-  $form['#redirect'] = 'admin/config/regional/language/configure';
+  $form_state['redirect'] = 'admin/config/regional/language/configure';
 
   return system_settings_form($form);
 }
@@ -785,7 +784,8 @@ function locale_translate_seek_screen() {
   // Add CSS.
   drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css', array('preprocess' => FALSE));
 
-  $output = drupal_render(drupal_get_form('locale_translation_filter_form'));
+  $elements = drupal_get_form('locale_translation_filter_form');
+  $output = drupal_render($elements);
   $output .= _locale_translate_seek();
   return $output;
 }
@@ -862,7 +862,10 @@ function locale_translation_filter_form() {
     }
   }
 
-  $form['filters']['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions', 'container-inline')));
+  $form['filters']['actions'] = array(
+    '#type' => 'actions',
+    '#attributes' => array('class' => array('container-inline')),
+  );
   $form['filters']['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Filter'),
@@ -1025,9 +1028,11 @@ function locale_translate_export_screen() {
   $output = '';
   // Offer translation export if any language is set up.
   if (count($names)) {
-    $output = drupal_render(drupal_get_form('locale_translate_export_po_form', $names));
+    $elements = drupal_get_form('locale_translate_export_po_form', $names);
+    $output = drupal_render($elements);
   }
-  $output .= drupal_render(drupal_get_form('locale_translate_export_pot_form'));
+  $elements = drupal_get_form('locale_translate_export_pot_form');
+  $output .= drupal_render($elements);
   return $output;
 }
 
@@ -1051,7 +1056,8 @@ function locale_translate_export_po_form($form, &$form_state, $names) {
     '#default_value' => 'default',
     '#options' => module_invoke_all('locale', 'groups'),
   );
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Export'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
   return $form;
 }
 
@@ -1069,7 +1075,8 @@ function locale_translate_export_pot_form() {
     '#default_value' => 'default',
     '#options' => module_invoke_all('locale', 'groups'),
   );
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Export'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
   // Reuse PO export submission callback.
   $form['#submit'][] = 'locale_translate_export_po_form_submit';
   return $form;
@@ -1158,7 +1165,8 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
     $form['translations'][$translation->language]['#default_value'] = $translation->translation;
   }
 
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
   return $form;
 }
 
@@ -1277,7 +1285,11 @@ function locale_translate_delete_form_submit($form, &$form_state) {
  */
 
 /**
- * Theme locale date format form.
+ * Returns HTML for a locale date format form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -1356,6 +1368,7 @@ function locale_date_format_form($form, &$form_state, $langcode) {
       $choices[$f] = format_date(REQUEST_TIME, 'custom', $f);
     }
   }
+  reset($formats);
 
   // Get configured formats for each language.
   $locale_formats = system_date_format_locale($langcode);
@@ -1365,7 +1378,7 @@ function locale_date_format_form($form, &$form_state, $langcode) {
       $default = $locale_formats[$type];
     }
     else {
-      $default = variable_get('date_format_' . $type, array_shift(array_keys($formats)));
+      $default = variable_get('date_format_' . $type, key($formats));
     }
 
     // Show date format select list.
@@ -1378,7 +1391,7 @@ function locale_date_format_form($form, &$form_state, $langcode) {
     );
   }
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save configuration'),
diff --git a/modules/locale/locale.api.php b/modules/locale/locale.api.php
index fe201a8a7768b6476ec0f929185ac604ce9c3965..d54b4b4d08396304f3c4020cff4a1525479084ff 100644
--- a/modules/locale/locale.api.php
+++ b/modules/locale/locale.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: locale.api.php,v 1.9 2010/03/07 07:44:18 webchick Exp $
+// $Id: locale.api.php,v 1.10 2010/04/07 05:15:51 dries Exp $
 
 /**
  * @file
@@ -24,6 +24,33 @@ function hook_locale($op = 'groups') {
   }
 }
 
+/**
+ * Allows modules to act after language initialization has been performed.
+ *
+ * This is primarily needed to provide translation for configuration variables
+ * in the proper bootstrap phase. Variables are user-defined strings and
+ * therefore should not be translated via t(), since the source string can
+ * change without notice and any previous translation would be lost. Moreover,
+ * since variables can be used in the bootstrap phase, we need a bootstrap hook
+ * to provide a translation early enough to avoid misalignments between code
+ * using the original values and code using the translated values. However
+ * modules implementing hook_boot() should be aware that language initialization
+ * did not happen yet and thus they cannot rely on translated variables.
+ */
+function hook_language_init() {
+  global $language, $conf;
+
+  switch ($language->language) {
+    case 'it':
+      $conf['site_name'] = 'Il mio sito Drupal';
+      break;
+
+    case 'fr':
+      $conf['site_name'] = 'Mon site Drupal';
+      break;
+  }
+}
+
 /**
  * Perform alterations on language switcher links.
  *
diff --git a/modules/locale/locale.field.inc b/modules/locale/locale.field.inc
deleted file mode 100644
index 8cc340af0a36b43d6d020f3a51fbf70b9c19f8bc..0000000000000000000000000000000000000000
--- a/modules/locale/locale.field.inc
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-// $Id: locale.field.inc,v 1.6 2009/12/26 16:50:09 dries Exp $
-
-/**
- * @file
- * Field API multilingual handling.
- */
-
-/**
- * Form submit handler for node_form().
- *
- * Update the field language according to the node language, changing the
- * previous language if necessary.
- */
-function locale_field_node_form_update_field_language($form, &$form_state, $reset_previous = TRUE) {
-  $node = (object) $form_state['values'];
-  $available_languages = field_multilingual_content_languages();
-  // @todo: Unify language neutral language codes.
-  $selected_language = empty($node->language) ? LANGUAGE_NONE : $node->language;
-  list(, , $bundle) = entity_extract_ids('node', $node);
-
-  foreach (field_info_instances('node', $bundle) as $instance) {
-    $field_name = $instance['field_name'];
-    $field = field_info_field($field_name);
-    $previous_language = $form[$field_name]['#language'];
-
-    // Handle a possible language change: previous language values are deleted,
-    // new ones are inserted.
-    if ($field['translatable'] && $previous_language != $selected_language) {
-      $form_state['values'][$field_name][$selected_language] = $node->{$field_name}[$previous_language];
-      if ($reset_previous) {
-        $form_state['values'][$field_name][$previous_language] = array();
-      }
-    }
-  }
-}
-
-/**
- * Apply fallback rules to the given object.
- *
- * Parameters are the same of hook_field_attach_view().
- */
-function locale_field_fallback_view(&$output, $context) {
-  // Lazily init fallback values and candidates to avoid unnecessary calls.
-  $fallback_values = array();
-  $fallback_candidates = NULL;
-  list(, , $bundle) = entity_extract_ids($context['obj_type'], $context['object']);
-
-  foreach (field_info_instances($context['obj_type'], $bundle) as $instance) {
-    $field_name = $instance['field_name'];
-    $field = field_info_field($field_name);
-
-    // If the items array is empty then we have a missing field translation.
-    // @todo: Verify this assumption.
-    if (isset($output[$field_name]) && count(element_children($output[$field_name])) == 0) {
-      if (!isset($fallback_candidates)) {
-        require_once DRUPAL_ROOT . '/includes/language.inc';
-        $fallback_candidates = language_fallback_get_candidates();
-      }
-
-      foreach ($fallback_candidates as $langcode) {
-        // Again if we have a non-empty array we assume the field translation is
-        // valid.
-        if (!empty($context['object']->{$field_name}[$langcode])) {
-          // Cache fallback values per language as fields might have different
-          // fallback values.
-          if (!isset($fallback_values[$langcode])) {
-            $fallback_values[$langcode] = field_attach_view($context['obj_type'], $context['object'], $context['view_mode'], $langcode);
-          }
-          // We are done, skip to the next field.
-          $output[$field_name] = $fallback_values[$langcode][$field_name];
-          break;
-        }
-      }
-    }
-  }
-}
diff --git a/modules/locale/locale.info b/modules/locale/locale.info
index d77727967ff2c84364d40840664bcefd4ada10ec..8b2b1a9d73e5445b042db189683b42f3775e0ed9 100644
--- a/modules/locale/locale.info
+++ b/modules/locale/locale.info
@@ -1,4 +1,4 @@
-; $Id: locale.info,v 1.14 2010/01/08 13:32:43 dries Exp $
+; $Id: locale.info,v 1.15 2010/03/25 11:46:21 dries Exp $
 name = Locale
 description = Adds language handling functionality and enables the translation of the user interface to languages other than English.
 package = Core
@@ -6,13 +6,12 @@ version = VERSION
 core = 7.x
 files[] = locale.module
 files[] = locale.install
-files[] = locale.field.inc
 files[] = locale.admin.inc
 files[] = locale.test
 configure = admin/config/regional/language
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/locale/locale.install b/modules/locale/locale.install
index 43227f52008236044fc9ab17557a19df48b2a05b..93fb79349c66ab2049282437292025b0a256aa2b 100644
--- a/modules/locale/locale.install
+++ b/modules/locale/locale.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: locale.install,v 1.56 2010/03/07 07:44:18 webchick Exp $
+// $Id: locale.install,v 1.57 2010/03/25 11:46:21 dries Exp $
 
 /**
  * @file
@@ -113,6 +113,7 @@ function locale_uninstall() {
   variable_del('locale_cache_strings');
   variable_del('locale_js_directory');
   variable_del('javascript_parsed');
+  variable_del('locale_field_language_fallback');
 
   foreach (language_types() as $type) {
     variable_del("language_negotiation_$type");
diff --git a/modules/locale/locale.module b/modules/locale/locale.module
index 40a34dfa0f6ac6b6aca939c982a5996855004971..b3cfac9b045a4f57125746a25cc2f61aaa9ba3c1 100644
--- a/modules/locale/locale.module
+++ b/modules/locale/locale.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: locale.module,v 1.287 2010/03/18 06:50:37 dries Exp $
+// $Id: locale.module,v 1.291 2010/04/08 18:16:43 dries Exp $
 
 /**
  * @file
@@ -106,6 +106,7 @@ function locale_menu() {
     'page arguments' => array('locale_languages_overview_form'),
     'access arguments' => array('administer languages'),
     'file' => 'locale.admin.inc',
+    'weight' => -10,
   );
   $items['admin/config/regional/language/overview'] = array(
     'title' => 'List',
@@ -167,6 +168,7 @@ function locale_menu() {
     'page callback' => 'locale_translate_overview_screen',
     'access arguments' => array('translate interface'),
     'file' => 'locale.admin.inc',
+    'weight' => -5,
   );
   $items['admin/config/regional/translate/overview'] = array(
     'title' => 'Overview',
@@ -340,7 +342,7 @@ function locale_form_path_admin_form_alter(&$form, &$form_state) {
   $form['language'] = array(
     '#type' => 'select',
     '#title' => t('Language'),
-    '#options' => array('' => t('All languages')) + locale_language_list('name'),
+    '#options' => array(LANGUAGE_NONE => t('All languages')) + locale_language_list('name'),
     '#default_value' => $form['language']['#value'],
     '#weight' => -10,
     '#description' => t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set for <em>All languages</em>.'),
@@ -410,13 +412,27 @@ function locale_form_alter(&$form, &$form_state, $form_id) {
 /**
  * Form submit handler for node_form().
  *
- * Check if Locale is registered as a translation handler and handle possible
+ * Checks if Locale is registered as a translation handler and handle possible
  * node language changes.
  */
 function locale_field_node_form_submit($form, &$form_state) {
-  if (field_multilingual_check_translation_handlers('node', 'locale')) {
-    module_load_include('inc', 'locale', 'locale.field');
-    locale_field_node_form_update_field_language($form, $form_state);
+  if (field_has_translation_handler('node', 'locale')) {
+    $node = (object) $form_state['values'];
+    $available_languages = field_content_languages();
+    list(, , $bundle) = entity_extract_ids('node', $node);
+
+    foreach (field_info_instances('node', $bundle) as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      $previous_language = $form[$field_name]['#language'];
+
+      // Handle a possible language change: new language values are inserted,
+      // previous ones are deleted.
+      if ($field['translatable'] && $previous_language != $node->language) {
+        $form_state['values'][$field_name][$node->language] = $node->{$field_name}[$previous_language];
+        $form_state['values'][$field_name][$previous_language] = array();
+      }
+    }
   }
 }
 
@@ -438,20 +454,57 @@ function locale_theme() {
 }
 
 /**
- * Implements hook_field_attach_view_alter().
+ * Implements hook_field_language_alter().
  */
-function locale_field_attach_view_alter(&$output, $context) {
-  // In locale_field_fallback_view() we might call field_attach_view(). The
-  // static variable avoids unnecessary recursion.
-  static $recursion;
+function locale_field_language_alter(&$display_language, $context) {
+  // Do not apply core language fallback rules if they are disabled or if Locale
+  // is not registered as a translation handler.
+  if (variable_get('locale_field_language_fallback', TRUE) && field_has_translation_handler($context['entity_type'], 'locale')) {
+    locale_field_language_fallback($display_language, $context['entity'], $context['language']);
+  }
+}
 
-  // Do not apply fallback rules if disabled or if Locale is not registered as a
-  // translation handler.
-  if (!$recursion && variable_get('locale_field_fallback_view', TRUE) && field_multilingual_check_translation_handlers($context['obj_type'], 'locale')) {
-    $recursion = TRUE;
-    module_load_include('inc', 'locale', 'locale.field');
-    locale_field_fallback_view($output, $context);
-    $recursion = FALSE;
+/**
+ * Applies language fallback rules to the fields attached to the given entity.
+ *
+ * Core language fallback rules simply check if fields have a field translation
+ * for the requested language code. If so the requested language is returned,
+ * otherwise all the fallback candidates are inspected to see if there is a
+ * field translation available in another language.
+ * By default this is called by locale_field_language_alter(), but this
+ * behavior can be disabled by setting the 'locale_field_language_fallback'
+ * variable to FALSE.
+ *
+ * @param $display_language
+ *   A reference to an array of language codes keyed by field name.
+ * @param $entity
+ *   The entity to be displayed.
+ * @param $langcode
+ *   The language code $entity has to be displayed in. 
+ */
+function locale_field_language_fallback(&$display_language, $entity, $langcode) {
+  // Lazily init fallback candidates to avoid unnecessary calls.
+  $fallback_candidates = NULL;
+  $field_languages = array();
+
+  foreach ($display_language as $field_name => $field_language) {
+    // If the requested language is defined for the current field use it,
+    // otherwise search for a fallback value among the fallback candidates.
+    if (isset($entity->{$field_name}[$langcode])) {
+      $display_language[$field_name] = $langcode;
+    }
+    elseif (!empty($entity->{$field_name})) {
+      if (!isset($fallback_candidates)) {
+        require_once DRUPAL_ROOT . '/includes/language.inc';
+        $fallback_candidates = language_fallback_get_candidates();
+      }
+      foreach ($fallback_candidates as $fallback_language) {
+        if (isset($entity->{$field_name}[$fallback_language])) {
+          $display_language[$field_name] = $fallback_language;
+          break;
+        }
+      }
+    }
   }
 }
 
@@ -459,10 +512,7 @@ function locale_field_attach_view_alter(&$output, $context) {
  * Implements hook_entity_info_alter().
  */
 function locale_entity_info_alter(&$entity_info) {
-  $enabled = drupal_multilingual();
-  foreach ($entity_info as $type => $info) {
-    $entity_info[$type]['translation']['locale'] = $enabled;
-  }
+  $entity_info['node']['translation']['locale'] = TRUE;
 }
 
 /**
diff --git a/modules/locale/locale.test b/modules/locale/locale.test
index 64ed929f39322aa3d55145d961fbf4e674dd3501..1f31b7f2532c80e9e6454ecdf3022cee43584d3c 100644
--- a/modules/locale/locale.test
+++ b/modules/locale/locale.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: locale.test,v 1.67 2010/03/21 04:17:02 webchick Exp $
+// $Id: locale.test,v 1.69 2010/03/31 20:05:06 dries Exp $
 
 /**
  * @file
@@ -1151,7 +1151,7 @@ class LanguageSwitchingFunctionalTest extends DrupalWebTestCase {
     $this->assertText(t('Languages'), t('Language switcher block found.'));
 
     // Assert that only the current language is marked as active.
-    list($language_switcher) = $this->xpath("//div[@id=\"block-locale-{$language_type}\"]/div[@class=\"content\"]");
+    list($language_switcher) = $this->xpath('//div[@id=:id]/div[@class="content"]', array(':id' => 'block-locale-' . $language_type));
     $links = array(
       'active' => array(),
       'inactive' => array(),
@@ -1270,7 +1270,7 @@ class LocaleUserLanguageFunctionalTest extends DrupalWebTestCase {
     // Ensure form was submitted successfully.
     $this->assertText(t('The changes have been saved.'), t('Changes were saved.'));
     // Check if language was changed.
-    $elements = $this->xpath('//input[@id="edit-language-' . $langcode . '"]');
+    $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-language-' . $langcode));
     $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Default language successfully updated.'));
 
     $this->drupalLogout();
@@ -1806,8 +1806,6 @@ class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase {
     locale_add_language('it', 'Italian', 'Italiano', LANGUAGE_LTR, '', '', TRUE, FALSE);
 
     // Set "Basic page" content type to use multilingual support.
-    $this->drupalGet('admin/structure/types/manage/page');
-    $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.'));
     $edit = array(
       'language_content_type' => 1,
     );
@@ -1852,6 +1850,16 @@ class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase {
 
     $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value;
     $this->assertTrue($assert, t('Field language correctly changed.'));
+
+    // Enable content language URL detection.
+    language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LOCALE_LANGUAGE_NEGOTIATION_URL => 0));
+
+    // Test multilingual field language fallback logic.
+    $this->drupalGet("it/node/$node->nid");
+    $this->assertRaw($body_value, t('Body correctly displayed using Italian as requested language'));
+
+    $this->drupalGet("node/$node->nid");
+    $this->assertRaw($body_value, t('Body correctly displayed using English as requested language'));
   }
 
   /*
@@ -1878,17 +1886,18 @@ class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase {
 
     // Check if node body is showed.
     $this->drupalGet("node/$node->nid");
-    $body_xpath = '//div[@id="node-' . $node->nid . '"]//div[@property="content:encoded"]/p';
-    $this->assertEqual(current($this->xpath($body_xpath)), $node->body['en'][0]['value'], 'Node body is correctly showed.');
+    $body = $this->xpath('//div[@id=:id]//div[@property="content:encoded"]/p', array(':id' => 'node-' . $node->nid));
+    $this->assertEqual(current($body), $node->body['en'][0]['value'], 'Node body is correctly showed.');
 
     $settings['body[full][type]'] = 'hidden';
     $this->drupalPost('admin/structure/types/manage/page/display', $settings, t('Save'));
-    $select_xpath = '//select[@id="edit-body-full-type"]/option[@selected="selected"]';
+    $select = $this->xpath('//select[@id="edit-body-full-type"]/option[@selected="selected"]');
     // Check if body display is actually "hidden" for the "full" view mode.
-    $this->assertEqual(current($this->xpath($select_xpath)), '<Hidden>', 'Body display is actually "hidden" for the "full" view mode');
+    $this->assertEqual(current($select), '<Hidden>', 'Body display is actually "hidden" for the "full" view mode');
     $this->drupalGet("node/$node->nid");
     // Check if node body is not showed.
-    $this->assertFalse(is_array($this->xpath($body_xpath)), 'Body correctly not showed.');
+    $body = $this->xpath('//div[@id=:id]//div[@property="content:encoded"]/p', array(':id' => 'node-' . $node->nid));
+    $this->assertFalse(is_array($body), 'Body correctly not showed.');
   }
 }
 
diff --git a/modules/locale/tests/locale_test.info b/modules/locale/tests/locale_test.info
index 3be16dfefb0935757ba52c38b4b537b669d804ae..395f532f20dc0bbbbce2f5a87de03ac24f588b3f 100644
--- a/modules/locale/tests/locale_test.info
+++ b/modules/locale/tests/locale_test.info
@@ -7,8 +7,8 @@ files[] = locale_test.module
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/menu/menu.admin.inc b/modules/menu/menu.admin.inc
index 71f671aa69d5beef989217d50773a1d2f55e9c45..76bbc80f3e290698bc28753cbaf5168f863f63f0 100644
--- a/modules/menu/menu.admin.inc
+++ b/modules/menu/menu.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: menu.admin.inc,v 1.76 2010/02/23 10:32:10 dries Exp $
+// $Id: menu.admin.inc,v 1.78 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -25,7 +25,14 @@ function menu_overview_page() {
 }
 
 /**
- * Theme the menu title and description for admin page
+ * Returns HTML for a menu title and description for the menu overview page.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - title: The menu's title.
+ *   - description: The menu's description.
+ *
+ * @ingroup themeable
  */
 function theme_menu_admin_overview($variables) {
   $output = check_plain($variables['title']);
@@ -65,7 +72,7 @@ function menu_overview_form($form, &$form_state, $menu) {
   $form['#menu'] =  $menu;
 
   if (element_children($form)) {
-    $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+    $form['actions'] = array('#type' => 'actions');
     $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Save configuration'),
@@ -177,7 +184,11 @@ function menu_overview_form_submit($form, &$form_state) {
 }
 
 /**
- * Theme the menu overview form into a table.
+ * Returns HTML for the menu overview form into a table.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -328,7 +339,8 @@ function menu_edit_item($form, &$form_state, $type, $item, $menu) {
     '#default_value' => $item['weight'],
     '#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'),
   );
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
 
   return $form;
 }
@@ -456,12 +468,13 @@ function menu_edit_menu($form, &$form_state, $type, $menu = array()) {
     '#title' => t('Description'),
     '#default_value' => $menu['description'],
   );
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
   );
   // Only custom menus may be deleted.
-  $form['delete'] = array(
+  $form['actions']['delete'] = array(
     '#type' => 'submit',
     '#value' => t('Delete'),
     '#access' => $type == 'edit' && !isset($system_menus[$menu['menu_name']]),
diff --git a/modules/menu/menu.info b/modules/menu/menu.info
index 2c296d7e807f5fbc614a02ee5a1478e2b2a75c25..a8d962634dd159282ae78d1c30e5fd56e8f475cf 100644
--- a/modules/menu/menu.info
+++ b/modules/menu/menu.info
@@ -10,8 +10,8 @@ files[] = menu.install
 files[] = menu.test
 configure = admin/structure/menu
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/menu/menu.js b/modules/menu/menu.js
index 4ff5300c2208bb386d76f21d1ffceb5ee76194c8..10f1fac819ee17a02df802faad0bad58ddc4cce0 100644
--- a/modules/menu/menu.js
+++ b/modules/menu/menu.js
@@ -1,10 +1,10 @@
-// $Id: menu.js,v 1.5 2010/02/14 09:39:45 dries Exp $
+// $Id: menu.js,v 1.6 2010/04/09 12:24:53 dries Exp $
 
 (function ($) {
 
 Drupal.behaviors.menuFieldsetSummaries = {
   attach: function (context) {
-    $('fieldset.menu-link-form', context).setSummary(function (context) {
+    $('fieldset.menu-link-form', context).drupalSetSummary(function (context) {
       if ($('#edit-menu-enabled', context).attr('checked')) {
         return Drupal.checkPlain($('#edit-menu-link-title', context).val());
       }
diff --git a/modules/node/content_types.inc b/modules/node/content_types.inc
index b1f7edc72a7913d0f37e4ecf50d898b7f51b45ad..03d199dcf88be8b0573ff49f400544e0f0db8df6 100644
--- a/modules/node/content_types.inc
+++ b/modules/node/content_types.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: content_types.inc,v 1.110 2010/01/13 23:36:50 dries Exp $
+// $Id: content_types.inc,v 1.112 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -54,6 +54,17 @@ function node_overview_types() {
   return $build;
 }
 
+/**
+ * Returns HTML for a node type description for the content type admin overview page.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - name: The human-readable name of the content type.
+ *   - type: An object containing the 'type' (machine name) and 'description' of
+ *     the content type.
+ *
+ * @ingroup themeable
+ */
 function theme_node_admin_overview($variables) {
   $name = $variables['name'];
   $type = $variables['type'];
@@ -234,7 +245,7 @@ function node_type_form($form, &$form_state, $type = NULL) {
     '#value' => $type->locked,
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save content type'),
diff --git a/modules/node/content_types.js b/modules/node/content_types.js
index 18cadd70e8c832489f8c818e2dc6b28a9d9899fb..2fc099822780bba6a570d45acf5288cfe8d9a4a4 100644
--- a/modules/node/content_types.js
+++ b/modules/node/content_types.js
@@ -1,16 +1,16 @@
-// $Id: content_types.js,v 1.8 2009/12/02 15:09:16 dries Exp $
+// $Id: content_types.js,v 1.9 2010/04/09 12:24:53 dries Exp $
 (function ($) {
 
 Drupal.behaviors.contentTypes = {
   attach: function (context) {
     // Provide the vertical tab summaries.
-    $('fieldset#edit-submission', context).setSummary(function(context) {
+    $('fieldset#edit-submission', context).drupalSetSummary(function(context) {
       var vals = [];
       vals.push(Drupal.checkPlain($('#edit-title-label', context).val()) || Drupal.t('Requires a title'));
       vals.push(Drupal.checkPlain($('#edit-body-label', context).val()) || Drupal.t('No body'));
       return vals.join(', ');
     });
-    $('fieldset#edit-workflow', context).setSummary(function(context) {
+    $('fieldset#edit-workflow', context).drupalSetSummary(function(context) {
       var vals = [];
       $("input[name^='node_options']:checked", context).parent().each(function() {
         vals.push(Drupal.checkPlain($(this).text()));
@@ -20,7 +20,7 @@ Drupal.behaviors.contentTypes = {
       }
       return vals.join(', ');
     });
-    $('fieldset#edit-display', context).setSummary(function(context) {
+    $('fieldset#edit-display', context).drupalSetSummary(function(context) {
       var vals = [];
       $('input:checked', context).next('label').each(function() {
         vals.push(Drupal.checkPlain($(this).text()));
diff --git a/modules/node/node.admin.inc b/modules/node/node.admin.inc
index 906f781c206cd41c53d3dfe2516f4ff3b22525a8..ab4556d0ea4facb32865065b34c7ceedbf02ab9c 100644
--- a/modules/node/node.admin.inc
+++ b/modules/node/node.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.admin.inc,v 1.92 2010/03/07 06:49:10 webchick Exp $
+// $Id: node.admin.inc,v 1.94 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -188,9 +188,9 @@ function node_filter_form() {
   }
 
   $form['filters']['actions'] = array(
-    '#type' => 'container',
+    '#type' => 'actions',
     '#id' => 'node-admin-buttons',
-    '#attributes' => array('class' => array('form-actions', 'container-inline')),
+    '#attributes' => array('class' => array('container-inline')),
   );
   $form['filters']['actions']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
   if (count($session)) {
@@ -204,7 +204,11 @@ function node_filter_form() {
 }
 
 /**
- * Theme node administration filter selector.
+ * Returns HTML for a node administration filter selector.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
diff --git a/modules/node/node.api.php b/modules/node/node.api.php
index 0546672bcac5204d7aa8e644c365aa10c1f65999..a2d79354d00cf48c84820d78e3780a7a28038bad 100644
--- a/modules/node/node.api.php
+++ b/modules/node/node.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.api.php,v 1.65 2010/03/18 06:49:17 dries Exp $
+// $Id: node.api.php,v 1.66 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -690,7 +690,7 @@ function hook_node_validate($node, $form) {
  * When $view_mode is 'rss', modules can also add extra RSS elements and
  * namespaces to $node->rss_elements and $node->rss_namespaces respectively for
  * the RSS item generated for this node.
- * For details on how this is used @see node_feed()
+ * For details on how this is used, see node_feed().
  *
  * @see taxonomy_node_view()
  * @see upload_node_view()
diff --git a/modules/node/node.css b/modules/node/node.css
index 6c25617488c4f17386c540afae13f39738893b6c..53fcb5f00751e1d42e3825ae348ec3f8eb7abf06 100644
--- a/modules/node/node.css
+++ b/modules/node/node.css
@@ -1,4 +1,4 @@
-/* $Id: node.css,v 1.15 2010/03/18 06:50:37 dries Exp $ */
+/* $Id: node.css,v 1.16 2010/04/08 18:26:42 dries Exp $ */
 
 .node-unpublished {
   background-color: #fff4f4;
@@ -24,6 +24,3 @@
 td.revision-current {
   background: #ffc;
 }
-.terms-inline {
-  display: inline;
-}
diff --git a/modules/node/node.info b/modules/node/node.info
index 7c41585981e9890a0b9b6da09dfb4adc79837e3c..3496f151808abd8e3ffd954ab1cd0c895d21a99c 100644
--- a/modules/node/node.info
+++ b/modules/node/node.info
@@ -14,8 +14,8 @@ files[] = node.tokens.inc
 required = TRUE
 configure = admin/structure/types
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/node/node.install b/modules/node/node.install
index a7a04a674ec9b4e430c420f299932cca22ea9bd0..e7e75df84a3eb26d90d4692200e5b036834391ea 100644
--- a/modules/node/node.install
+++ b/modules/node/node.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.install,v 1.45 2010/03/01 07:39:12 dries Exp $
+// $Id: node.install,v 1.46 2010/03/28 11:16:29 dries Exp $
 
 /**
  * @file
@@ -353,6 +353,34 @@ function node_schema() {
     'primary key' => array('type'),
   );
 
+  $schema['block_node_type'] = array(
+    'description' => 'Sets up display criteria for blocks based on content types',
+    'fields' => array(
+      'module' => array(
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'description' => "The block's origin module, from {block}.module.",
+      ),
+      'delta' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'description' => "The block's unique delta within module, from {block}.delta.",
+      ),
+      'type' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'description' => "The machine-readable name of this type from {node_type}.type.",
+      ),
+    ),
+    'primary key' => array('module', 'delta', 'type'),
+    'indexes' => array(
+      'type' => array('type'),
+    ),
+  );
+
   return $schema;
 }
 
@@ -609,6 +637,41 @@ function node_update_7009() {
     ->execute();
 }
 
+/**
+ * Add the {block_node_type} table.
+ */
+function node_update_7010() {
+  $schema['block_node_type'] = array(
+    'description' => 'Sets up display criteria for blocks based on content types',
+    'fields' => array(
+      'module' => array(
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => TRUE,
+        'description' => "The block's origin module, from {block}.module.",
+      ),
+      'delta' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'description' => "The block's unique delta within module, from {block}.delta.",
+      ),
+      'type' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'description' => "The machine-readable name of this type from {node_type}.type.",
+      ),
+    ),
+    'primary key' => array('module', 'delta', 'type'),
+    'indexes' => array(
+      'type' => array('type'),
+    ),
+  );
+
+  db_create_table('block_node_type', $schema['block_node_type']);
+}
+
 /**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
diff --git a/modules/node/node.js b/modules/node/node.js
index 3b81195a0fe5f574c974532f5fed2051f7d0704b..76c98224402873dc34acba9c0b86af5a56c5c4ee 100644
--- a/modules/node/node.js
+++ b/modules/node/node.js
@@ -1,23 +1,23 @@
-// $Id: node.js,v 1.4 2009/04/27 20:19:37 webchick Exp $
+// $Id: node.js,v 1.5 2010/04/09 12:24:53 dries Exp $
 
 (function ($) {
 
 Drupal.behaviors.nodeFieldsetSummaries = {
   attach: function (context) {
-    $('fieldset#edit-revision-information', context).setSummary(function (context) {
+    $('fieldset#edit-revision-information', context).drupalSetSummary(function (context) {
       return $('#edit-revision', context).is(':checked') ?
         Drupal.t('New revision') :
         Drupal.t('No revision');
     });
 
-    $('fieldset#edit-author', context).setSummary(function (context) {
+    $('fieldset#edit-author', context).drupalSetSummary(function (context) {
       var name = $('#edit-name').val(), date = $('#edit-date').val();
       return date ?
         Drupal.t('By @name on @date', { '@name': name, '@date': date }) :
         Drupal.t('By @name', { '@name': name });
     });
 
-    $('fieldset#edit-options', context).setSummary(function (context) {
+    $('fieldset#edit-options', context).drupalSetSummary(function (context) {
       var vals = [];
 
       $('input:checked', context).parent().each(function () {
diff --git a/modules/node/node.module b/modules/node/node.module
index 3e24530fac997c8b7546f87297ab86bc9fe3c390..dd759bb6d1f98c6eba30a0d774c6b0f24285bf02 100644
--- a/modules/node/node.module
+++ b/modules/node/node.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.module,v 1.1247 2010/03/18 06:50:37 dries Exp $
+// $Id: node.module,v 1.1264 2010/04/22 09:12:35 webchick Exp $
 
 /**
  * @file
@@ -129,9 +129,6 @@ function node_theme() {
       'render element' => 'elements',
       'template' => 'node',
     ),
-    'node_list' => array(
-      'variables' => array('items' => NULL, 'title' => NULL),
-    ),
     'node_search_admin' => array(
       'render element' => 'form',
     ),
@@ -147,9 +144,6 @@ function node_theme() {
       'variables' => array('node' => NULL),
       'file' => 'node.pages.inc',
     ),
-    'node_log_message' => array(
-      'variables' => array('log' => NULL),
-    ),
     'node_admin_overview' => array(
       'variables' => array('name' => NULL, 'type' => NULL),
     ),
@@ -183,7 +177,7 @@ function node_entity_info() {
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
       'fieldable' => TRUE,
-      'object keys' => array(
+      'entity keys' => array(
         'id' => 'nid',
         'revision' => 'vid',
         'bundle' => 'type',
@@ -191,8 +185,6 @@ function node_entity_info() {
       'bundle keys' => array(
         'bundle' => 'type',
       ),
-      // Node.module handles its own caching.
-      // 'cacheable' => FALSE,
       'bundles' => array(),
       'view modes' => array(
         'full' => array(
@@ -286,16 +278,7 @@ function node_title_list($result, $title = NULL) {
     $num_rows = TRUE;
   }
 
-  return $num_rows ? theme('node_list', array('items' => $items, 'title' => $title)) : FALSE;
-}
-
-/**
- * Format a listing of links to nodes.
- *
- * @ingroup themeable
- */
-function theme_node_list($variables) {
-  return theme('item_list', $variables);
+  return $num_rows ? theme('item_list__node', array('items' => $items, 'title' => $title)) : FALSE;
 }
 
 /**
@@ -377,8 +360,12 @@ function _node_extract_type($node) {
 /**
  * Returns a list of all the available node types.
  *
+ * This list can include types that are queued for addition or deletion.
+ * See _node_types_build() for details.
+ *
  * @return
  *   An array of node types, keyed by the type.
+ *
  * @see node_type_get_type()
  */
 function node_type_get_types() {
@@ -395,10 +382,8 @@ function node_type_get_types() {
  *   A single node type, as an object, or FALSE if the node type is not found.
  *   The node type is an object containing fields from hook_node_info() return
  *   values, as well as the field 'type' (the machine-readable type) and other
- *   fields used internally and defined in _node_types_build() and
- *   node_type_set_defaults().
- *
- * @see hook_node_info();
+ *   fields used internally and defined in _node_types_build(),
+ *   hook_node_info(), and node_type_set_defaults().
  */
 function node_type_get_type($node) {
   $type = _node_extract_type($node);
@@ -409,15 +394,17 @@ function node_type_get_type($node) {
 /**
  * Returns the node type base of the passed node or node type string.
  *
- * The base indicates which module implement this node type and is used to
- * execute node type specific hooks.
- *
- * @see node_invoke()
+ * The base indicates which module implements this node type and is used to
+ * execute node-type-specific hooks. For types defined in the user interface
+ * and managed by node.module, the base is 'node_content'.
  *
  * @param $node
  *   A node object or string that indicates the node type to return.
+ *
  * @return
  *   The node type base or FALSE if the node type is not found.
+ *
+ * @see node_invoke()
  */
 function node_type_get_base($node) {
   $type = _node_extract_type($node);
@@ -426,7 +413,10 @@ function node_type_get_base($node) {
 }
 
 /**
- * Returns a list of available node names.
+ * Returns a list of available node type names.
+ *
+ * This list can include types that are queued for addition or deletion.
+ * See _node_types_build() for details.
  *
  * @return
  *   An array of node type names, keyed by the type.
@@ -451,9 +441,12 @@ function node_type_get_name($node) {
 }
 
 /**
- * Resets the database cache of node types.
+ * Updates the database cache of node types.
  *
- * All new or non-modified module-defined node types are saved to the database.
+ * All new module-defined node types are saved to the database via a call to
+ * node_type_save(), and obsolete ones are deleted via a call to
+ * node_type_delete(). See _node_types_build() for an explanation of the new 
+ * and obsolete types.
  */
 function node_types_rebuild() {
   // Reset and load updated node types.
@@ -469,11 +462,11 @@ function node_types_rebuild() {
 }
 
 /**
- * Menu argument loader; Load a node type by string.
+ * Menu argument loader: loads a node type by string.
  *
  * @param $name
- *   The machine-readable name of a node type to load; having '_' replaced with
- *   '-'.
+ *   The machine-readable name of a node type to load, where '_' is replaced
+ *   with '-'.
  *
  * @return
  *   A node type object or FALSE if $name does not exist.
@@ -562,7 +555,7 @@ function node_configure_fields($type) {
       $field = array(
         'field_name' => 'body',
         'type' => 'text_with_summary',
-        'object_types' => array('node'),
+        'entity_types' => array('node'),
         'translatable' => TRUE,
       );
       $field = field_create_field($field);
@@ -570,7 +563,7 @@ function node_configure_fields($type) {
     if (empty($instance)) {
       $instance = array(
         'field_name' => 'body',
-        'object_type' => 'node',
+        'entity_type' => 'node',
         'bundle' => $type->type,
         'label' => $type->body_label,
         'widget_type' => 'text_textarea_with_summary',
@@ -660,9 +653,23 @@ function node_type_update_nodes($old_type, $type) {
 /**
  * Builds and returns the list of available node types.
  *
- * The list of types is built by querying hook_node_info() in all modules, and
- * by comparing this information with the node types in the {node_type} table.
+ * The list of types is built by invoking hook_node_info() on all modules and
+ * comparing this information with the node types in the {node_type} table.
+ * These two information sources are not synchronized during module installation
+ * until node_types_rebuild() is called.
  *
+ * @return
+ *   Associative array with two components:
+ *   - names: Associative array of the names of node types, keyed by the type.
+ *   - types: Associative array of node type objects, keyed by the type.
+ *   Both of these arrays will include new types that have been defined by
+ *   hook_node_info() implementations but not yet saved in the {node_type}
+ *   table. These are indicated in the type object by $type->is_new being set
+ *   to the value 1. These arrays will also include obsolete types: types that
+ *   were previously defined by modules that have now been disabled, or for
+ *   whatever reason are no longer being defined in hook_node_info()
+ *   implementations, but are still in the database. These are indicated in the
+ *   type object by $type->disabled being set to TRUE.
  */
 function _node_types_build() {
   $_node_types = &drupal_static(__FUNCTION__);
@@ -978,8 +985,6 @@ function node_save($node) {
 
   try {
     field_attach_presave('node', $node);
-    // Let modules modify the node before it is saved to the database.
-    module_invoke_all('node_presave', $node);
     global $user;
 
     // Determine if we will be inserting a new node.
@@ -987,6 +992,20 @@ function node_save($node) {
       $node->is_new = empty($node->nid);
     }
 
+    // Set the timestamp fields.
+    if (empty($node->created)) {
+      $node->created = REQUEST_TIME;
+    }
+    // The changed timestamp is always updated for bookkeeping purposes,
+    // for example: revisions, searching, etc.
+    $node->changed = REQUEST_TIME;
+
+    $node->timestamp = REQUEST_TIME;
+    $update_node = TRUE;
+
+    // Let modules modify the node before it is saved to the database.
+    module_invoke_all('node_presave', $node);
+
     if ($node->is_new || !empty($node->revision)) {
       // When inserting either a new node or a new node revision, $node->log
       // must be set because {node_revision}.log is a text column and therefore
@@ -1018,18 +1037,6 @@ function node_save($node) {
       unset($node->vid);
     }
 
-    // Set the timestamp fields.
-    if (empty($node->created)) {
-      $node->created = REQUEST_TIME;
-    }
-    // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
-    if (empty($node->changed)) {
-      $node->changed = REQUEST_TIME;
-    }
-
-    $node->timestamp = REQUEST_TIME;
-    $update_node = TRUE;
-
     // Save the node and node revision.
     if ($node->is_new) {
       // For new nodes, save new records for both the node itself and the node
@@ -1073,21 +1080,21 @@ function node_save($node) {
     module_invoke_all('node_' . $op, $node);
     entity_invoke($op, 'node', $node);
 
-    // Update the node access table for this node.
-    node_access_acquire_grants($node);
+    // Update the node access table for this node. There's no need to delete
+    // existing records if the node is new.
+    $delete = $op == 'update';
+    node_access_acquire_grants($node, $delete);
 
     // Clear internal properties.
     unset($node->is_new);
 
-    // Clear the page and block caches.
-    cache_clear_all();
-
     // Ignore slave server temporarily to give time for the
     // saved node to be propagated to the slave.
     db_ignore_slave();
   }
   catch (Exception $e) {
     $transaction->rollback('node', $e->getMessage(), array(), WATCHDOG_ERROR);
+    throw $e;
   }
 }
 
@@ -1342,8 +1349,10 @@ function template_preprocess_node(&$variables) {
 
   $variables['date']      = format_date($node->created);
   $variables['name']      = theme('username', array('account' => $node));
-  $variables['node_url']  = url('node/' . $node->nid);
-  $variables['node_title'] = check_plain($node->title);
+
+  $uri = entity_uri('node', $node);
+  $variables['node_url']  = url($uri['path'], $uri['options']);
+  $variables['title']     = check_plain($node->title);
   $variables['page']      = node_is_page($node);
 
   if (!empty($node->in_preview)) {
@@ -1394,15 +1403,6 @@ function template_preprocess_node(&$variables) {
   $variables['theme_hook_suggestions'][] = 'node__' . $node->nid;
 }
 
-/**
- * Theme a log message.
- *
- * @ingroup themeable
- */
-function theme_node_log_message($variables) {
-  return '<div class="log"><div class="title">' . t('Log') . ':</div>' . $variables['log'] . '</div>';
-}
-
 /**
  * Implements hook_permission().
  */
@@ -1410,11 +1410,11 @@ function node_permission() {
   $perms = array(
     'administer content types' => array(
       'title' => t('Administer content types'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
     'administer nodes' => array(
       'title' => t('Administer content'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
     'access content' => array(
       'title' => t('View published content'),
@@ -1424,7 +1424,8 @@ function node_permission() {
     ),
     'bypass node access' => array(
       'title' => t('Bypass content access control'),
-      'description' => t('View, edit and delete all content regardless of permission restrictions. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'description' => t('View, edit and delete all content regardless of permission restrictions.'),
+      'restrict access' => TRUE,
     ),
     'view revisions' => array(
       'title' => t('View content revisions'),
@@ -1582,8 +1583,9 @@ function node_search_execute($keys = NULL) {
 
     $extra = module_invoke_all('node_search_result', $node);
 
+    $uri = entity_uri('node', $node);
     $results[] = array(
-      'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
+      'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE))),
       'type' => check_plain(node_type_get_name($node)),
       'title' => $node->title,
       'user' => theme('username', array('account' => $node)),
@@ -1694,7 +1696,11 @@ function node_user_delete($account) {
 }
 
 /**
- * Theme the content ranking part of the search settings admin page.
+ * Returns HTML for the content ranking part of the search settings admin page.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -2128,10 +2134,12 @@ function node_get_recent($number = 10) {
 }
 
 /**
- * Returns a formatted list of recent nodes.
+ * Returns HTML for a list of recent content.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - nodes: An array of recent node objects.
  *
- * @return
- *   The recent content table HTML.
  * @ingroup themeable
  */
 function theme_node_recent_block($variables) {
@@ -2167,10 +2175,12 @@ function theme_node_recent_block($variables) {
 }
 
 /**
- * Returns a formatted recent node to be displayed in the recent content block.
+ * Returns HTML for a recent node to be displayed in the recent content block.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - node: A node object.
  *
- * @return
- *   The recent content node's HTML.
  * @ingroup themeable
  */
 function theme_node_recent_content($variables) {
@@ -2190,6 +2200,8 @@ function theme_node_recent_content($variables) {
  * Implements hook_form_FORMID_alter().
  *
  * Adds node-type specific visibility options to add block form.
+ *
+ * @see block_add_block_form()
  */
 function node_form_block_add_block_form_alter(&$form, &$form_state) {
   node_form_block_admin_configure_alter($form, $form_state);
@@ -2199,6 +2211,8 @@ function node_form_block_add_block_form_alter(&$form, &$form_state) {
  * Implements hook_form_FORMID_alter().
  *
  * Adds node-type specific visibility options to block configuration form.
+ *
+ * @see block_admin_configure()
  */
 function node_form_block_admin_configure_alter(&$form, &$form_state) {
   $default_type_options = db_query("SELECT type FROM {block_node_type} WHERE module = :module AND delta = :delta", array(
@@ -2220,13 +2234,15 @@ function node_form_block_admin_configure_alter(&$form, &$form_state) {
     '#options' => node_type_get_names(),
     '#description' => t('Show this block only on pages that display content of the given type(s). If you select no types, there will be no type-specific limitation.'),
   );
-  $form['#submit'][] = 'node_block_admin_configure_submit';
+  $form['#submit'][] = 'node_form_block_admin_configure_submit';
 }
 
 /**
  * Form submit handler for block configuration form.
+ *
+ * @see node_form_block_admin_configure_alter()
  */
-function node_block_admin_configure_submit($form, &$form_state) {
+function node_form_block_admin_configure_submit($form, &$form_state) {
   db_delete('block_node_type')
     ->condition('module', $form_state['values']['module'])
     ->condition('delta', $form_state['values']['delta'])
@@ -2242,6 +2258,96 @@ function node_block_admin_configure_submit($form, &$form_state) {
   $query->execute();
 }
 
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Adds node specific submit handler to delete custom block form.
+ *
+ * @see block_custom_block_delete()
+ */
+function node_form_block_custom_block_delete_alter(&$form, &$form_state) {
+  $form['#submit'][] = 'node_form_block_custom_block_delete_submit';
+}
+
+/**
+ * Form submit handler for custom block delete form.
+ *
+ * @see node_form_block_custom_block_delete_alter()
+ */
+function node_form_block_custom_block_delete_submit($form, &$form_state) {
+  db_delete('block_node_type')
+    ->condition('module', 'block')
+    ->condition('delta', $form_state['values']['bid'])
+    ->execute();
+}
+
+/**
+ * Implements hook_modules_uninstalled().
+ *
+ * Cleanup {block_node_type} table from modules' blocks.
+ */
+function node_modules_uninstalled($modules) {
+  db_delete('block_node_type')
+    ->condition('module', $modules, 'IN')
+    ->execute();
+}
+
+/**
+ * Implements hook_block_list_alter().
+ *
+ * Check the content type specific visibilty settings.
+ * Remove the block if the visibility conditions are not met.
+ */
+function node_block_list_alter(&$blocks) {
+  global $theme_key;
+
+  // Build an array of node types for each block.
+  $block_node_types = array();
+  $result = db_query('SELECT module, delta, type FROM {block_node_type}');
+  foreach ($result as $record) {
+    $block_node_types[$record->module][$record->delta][$record->type] = TRUE;
+  }
+
+  $node = menu_get_object();
+  $node_types = node_type_get_types();
+  if (arg(0) == 'node' && arg(1) == 'add' && arg(2)) {
+    $node_add_arg = strtr(arg(2), '-', '_');
+  }
+  foreach ($blocks as $key => $block) {
+    if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) {
+      // This block was added by a contrib module, leave it in the list.
+      continue;
+    }
+
+    // If a block has no node types associated, it is displayed for every type.
+    // For blocks with node types associated, if the node type does not match
+    // the settings from this block, remove it from the block list.
+    if (isset($block_node_types[$block->module][$block->delta])) {
+      if (!empty($node)) {
+        // This is a node or node edit page.
+        if (!isset($block_node_types[$block->module][$block->delta][$node->type])) {
+          // This block should not be displayed for this node type.
+          unset($blocks[$key]);
+          continue;
+        }
+      }
+      elseif (isset($node_add_arg) && isset($node_types[$node_add_arg])) {
+        // This is a node creation page
+        if (!isset($block_node_types[$block->module][$block->delta][$node_add_arg])) {
+          // This block should not be displayed for this node type.
+          unset($blocks[$key]);
+          continue;
+        }
+      }
+      else {
+        // This is not a node page, remove the block.
+        unset($blocks[$key]);
+        continue;
+      }
+    }
+  }
+}
+
 /**
  * A generic function for generating RSS feeds from a set of nodes.
  *
@@ -2405,10 +2511,11 @@ function node_page_default() {
  */
 function node_page_view($node) {
   drupal_set_title($node->title);
+  $uri = entity_uri('node', $node);
   // Set the node path as the canonical URL to prevent duplicate content.
-  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)), TRUE);
+  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
   // Set the non-aliased path as a default shortlink.
-  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url('node/' . $node->nid, array('alias' => TRUE))), TRUE);
+  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
   return node_show($node);
 }
 
@@ -2966,8 +3073,12 @@ function node_query_node_access_alter(QueryAlterableInterface $query) {
  *
  * @param $node
  *   The $node to acquire grants for.
+ *
+ * @param $delete
+ *   Whether to delete existing node access records before inserting new ones.
+ *   Defaults to TRUE.
  */
-function node_access_acquire_grants($node) {
+function node_access_acquire_grants($node, $delete = TRUE) {
   $grants = module_invoke_all('node_access_records', $node);
   // Let modules alter the grants.
   drupal_alter('node_access_records', $grants, $node);
@@ -2985,7 +3096,7 @@ function node_access_acquire_grants($node) {
     $grants = array_shift($grant_by_priority);
   }
 
-  node_access_write_grants($node, $grants);
+  node_access_write_grants($node, $grants, NULL, $delete);
 }
 
 /**
@@ -3486,7 +3597,8 @@ function node_unpublish_by_keyword_action_submit($form, $form_state) {
  */
 function node_unpublish_by_keyword_action($node, $context) {
   foreach ($context['keywords'] as $keyword) {
-    if (strpos(drupal_render(node_view(clone $node)), $keyword) !== FALSE || strpos($node->title, $keyword) !== FALSE) {
+    $elements = node_view(clone $node);
+    if (strpos(drupal_render($elements), $keyword) !== FALSE || strpos($node->title, $keyword) !== FALSE) {
       $node->status = NODE_NOT_PUBLISHED;
       watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_type_get_name($node), '%title' => $node->title));
       break;
diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc
index da52f4545ccf002a379fbeda6197fb0132bc7efd..c70c0eaf014e1baf9cbd87c8e2e0ce2320af7620 100644
--- a/modules/node/node.pages.inc
+++ b/modules/node/node.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.pages.inc,v 1.118 2010/03/18 06:50:37 dries Exp $
+// $Id: node.pages.inc,v 1.123 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -28,7 +28,11 @@ function node_add_page() {
 }
 
 /**
- * Display the list of available node types for node creation.
+ * Returns HTML for a list of available node types for node creation.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - content: An array of content types.
  *
  * @ingroup themeable
  */
@@ -163,14 +167,18 @@ function node_form($form, &$form_state, $node) {
     '#type' => 'checkbox',
     '#title' => t('Create new revision'),
     '#default_value' => $node->revision,
-    '#states' => array(
-      // Check the revision log checkbox when the log textarea is filled in.
+    '#access' => user_access('administer nodes'),
+  );
+  // Check the revision log checkbox when the log textarea is filled in.
+  // This must not happen if "Create new revision" is enabled by default, since
+  // the state would auto-disable the checkbox otherwise.
+  if (!$node->revision) {
+    $form['revision_information']['revision']['#states'] = array(
       'checked' => array(
         'textarea[name="log"]' => array('empty' => FALSE),
       ),
-    ),
-    '#access' => user_access('administer nodes'),
-  );
+    );
+  }
   $form['revision_information']['log'] = array(
     '#type' => 'textarea',
     '#title' => t('Revision log message'),
@@ -239,11 +247,7 @@ function node_form($form, &$form_state, $node) {
   );
 
   // Add the buttons.
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-    '#weight' => 100,
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])),
@@ -336,7 +340,7 @@ function node_preview($node) {
 }
 
 /**
- * Display a node preview for display during node creation and editing.
+ * Returns HTML for a node preview for display during node creation and editing.
  *
  * @param $variables
  *   An associative array containing:
@@ -351,8 +355,10 @@ function theme_node_preview($variables) {
 
   $preview_trimmed_version = FALSE;
 
-  $trimmed = drupal_render(node_view(clone $node, 'teaser'));
-  $full = drupal_render(node_view($node, 'full'));
+  $elements = node_view(clone $node, 'teaser');
+  $trimmed = drupal_render($elements);
+  $elements = node_view($node, 'full');
+  $full = drupal_render($elements);
 
   // Do we need to preview trimmed version of post as well as full version?
   if ($trimmed != $full) {
@@ -397,6 +403,8 @@ function node_form_submit($form, &$form_state) {
     // rebuilt and node form redisplayed the same way as in preview.
     drupal_set_message(t('The post could not be saved.'), 'error');
   }
+  // Clear the page and block caches.
+  cache_clear_all();
 }
 
 /**
diff --git a/modules/node/node.test b/modules/node/node.test
index fa9f09cd7eacec491b56b90bc3556636c7165bb3..73b57d5a36b58dc5e3ee32fc251a2cb0beb92176 100644
--- a/modules/node/node.test
+++ b/modules/node/node.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.test,v 1.76 2010/03/06 06:39:00 dries Exp $
+// $Id: node.test,v 1.81 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * Test the node_load_multiple() function.
@@ -427,7 +427,7 @@ class NodeCreationTestCase extends DrupalWebTestCase {
   }
 
   function setUp() {
-    // Enable dummy module that implements hook_node_post_save for exceptions.
+    // Enable dummy module that implements hook_node_insert for exceptions.
     parent::setUp('node_test_exception');
 
     $web_user = $this->drupalCreateUser(array('create page content', 'edit own page content'));
@@ -458,20 +458,30 @@ class NodeCreationTestCase extends DrupalWebTestCase {
    */
   function testFailedPageCreation() {
     // Create a node.
-    $edit = array();
-    $langcode = LANGUAGE_NONE;
-    $edit["title"] = 'testing_transaction_exception';
-    $edit["body[$langcode][0][value]"] = $this->randomName(16);
-    $this->drupalPost('node/add/page', $edit, t('Save'));
+    $edit = array(
+      'uid'      => $this->loggedInUser->uid,
+      'name'     => $this->loggedInUser->name,
+      'type'     => 'page',
+      'language' => LANGUAGE_NONE,
+      'title'    => 'testing_transaction_exception',
+    );
+
+    try {
+      node_save((object) $edit);
+      $this->fail(t('Expected exception has not been thrown.'));
+    }
+    catch (Exception $e) {
+      $this->pass(t('Expected exception has been thrown.'));
+    }
 
     if (Database::getConnection()->supportsTransactions()) {
       // Check that the node does not exist in the database.
-      $node = $this->drupalGetNodeByTitle($edit["title"]);
+      $node = $this->drupalGetNodeByTitle($edit['title']);
       $this->assertFalse($node, t('Transactions supported, and node not found in database.'));
     }
     else {
       // Check that the node exists in the database.
-      $node = $this->drupalGetNodeByTitle($edit["title"]);
+      $node = $this->drupalGetNodeByTitle($edit['title']);
       $this->assertTrue($node, t('Transactions not supported, and node found in database.'));
 
       // Check that the failed rollback was logged.
@@ -915,7 +925,7 @@ class NodeSaveTestCase extends DrupalWebTestCase {
   }
 
   function setUp() {
-    parent::setUp();
+    parent::setUp('node_test');
     // Create a user that is allowed to post; we'll use this to test the submission.
     $web_user = $this->drupalCreateUser(array('create article content'));
     $this->drupalLogin($web_user);
@@ -942,7 +952,7 @@ class NodeSaveTestCase extends DrupalWebTestCase {
       'nid' => $test_nid,
       'is_new' => TRUE,
     );
-    $node = node_submit((object)$node);
+    $node = node_submit((object) $node);
 
     // Verify that node_submit did not overwrite the user ID.
     $this->assertEqual($node->uid, $this->web_user->uid, t('Function node_submit() preserves user ID'));
@@ -955,6 +965,63 @@ class NodeSaveTestCase extends DrupalWebTestCase {
     $node_by_title = $this->drupalGetNodeByTitle($title);
     $this->assertTrue($node_by_title, t('Node load by node title.'));
   }
+
+  /**
+   * Check that the "created" and "changed" timestamps are set correctly when
+   * saving a new node or updating an existing node.
+   */
+  function testTimestamps() {
+    // Use the default timestamps.
+    $edit = array(
+      'uid' => $this->web_user->uid,
+      'type' => 'article',
+      'title' => $this->randomName(8),
+    );
+
+    node_save((object) $edit);
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    $this->assertEqual($node->created, REQUEST_TIME, t('Creating a node sets default "created" timestamp.'));
+    $this->assertEqual($node->changed, REQUEST_TIME, t('Creating a node sets default "changed" timestamp.'));
+
+    // Store the timestamps.
+    $created = $node->created;
+    $changed = $node->changed;
+
+    node_save($node);
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    $this->assertEqual($node->created, $created, t('Updating a node preserves "created" timestamp.'));
+
+    // Programmatically set the timestamps using hook_node_presave.
+    $node->title = 'testing_node_presave';
+
+    node_save($node);
+    $node = $this->drupalGetNodeByTitle('testing_node_presave');
+    $this->assertEqual($node->created, 280299600, t('Saving a node uses "created" timestamp set in presave hook.'));
+    $this->assertEqual($node->changed, 979534800, t('Saving a node uses "changed" timestamp set in presave hook.'));
+
+    // Programmatically set the timestamps on the node.
+    $edit = array(
+      'uid' => $this->web_user->uid,
+      'type' => 'article',
+      'title' => $this->randomName(8),
+      'created' => 280299600, // Sun, 19 Nov 1978 05:00:00 GMT
+      'changed' => 979534800, // Drupal 1.0 release.
+    );
+
+    node_save((object) $edit);
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    $this->assertEqual($node->created, 280299600, t('Creating a node uses user-set "created" timestamp.'));
+    $this->assertNotEqual($node->changed, 979534800, t('Creating a node doesn\'t use user-set "changed" timestamp.'));
+
+    // Update the timestamps.
+    $node->created = 979534800;
+    $node->changed = 280299600;
+
+    node_save($node);
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    $this->assertEqual($node->created, 979534800, t('Updating a node uses user-set "created" timestamp.'));
+    $this->assertNotEqual($node->changed, 280299600, t('Updating a node doesn\'t use user-set "changed" timestamp.'));
+  }
 }
 
 /**
@@ -1247,8 +1314,7 @@ class NodeTitleTestCase extends DrupalWebTestCase {
     $this->assertEqual(current($this->xpath($xpath)), $node->title, 'Node breadcrumb is equal to node title.', 'Node');
 
     // Test node title in comment preview.
-    $xpath = '//div[@id="node-'. $node->nid .'"]/h2/a';
-    $this->assertEqual(current($this->xpath($xpath)), $node->title, 'Node preview title is equal to node title.', 'Node');
+    $this->assertEqual(current($this->xpath('//div[@id=:id]/h2/a', array(':id' => 'node-' . $node->nid))), $node->title, 'Node preview title is equal to node title.', 'Node');
   }
 }
 
@@ -1289,7 +1355,7 @@ class NodeBlockFunctionalTest extends DrupalWebTestCase {
   }
 
   function setUp() {
-    parent::setUp('node');
+    parent::setUp('node', 'block');
 
     // Create users and test node.
     $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer nodes', 'administer blocks'));
@@ -1379,6 +1445,32 @@ class NodeBlockFunctionalTest extends DrupalWebTestCase {
     $this->assertText($node2->title, t('Node found in block.'));
     $this->assertText($node3->title, t('Node found in block.'));
     $this->assertText($node4->title, t('Node found in block.'));
+
+    // Create the custom block.
+    $custom_block = array();
+    $custom_block['info'] = $this->randomName();
+    $custom_block['title'] = $this->randomName();
+    $custom_block['types[article]'] = TRUE;
+    $custom_block['body[value]'] = $this->randomName(32);
+    $custom_block['regions[garland]'] = 'content';
+    $custom_block['regions[seven]'] = 'content';
+    $this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
+
+    $bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
+    $this->assertTrue($bid, t('Custom block with visibility rule was created.'));
+
+    // Verify visibility rules.
+    $this->drupalGet('');
+    $this->assertNoText($custom_block['title'], t('Block was displayed on the front page.'));
+    $this->drupalGet('node/add/article');
+    $this->assertText($custom_block['title'], t('Block was displayed on the node/add/article page.'));
+    $this->drupalGet('node/' . $node1->nid);
+    $this->assertText($custom_block['title'], t('Block was displayed on the node/N.'));
+
+    // Delete the created custom block & verify that it's been deleted.
+    $this->drupalPost('admin/structure/block/manage/block/' . $bid . '/delete', array(), t('Delete'));
+    $bid = db_query("SELECT 1 FROM {block_node_type} WHERE module = 'block' AND delta = :delta", array(':delta' => $bid))->fetchField();
+    $this->assertFalse($bid, t('Custom block was deleted.'));
   }
 }
 /**
@@ -1574,3 +1666,79 @@ class NodeQueryAlter extends DrupalWebTestCase {
     }
   }
 }
+
+/**
+ * Test node token replacement in strings.
+ */
+class NodeTokenReplaceTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Node token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check node token replacement.',
+      'group' => 'Node',
+    );
+  }
+
+  /**
+   * Creates a node, then tests the tokens generated from it.
+   */
+  function testNodeTokenReplacement() {
+    global $language;
+    $url_options = array(
+      'absolute' => TRUE,
+      'language' => $language,
+    );
+
+    // Create a user and a node.
+    $account = $this->drupalCreateUser();
+    $settings = array(
+      'type' => 'article',
+      'uid' => $account->uid,
+      'title' => '<blink>Blinking Text</blink>',
+      'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomName(32), 'summary' => $this->randomName(16)))),
+    );
+    $node = $this->drupalCreateNode($settings);
+
+    // Load node so that the body and summary fields are structured properly. 
+    $node = node_load($node->nid);
+    $instance = field_info_instance('node', 'body', $node->type);
+
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[node:nid]'] = $node->nid;
+    $tests['[node:vid]'] = $node->vid;
+    $tests['[node:tnid]'] = $node->tnid;
+    $tests['[node:uid]'] = $node->uid;
+    $tests['[node:type]'] = 'article';
+    $tests['[node:type-name]'] = 'Article';
+    $tests['[node:title]'] = check_plain($node->title);
+    $tests['[node:body]'] = _text_sanitize($instance, $node->language, $node->body[$node->language][0], 'value');
+    $tests['[node:summary]'] = _text_sanitize($instance, $node->language, $node->body[$node->language][0], 'summary');
+    $tests['[node:language]'] = check_plain($node->language);
+    $tests['[node:url]'] = url('node/' . $node->nid, $url_options);
+    $tests['[node:edit-url]'] = url('node/' . $node->nid . '/edit', $url_options);
+    $tests['[node:author:name]'] = check_plain($account->name);
+    $tests['[node:created:since]'] = format_interval(REQUEST_TIME - $node->created, 2, $language->language);
+    $tests['[node:changed:since]'] = format_interval(REQUEST_TIME - $node->changed, 2, $language->language);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('node' => $node), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized node token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[node:title]'] = $node->title;
+    $tests['[node:body]'] = $node->body[$node->language][0]['value'];
+    $tests['[node:summary]'] = $node->body[$node->language][0]['summary'];
+    $tests['[node:language]'] = $node->language;
+    $tests['[node:author:name]'] = $account->name;
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('node' => $node), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized node token %token replaced.', array('%token' => $input)));
+    }
+  }
+}
diff --git a/modules/node/node.tokens.inc b/modules/node/node.tokens.inc
index aa9d7f0c2e5c32e11520815a5cf2320cf50bbbd3..499b9a40cc601b753919444f4910b53442ae1725 100644
--- a/modules/node/node.tokens.inc
+++ b/modules/node/node.tokens.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.tokens.inc,v 1.11 2010/01/14 05:53:55 webchick Exp $
+// $Id: node.tokens.inc,v 1.14 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -129,8 +129,13 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr
           $replacements[$original] = $node->uid;
           break;
 
-        case 'name':
-          $replacements[$original] = $sanitize ? check_plain($node->name) : $node->name;
+        case 'type':
+          $replacements[$original] = $sanitize ? check_plain($node->type) : $node->type;
+          break;
+
+        case 'type-name':
+          $type_name = node_type_get_name($node);
+          $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
           break;
 
         case 'title':
@@ -138,26 +143,15 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr
           break;
 
         case 'body':
-          if (!empty($node->body)) {
-            $replacements[$original] = $sanitize ? $node->body[LANGUAGE_NONE][0]['safe'] : $node->body[LANGUAGE_NONE][0]['value'];
-          }
-          break;
-
         case 'summary':
           if (!empty($node->body)) {
-            $replacements[$original] = $sanitize ? $node->body[LANGUAGE_NONE][0]['safe_summary'] : $node->body[LANGUAGE_NONE][0]['summary'];
+            $item = $node->body[$node->language][0];
+            $column = ($name == 'body') ? 'value' : 'summary';
+            $instance = field_info_instance('node', 'body', $node->type);
+            $replacements[$original] = $sanitize ? _text_sanitize($instance, $node->language, $item, $column) : $item[$column];
           }
           break;
 
-        case 'type':
-          $replacements[$original] = $sanitize ? check_plain($node->type) : $node->type;
-          break;
-
-        case 'type-name':
-          $type_name = node_type_get_name($node);
-          $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
-          break;
-
         case 'language':
           $replacements[$original] = $sanitize ? check_plain($node->language) : $node->language;
           break;
diff --git a/modules/node/node.tpl.php b/modules/node/node.tpl.php
index b400c93ec7f31149f9e879382b503ec74f5f044a..19b5f0cda7a329f42f358fbcb7dc215ea92cb898 100644
--- a/modules/node/node.tpl.php
+++ b/modules/node/node.tpl.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.tpl.php,v 1.31 2010/01/04 03:57:19 webchick Exp $
+// $Id: node.tpl.php,v 1.33 2010/04/08 18:26:42 dries Exp $
 
 /**
  * @file
@@ -7,8 +7,8 @@
  *
  * Available variables:
  * - $title: the (sanitized) title of the node.
- * - $content: An array of node items. Use render($content) to print them all, or
- *   print a subset such as render($content['field_example']). Use
+ * - $content: An array of node items. Use render($content) to print them all,
+ *   or print a subset such as render($content['field_example']). Use
  *   hide($content['field_example']) to temporarily suppress the printing of a
  *   given element.
  * - $user_picture: The node author's picture from user-picture.tpl.php.
@@ -16,11 +16,11 @@
  *   calling format_date() with the desired parameters on the $created variable.
  * - $name: Themed username of node author output from theme_username().
  * - $node_url: Direct url of the current node.
- * - $terms: the themed list of taxonomy term links output from theme_links().
  * - $display_submitted: whether submission information should be displayed.
  * - $classes: String of classes that can be used to style contextually through
  *   CSS. It can be manipulated through the variable $classes_array from
- *   preprocess functions. The default values can be one or more of the following:
+ *   preprocess functions. The default values can be one or more of the
+ *   following:
  *   - node: The current template type, i.e., "theming hook".
  *   - node-[type]: The current node type. For example, if the node is a
  *     "Blog entry" it would result in "node-blog". Note that the machine
@@ -29,7 +29,8 @@
  *   - node-preview: Nodes in preview mode.
  *   The following are controlled through the node publishing options.
  *   - node-promoted: Nodes promoted to the front page.
- *   - node-sticky: Nodes ordered above other non-sticky nodes in teaser listings.
+ *   - node-sticky: Nodes ordered above other non-sticky nodes in teaser
+ *     listings.
  *   - node-unpublished: Unpublished nodes visible only to administrators.
  * - $title_prefix (array): An array containing additional output populated by
  *   modules, intended to be displayed in front of the main title tag that
@@ -82,24 +83,16 @@
 
   <?php print render($title_prefix); ?>
   <?php if (!$page): ?>
-    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $node_title; ?></a></h2>
+    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>
   <?php endif; ?>
   <?php print render($title_suffix); ?>
 
-  <?php if ($display_submitted || !empty($content['links']['terms'])): ?>
-    <div class="meta">
-      <?php if ($display_submitted): ?>
-        <span class="submitted">
-          <?php
-            print t('Submitted by !username on !datetime',
-              array('!username' => $name, '!datetime' => $date));
-          ?>
-        </span>
-      <?php endif; ?>
-
-      <?php if (!empty($content['links']['terms'])): ?>
-        <div class="terms terms-inline"><?php print render($content['links']['terms']); ?></div>
-      <?php endif; ?>
+  <?php if ($display_submitted): ?>
+    <div class="submitted">
+      <?php
+        print t('Submitted by !username on !datetime',
+          array('!username' => $name, '!datetime' => $date));
+      ?>
     </div>
   <?php endif; ?>
 
diff --git a/modules/node/tests/node_access_test.info b/modules/node/tests/node_access_test.info
index d61de2ece07e5f6435fbb71252b6e9d942253e78..98b5e2696a98561ea2848b694807ad1290b71e2e 100644
--- a/modules/node/tests/node_access_test.info
+++ b/modules/node/tests/node_access_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = node_access_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/node/tests/node_presave_test.info b/modules/node/tests/node_presave_test.info
new file mode 100644
index 0000000000000000000000000000000000000000..b72e325815c6afdb383892861d775a14f0512d28
--- /dev/null
+++ b/modules/node/tests/node_presave_test.info
@@ -0,0 +1,14 @@
+; $Id: node_presave_test.info,v 1.1 2010/04/20 09:17:20 webchick Exp $
+name = "Node module presave tests"
+description = "Support module for hook_node_presave testing."
+package = Testing
+version = VERSION
+core = 7.x
+files[] = node_presave_test.module
+hidden = TRUE
+
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
+project = "drupal"
+datestamp = "1272318008"
+
diff --git a/modules/node/tests/node_presave_test.module b/modules/node/tests/node_presave_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..4df0143e21a50c6c199a8061500b4ec627e68904
--- /dev/null
+++ b/modules/node/tests/node_presave_test.module
@@ -0,0 +1,18 @@
+<?php
+// $Id: node_presave_test.module,v 1.1 2010/04/20 09:17:20 webchick Exp $
+
+/**
+ * @file
+ * Dummy module implementing node related hooks to test API interaction with
+ * the Node module.
+ */
+
+/**
+ * Implements hook_node_presave().
+ */
+function node_presave_test_node_presave($node) {
+  if ($node->title == 'testing_node_presave') {
+    $node->created = 280299600; // Sun, 19 Nov 1978 05:00:00 GMT
+    $node->changed = 979534800; // Drupal 1.0 release.
+  }
+}
diff --git a/modules/node/tests/node_test.info b/modules/node/tests/node_test.info
index 7369144db41ec1405243e33160f3d369a2ec4975..1d2bef874850f8b74c503faf8b396e69fbef748b 100644
--- a/modules/node/tests/node_test.info
+++ b/modules/node/tests/node_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = node_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/node/tests/node_test.module b/modules/node/tests/node_test.module
index 7ce50212840a1e306ec2793112c32d46a77aebd6..5b4058b82424f5b45053d1de03c17a8029165ae8 100644
--- a/modules/node/tests/node_test.module
+++ b/modules/node/tests/node_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: node_test.module,v 1.9 2009/12/26 16:50:09 dries Exp $
+// $Id: node_test.module,v 1.11 2010/04/20 09:30:21 webchick Exp $
 
 /**
  * @file
@@ -40,7 +40,7 @@ function node_test_node_view($node, $view_mode) {
 function node_test_node_grants($account, $op) {
   // Give everyone full grants so we don't break other node tests.
   // Our node access tests asserts three realms of access.
-  // @see testGrantAlter()
+  // See testGrantAlter().
   return array(
     'test_article_realm' => array(1),
     'test_page_realm' => array(1),
@@ -100,3 +100,15 @@ function node_test_node_grants_alter(&$grants, $account, $op) {
   // Return an empty array of grants to prove that we can alter by reference.
   $grants = array();
 }
+
+/**
+ * Implements hook_node_presave().
+ */
+function node_test_node_presave($node) {
+  if ($node->title == 'testing_node_presave') {
+    // Sun, 19 Nov 1978 05:00:00 GMT
+    $node->created = 280299600;
+    // Drupal 1.0 release.
+    $node->changed = 979534800;
+  }
+}
diff --git a/modules/node/tests/node_test_exception.info b/modules/node/tests/node_test_exception.info
index 3ea169e69581176707be4a66e585308436a53ed5..a0fa4462fa49e9679e16b84671ecd1206a1c3b39 100644
--- a/modules/node/tests/node_test_exception.info
+++ b/modules/node/tests/node_test_exception.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = node_test_exception.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc
index db0e787c9c3b8b3650f08add566c8f96ebc4c8d3..0a031856863ac10bdd8c069d203d9ac9e08166f0 100644
--- a/modules/openid/openid.inc
+++ b/modules/openid/openid.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: openid.inc,v 1.26 2010/03/06 06:39:00 dries Exp $
+// $Id: openid.inc,v 1.31 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -60,6 +60,16 @@ define('OPENID_NS_SREG', 'http://openid.net/extensions/sreg/1.1');
  */
 define('OPENID_NS_AX', 'http://openid.net/srv/ax/1.0');
 
+/**
+ * Extensible Resource Descriptor documents.
+ */
+define('OPENID_NS_XRD', 'xri://$xrd*($v*2.0)');
+
+/**
+ * OpenID IDP for Google hosted domains.
+ */
+define('OPENID_NS_GOOGLE', 'http://namespace.google.com/openid/xmlns');
+
 /**
  * Performs an HTTP 302 redirect (for the 1.x protocol).
  */
@@ -80,7 +90,8 @@ function openid_redirect_http($url, $message) {
  */
 function openid_redirect($url, $message) {
   $output = '<html><head><title>' . t('OpenID redirect') . "</title></head>\n<body>";
-  $output .= drupal_render(drupal_get_form('openid_redirect_form', $url, $message));
+  $elements = drupal_get_form('openid_redirect_form', $url, $message);
+  $output .= drupal_render($elements);
   $output .= '<script type="text/javascript">document.getElementById("openid-redirect-form").submit();</script>';
   $output .= "</body></html>\n";
   print $output;
@@ -98,7 +109,8 @@ function openid_redirect_form($form, &$form_state, $url, $message) {
       '#value' => $value,
     );
   }
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#prefix' => '<noscript>',
     '#suffix' => '</noscript>',
@@ -108,6 +120,49 @@ function openid_redirect_form($form, &$form_state, $url, $message) {
   return $form;
 }
 
+/**
+ * Parse an XRDS document.
+ *
+ * @param $raw_xml
+ *   A string containing the XRDS document.
+ * @return
+ *   An array of service entries.
+ */
+function _openid_xrds_parse($raw_xml) {
+  $services = array();
+  try {
+    $xml = @new SimpleXMLElement($raw_xml);
+    foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) {
+      foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) {
+        $service = array(
+          'priority' => $service_element->attributes()->priority ? (int)$service_element->attributes()->priority : PHP_INT_MAX,
+          'types' => array(),
+          'uri' => (string)$service_element->children(OPENID_NS_XRD)->URI,
+          'service' => $service_element,
+          'xrd' => $xrd,
+        );
+        foreach ($service_element->Type as $type) {
+          $service['types'][] = (string)$type;
+        }
+        if ($service_element->children(OPENID_NS_XRD)->Delegate) {
+          $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->Delegate;
+        }
+        if ($service_element->children(OPENID_NS_XRD)->LocalID) {
+          $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->LocalID;
+        }
+        else {
+          $service['identity'] = FALSE;
+        }
+        $services[] = $service;
+      }
+    }
+  }
+  catch (Exception $e) {
+    // Invalid XML.
+  }
+  return $services;
+}
+
 /**
  * Select a service element.
  *
@@ -155,6 +210,12 @@ function _openid_select_service(array $services) {
     }
   }
 
+  if ($selected_service) {
+    // Unset SimpleXMLElement instances that cannot be saved in $_SESSION.
+    unset($selected_service['xrd']);
+    unset($selected_service['service']);
+  }
+
   return $selected_service;
 }
 
@@ -163,11 +224,10 @@ function _openid_select_service(array $services) {
  */
 function _openid_is_xri($identifier) {
   // Strip the xri:// scheme from the identifier if present.
-  if (stripos($identifier, 'xri://') !== FALSE) {
+  if (stripos($identifier, 'xri://') === 0) {
     $identifier = substr($identifier, 6);
   }
 
-
   // Test whether the identifier starts with an XRI global context symbol or (.
   $firstchar = substr($identifier, 0, 1);
   if (strpos("=@+$!(", $firstchar) !== FALSE) {
@@ -606,3 +666,35 @@ function openid_extract_namespace($response, $extension_namespace, $fallback_pre
 
   return $output;
 }
+
+/**
+ * Extracts values from an OpenID AX Response.
+ *
+ * The values can be returned in two forms:
+ *   - only openid.ax.value.<alias> (for single-valued answers)
+ *   - both openid.ax.count.<alias> and openid.ax.value.<alias>.<count> (for both
+ *     single and multiple-valued answers)
+ *
+ * @param $values
+ *   An array as returned by openid_extract_namespace(..., OPENID_NS_AX).
+ * @param $aliases
+ *   An array of aliases used in the fetch request.
+ * @return
+ *   An array of values.
+ * @see http://openid.net/specs/openid-attribute-exchange-1_0.html#fetch_response
+ */
+function openid_extract_ax_values($values, $aliases) {
+  $output = array();
+  foreach ($aliases as $alias) {
+    if (isset($values['count.' . $alias])) {
+      for ($i = 1; $i <= $values['count.' . $alias]; $i++) {
+        $output[] = $values['value.' . $alias . '.' . $i];
+      }
+    }
+    elseif (isset($values['value.' . $alias])) {
+      $output[] = $values['value.' . $alias];
+    }
+  }
+  return $output;
+}
+
diff --git a/modules/openid/openid.info b/modules/openid/openid.info
index 774983bee1dcd2d54fa1b1b2506a5b81d18bdd5a..d682c5654734598a232d3248e359270fe06a22d5 100644
--- a/modules/openid/openid.info
+++ b/modules/openid/openid.info
@@ -11,8 +11,8 @@ files[] = xrds.inc
 files[] = openid.install
 files[] = openid.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/openid/openid.js b/modules/openid/openid.js
index 67179aff661349fbb540d823ab1402cb29ae2be1..e60f2b77a628bc134f6b784fd8737e92fdd16765 100644
--- a/modules/openid/openid.js
+++ b/modules/openid/openid.js
@@ -1,18 +1,25 @@
-// $Id: openid.js,v 1.12 2009/08/24 03:11:34 webchick Exp $
+// $Id: openid.js,v 1.13 2010/03/22 18:55:45 dries Exp $
 (function ($) {
 
 Drupal.behaviors.openid = {
   attach: function (context) {
     var loginElements = $('.form-item-name, .form-item-pass, li.openid-link');
     var openidElements = $('.form-item-openid-identifier, li.user-link');
+    var cookie = $.cookie('Drupal.visitor.openid_identifier');
 
     // This behavior attaches by ID, so is only valid once on a page.
-    if (!$('#edit-openid-identifier.openid-processed').size() && $('#edit-openid-identifier').val()) {
-      $('#edit-openid-identifier').addClass('openid-processed');
-      loginElements.hide();
-      // Use .css('display', 'block') instead of .show() to be Konqueror friendly.
-      openidElements.css('display', 'block');
+    if (!$('#edit-openid-identifier.openid-processed').size()) {
+      if (cookie) {
+        $('#edit-openid-identifier').val(cookie);
+      }
+      if ($('#edit-openid-identifier').val()) {
+        $('#edit-openid-identifier').addClass('openid-processed');
+        loginElements.hide();
+        // Use .css('display', 'block') instead of .show() to  Konqueror friendly.
+        openidElements.css('display', 'block');
+      }
     }
+
     $('li.openid-link:not(.openid-processed)', context)
       .addClass('openid-processed')
       .click(function () {
diff --git a/modules/openid/openid.module b/modules/openid/openid.module
index d63ee561f7e761a4ecbc8adb5b763f5a1ad3d08f..c1d6e58827f10b36a2837f4afae01cd31a742a67 100644
--- a/modules/openid/openid.module
+++ b/modules/openid/openid.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: openid.module,v 1.75 2010/03/09 12:07:37 dries Exp $
+// $Id: openid.module,v 1.84 2010/04/07 16:35:03 dries Exp $
 
 /**
  * @file
@@ -65,13 +65,38 @@ function openid_help($path, $arg) {
  * Implements hook_user_insert().
  */
 function openid_user_insert(&$edit, $account, $category) {
-  if (isset($_SESSION['openid']['values'])) {
+  if (!empty($edit['openid_claimed_id'])) {
     // The user has registered after trying to log in via OpenID.
     if (variable_get('user_email_verification', TRUE)) {
       drupal_set_message(t('Once you have verified your e-mail address, you may log in via OpenID.'));
     }
-    user_set_authmaps($account, array('authname_openid' => $_SESSION['openid']['values']['response']['openid.claimed_id']));
+    user_set_authmaps($account, array('authname_openid' => $edit['openid_claimed_id']));
     unset($_SESSION['openid']);
+    unset($edit['openid_claimed_id']);
+  }
+}
+
+/**
+ * Implements hook_user_login().
+ *
+ * Save openid_identifier to visitor cookie.
+ */
+function openid_user_login(&$edit, $account) {
+  if (isset($_SESSION['openid'])) {
+    // The user has logged in via OpenID.
+    user_cookie_save(array_intersect_key($_SESSION['openid']['user_login_values'], array_flip(array('openid_identifier'))));
+    unset($_SESSION['openid']);
+  }
+}
+
+/**
+ * Implements hook_user_logout().
+ *
+ * Delete any openid_identifier in visitor cookie.
+ */
+function openid_user_logout($account) {
+  if (isset($_COOKIE['Drupal_visitor_openid_identifier'])) {
+    user_cookie_delete('openid_identifier');
   }
 }
 
@@ -90,8 +115,9 @@ function openid_form_user_login_alter(&$form, &$form_state) {
 }
 
 function _openid_user_login_form_alter(&$form, &$form_state) {
-  drupal_add_css(drupal_get_path('module', 'openid') . '/openid.css');
-  drupal_add_js(drupal_get_path('module', 'openid') . '/openid.js');
+  $form['#attached']['css'][] = drupal_get_path('module', 'openid') . '/openid.css';
+  $form['#attached']['js'][] = drupal_get_path('module', 'openid') . '/openid.js';
+  $form['#attached']['library'][] = array('system', 'cookie');
   if (!empty($form_state['input']['openid_identifier'])) {
     $form['name']['#required'] = FALSE;
     $form['pass']['#required'] = FALSE;
@@ -128,28 +154,58 @@ function _openid_user_login_form_alter(&$form, &$form_state) {
 }
 
 /**
- * Implements hook_form_alter().
+ * Implements hook_form_FORM_ID_alter().
  *
- * Adds OpenID login to the login forms.
+ * Prefills the login form with values acquired via OpenID.
  */
 function openid_form_user_register_form_alter(&$form, &$form_state) {
-  if (isset($_SESSION['openid']['values'])) {
-    // We were unable to auto-register a new user. Prefill the registration
-    // form with the values we have.
-    $form['account']['name']['#default_value'] = $_SESSION['openid']['values']['name'];
-    $form['account']['mail']['#default_value'] = $_SESSION['openid']['values']['mail'];
+  if (isset($_SESSION['openid']['response'])) {
+    module_load_include('inc', 'openid');
+
+    $response = $_SESSION['openid']['response'];
+
+    // Extract Simple Registration keys from the response.
+    $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg');
+    // Extract Attribute Exchanges keys from the response.
+    $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax');
+
+    if (!empty($sreg_values['nickname'])) {
+      // Use the nickname returned by Simple Registration if available.
+      $form['account']['name']['#default_value'] = $sreg_values['nickname'];
+    }
+    elseif ($ax_name_values = openid_extract_ax_values($ax_values, array('name_ao', 'name_son'))) {
+      // Else, use the first nickname returned by AX if available.
+      $form['account']['name']['#default_value'] = current($ax_name_values);
+    }
+    else {
+      $form['account']['name']['#default_value'] = '';
+    }
+
+    if (!empty($sreg_values['email'])) {
+      // Use the email returned by Simple Registration if available.
+      $form['account']['mail']['#default_value'] = $sreg_values['email'];
+    }
+    elseif ($ax_mail_values = openid_extract_ax_values($ax_values, array('mail_ao', 'mail_son'))) {
+      // Else, use the first nickname returned by AX if available.
+      $form['account']['mail']['#default_value'] = current($ax_mail_values);
+    }
 
     // If user_email_verification is off, hide the password field and just fill
     // with random password to avoid confusion.
     if (!variable_get('user_email_verification', TRUE)) {
-      $form['pass']['#type'] = 'hidden';
-      $form['pass']['#value'] = user_password();
+      $form['account']['pass']['#type'] = 'hidden';
+      $form['account']['pass']['#value'] = user_password();
     }
+
+    $form['openid_claimed_id'] = array(
+      '#type' => 'value',
+      '#default_value' => $response['openid.claimed_id'],
+    );
     $form['openid_display'] = array(
       '#type' => 'item',
       '#title' => t('Your OpenID'),
       '#description' => t('This OpenID will be attached to your account after registration.'),
-      '#markup' => check_plain($_SESSION['openid']['values']['response']['openid.claimed_id']),
+      '#markup' => check_plain($response['openid.claimed_id']),
     );
   }
 }
@@ -208,16 +264,12 @@ function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
     $claimed_id = $identity = 'http://specs.openid.net/auth/2.0/identifier_select';
   }
   else {
-    // Look for OP-Local Identifier.
-    if (!empty($service['localid'])) {
-      $identity = $service['localid'];
-    }
-    elseif (!empty($service['delegate'])) {
-      $identity = $service['delegate'];
-    }
-    else {
-      $identity = $claimed_id;
+    // Use Claimed ID and/or OP-Local Identifier from service description, if
+    // available.
+    if (!empty($service['claimed_id'])) {
+      $claimed_id = $service['claimed_id'];
     }
+    $identity = !empty($service['identity']) ? $service['identity'] : $claimed_id;
   }
   $request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service);
 
@@ -258,21 +310,33 @@ function openid_complete($response = array()) {
       }
       else {
         if (openid_verify_assertion($service['uri'], $response)) {
-          // OpenID Authentication, section 11.2:
-          // If the returned Claimed Identifier is different from the one sent
-          // to the OpenID Provider, we need to do discovery on the returned
-          // identififer to make sure that the provider is authorized to respond
-          // on behalf of this.
-          if ($service['version'] == 2 && $response['openid.claimed_id'] != openid_normalize($claimed_id)) {
-            $services = openid_discovery($response['openid.claimed_id']);
-            $uris = array();
-            foreach ($services as $discovered_service) {
-              if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) {
-                $uris[] = $discovered_service['uri'];
+          // OpenID Authentication, section 7.3.2.3 and Appendix A.5:
+          // The CanonicalID specified in the XRDS document must be used as the
+          // account key. We rely on the XRI proxy resolver to verify that the
+          // provider is authorized to respond on behalf of the specified
+          // identifer (required per Extensible Resource Identifier (XRI)
+          // (XRI) Resolution Version 2.0, section 14.3):
+          if (!empty($service['claimed_id'])) {
+            $response['openid.claimed_id'] = $service['claimed_id'];
+          }
+          elseif ($service['version'] == 2) {
+            $response['openid.claimed_id'] = openid_normalize($response['openid.claimed_id']);
+            // OpenID Authentication, section 11.2:
+            // If the returned Claimed Identifier is different from the one sent
+            // to the OpenID Provider, we need to do discovery on the returned
+            // identififer to make sure that the provider is authorized to
+            // respond on behalf of this.
+            if ($response['openid.claimed_id'] != $claimed_id) {
+              $services = openid_discovery($response['openid.claimed_id']);
+              $uris = array();
+              foreach ($services as $discovered_service) {
+                if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) {
+                  $uris[] = $discovered_service['uri'];
+                }
+              }
+              if (!in_array($service['uri'], $uris)) {
+                return $response;
               }
-            }
-            if (!in_array($service['uri'], $uris)) {
-              return $response;
             }
           }
           else {
@@ -338,8 +402,21 @@ function openid_openid_discovery_method_info() {
  */
 function _openid_xri_discovery($claimed_id) {
   if (_openid_is_xri($claimed_id)) {
-    $xrds_url = 'http://xri.net/' . $claimed_id;
-    _openid_xrds_discovery($xrds_url);
+    // Resolve XRI using a proxy resolver (Extensible Resource Identifier (XRI)
+    // Resolution Version 2.0, section 11.2 and 14.3).
+    $xrds_url = variable_get('xri_proxy_resolver', 'http://xri.net/') . rawurlencode($claimed_id) . '?_xrd_r=application/xrds+xml';
+    $services = _openid_xrds_discovery($xrds_url);
+    foreach ($services as $i => &$service) {
+      $status = $service['xrd']->children(OPENID_NS_XRD)->Status;
+      if ($status && $status->attributes()->cid == 'verified') {
+        $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID);
+      }
+      else {
+        // Ignore service if CanonicalID could not be verified.
+        unset($services[$i]);
+      }
+    }
+    return $services;
   }
 }
 
@@ -362,7 +439,7 @@ function _openid_xrds_discovery($claimed_id) {
     if (!isset($result->error)) {
       if (isset($result->headers['Content-Type']) && preg_match("/application\/xrds\+xml/", $result->headers['Content-Type'])) {
         // Parse XML document to find URL
-        $services = xrds_parse($result->data);
+        $services = _openid_xrds_parse($result->data);
       }
       else {
         $xrds_url = NULL;
@@ -377,7 +454,7 @@ function _openid_xrds_discovery($claimed_id) {
           $headers = array('Accept' => 'application/xrds+xml');
           $xrds_result = drupal_http_request($xrds_url, array('headers' => $headers));
           if (!isset($xrds_result->error)) {
-            $services = xrds_parse($xrds_result->data);
+            $services = _openid_xrds_parse($xrds_result->data);
           }
         }
       }
@@ -386,19 +463,19 @@ function _openid_xrds_discovery($claimed_id) {
       if (count($services) == 0) {
         // Look for 2.0 links
         $uri = _openid_link_href('openid2.provider', $result->data);
-        $delegate = _openid_link_href('openid2.local_id', $result->data);
+        $identity = _openid_link_href('openid2.local_id', $result->data);
         $type = 'http://specs.openid.net/auth/2.0/signon';
 
         // 1.x links
         if (empty($uri)) {
           $uri = _openid_link_href('openid.server', $result->data);
-          $delegate = _openid_link_href('openid.delegate', $result->data);
+          $identity = _openid_link_href('openid.delegate', $result->data);
           $type = 'http://openid.net/signon/1.1';
         }
         if (!empty($uri)) {
           $services[] = array(
             'uri' => $uri,
-            'delegate' => $delegate,
+            'identity' => $identity,
             'types' => array($type),
           );
         }
@@ -432,9 +509,9 @@ function _openid_google_user_discovery($claimed_id) {
     $xrds_url = $matches[1];
     $services = _openid_xrds_discovery($xrds_url);
 
-    foreach ($services as $service) {
-      if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && !empty($service['additional']['URITEMPLATE'])) {
-        $template = $service['additional']['URITEMPLATE'];
+    foreach ($services as $i => $service) {
+      if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && $service['service']->children(OPENID_NS_GOOGLE)->URITemplate) {
+        $template = (string)$service['service']->children(OPENID_NS_GOOGLE)->URITemplate;
         $xrds_url = str_replace('{%uri}', rawurlencode($claimed_id), $template);
         return _openid_xrds_discovery($xrds_url);
       }
@@ -529,8 +606,6 @@ function openid_association($op_endpoint) {
  * @param $response Response values from the OpenID Provider.
  */
 function openid_authentication($response) {
-  module_load_include('inc', 'openid');
-
   $identity = $response['openid.claimed_id'];
 
   $account = user_external_load($identity);
@@ -548,92 +623,44 @@ function openid_authentication($response) {
       }
     }
     else {
-      drupal_set_message(t('You must validate your email address for this account before logging in via OpenID'));
+      drupal_set_message(t('You must validate your email address for this account before logging in via OpenID.'));
     }
   }
   elseif (variable_get('user_register', 1)) {
     // Register new user.
 
-    // Extract Simple Registration keys from the response.
-    $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg');
-    // Extract Attribute Exchanges keys from the response.
-    $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax');
+    // Save response for use in openid_form_user_register_form_alter().
+    $_SESSION['openid']['response'] = $response;
 
-    $form_state['build_info']['args'] = array();
-    $form_state['redirect'] = NULL;
-
-    if (!empty($sreg_values['nickname'])) {
-      // Use the nickname returned by Simple Registration if available.
-      $form_state['values']['name'] = $sreg_values['nickname'];
-    }
-    else if (!empty($ax_values['value.email'])) {
-      // Else, extract the name part of the email address returned by AX if available.
-      list ($name, $domain) = explode('@', $ax_values['value.email'], 2);
-      $form_state['values']['name'] = $name;
-    }
-    else {
-      $form_state['values']['name'] = '';
-    }
+    $form_state['values'] = array();
+    $form_state['values']['op'] = t('Create new account');
+    drupal_form_submit('user_register_form', $form_state);
 
-    if (!empty($sreg_values['email'])) {
-      // Use the email returned by Simple Registration if available.
-      $form_state['values']['mail'] = $sreg_values['email'];
+    if (!empty($form_state['user'])) {
+      module_invoke_all('openid_response', $response, $form_state['user']);
+      drupal_goto();
     }
-    else if (!empty($ax_values['value.email'])) {
-      // Else, use the email returned by AX if available.
-      $form_state['values']['mail'] = $ax_values['value.email'];
-    }
-    else {
-      $form_state['values']['mail'] = '';
-    }
-
-    $form_state['values']['pass']  = user_password();
-    $form_state['values']['status'] = variable_get('user_register', 1) == 1;
-    $form_state['values']['response'] = $response;
 
+    $messages = drupal_get_messages('error');
     if (empty($form_state['values']['name']) || empty($form_state['values']['mail'])) {
+      // If the OpenID provider did not provide both a user name and an email
+      // address, ask the user to complete the registration manually instead of
+      // showing the error messages about the missing values generated by FAPI.
       drupal_set_message(t('Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'warning');
-      $success = FALSE;
     }
     else {
-      $form = drupal_retrieve_form('user_register_form', $form_state);
-      drupal_prepare_form('user_register_form', $form, $form_state);
-      drupal_validate_form('user_register_form', $form, $form_state);
-      $success = !form_get_errors();
-      if (!$success) {
-        drupal_set_message(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'warning');
-        // Append form validation errors below the above warning.
-        $messages = drupal_get_messages('error');
-        foreach ($messages['error'] as $message) {
-          drupal_set_message( $message, 'error');
-        }
-      }
-    }
-    if (!$success) {
-      // We were unable to register a valid new user, redirect to standard
-      // user/register and prefill with the values we received.
-      $_SESSION['openid']['values'] = $form_state['values'];
-      // We'll want to redirect back to the same place.
-      $destination = drupal_get_destination();
-      unset($_GET['destination']);
-      drupal_goto('user/register', array('query' => $destination));
-    }
-    else {
-      unset($form_state['values']['response']);
-      $account = user_save(drupal_anonymous_user(), $form_state['values']);
-      // Terminate if an error occurred during user_save().
-      if (!$account) {
-        drupal_set_message(t("Error saving user account."), 'error');
-        drupal_goto();
+      drupal_set_message(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'warning');
+      // Append form validation errors below the above warning.
+      foreach ($messages['error'] as $message) {
+        drupal_set_message( $message, 'error');
       }
-      user_set_authmaps($account, array("authname_openid" => $identity));
-      // Load global $user and perform final login tasks.
-      $form_state['uid'] = $account->uid;
-      user_login_submit(array(), $form_state);
-      // Let other modules act on OpenID login
-      module_invoke_all('openid_response', $response, $account);
     }
-    drupal_redirect_form($form_state);
+
+    // We were unable to register a valid new user. Redirect to the normal
+    // registration page and prefill with the values we received.
+    $destination = drupal_get_destination();
+    unset($_GET['destination']);
+    drupal_goto('user/register', array('query' => $destination));
   }
   else {
     drupal_set_message(t('Only site administrators can create new user accounts.'), 'error');
@@ -689,8 +716,20 @@ function openid_authentication_request($claimed_id, $identity, $return_to = '',
   if (in_array(OPENID_NS_AX, $service['types'])) {
     $request['openid.ns.ax'] = OPENID_NS_AX;
     $request['openid.ax.mode'] = 'fetch_request';
-    $request['openid.ax.required'] = 'email';
-    $request['openid.ax.type.email'] = 'http://schema.openid.net/contact/email';
+    $request['openid.ax.required'] = 'mail_ao,name_ao,mail_son,name_son';
+
+    // Implementors disagree on which URIs to use, even for simple
+    // attributes like name and email (*sigh*). We ask for both axschema.org
+    // attributes (which are supposed to be newer), and schema.openid.net ones
+    // (which are supposed to be legacy).
+
+    // Attributes as defined by axschema.org.
+    $request['openid.ax.type.mail_ao'] = 'http://axschema.org/contact/email';
+    $request['openid.ax.type.name_ao'] = 'http://axschema.org/namePerson/friendly';
+
+    // Attributes as defined by schema.openid.net.
+    $request['openid.ax.type.mail_son'] = 'http://schema.openid.net/contact/email';
+    $request['openid.ax.type.name_son'] = 'http://schema.openid.net/namePerson/friendly';
   }
 
   $request = array_merge($request, module_invoke_all('openid', 'request', $request));
diff --git a/modules/openid/openid.pages.inc b/modules/openid/openid.pages.inc
index d19a53b39e9c0d25d1385a57591fdd9548acfab6..06738a1e59f8b95db597368d4fdac7f3347c00dc 100644
--- a/modules/openid/openid.pages.inc
+++ b/modules/openid/openid.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: openid.pages.inc,v 1.27 2010/03/02 08:59:54 dries Exp $
+// $Id: openid.pages.inc,v 1.28 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -73,7 +73,7 @@ function openid_user_add() {
     '#type' => 'textfield',
     '#title' => t('OpenID'),
   );
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Add an OpenID'));
   return $form;
 }
diff --git a/modules/openid/openid.test b/modules/openid/openid.test
index f8d50679fc435292c27dd15bd0243313eb20c224..3a2ebec53cad58f9db137ce82840bbe0aa2edfbd 100644
--- a/modules/openid/openid.test
+++ b/modules/openid/openid.test
@@ -1,16 +1,37 @@
 <?php
-// $Id: openid.test,v 1.15 2010/03/02 08:59:54 dries Exp $
+// $Id: openid.test,v 1.22 2010/04/07 14:22:34 dries Exp $
 
 /**
- * Test login and account registration using OpenID.
+ * Base class for OpenID tests.
  */
-class OpenIDFunctionalTest extends DrupalWebTestCase {
+abstract class OpenIDWebTestCase extends DrupalWebTestCase {
+
+  /**
+   * Initiates the login procedure using the specified User-supplied Identity.
+   */
+  function submitLoginForm($identity) {
+    // Fill out and submit the login form.
+    $edit = array('openid_identifier' => $identity);
+    $this->drupalPost('', $edit, t('Log in'));
+
+    // Check we are on the OpenID redirect form.
+    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
+
+    // Submit form to the OpenID Provider Endpoint.
+    $this->drupalPost(NULL, array(), t('Send'));
+  }
+}
+
+/**
+ * Test discovery and login using OpenID
+ */
+class OpenIDFunctionalTestCase extends OpenIDWebTestCase {
   protected $web_user;
 
   public static function getInfo() {
     return array(
-      'name' => 'OpenID login and account registration',
-      'description' => "Adds an identity to a user's profile and uses it to log in, creates a user account using auto-registration.",
+      'name' => 'OpenID discovery and login',
+      'description' => "Adds an identity to a user's profile and uses it to log in.",
       'group' => 'OpenID'
     );
   }
@@ -43,13 +64,22 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
     // the URL of the OpenID Provider Endpoint.
 
     // Identifier is the URL of an XRDS document.
-    $this->addIdentity(url('openid-test/yadis/xrds', array('absolute' => TRUE)), 2);
+    // The URL scheme is stripped in order to test that the supplied identifier
+    // is normalized in openid_begin().
+    $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
+    $this->addIdentity(preg_replace('@^https?://@', '', $identity), 2, $identity);
 
     // Identifier is the URL of an XRDS document containing an OP Identifier
     // Element. The Relying Party sends the special value
     // "http://specs.openid.net/auth/2.0/identifier_select" as Claimed
     // Identifier. The OpenID Provider responds with the actual identifier.
-    $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE)));
+    $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE));
+    // Tell openid_test.module to respond with this identifier. The URL scheme
+    // is stripped in order to test that the returned identifier is normalized in
+    // openid_complete().
+    variable_set('openid_test_response', array('openid.claimed_id' => preg_replace('@^https?://@', '', $identity)));
+    $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, $identity);
+    variable_set('openid_test_response', array());
 
     // Identifier is the URL of an HTML page that is sent with an HTTP header
     // that contains the URL of an XRDS document.
@@ -59,6 +89,13 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
     // element that contains the URL of an XRDS document.
     $this->addIdentity(url('openid-test/yadis/http-equiv', array('absolute' => TRUE)), 2);
 
+    // Identifier is an XRI. Resolve using our own dummy proxy resolver.
+    variable_set('xri_proxy_resolver', url('openid-test/yadis/xrds/xri', array('absolute' => TRUE)) . '/');
+    $this->addIdentity('@example*résumé;%25', 2, 'http://example.com/user');
+
+    // Make sure that unverified CanonicalID are not trusted.
+    variable_set('openid_test_canonical_id_status', 'bad value');
+    $this->addIdentity('@example*résumé;%25', 2, FALSE);
 
     // HTML-based discovery:
     // If the User-supplied Identifier is a URL of an HTML page, the page may
@@ -84,19 +121,14 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
 
     $this->drupalLogout();
 
-    // Fill out and submit the login form.
-    $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Log in'));
-
-    // Check we are on the OpenID redirect form.
-    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
-
-    // Submit form to the OpenID Provider Endpoint.
-    $this->drupalPost(NULL, array(), t('Send'));
+    // Test logging in via the login block on the front page.
+    $this->submitLoginForm($identity);
     $this->assertLink($this->web_user->name, 0, t('User was logged in.'));
 
-    // Test logging in via the user/login page.
     $this->drupalLogout();
+
+    // Test logging in via the user/login page.
+    $edit = array('openid_identifier' => $identity);
     $this->drupalPost('user/login', $edit, t('Log in'));
 
     // Check we are on the OpenID redirect form.
@@ -154,15 +186,7 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
     $this->assertRaw('The update has been performed.', t('Account was blocked.'));
     $this->drupalLogout();
 
-    // Fill out and submit the login form.
-    $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Log in'));
-
-    // Check we are on the OpenID redirect form.
-    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
-
-    // Submit form to the OpenID Provider Endpoint.
-    $this->drupalPost(NULL, array(), t('Send'));
+    $this->submitLoginForm($identity);
     $this->assertRaw(t('The username %name has not been activated or is blocked.', array('%name' => $this->web_user->name)), t('User login was blocked.'));
   }
 
@@ -174,55 +198,107 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
    * @param $version
    *   The protocol version used by the service.
    * @param $claimed_id
-   *   The expected Claimed Identifier returned by the OpenID Provider.
+   *   The expected Claimed Identifier returned by the OpenID Provider, or FALSE
+   *   if the discovery is expected to fail.
    */
   function addIdentity($identity, $version = 2, $claimed_id = NULL) {
-    $this->drupalGet('user/' . $this->web_user->uid . '/openid');
     $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Add an OpenID'));
+    $this->drupalPost('user/' . $this->web_user->uid . '/openid', $edit, t('Add an OpenID'));
+
+    if ($claimed_id === FALSE) {
+      $this->assertRaw(t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.'), t('Invalid identity was rejected.'));
+      return;
+    }
 
     // OpenID 1 used a HTTP redirect, OpenID 2 uses a HTML form that is submitted automatically using JavaScript.
     if ($version == 2) {
-      // Manually submit form because SimpleTest is not able to execute JavaScript.
-      $this->assertRaw('<script type="text/javascript">document.getElementById("openid-redirect-form").submit();</script>', t('JavaScript form submission found.'));
+      // Check we are on the OpenID redirect form.
+      $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
+
+      // Submit form to the OpenID Provider Endpoint.
       $this->drupalPost(NULL, array(), t('Send'));
     }
 
-    if (!$claimed_id) {
+    if (!isset($claimed_id)) {
       $claimed_id = $identity;
     }
     $this->assertRaw(t('Successfully added %identity', array('%identity' => $claimed_id)), t('Identity %identity was added.', array('%identity' => $identity)));
   }
+}
+
+/**
+ * Test account registration using Simple Registration and Attribute Exchange.
+ */
+class OpenIDRegistrationTestCase extends OpenIDWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'OpenID account registration',
+      'description' => 'Creates a user account using auto-registration.',
+      'group' => 'OpenID'
+    );
+  }
+
+  function setUp() {
+    parent::setUp('openid', 'openid_test');
+  }
 
   /**
-   * Test OpenID auto-registration with e-mail verification disabled.
+   * Test OpenID auto-registration with e-mail verification enabled.
    */
-  function testRegisterUserWithoutEmailVerification() {
-    variable_set('user_email_verification', FALSE);
+  function testRegisterUserWithEmailVerification() {
+    variable_set('user_email_verification', TRUE);
 
-    // Load the front page to get the user login block.
-    $this->drupalGet('');
+    // Tell openid_test.module to respond with these SREG fields.
+    variable_set('openid_test_response', array('openid.sreg.nickname' => 'john', 'openid.sreg.email' => 'john@example.com'));
 
     // Use a User-supplied Identity that is the URL of an XRDS document.
     $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
+    $this->submitLoginForm($identity);
+    $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), t('User was asked to verify e-mail address.'));
+    $this->assertRaw(t('A welcome message with further instructions has been sent to your e-mail address.'), t('A welcome message was sent to the user.'));
 
-    // Tell openid_test.module to respond with these SREG fields.
-    variable_set('openid_test_response', array('openid.sreg.nickname' => 'john', 'openid.sreg.email' => 'john@example.com'));
+    $user = user_load_by_name('john');
+    $this->assertTrue($user, t('User was registered with right username.'));
+    $this->assertEqual($user->mail, 'john@example.com', t('User was registered with right email address.'));
+    $this->assertFalse($user->data, t('No additional user info was saved.'));
 
-    // Fill out and submit the login form.
-    $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Log in'));
+    $this->submitLoginForm($identity);
+    $this->assertRaw(t('You must validate your email address for this account before logging in via OpenID.'));
 
-    // Check we are on the OpenID redirect form.
-    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
+    // Follow the one-time login that was sent in the welcome e-mail.
+    $this->drupalGet(user_pass_reset_url($user));
+    $this->drupalPost(NULL, array(), t('Log in'));
 
-    // Submit form to the OpenID Provider Endpoint.
-    $this->drupalPost(NULL, array(), t('Send'));
+    $this->drupalLogout();
+
+    // Verify that the account was activated.
+    $this->submitLoginForm($identity);
+    $this->assertLink('john', 0, t('User was logged in.'));
+  }
+
+  /**
+   * Test OpenID auto-registration with e-mail verification disabled.
+   */
+  function testRegisterUserWithoutEmailVerification() {
+    variable_set('user_email_verification', FALSE);
+
+    // Tell openid_test.module to respond with these SREG fields.
+    variable_set('openid_test_response', array('openid.sreg.nickname' => 'john', 'openid.sreg.email' => 'john@example.com'));
+
+    // Use a User-supplied Identity that is the URL of an XRDS document.
+    $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
+    $this->submitLoginForm($identity);
     $this->assertLink('john', 0, t('User was logged in.'));
 
     $user = user_load_by_name('john');
     $this->assertTrue($user, t('User was registered with right username.'));
     $this->assertEqual($user->mail, 'john@example.com', t('User was registered with right email address.'));
+    $this->assertFalse($user->data, t('No additional user info was saved.'));
+
+    $this->drupalLogout();
+
+    $this->submitLoginForm($identity);
+    $this->assertLink('john', 0, t('User was logged in.'));
   }
 
   /**
@@ -230,27 +306,16 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
    * information (a username that is already taken, and no e-mail address).
    */
   function testRegisterUserWithInvalidSreg() {
-    // Load the front page to get the user login block.
-    $this->drupalGet('');
+    // Tell openid_test.module to respond with these SREG fields.
+    $web_user = $this->drupalCreateUser(array());
+    variable_set('openid_test_response', array('openid.sreg.nickname' => $web_user->name, 'openid.sreg.email' => 'mail@invalid#'));
 
     // Use a User-supplied Identity that is the URL of an XRDS document.
     $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
-
-    // Tell openid_test.module to respond with these SREG fields.
-    variable_set('openid_test_response', array('openid.sreg.nickname' => $this->web_user->name, 'openid.sreg.email' => 'mail@invalid#'));
-
-    // Fill out and submit the login form.
-    $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Log in'));
-
-    // Check we are on the OpenID redirect form.
-    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
-
-    // Submit form to the OpenID Provider Endpoint.
-    $this->drupalPost(NULL, array(), t('Send'));
+    $this->submitLoginForm($identity);
 
     $this->assertRaw(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), t('User was asked to complete the registration process manually.'));
-    $this->assertRaw(t('The name %name is already taken.', array('%name' => $this->web_user->name)), t('Form validation error for username was displayed.'));
+    $this->assertRaw(t('The name %name is already taken.', array('%name' => $web_user->name)), t('Form validation error for username was displayed.'));
     $this->assertRaw(t('The e-mail address %mail is not valid.', array('%mail' => 'mail@invalid#')), t('Form validation error for e-mail address was displayed.'));
 
     // Enter username and e-mail address manually.
@@ -260,8 +325,9 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
 
     $user = user_load_by_name('john');
     $this->assertTrue($user, t('User was registered with right username.'));
+    $this->assertFalse($user->data, t('No additional user info was saved.'));
 
-    // Follow the one-time login that was sent in the confirmation e-mail.
+    // Follow the one-time login that was sent in the welcome e-mail.
     $this->drupalGet(user_pass_reset_url($user));
     $this->drupalPost(NULL, array(), t('Log in'));
 
@@ -282,17 +348,7 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
 
     // Use a User-supplied Identity that is the URL of an XRDS document.
     $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
-
-    // Fill out and submit the login form.
-    $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Log in'));
-
-    // Check we are on the OpenID redirect form.
-    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
-
-    // Submit form to the OpenID Provider Endpoint.
-    $this->drupalPost(NULL, array(), t('Send'));
-
+    $this->submitLoginForm($identity);
     $this->assertRaw(t('Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), t('User was asked to complete the registration process manually.'));
     $this->assertNoRaw(t('You must enter a username.'), t('Form validation error for username was not displayed.'));
     $this->assertNoRaw(t('You must enter an e-mail address.'), t('Form validation error for e-mail address was not displayed.'));
@@ -304,8 +360,9 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
 
     $user = user_load_by_name('john');
     $this->assertTrue($user, t('User was registered with right username.'));
+    $this->assertFalse($user->data, t('No additional user info was saved.'));
 
-    // Follow the one-time login that was sent in the confirmation e-mail.
+    // Follow the one-time login that was sent in the welcome e-mail.
     $this->drupalGet(user_pass_reset_url($user));
     $this->drupalPost(NULL, array(), t('Log in'));
 
@@ -323,27 +380,17 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
   function testRegisterUserWithAXButNoSREG() {
     variable_set('user_email_verification', FALSE);
 
-    // Load the front page to get the user login block.
-    $this->drupalGet('');
-
-    // Use a User-supplied Identity that is the URL of an XRDS document.
-    $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
-
     // Tell openid_test.module to respond with these AX fields.
     variable_set('openid_test_response', array(
       'openid.ns.ext123' => 'http://openid.net/srv/ax/1.0',
-      'openid.ext123.value.email' => 'john@example.com',
+      'openid.ext123.value.mail_ao' => 'john@example.com',
+      'openid.ext123.count.name_son' => '1',
+      'openid.ext123.value.name_son.1' => 'john',
     ));
 
-    // Fill out and submit the login form.
-    $edit = array('openid_identifier' => $identity);
-    $this->drupalPost(NULL, $edit, t('Log in'));
-
-    // Check we are on the OpenID redirect form.
-    $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.'));
-
-    // Submit form to the OpenID Provider Endpoint.
-    $this->drupalPost(NULL, array(), t('Send'));
+    // Use a User-supplied Identity that is the URL of an XRDS document.
+    $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE));
+    $this->submitLoginForm($identity);
     $this->assertLink('john', 0, t('User was logged in.'));
 
     $user = user_load_by_name('john');
diff --git a/modules/openid/tests/openid_test.info b/modules/openid/tests/openid_test.info
index 5f0c935b76e3ebb3cd920bc20edd954a15a930bf..bcf5457fcc71145c2f7dfb33d211153ec1d99632 100644
--- a/modules/openid/tests/openid_test.info
+++ b/modules/openid/tests/openid_test.info
@@ -9,8 +9,8 @@ files[] = openid_test.module
 dependencies[] = openid
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/openid/tests/openid_test.module b/modules/openid/tests/openid_test.module
index ab76c13cc378dac8c2e9a648cc7467c0b758ce6e..a11b1d1f43da5789176240304ff487e1e0419cb5 100644
--- a/modules/openid/tests/openid_test.module
+++ b/modules/openid/tests/openid_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: openid_test.module,v 1.10 2010/03/02 08:55:42 dries Exp $
+// $Id: openid_test.module,v 1.14 2010/04/23 05:48:13 webchick Exp $
 
 /**
  * @file
@@ -69,10 +69,30 @@ function openid_test_menu() {
  */
 function openid_test_yadis_xrds() {
   if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') {
+    // Only respond to XRI requests for one specific XRI. The is used to verify
+    // that the XRI has been properly encoded. The "+" sign in the _xrd_r query
+    // parameter is decoded to a space by PHP.
+    if (arg(3) == 'xri') {
+      if (variable_get('clean_url', 0)) {
+        if (arg(4) != '@example*résumé;%25' || $_GET['_xrd_r'] != 'application/xrds xml') {
+          drupal_not_found();
+        }
+      }
+      else {
+        // Drupal cannot properly emulate an XRI proxy resolver using unclean
+        // URLs, so the arguments gets messed up.
+        if (arg(4) . '/' . arg(5) != '@example*résumé;%25?_xrd_r=application/xrds xml') {
+          drupal_not_found();
+        }
+      }
+    }
     drupal_add_http_header('Content-Type', 'application/xrds+xml');
     print '<?xml version="1.0" encoding="UTF-8"?>
       <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
         <XRD>
+          <Status cid="' . check_plain(variable_get('openid_test_canonical_id_status', 'verified')) . '"/>
+          <ProviderID>xri://@</ProviderID>
+          <CanonicalID>http://example.com/user</CanonicalID>
           <Service>
             <Type>http://example.com/this-is-ignored</Type>
           </Service>
@@ -102,7 +122,7 @@ function openid_test_yadis_xrds() {
           </Service>';
     }
     print '
-        <XRD>
+        </XRD>
       </xrds:XRDS>';
   }
   else {
@@ -158,10 +178,10 @@ function openid_test_html_openid2() {
  */
 function openid_test_endpoint() {
   switch ($_REQUEST['openid_mode']) {
-    case 'associate';
+    case 'associate':
       _openid_test_endpoint_associate();
       break;
-    case 'checkid_setup';
+    case 'checkid_setup':
       _openid_test_endpoint_authenticate();
       break;
   }
@@ -232,19 +252,6 @@ function _openid_test_endpoint_authenticate() {
   // Generate unique identifier for this authentication.
   $nonce = _openid_nonce();
 
-  if (!isset($_REQUEST['openid_claimed_id'])) {
-    // openid.claimed_id is not used in OpenID 1.x.
-    $claimed_id = '';
-  }
-  elseif ($_REQUEST['openid_claimed_id'] == 'http://specs.openid.net/auth/2.0/identifier_select') {
-    // The Relying Party did not specify a Claimed Identifier, so the OpenID
-    // Provider decides on one.
-    $claimed_id = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE));
-  }
-  else {
-    $claimed_id = $_REQUEST['openid_claimed_id'];
-  }
-
   // Generate response containing the user's identity. The openid.sreg.xxx
   // entries contain profile data stored by the OpenID Provider (see OpenID
   // Simple Registration Extension 1.0).
@@ -252,7 +259,7 @@ function _openid_test_endpoint_authenticate() {
     'openid.ns' => OPENID_NS_2_0,
     'openid.mode' => 'id_res',
     'openid.op_endpoint' => url('openid-test/endpoint', array('absolute' => TRUE)),
-    'openid.claimed_id' => $claimed_id,
+    'openid.claimed_id' => !empty($_REQUEST['openid_claimed_id']) ? $_REQUEST['openid_claimed_id'] : '',
     'openid.identity' => $_REQUEST['openid_identity'],
     'openid.return_to' => $_REQUEST['openid_return_to'],
     'openid.response_nonce' => $nonce,
diff --git a/modules/openid/xrds.inc b/modules/openid/xrds.inc
deleted file mode 100644
index e5b0808c8028598c79a956becbbc29833ccee677..0000000000000000000000000000000000000000
--- a/modules/openid/xrds.inc
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-// $Id: xrds.inc,v 1.6 2010/03/02 08:59:54 dries Exp $
-
-// Global variables to track parsing state
-$xrds_open_elements = array();
-$xrds_services = array();
-$xrds_current_service = array();
-
-/**
- * Main entry point for parsing XRDS documents
- */
-function xrds_parse($xml) {
-  global $xrds_services;
-
-  $parser = xml_parser_create_ns();
-  xml_set_element_handler($parser, '_xrds_element_start', '_xrds_element_end');
-  xml_set_character_data_handler($parser, '_xrds_cdata');
-
-  xml_parse($parser, $xml);
-  xml_parser_free($parser);
-
-  return $xrds_services;
-}
-
-/**
- * Parser callback functions
- */
-function _xrds_element_start(&$parser, $name, $attributes) {
-  global $xrds_open_elements, $xrds_current_service;
-
-  $xrds_open_elements[] = _xrds_strip_namespace($name);
-
-  $path = strtoupper(implode('/', $xrds_open_elements));
-  if ($path == 'XRDS/XRD/SERVICE') {
-    foreach ($attributes as $attribute_name => $value) {
-      if (_xrds_strip_namespace($attribute_name) == 'PRIORITY') {
-        $xrds_current_service['priority'] = intval($value);
-      }
-    }
-  }
-}
-
-function _xrds_element_end(&$parser, $name) {
-  global $xrds_open_elements, $xrds_services, $xrds_current_service;
-
-  $name = _xrds_strip_namespace($name);
-  if ($name == 'SERVICE') {
-    if (!isset($xrds_current_service['priority'])) {
-      // If the priority attribute is absent, the default is infinity.
-      $xrds_current_service['priority'] = PHP_INT_MAX;
-    }
-    $xrds_services[] = $xrds_current_service;
-    $xrds_current_service = array();
-  }
-  array_pop($xrds_open_elements);
-}
-
-function _xrds_cdata(&$parser, $data) {
-  global $xrds_open_elements, $xrds_services, $xrds_current_service;
-  $path = strtoupper(implode('/', $xrds_open_elements));
-  switch ($path) {
-    case 'XRDS/XRD/SERVICE/TYPE':
-      $xrds_current_service['types'][] = $data;
-      break;
-    case 'XRDS/XRD/SERVICE/URI':
-      $xrds_current_service['uri'] = $data;
-      break;
-    case 'XRDS/XRD/SERVICE/DELEGATE':
-      $xrds_current_service['delegate'] = $data;
-      break;
-    case 'XRDS/XRD/SERVICE/LOCALID':
-      $xrds_current_service['localid'] = $data;
-      break;
-    default:
-      if (preg_match('@^XRDS/XRD/SERVICE/(.*)$@', $path, $matches)) {
-        $xrds_current_service['additional'][$matches[1]] = $data;
-      }
-      break;
-  }
-}
-
-function _xrds_strip_namespace($name) {
-  // Strip namespacing.
-  $pos = strrpos($name, ':');
-  if ($pos !== FALSE) {
-    $name = substr($name, $pos + 1, strlen($name));
-  }
-
-  return $name;
-}
diff --git a/modules/overlay/overlay-child.js b/modules/overlay/overlay-child.js
index 47920a428805f6db636cd510e13c10514163fa6d..8eb9ee6b94e67c3edf8fa3cafc92f4427038e50e 100644
--- a/modules/overlay/overlay-child.js
+++ b/modules/overlay/overlay-child.js
@@ -1,4 +1,4 @@
-// $Id: overlay-child.js,v 1.6 2010/03/09 20:52:27 webchick Exp $
+// $Id: overlay-child.js,v 1.8 2010/04/24 07:14:29 dries Exp $
 
 (function ($) {
 
@@ -31,13 +31,13 @@ Drupal.behaviors.overlayChild = {
     // may have decided to tell us the parent window to close the popup dialog.
     if (settings.closeOverlay) {
       parent.Drupal.overlay.bindChild(window, true);
-      // Close the child window from a separate thread because the current
-      // one is busy processing Drupal behaviors.
+      // Use setTimeout to close the child window from a separate thread,
+      // because the current one is busy processing Drupal behaviors.
       setTimeout(function () {
         // We need to store the parent variable locally because it will
         // disappear as soon as we close the iframe.
         var p = parent;
-        p.Drupal.overlay.close(settings.args, settings.statusMessages);
+        p.Drupal.overlay.close();
         if (typeof settings.redirect == 'string') {
           p.Drupal.overlay.redirect(settings.redirect);
         }
diff --git a/modules/overlay/overlay-parent.js b/modules/overlay/overlay-parent.js
index 14be94867270bb8f7d5aeea32e8dfb0ced2bdf6f..232c4c2f82c10d26ef74e837c64e2b809b078282 100644
--- a/modules/overlay/overlay-parent.js
+++ b/modules/overlay/overlay-parent.js
@@ -1,4 +1,4 @@
-// $Id: overlay-parent.js,v 1.32 2010/03/10 20:34:57 webchick Exp $
+// $Id: overlay-parent.js,v 1.38 2010/04/24 07:14:29 dries Exp $
 
 (function ($) {
 
@@ -39,9 +39,6 @@ Drupal.overlay = Drupal.overlay || {
   isClosing: false,
   isLoading: false,
 
-  onOverlayCloseArgs: null,
-  onOverlayCloseStatusMessages: null,
-
   resizeTimeoutID: null,
   lastHeight: 0,
 
@@ -220,10 +217,8 @@ Drupal.overlay.create = function () {
     self.lastHeight = 0;
 
     if ($.isFunction(self.options.onOverlayClose)) {
-      self.options.onOverlayClose(self.onOverlayCloseArgs, self.onOverlayCloseStatusMessages);
+      self.options.onOverlayClose();
     }
-    self.onOverlayCloseArgs = null;
-    self.onOverlayCloseStatusMessages = null;
   };
 
   // Default jQuery UI Dialog options.
@@ -301,10 +296,10 @@ Drupal.overlay.load = function (url) {
       // Get the secondary tabs
       var $secondary = self.$iframeBody.find('ul.secondary');
       var $secondaryLinks = $secondary.find('> li > a');
-  
+
       // Check if clicked on a secondary tab
       var $activeLinkSecondary = $secondaryLinks.filter(function () { return self.getPath(this) == urlPath; });
-  
+
       if ($activeLinkSecondary.length) {
         var active_tab = Drupal.t('(active tab)');
         $secondaryLinks.parent().removeClass('active').find('element-invisible:contains(' + active_tab + ')').appendTo($activeLinkSecondary);
@@ -327,7 +322,7 @@ Drupal.overlay.load = function (url) {
   // While the overlay is loading, we remove the loaded class from the dialog.
   // After the loading is finished, the loaded class is added back. The loaded
   // class is being used to hide the iframe while loading.
-  // @see overlay-parent.css .overlay-loaded #overlay-element
+  // See overlay-parent.css .overlay-loaded #overlay-element.
   self.$dialog.removeClass('overlay-loaded');
   self.$iframe
     .bind('load.overlay-event', function () {
@@ -343,7 +338,7 @@ Drupal.overlay.load = function (url) {
   });
 
   // Get the document object of the iframe window.
-  // @see http://xkr.us/articles/dom/iframe-document/
+  // See http://xkr.us/articles/dom/iframe-document/.
   var iframeDocument = (iframeElement.contentWindow || iframeElement.contentDocument);
   if (iframeDocument.document) {
     iframeDocument = iframeDocument.document;
@@ -358,13 +353,8 @@ Drupal.overlay.load = function (url) {
 /**
  * Close the overlay and remove markup related to it from the document.
  */
-Drupal.overlay.close = function (args, statusMessages) {
-  var self = this;
-
-  self.onOverlayCloseArgs = args;
-  self.onOverlayCloseStatusMessages = statusMessages;
-
-  return self.$container.dialog('close');
+Drupal.overlay.close = function () {
+  return this.$container.dialog('close');
 };
 
 /**
@@ -431,8 +421,8 @@ Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
   // handler interferes with use of the scroll bar in Chrome & Safari.
   // After unbinding from the document we bind a handler to the dialog overlay
   // which returns false to prevent event bubbling.
-  // @see http://dev.jqueryui.com/ticket/4671
-  // @see https://bugs.webkit.org/show_bug.cgi?id=19033
+  // See http://dev.jqueryui.com/ticket/4671.
+  // See https://bugs.webkit.org/show_bug.cgi?id=19033.
   // Do the same for the click handler as prevents default handling of clicks in
   // displaced regions (e.g. opening a link in a new browser tab when CTRL was
   // pressed while clicking).
@@ -517,7 +507,7 @@ Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
       if (!$target.size()) {
         $target = self.$iframeDocument;
       }
-      setTimeout(function () { $target.focus(); }, 10);
+      $target.focus();
       return false;
     }
   });
@@ -528,16 +518,16 @@ Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
     if (event.keyCode) {
       if (event.keyCode == $.ui.keyCode.TAB) {
         if (event.shiftKey && event.target == $firstTabbable.get(0)) {
-          setTimeout(function () { $closeButton.focus(); }, 10);
+          $closeButton.focus();
           return false;
         }
         else if (!event.shiftKey && event.target == $lastTabbable.get(0)) {
-          setTimeout(function () { $closeButton.focus(); }, 10);
+          $closeButton.focus();
           return false;
         }
       }
       else if (event.keyCode == $.ui.keyCode.ESCAPE) {
-        setTimeout(function () { self.close(); }, 10);
+        self.close();
         return false;
       }
     }
@@ -548,11 +538,9 @@ Drupal.overlay.bindChild = function (iframeWindow, isClosing) {
   // close button of the dialog (default).
   $(document).bind('keydown.overlay-event', function (event) {
     if (event.keyCode && event.keyCode == $.ui.keyCode.TAB) {
-      setTimeout(function () {
-        if (!self.$iframeWindow(':tabbable:not(form):first').focus().size()) {
+      if (!self.$iframeWindow(':tabbable:not(form):first').focus().size()) {
           $closeButton.focus();
-        }
-      }, 10);
+      }
       return false;
     }
   });
@@ -626,14 +614,14 @@ Drupal.overlay.innerResize = function (height) {
   if (!height && self.$iframeBody) {
     height = self.$iframeBody.outerHeight() + 25;
   }
-  
+
   // Only resize when height actually is changed.
   if (height && height != self.lastHeight) {
     // Resize the container.
     self.$container.height(height);
     // Keep the dim background grow or shrink with the dialog.
     $.ui.dialog.overlay.resize();
-    
+
     self.lastHeight = height;
   }
 };
@@ -752,7 +740,9 @@ Drupal.overlay.clickHandler = function (event) {
     else if ($target.get(0).hostname != window.location.hostname) {
       // Add a target attribute to the clicked link. This is being picked up by
       // the default action handler.
-      $target.attr('target', '_new');
+      if (!$target.attr('target')) {
+        $target.attr('target', '_new');
+      }
     }
     // Non-admin links should close the overlay and open in the main window.
     // Only handle them if the overlay is open and the clicked link is inside
@@ -862,13 +852,8 @@ Drupal.overlay.fragmentizeLink = function (link) {
   // Preserve existing query and fragment parameters in the URL.
   var destination = path + link.search + link.hash;
 
-  // Assemble the overlay-ready link.
-  var newLink = $.param.fragment(window.location.href, { overlay: destination });
-  // $.param.fragment() escaped slashes in the overlay part: unescape them.
-  var regexp = new RegExp("[#&]overlay=" + encodeURIComponent(path));
-  newLink = newLink.replace(regexp, decodeURIComponent);
-
-  return newLink;
+  // Assemble and return the overlay-ready link.
+  return $.param.fragment(window.location.href, { overlay: destination });
 };
 
 /**
@@ -967,7 +952,7 @@ Drupal.overlay.getPath = function (link, ignorePathFromQueryString) {
   if (path.charAt(0) != '/') {
     path = '/' + path;
   }
-  path = path.replace(new RegExp(Drupal.settings.basePath), '');
+  path = path.replace(new RegExp(Drupal.settings.basePath + "(?:index.php)?"), '');
   if (path == '' && !ignorePathFromQueryString) {
     // If the path appears empty, it might mean the path is represented in the
     // query string (clean URLs are not used).
diff --git a/modules/overlay/overlay.info b/modules/overlay/overlay.info
index cda274490630f5e70372b0c8ebb0f0f8646666d7..23fcde2827ada83f0dd8d1007b90f2899974f579 100644
--- a/modules/overlay/overlay.info
+++ b/modules/overlay/overlay.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = overlay.module
 files[] = overlay.install
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/overlay/overlay.module b/modules/overlay/overlay.module
index 74f11701b719725b46f94ab11d27b6465efbd427..e64400c57aae6fe9fbc71ed7a0bb904f67703e37 100644
--- a/modules/overlay/overlay.module
+++ b/modules/overlay/overlay.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: overlay.module,v 1.13 2010/03/21 03:47:32 webchick Exp $
+// $Id: overlay.module,v 1.17 2010/04/24 07:14:29 dries Exp $
 
 /**
  * @file
@@ -59,17 +59,18 @@ function overlay_init() {
   // Only act if the user has access to administration pages. Other modules can
   // also enable the overlay directly for other uses of the JavaScript.
   if (user_access('access overlay')) {
+    $current_path = current_path();
     // After overlay is enabled on the modules page, redirect to
     // <front>#overlay=admin/modules to actually enable the overlay.
     if (isset($_SESSION['overlay_enable_redirect']) && $_SESSION['overlay_enable_redirect']) {
       unset($_SESSION['overlay_enable_redirect']);
-      drupal_goto('<front>', array('fragment' => 'overlay=' . current_path()));
+      drupal_goto('<front>', array('fragment' => 'overlay=' . $current_path));
     }
 
     if (isset($_GET['render']) && $_GET['render'] == 'overlay') {
       // If this page shouldn't be rendered here, redirect to the parent.
-      if (!path_is_admin($_GET['q'])) {
-        overlay_close_dialog();
+      if (!path_is_admin($current_path)) {
+        overlay_close_dialog($current_path);
       }
       // If system module did not switch the theme yet (i.e. this is not an
       // admin page, per se), we should switch the theme here.
@@ -85,7 +86,7 @@ function overlay_init() {
       unset($_GET['render']);
     }
     // Do not enable the overlay if we already are on an admin page.
-    else if (!path_is_admin(current_path())) {
+    else if (!path_is_admin($current_path)) {
       // Otherwise add overlay parent code and our behavior.
       overlay_set_mode('parent');
     }
@@ -223,9 +224,9 @@ function overlay_page_alter(&$page) {
 }
 
 /**
- * Implements hook_block_info_alter().
+ * Implements hook_block_list_alter().
  */
-function overlay_block_info_alter(&$blocks) {
+function overlay_block_list_alter(&$blocks) {
   // If we are limiting rendering to a subset of page regions, hide all blocks
   // which appear in regions not on that list. Note that overlay_page_alter()
   // does a more comprehensive job of preventing unwanted regions from being
@@ -317,8 +318,6 @@ function overlay_preprocess_toolbar(&$variables) {
  * processing, so that it's possible to close the overlay after submitting
  * a form.
  *
- * @see _form_builder_handle_input_element()
- * @see _form_builder_ie_cleanup()
  * @see form_execute_handlers()
  * @see form_builder()
  * @see overlay_form_submit()
@@ -327,17 +326,6 @@ function overlay_preprocess_toolbar(&$variables) {
  */
 function overlay_form_after_build($form, &$form_state) {
   if (overlay_get_mode() == 'child') {
-    // Form API may have already captured submit handlers from the submitted
-    // button before after_build callback is invoked. This may have been done
-    // by _form_builder_handle_input_element(). If so, the list of submit
-    // handlers is stored in the $form_state array, which is something we can
-    // also alter from here, luckily. Rememeber: our goal here is to set
-    // $form_state['redirect'] to FALSE if the API function
-    // overlay_request_dialog_close() has been invoked. That's because we want
-    // to tell the parent window to close the overlay.
-    if (!empty($form_state['submit_handlers']) && !in_array('overlay_form_submit', $form_state['submit_handlers'])) {
-      $form_state['submit_handlers'][] = 'overlay_form_submit';
-    }
     // If this element has submit handlers, then append our own.
     if (isset($form['#submit'])) {
       $form['#submit'][] = 'overlay_form_submit';
@@ -350,28 +338,15 @@ function overlay_form_after_build($form, &$form_state) {
  * Generic form submit handler.
  *
  * When we are requested to close an overlay, we don't want Form API to
- * perform any redirection once the submitted form has been processed.
- *
- * When $form_state['redirect'] is set to FALSE, then Form API will simply
- * re-render the form with the values still in its fields. And this is all
- * we need to output the JavaScript that will tell the parent window to close
- * the child dialog.
+ * perform any redirection once the submitted form has been processed. Instead,
+ * we set $form_state['redirect'] to FALSE so that Form API will simply
+ * re-render the current page, and pass the redirect information on to the
+ * overlay JavaScript so that the redirection can be performed there.
  *
  * @see overlay_get_mode()
  * @ingroup forms
  */
 function overlay_form_submit($form, &$form_state) {
-  $settings = &drupal_static(__FUNCTION__);
-
-  // Check if we have a request to close the overlay.
-  $args = overlay_request_dialog_close();
-
-  // Close the overlay if the overlay module has been disabled
-  if (!module_exists('overlay')) {
-    $args = overlay_request_dialog_close(TRUE);
-  }
-
-  // If there is a form redirect to a non-admin page, close the overlay.
   if (isset($form_state['redirect'])) {
     // A destination set in the URL trumps $form_state['redirect'].
     if (isset($_GET['destination'])) {
@@ -386,33 +361,72 @@ function overlay_form_submit($form, &$form_state) {
       $url = $form_state['redirect'];
       $url_settings = array();
     }
-    if (!path_is_admin($url)) {
-      $args = overlay_request_dialog_close(TRUE);
+    // Close the overlay if we are redirecting to a non-admin page or if the
+    // overlay module has just been disabled.
+    if (!path_is_admin($url) || !module_exists('overlay')) {
+      overlay_close_dialog($url, $url_settings);
+      // Tell FAPI to stay on the same page after all submit callbacks have
+      // been processed.
+      $form_state['redirect'] = FALSE;
     }
   }
+}
 
-  // If the overlay is to be closed, pass that information through JavaScript.
-  if ($args !== FALSE) {
-    if (!isset($settings)) {
-      $settings = array(
-        'overlayChild' => array(
-          'closeOverlay' => TRUE,
-          'statusMessages' => theme('status_messages'),
-          'args' => $args,
-        ),
-      );
-      // Tell the child window to perform the redirection when requested to.
-      if (!empty($form_state['redirect'])) {
-        $settings['overlayChild']['redirect'] = url($url, $settings);
-      }
-      drupal_add_js($settings, array('type' => 'setting'));
-    }
-    // Tell FAPI to redraw the form without redirection after all submit
-    // callbacks have been processed.
-    $form_state['redirect'] = FALSE;
+/**
+ * Callback to request that the overlay display an empty page.
+ *
+ * This is used to prevent a page request which closes the overlay (for
+ * example, a form submission) from being fully re-rendered before the overlay
+ * is closed. Instead, we store a variable which will cause the page to be
+ * rendered by a delivery callback function that does not actually print
+ * visible HTML (but rather only the bare minimum scripts and styles necessary
+ * to trigger the overlay to close), thereby allowing the dialog to be closed
+ * faster and with less interruption, and also allowing the display of messages
+ * to be deferred to the parent window (rather than displaying them in the
+ * child window, which will close before the user has had a chance to read
+ * them).
+ *
+ * @param $value
+ *   By default, an empty page will not be displayed. Set to TRUE to request
+ *   an empty page display, or FALSE to disable the empty page display (if it
+ *   was previously enabled on this page request).
+ *
+ * @return
+ *   TRUE if the current behavior is to display an empty page, or FALSE if not.
+ *
+ * @see overlay_page_delivery_callback_alter()
+ */
+function overlay_display_empty_page($value = NULL) {
+  $display_empty_page = &drupal_static(__FUNCTION__, FALSE);
+  if (isset($value)) {
+    $display_empty_page = $value;
+  }
+  return $display_empty_page;
+}
+
+/**
+ * Implements hook_page_delivery_callback_alter().
+ */
+function overlay_page_delivery_callback_alter(&$callback) {
+  if (overlay_display_empty_page()) {
+    $callback = 'overlay_deliver_empty_page';
   }
 }
 
+/**
+ * Delivery callback to display an empty page.
+ *
+ * This function is used to print out a bare minimum empty page which still has
+ * the scripts and styles necessary in order to trigger the overlay to close.
+ *
+ * @see overlay_form_submit()
+ */
+function overlay_deliver_empty_page() {
+  $empty_page = '<html><head><title></title>' . drupal_get_css() . drupal_get_js() . '</head><body class="overlay"></body></html>';
+  print $empty_page;
+  drupal_exit();
+}
+
 /**
  * Get the current overlay mode.
  *
@@ -514,46 +528,33 @@ function overlay_overlay_child_initialize() {
 }
 
 /**
- * Callback to request that the overlay close on the next page load.
- *
- * @param $value
- *   By default, the dialog will not close. Set to TRUE or a value evaluating to
- *   TRUE to request the dialog to close. Use FALSE to disable closing the
- *   dialog (if it was previously enabled). The value passed will be forwarded
- *   to the onOverlayClose callback of the overlay.
- *
- * @return
- *   The current overlay close dialog mode, a value evaluating to TRUE if the
- *   overlay should close or FALSE if it should not (default).
- */
-function overlay_request_dialog_close($value = NULL) {
-  $close = &drupal_static(__FUNCTION__, FALSE);
-  if (isset($value)) {
-    $close = $value;
-  }
-  return $close;
-}
-
-/**
- * Close the overlay and redirect the parent window to a new path.
+ * Callback to request that the overlay close as soon as the page is displayed.
  *
  * @param $redirect
- *   The path that should open in the parent window after the overlay closes.
- */
-function overlay_close_dialog($redirect = NULL) {
-  if (!isset($redirect)) {
-    $redirect = current_path();
-  }
+ *   (optional) The path that should open in the parent window after the
+ *   overlay closes. If not set, no redirect will be performed on the parent
+ *   window.
+ * @param $redirect_options
+ *   (optional) An associative array of options to use when generating the
+ *   redirect URL.
+ */
+function overlay_close_dialog($redirect = NULL, $redirect_options = array()) {
   $settings = array(
     'overlayChild' => array(
       'closeOverlay' => TRUE,
-      'statusMessages' => theme('status_messages'),
-      'args' => $args,
-      'redirect' => url($redirect),
     ),
   );
+
+  // Tell the child window to perform the redirection when requested to.
+  if (isset($redirect)) {
+    $settings['overlayChild']['redirect'] = url($redirect, $redirect_options);
+  }
+
   drupal_add_js($settings, array('type' => 'setting'));
-  return $settings;
+
+  // Since we are closing the overlay as soon as the page is displayed, we do
+  // not want to show any of the page's actual content.
+  overlay_display_empty_page(TRUE);
 }
 
 /**
@@ -629,7 +630,7 @@ function _overlay_region_list($type) {
  *   and all regions of the page will be rendered.
  *
  * @see overlay_page_alter()
- * @see overlay_block_info_alter()
+ * @see overlay_block_list_alter()
  * @see overlay_set_regions_to_render()
  */
 function overlay_get_regions_to_render() {
@@ -651,7 +652,7 @@ function overlay_get_regions_to_render() {
  *   are not being limited.
  *
  * @see overlay_page_alter()
- * @see overlay_block_info_alter()
+ * @see overlay_block_list_alter()
  * @see overlay_get_regions_to_render()
  */
 function overlay_set_regions_to_render($regions = NULL) {
@@ -679,7 +680,7 @@ function overlay_set_regions_to_render($regions = NULL) {
  */
 function overlay_render_region($region) {
   // Indicate the region that we will be rendering, so that other regions will
-  // be hidden by overlay_page_alter() and overlay_block_info_alter().
+  // be hidden by overlay_page_alter() and overlay_block_list_alter().
   overlay_set_regions_to_render(array($region));
   // Do what is necessary to force drupal_render_page() to only display HTML
   // from the requested region. Specifically, declare that the main page
diff --git a/modules/path/path.admin.inc b/modules/path/path.admin.inc
index 737590f68460a70f61ed71d98d8ad6e8cc47978e..e19048326855b877bc03b244f36caa4e3a719135 100644
--- a/modules/path/path.admin.inc
+++ b/modules/path/path.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: path.admin.inc,v 1.42 2010/03/03 19:46:25 dries Exp $
+// $Id: path.admin.inc,v 1.43 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -122,10 +122,7 @@ function path_admin_form($form, &$form_state, $path = array('source' => '', 'ali
     '#value' => $path['language']
   );
 
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
diff --git a/modules/path/path.info b/modules/path/path.info
index fa861c9c76378e3406c3cbc301a79f00016a0e03..f16c083ea3e5fd1c1c5c6824b048554e2212b149 100644
--- a/modules/path/path.info
+++ b/modules/path/path.info
@@ -9,8 +9,8 @@ files[] = path.admin.inc
 files[] = path.test
 configure = admin/config/search/path
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/path/path.js b/modules/path/path.js
index 537945b1fcaca79d01c772bb07512ba17d57813a..bc8b5d64d618c10cd254db2eed6576a31207b416 100644
--- a/modules/path/path.js
+++ b/modules/path/path.js
@@ -1,10 +1,10 @@
-// $Id: path.js,v 1.3 2009/10/20 01:24:34 dries Exp $
+// $Id: path.js,v 1.4 2010/04/09 12:24:53 dries Exp $
 
 (function ($) {
 
 Drupal.behaviors.pathFieldsetSummaries = {
   attach: function (context) {
-    $('fieldset#edit-path', context).setSummary(function (context) {
+    $('fieldset#edit-path', context).drupalSetSummary(function (context) {
       var path = $('#edit-path-alias').val();
 
       return path ?
diff --git a/modules/path/path.test b/modules/path/path.test
index 08aee19ae88a21b69cee575cb7db5b3504a7b26f..92525e8d814b60bc7c82f1c3838697013d8079ef 100644
--- a/modules/path/path.test
+++ b/modules/path/path.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: path.test,v 1.33 2010/03/07 23:14:20 webchick Exp $
+// $Id: path.test,v 1.35 2010/03/26 12:37:30 dries Exp $
 
 /**
  * @file
@@ -190,12 +190,13 @@ class PathTaxonomyTermTestCase extends DrupalWebTestCase {
    */
   function testTermAlias() {
     // Create a term in the default 'Tags' vocabulary with URL alias.
+    $vocabulary = taxonomy_vocabulary_load(1);
     $description = $this->randomName();;
     $edit = array();
     $edit['name'] = $this->randomName();
     $edit['description[value]'] = $description;
     $edit['path[alias]'] = $this->randomName();
-    $this->drupalPost('admin/structure/taxonomy/1/add', $edit, t('Save'));
+    $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add', $edit, t('Save'));
 
     // Confirm that the alias works.
     $this->drupalGet($edit['path[alias]']);
@@ -307,3 +308,83 @@ class PathLanguageTestCase extends DrupalWebTestCase {
     $this->assertTrue(strpos($url, $edit['path[alias]']), t('URL contains the path alias.'));
   }
 }
+
+/**
+ * Tests the user interface for creating path aliases, with languages.
+ */
+class PathLanguageUITestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Path aliases with languages',
+      'description' => 'Confirm that the Path module user interface works with languages.',
+      'group' => 'Path',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('path', 'locale');
+
+    // Create and login user.
+    $web_user = $this->drupalCreateUser(array('edit any page content', 'create page content', 'administer url aliases', 'create url aliases', 'administer languages', 'access administration pages'));
+    $this->drupalLogin($web_user);
+
+    // Enable French language.
+    $edit = array();
+    $edit['langcode'] = 'fr';
+
+    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+
+    // Set language negotiation to "Path prefix with fallback".
+    include_once DRUPAL_ROOT . '/includes/locale.inc';
+    variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info());
+    variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);
+
+    // Force inclusion of language.inc.
+    drupal_language_initialize();
+  }
+
+  /**
+   * Tests that a language-neutral URL alias works.
+   */
+  function testLanguageNeutralURLs() {
+    $name = $this->randomName(8);
+    $edit = array();
+    $edit['source'] = 'admin/config/search/path';
+    $edit['alias'] = $name;
+    $this->drupalPost('admin/config/search/path/add', $edit, t('Save'));
+
+    $this->drupalGet($name);
+    $this->assertText(t('Filter aliases'), 'Language-neutral URL alias works');
+  }
+
+  /**
+   * Tests that a default language URL alias works.
+   */
+  function testDefaultLanguageURLs() {
+    $name = $this->randomName(8);
+    $edit = array();
+    $edit['source'] = 'admin/config/search/path';
+    $edit['alias'] = $name;
+    $edit['language'] = 'en';
+    $this->drupalPost('admin/config/search/path/add', $edit, t('Save'));
+
+    $this->drupalGet($name);
+    $this->assertText(t('Filter aliases'), 'English URL alias works');
+  }
+
+  /**
+   * Tests that a non-default language URL alias works.
+   */
+  function testNonDefaultURLs() {
+    $name = $this->randomName(8);
+    $edit = array();
+    $edit['source'] = 'admin/config/search/path';
+    $edit['alias'] = $name;
+    $edit['language'] = 'fr';
+    $this->drupalPost('admin/config/search/path/add', $edit, t('Save'));
+
+    $this->drupalGet('fr/' . $name);
+    $this->assertText(t('Filter aliases'), 'Foreign URL alias works');
+  }
+
+}
diff --git a/modules/php/php.info b/modules/php/php.info
index a63ef323d8d96e60842cf3921da7799dd89b1568..16b501d42eb25b2a4583e47322d302f83a25132b 100644
--- a/modules/php/php.info
+++ b/modules/php/php.info
@@ -8,8 +8,8 @@ files[] = php.module
 files[] = php.install
 files[] = php.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/php/php.module b/modules/php/php.module
index b725083efd00158b8f809ac10b32dd61c9cd36c2..f163b987dd284a237bec73bcb668bd25a2b08898 100644
--- a/modules/php/php.module
+++ b/modules/php/php.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: php.module,v 1.27 2010/01/09 23:15:26 webchick Exp $
+// $Id: php.module,v 1.28 2010/03/21 21:20:43 dries Exp $
 
 /**
  * @file
@@ -31,7 +31,7 @@ function php_permission() {
   return array(
     'use PHP for settings' => array(
       'title' => t('Use PHP for settings'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
   );
 }
diff --git a/modules/poll/poll.css b/modules/poll/poll.css
index ce3daa609963017f8cd05babaf1af00a1a127a0e..fd99c2159ba15a9722cdd727835ed9ddd66521e9 100644
--- a/modules/poll/poll.css
+++ b/modules/poll/poll.css
@@ -1,5 +1,8 @@
-/* $Id: poll.css,v 1.7 2008/05/15 20:55:58 dries Exp $ */
+/* $Id: poll.css,v 1.8 2010/04/06 15:25:51 dries Exp $ */
 
+.poll {
+  overflow: hidden;
+}
 .poll .bar {
   height: 1em;
   margin: 1px 0;
diff --git a/modules/poll/poll.info b/modules/poll/poll.info
index e118dff74bf50fbdfc1603c344a14d2275d26816..50f1c19fe2ce9c412208ceb03d9be061a57e963b 100644
--- a/modules/poll/poll.info
+++ b/modules/poll/poll.info
@@ -10,8 +10,8 @@ files[] = poll.install
 files[] = poll.test
 files[] = poll.tokens.inc
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/poll/poll.module b/modules/poll/poll.module
index 41ac9e2d1d84ea7e016f06e002efb3858815c58e..876f8b84400f952e8987384d8e2f578d806d100a 100644
--- a/modules/poll/poll.module
+++ b/modules/poll/poll.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: poll.module,v 1.339 2010/03/12 15:56:29 dries Exp $
+// $Id: poll.module,v 1.345 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -139,10 +139,8 @@ function _poll_menu_access($node, $perm, $inspect_allowvotes) {
  * Implements hook_block_info().
  */
 function poll_block_info() {
-  if (user_access('access content')) {
-    $blocks['recent']['info'] = t('Most recent poll');
-    return $blocks;
-  }
+  $blocks['recent']['info'] = t('Most recent poll');
+  return $blocks;
 }
 
 /**
@@ -480,17 +478,17 @@ function poll_load($nodes) {
     $poll->allowvotes = FALSE;
     if (user_access('vote on polls') && $poll->active) {
       if ($user->uid) {
-        $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetchObject();
+        $poll->vote = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetchField();
+        if (empty($poll->vote)) {
+          $poll->vote = -1;
+          $poll->allowvotes = TRUE;
+        }
       }
-      else {
-        $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetchObject();
-      }
-      if ($result) {
-        $poll->vote = $result->chid;
+      elseif (!empty($_SESSION['poll_vote'][$node->nid])) {
+        $poll->vote = $_SESSION['poll_vote'][$node->nid];
       }
       else {
-        $poll->vote = -1;
-        $poll->allowvotes = TRUE;
+        $poll->allowvotes = !db_query("SELECT 1 FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetchField();
       }
     }
     foreach ($poll as $key => $value) {
@@ -732,6 +730,16 @@ function poll_vote($form, &$form_state) {
     ->execute();
 
   cache_clear_all();
+
+  if (!$user->uid) {
+    // The vote is recorded so the user gets the result view instead of the
+    // voting form when viewing the poll. Saving a value in $_SESSION has the
+    // convenient side effect of preventing the user from hitting the page
+    // cache. When anonymous voting is allowed, the page cache should only
+    // contain the voting form, not the results.
+    $_SESSION['poll_vote'][$node->nid] = $choice;
+  }
+
   drupal_set_message(t('Your vote was recorded.'));
 
   // Return the user to whatever page they voted from.
@@ -781,7 +789,11 @@ function poll_view_results($node, $view_mode, $block = FALSE) {
 
 
 /**
- * Theme the admin poll form for choices.
+ * Returns HTML for an admin poll form for choices.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -838,13 +850,13 @@ function theme_poll_choices($variables) {
  * are left in with a different name for that purpose.
  *
  * @see poll-results.tpl.php
- * @see poll-results-block.tpl.php
- * @see theme_poll_results()
+ * @see poll-results--block.tpl.php
  */
 function template_preprocess_poll_results(&$variables) {
   $variables['links'] = theme('links__poll_results', array('links' => $variables['raw_links']));
   if (isset($variables['vote']) && $variables['vote'] > -1 && user_access('cancel own vote')) {
-    $variables['cancel_form'] = drupal_render(drupal_get_form('poll_cancel_form', $variables['nid']));
+    $elements = drupal_get_form('poll_cancel_form', $variables['nid']);
+    $variables['cancel_form'] = drupal_render($elements);
   }
   $variables['title'] = check_plain($variables['raw_title']);
 
@@ -859,7 +871,7 @@ function template_preprocess_poll_results(&$variables) {
  * Inputs: $title, $votes, $total_votes, $voted, $block
  *
  * @see poll-bar.tpl.php
- * @see poll-bar-block.tpl.php
+ * @see poll-bar--block.tpl.php
  * @see theme_poll_bar()
  */
 function template_preprocess_poll_bar(&$variables) {
@@ -882,7 +894,8 @@ function poll_cancel_form($form, &$form_state, $nid) {
   // Store the nid so we can get to it in submit functions.
   $form['#nid'] = $nid;
 
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Cancel your vote'),
     '#submit' => array('poll_cancel')
@@ -909,6 +922,8 @@ function poll_cancel($form, &$form_state) {
     ->condition('chid', $node->vote)
     ->execute();
 
+  unset($_SESSION['poll_vote'][$node->nid]);
+
   drupal_set_message(t('Your vote was cancelled.'));
 }
 
diff --git a/modules/poll/poll.test b/modules/poll/poll.test
index f2bd19925e55571002ba6ff4c5c34cafa03e24ef..717a3d82bdb169c6fd97f9e21b217caf4f1dbdbc 100644
--- a/modules/poll/poll.test
+++ b/modules/poll/poll.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: poll.test,v 1.30 2010/03/06 06:39:00 dries Exp $
+// $Id: poll.test,v 1.33 2010/04/22 23:59:04 dries Exp $
 
 /**
  * @file
@@ -341,7 +341,7 @@ class PollJSAddChoice extends DrupalWebTestCase {
 
     // Press 'add choice' button through AJAX, and place the expected HTML result
     // as the tested content.
-    $commands = $this->drupalPostAJAX(NULL, $edit, 'poll_more');
+    $commands = $this->drupalPostAJAX(NULL, $edit, array('op' => t('More choices')));
     $this->content = $commands[1]['data'];
 
     $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0)));
@@ -370,8 +370,13 @@ class PollVoteCheckHostname extends PollTestCase {
     user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array(
       'access content' => TRUE,
       'vote on polls' => TRUE,
+      'cancel own vote' => TRUE,
     ));
 
+    // Enable page cache to verify that the result page is not saved in the
+    // cache when anonymous voting is allowed.
+    variable_set('cache', CACHE_NORMAL);
+
     // Create poll.
     $title = $this->randomName();
     $choices = $this->_generateChoices(3);
@@ -380,7 +385,7 @@ class PollVoteCheckHostname extends PollTestCase {
     $this->drupalLogout();
 
     // Create web users.
-    $this->web_user1 = $this->drupalCreateUser(array('access content', 'vote on polls'));
+    $this->web_user1 = $this->drupalCreateUser(array('access content', 'vote on polls', 'cancel own vote'));
     $this->web_user2 = $this->drupalCreateUser(array('access content', 'vote on polls'));
   }
 
@@ -405,19 +410,32 @@ class PollVoteCheckHostname extends PollTestCase {
     $this->drupalGet('node/' . $this->poll_nid);
     $elements = $this->xpath('//input[@value="Vote"]');
     $this->assertTrue(empty($elements), t("%user is not able to vote again.", array('%user' => $this->web_user1->name)));
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears."));
 
     // Logout User1.
     $this->drupalLogout();
 
+    // Fill the page cache by requesting the poll.
+    $this->drupalGet('node/' . $this->poll_nid);
+    $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', t('Page was cacheable but was not in the cache.'));
+    $this->drupalGet('node/' . $this->poll_nid);
+    $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'HIT', t('Page was cached.'));
+
     // Anonymous user vote on Poll.
-    $this->drupalPost('node/' . $this->poll_nid, $edit, t('Vote'));
+    $this->drupalPost(NULL, $edit, t('Vote'));
     $this->assertText(t('Your vote was recorded.'), t('Anonymous vote was recorded.'));
     $this->assertText(t('Total votes: @votes', array('@votes' => 2)), t('Vote count updated correctly.'));
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears."));
 
     // Check to make sure Anonymous user cannot vote again.
     $this->drupalGet('node/' . $this->poll_nid);
+    $this->assertFalse($this->drupalGetHeader('x-drupal-cache'), t('Page was not cacheable.'));
     $elements = $this->xpath('//input[@value="Vote"]');
     $this->assertTrue(empty($elements), t("Anonymous is not able to vote again."));
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears."));
 
     // Login User2.
     $this->drupalLogin($this->web_user2);
@@ -426,6 +444,8 @@ class PollVoteCheckHostname extends PollTestCase {
     $this->drupalPost('node/' . $this->poll_nid, $edit, t('Vote'));
     $this->assertText(t('Your vote was recorded.'), t('%user vote was recorded.', array('%user' => $this->web_user2->name)));
     $this->assertText(t('Total votes: @votes', array('@votes' => 3)), 'Vote count updated correctly.');
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(empty($elements), t("'Cancel your vote' button does not appear."));
 
     // Logout User2.
     $this->drupalLogout();
@@ -433,20 +453,30 @@ class PollVoteCheckHostname extends PollTestCase {
     // Change host name for anonymous users.
     db_update('poll_vote')
       ->fields(array(
-        'hostname' => '123.456.789.20',
+        'hostname' => '123.456.789.1',
       ))
-      ->condition('hostname', '', '!=')
+      ->condition('hostname', '', '<>')
       ->execute();
 
-    // Check to make sure Anonymous user can vote again after hostname change.
-    $this->drupalPost('node/' . $this->poll_nid, $edit, t('Vote'));
+    // Check to make sure Anonymous user can vote again with a new session after
+    // a hostname change.
+    $this->drupalGet('node/' . $this->poll_nid);
+    $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', t('Page was cacheable but was not in the cache.'));
+    $this->drupalPost(NULL, $edit, t('Vote'));
     $this->assertText(t('Your vote was recorded.'), t('%user vote was recorded.', array('%user' => $this->web_user2->name)));
     $this->assertText(t('Total votes: @votes', array('@votes' => 4)), 'Vote count updated correctly.');
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears."));
 
-    // Check to make sure Anonymous user cannot vote again.
+    // Check to make sure Anonymous user cannot vote again with a new session,
+    // and that the vote from the previous session cannot be cancelledd.
+    $this->curlClose();
     $this->drupalGet('node/' . $this->poll_nid);
+    $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', t('Page was cacheable but was not in the cache.'));
     $elements = $this->xpath('//input[@value="Vote"]');
-    $this->assertTrue(empty($elements), t("Anonymous is not able to vote again."));
+    $this->assertTrue(empty($elements), t('Anonymous is not able to vote again.'));
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(empty($elements), t("'Cancel your vote' button does not appear."));
 
     // Login User1.
     $this->drupalLogin($this->web_user1);
@@ -455,5 +485,96 @@ class PollVoteCheckHostname extends PollTestCase {
     $this->drupalGet('node/' . $this->poll_nid);
     $elements = $this->xpath('//input[@value="Vote"]');
     $this->assertTrue(empty($elements), t("%user is not able to vote again.", array('%user' => $this->web_user1->name)));
+    $elements = $this->xpath('//input[@value="Cancel your vote"]');
+    $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears."));
+  }
+}
+
+/**
+ * Test poll token replacement in strings.
+ */
+class PollTokenReplaceTestCase extends PollTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Poll token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check poll token replacement.',
+      'group' => 'Poll',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('poll');
+  }
+
+  /**
+   * Creates a poll, then tests the tokens generated from it.
+   */
+  function testPollTokenReplacement() {
+    global $language;
+
+    // Craete a poll with three choices.
+    $title = $this->randomName();
+    $choices = $this->_generateChoices(3);
+    $poll_nid = $this->pollCreate($title, $choices, FALSE);
+    $this->drupalLogout();
+
+    // Create four users and have each of them vote.
+    $vote_user1 = $this->drupalCreateUser(array('vote on polls', 'access content'));
+    $this->drupalLogin($vote_user1);
+    $edit = array(
+      'choice' => '1',
+    );
+    $this->drupalPost('node/' . $poll_nid, $edit, t('Vote'));
+    $this->drupalLogout();
+
+    $vote_user2 = $this->drupalCreateUser(array('vote on polls', 'access content'));
+    $this->drupalLogin($vote_user2);
+    $edit = array(
+      'choice' => '1',
+    );
+    $this->drupalPost('node/' . $poll_nid, $edit, t('Vote'));
+    $this->drupalLogout();
+
+    $vote_user3 = $this->drupalCreateUser(array('vote on polls', 'access content'));
+    $this->drupalLogin($vote_user3);
+    $edit = array(
+      'choice' => '2',
+    );
+    $this->drupalPost('node/' . $poll_nid, $edit, t('Vote'));
+    $this->drupalLogout();
+
+    $vote_user4 = $this->drupalCreateUser(array('vote on polls', 'access content'));
+    $this->drupalLogin($vote_user4);
+    $edit = array(
+      'choice' => '3',
+    );
+    $this->drupalPost('node/' . $poll_nid, $edit, t('Vote'));
+    $this->drupalLogout();
+
+    $poll = node_load($poll_nid, NULL, TRUE);
+
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[node:poll-votes]'] = 4;
+    $tests['[node:poll-winner]'] = filter_xss($poll->choice[1]['chtext']);
+    $tests['[node:poll-winner-votes]'] = 2;
+    $tests['[node:poll-winner-percent]'] = 50;
+    $tests['[node:poll-duration]'] = format_interval($poll->runtime, 1, $language->language);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('node' => $poll), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized poll token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[node:poll-winner]'] = $poll->choice[1]['chtext'];
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('node' => $poll), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized poll token %token replaced.', array('%token' => $input)));
+    }
   }
 }
diff --git a/modules/poll/poll.tokens.inc b/modules/poll/poll.tokens.inc
index 094f43a2d437a6867f6d8e9bd995a277f95cca07..28abeb3cfda616536e78d8fc2a2272dacc9774c5 100644
--- a/modules/poll/poll.tokens.inc
+++ b/modules/poll/poll.tokens.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: poll.tokens.inc,v 1.4 2010/01/07 20:55:49 dries Exp $
+// $Id: poll.tokens.inc,v 1.5 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -41,6 +41,14 @@ function poll_token_info() {
  */
 function poll_tokens($type, $tokens, array $data = array(), array $options = array()) {
   $sanitize = !empty($options['sanitize']);
+  if (isset($options['language'])) {
+    $url_options['language'] = $options['language'];
+    $language_code = $options['language']->language;
+  }
+  else {
+    $language_code = NULL;
+  }
+
   $replacements = array();
 
   if ($type == 'node' && !empty($data['node']) && $data['node']->type == 'poll') {
@@ -65,12 +73,18 @@ function poll_tokens($type, $tokens, array $data = array(), array $options = arr
           if (isset($winner)) {
             $replacements[$original] = $sanitize ? filter_xss($winner['chtext']) : $winner['chtext'];
           }
+          else {
+            $replacements[$original] = '';
+          }
           break;
 
         case 'poll-winner-votes':
           if (isset($winner)) {
             $replacements[$original] = $winner['chvotes'];
           }
+          else {
+            $replacements[$original] = '';
+          }
           break;
 
         case 'poll-winner-percent':
@@ -78,6 +92,9 @@ function poll_tokens($type, $tokens, array $data = array(), array $options = arr
             $percent = ($winner['chvotes'] / $total_votes) * 100;
             $replacements[$original] = number_format($percent, 0);
           }
+          else {
+            $replacements[$original] = '';
+          }
           break;
 
         case 'poll-duration':
diff --git a/modules/profile/profile.admin.inc b/modules/profile/profile.admin.inc
index b2d765a4458e7a955991d409079ef6f008fe851a..8cf48a662a6cac6f761957115b8252bfddfac19f 100644
--- a/modules/profile/profile.admin.inc
+++ b/modules/profile/profile.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: profile.admin.inc,v 1.39 2010/03/07 08:05:02 webchick Exp $
+// $Id: profile.admin.inc,v 1.42 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -40,7 +40,8 @@ function profile_admin_overview($form) {
 
   // Display the submit button only when there's more than one field
   if (count($form) > 1) {
-    $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
   }
   else {
     // Disable combo boxes when there isn't a submit button
@@ -91,17 +92,21 @@ function profile_admin_overview_submit($form, &$form_state) {
 }
 
 /**
- * Theme the profile field overview into a drag and drop enabled table.
+ * Returns HTML for the profile field overview form into a drag and drop enabled table.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
- * @ingroup themeable
  * @see profile_admin_overview()
+ * @ingroup themeable
  */
 function theme_profile_admin_overview($variables) {
   $form = $variables['form'];
 
   drupal_add_css(drupal_get_path('module', 'profile') . '/profile.css');
   // Add javascript if there's more than one field.
-  if (isset($form['submit'])) {
+  if (isset($form['actions'])) {
     drupal_add_js(drupal_get_path('module', 'profile') . '/profile.js');
   }
 
@@ -119,11 +124,11 @@ function theme_profile_admin_overview($variables) {
         // class names won't contain invalid characters.
         $categories[$category] = $category_number;
         $category_field['#attributes']['class'] = array('profile-category', 'profile-category-' . $category_number);
-        $rows[] = array(array('data' => $category, 'colspan' => 7, 'class' => array('category')));
+        $rows[] = array(array('data' => check_plain($category), 'colspan' => 7, 'class' => array('category')));
         $rows[] = array('data' => array(array('data' => '<em>' . t('No fields in this category. If this category remains empty when saved, it will be removed.') . '</em>', 'colspan' => 7)), 'class' => array('category-' . $category_number . '-message', 'category-message', 'category-populated'));
 
         // Make it draggable only if there is more than one field
-        if (isset($form['submit'])) {
+        if (isset($form['actions'])) {
           drupal_add_tabledrag('profile-fields', 'order', 'sibling', 'profile-weight', 'profile-weight-' . $category_number);
           drupal_add_tabledrag('profile-fields', 'match', 'sibling', 'profile-category', 'profile-category-' . $category_number);
         }
@@ -139,7 +144,7 @@ function theme_profile_admin_overview($variables) {
       $row[] = drupal_render($field['title']);
       $row[] = drupal_render($field['name']);
       $row[] = drupal_render($field['type']);
-      if (isset($form['submit'])) {
+      if (isset($form['actions'])) {
         $row[] = drupal_render($field['category']);
         $row[] = drupal_render($field['weight']);
       }
@@ -150,7 +155,7 @@ function theme_profile_admin_overview($variables) {
   }
 
   $header = array(t('Title'), t('Name'), t('Type'));
-  if (isset($form['submit'])) {
+  if (isset($form['actions'])) {
     $header[] = t('Category');
     $header[] = t('Weight');
   }
@@ -282,7 +287,7 @@ Unless you know what you are doing, it is highly recommended that you prefix the
     '#default_value' => $edit['register'],
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit',
     '#value' => t('Save field'),
   );
diff --git a/modules/profile/profile.info b/modules/profile/profile.info
index 3c241aa3b8a2755bcfad538925ec5ebd642583db..04be1fa375df1af604d14afd219ab32fbc29a966 100644
--- a/modules/profile/profile.info
+++ b/modules/profile/profile.info
@@ -11,8 +11,8 @@ files[] = profile.install
 files[] = profile.test
 configure = admin/config/people/profile
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/profile/profile.module b/modules/profile/profile.module
index 542799a50586433c823e95becdad1256a779b8fd..c8acf9faccbbc5317c541f8bfbdf83fa1c313fbb 100644
--- a/modules/profile/profile.module
+++ b/modules/profile/profile.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: profile.module,v 1.288 2010/03/12 15:56:29 dries Exp $
+// $Id: profile.module,v 1.290 2010/04/23 04:32:16 webchick Exp $
 
 /**
  * @file
@@ -273,8 +273,6 @@ function profile_save_profile(&$edit, $account, $category, $register = FALSE) {
       ))
       ->fields(array('value' => $edit[$field->name]))
       ->execute();
-    // Mark field as handled (prevents saving to user->data).
-    $edit[$field->name] = NULL;
   }
 }
 
@@ -362,7 +360,7 @@ function profile_user_view($account) {
 }
 
 function _profile_form_explanation($field) {
-  $output = $field->explanation;
+  $output = filter_xss_admin($field->explanation);
 
   if ($field->type == 'list') {
     $output .= ' ' . t('Put each item on a separate line or separate them by commas. No HTML allowed.');
diff --git a/modules/rdf/rdf.info b/modules/rdf/rdf.info
index ea10a8fdd49a1a591c6eaba2f9775e11493b8753..867eda3901c596c4fdb144346e0e04ce2da12a93 100644
--- a/modules/rdf/rdf.info
+++ b/modules/rdf/rdf.info
@@ -8,8 +8,8 @@ files[] = rdf.install
 files[] = rdf.module
 files[] = rdf.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/rdf/rdf.module b/modules/rdf/rdf.module
index 91cb94fc119faeb951abf70a2db40d280028358d..15061512a3afe14968768b5e89eaeb71404e5cb2 100644
--- a/modules/rdf/rdf.module
+++ b/modules/rdf/rdf.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: rdf.module,v 1.31 2010/03/10 14:23:32 dries Exp $
+// $Id: rdf.module,v 1.39 2010/04/22 21:41:09 webchick Exp $
 
 /**
  * @file
@@ -77,7 +77,6 @@ define('RDF_DEFAULT_BUNDLE', '');
  */
 function rdf_rdf_namespaces() {
   return array(
-    'admin'    => 'http://webns.net/mvcb/',
     'content'  => 'http://purl.org/rss/1.0/modules/content/',
     'dc'       => 'http://purl.org/dc/terms/',
     'foaf'     => 'http://xmlns.com/foaf/0.1/',
@@ -85,15 +84,39 @@ function rdf_rdf_namespaces() {
     'rdf'      => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
     'rdfs'     => 'http://www.w3.org/2000/01/rdf-schema#',
     'rss'      => 'http://purl.org/rss/1.0/',
-    'tags'     => 'http://www.holygoat.co.uk/owl/redwood/0.1/tags/',
     'sioc'     => 'http://rdfs.org/sioc/ns#',
     'sioct'    => 'http://rdfs.org/sioc/types#',
-    'ctag'     => 'http://commontag.org/ns#',
     'skos'     => 'http://www.w3.org/2004/02/skos/core#',
     'xsd'      => 'http://www.w3.org/2001/XMLSchema#',
   );
 }
 
+/**
+ * Returns an array of RDF namespaces defined via hook_rdf_namespaces().
+ */
+function rdf_get_namespaces() {
+  $rdf_namespaces = module_invoke_all('rdf_namespaces');
+  // module_invoke_all() uses array_merge_recursive() which might return nested
+  // arrays if several modules redefine the same prefix multiple times.
+  // We need to ensure the array of namespaces is flat and only contains
+  // strings as URIs.
+  foreach ($rdf_namespaces as $prefix => $uri) {
+    if (is_array($uri)) {
+      if (count(array_unique($uri)) == 1) {
+        // All namespaces declared for this prefix are the same, merge them all
+        // into a single namespace.
+        $rdf_namespaces[$prefix] = $uri[0];
+      }
+      else {
+        // There are conflicting namespaces for this prefix, do not include it
+        // in order to avoid asserting any inaccurate RDF statement.
+        unset($rdf_namespaces[$prefix]);
+      }
+    }
+  }
+  return $rdf_namespaces;
+}
+
 /**
  * Returns the mapping for attributes of a given type/bundle pair.
  *
@@ -409,8 +432,8 @@ function rdf_theme() {
 function rdf_process(&$variables, $hook) {
   // Handles attributes needed for content not covered by title, content,
   // and field items. Does this by adjusting the variable sent to the template
-  // so that the template doesn't have to worry about it.
-  // @see theme_rdf_template_variable_wrapper()
+  // so that the template doesn't have to worry about it. See 
+  // theme_rdf_template_variable_wrapper().
   if (!empty($variables['rdf_template_variable_attributes_array'])) {
     foreach ($variables['rdf_template_variable_attributes_array'] as $variable_name => $attributes) {
       $context = array(
@@ -439,7 +462,7 @@ function rdf_process(&$variables, $hook) {
 function rdf_preprocess_node(&$variables) {
   // Adds RDFa markup to the node container. The about attribute specifies the
   // URI of the resource described within the HTML element, while the typeof
-  // attribute indicates its RDF type (foaf:Document, or sioc:User, etc.).
+  // attribute indicates its RDF type (foaf:Document, or sioc:Person, etc.).
   $variables['attributes_array']['about'] = empty($variables['node_url']) ? NULL: $variables['node_url'];
   $variables['attributes_array']['typeof'] = empty($variables['node']->rdf_mapping['rdftype']) ? NULL : $variables['node']->rdf_mapping['rdftype'];
 
@@ -456,7 +479,7 @@ function rdf_preprocess_node(&$variables) {
     $element = array(
       '#tag' => 'meta',
       '#attributes' => array(
-        'content' => $variables['node_title'],
+        'content' => $variables['title'],
         'about' => $variables['node_url'],
       ),
     );
@@ -483,6 +506,12 @@ function rdf_preprocess_node(&$variables) {
       $comment_count_attributes['property'] = $variables['node']->rdf_mapping['comment_count']['predicates'];
       $comment_count_attributes['content'] = $variables['node']->comment_count;
       $comment_count_attributes['datatype'] = $variables['node']->rdf_mapping['comment_count']['datatype'];
+      // According to RDFa parsing rule number 4, a new subject URI is created
+      // from the href attribute if no rel/rev attribute is present. To get
+      // the original node URL from the about attribute of the parent container
+      // we set an empty rel attribute which triggers rule number 5. See
+      // http://www.w3.org/TR/rdfa-syntax/#sec_5.5.
+      $comment_count_attributes['rel'] = '';
       $variables['content']['links']['comment']['#links']['comment_comments']['attributes'] += $comment_count_attributes;
     }
     // In full node view, the number of comments is not displayed by
@@ -507,7 +536,7 @@ function rdf_preprocess_node(&$variables) {
  */
 function rdf_preprocess_field(&$variables) {
   $element = $variables['element'];
-  $mapping = rdf_mapping_load($element['#object_type'], $element['#bundle']);
+  $mapping = rdf_mapping_load($element['#entity_type'], $element['#bundle']);
   $field_name = $element['#field_name'];
 
   if (!empty($mapping) && !empty($mapping[$field_name])) {
@@ -534,29 +563,31 @@ function rdf_preprocess_field(&$variables) {
  * Implements MODULE_preprocess_HOOK().
  */
 function rdf_preprocess_user_profile(&$variables) {
-  // Adds RDFa markup to the user profile page. Fields displayed in this page
-  // will automatically describe the user.
   $account = $variables['elements']['#account'];
+  $uri = entity_uri('user', $account);
+
+  // Adds RDFa markup to the user profile page. Fields displayed in this page
+  // will automatically describe the user.  
   if (!empty($account->rdf_mapping['rdftype'])) {
     $variables['attributes_array']['typeof'] = $account->rdf_mapping['rdftype'];
-    $variables['attributes_array']['about'] = url('user/' . $account->uid);
+    $variables['attributes_array']['about'] = url($uri['path'], $uri['options']);
   }
-  // Adds the relationship between the sioc:User and the foaf:Person who holds
-  // the account.
+  // Adds the relationship between the sioc:UserAccount and the foaf:Person who
+  // holds the account.
   $account_holder_meta = array(
     '#tag' => 'meta',
     '#attributes' => array(
-      'about' => url('user/' . $account->uid, array('fragment' => 'me')),
+      'about' => url($uri['path'], array_merge($uri['options'], array('fragment' => 'me'))),
       'typeof' => array('foaf:Person'),
       'rel' => array('foaf:account'),
-      'resource' => url('user/' . $account->uid),
+      'resource' => url($uri['path'], $uri['options']),
     ),
   );
   // Adds the markup for username.
   $username_meta = array(
     '#tag' => 'meta',
     '#attributes' => array(
-      'about' => url('user/' . $account->uid),
+      'about' => url($uri['path'], $uri['options']),
       'property' => $account->rdf_mapping['name']['predicates'],
       'content' => $account->name,
     )
@@ -629,7 +660,8 @@ function rdf_preprocess_comment(&$variables) {
     // Adds RDFa markup to the comment container. The about attribute specifies
     // the URI of the resource described within the HTML element, while the
     // typeof attribute indicates its RDF type (e.g. sioc:Post, etc.).
-    $variables['attributes_array']['about'] = url('comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid));
+    $uri = entity_uri('comment', $comment);
+    $variables['attributes_array']['about'] = url($uri['path'], $uri['options']);
     $variables['attributes_array']['typeof'] = $comment->rdf_mapping['rdftype'];
   }
 
@@ -715,7 +747,7 @@ function rdf_preprocess_image(&$variables) {
 }
 
 /**
- * Wraps a template variable in an HTML element with the desired attributes.
+ * Returns HTML for a template variable wrapped in an HTML element with the desired attributes.
  *
  * This is called by rdf_process() shortly before the theme system renders
  * a template file. It is called once for each template variable for which
@@ -726,6 +758,14 @@ function rdf_preprocess_image(&$variables) {
  * that need containing attributes are routed through this function, allowing
  * the template file to receive properly wrapped variables.
  *
+ * Tip for themers: if you're already outputting a wrapper element around a
+ * particular template variable in your template file and if you don't want
+ * an extra wrapper element, you can override this function to not wrap that
+ * variable and instead print the following inside your template file:
+ * @code
+ *   drupal_attributes($rdf_template_variable_attributes_array[$variable_name])
+ * @endcode
+ *
  * @param $variables
  *   An associative array containing:
  *   - content: A string of content to be wrapped with attributes.
@@ -754,20 +794,7 @@ function rdf_preprocess_image(&$variables) {
  *     hook_preprocess_rdf_template_variable_wrapper() and set 'inline'
  *     accordingly.
  *
- * @return
- *   A string containing the wrapped content. The template receives the for its
- *   variable instead of the original content.
- *
- * Tip for themers: if you're already outputting a wrapper element around a
- * particular template variable in your template file and if you don't want
- * an extra wrapper element, you can override this function to not wrap that
- * variable and instead print the following inside your template file:
- * @code
- *   drupal_attributes($rdf_template_variable_attributes_array[$variable_name])
- * @endcode
- *
  * @see rdf_process()
- *
  * @ingroup themeable
  * @ingroup rdf
  */
@@ -781,7 +808,7 @@ function theme_rdf_template_variable_wrapper($variables) {
 }
 
 /**
- * Outputs a series of empty spans for exporting RDF metadata in RDFa.
+ * Returns HTML for a series of empty spans for exporting RDF metadata in RDFa.
  *
  * Sometimes it is useful to export data which is not semantically present in
  * the HTML output. For example, a hierarchy of comments is visible for a human
@@ -795,11 +822,7 @@ function theme_rdf_template_variable_wrapper($variables) {
  *     corresponds to its own set of attributes, and therefore, needs its own
  *     element.
  *
- * @return
- *   A string of HTML containing markup that can be understood by RDFa parsers.
- *
  * @see rdf_process()
- *
  * @ingroup themeable
  * @ingroup rdf
  */
diff --git a/modules/rdf/rdf.test b/modules/rdf/rdf.test
index 56ed6579e22be8de3571d16890b499492ff379b8..1e1401dc4143c550945cfae6eddd8941c44b48f0 100644
--- a/modules/rdf/rdf.test
+++ b/modules/rdf/rdf.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: rdf.test,v 1.16 2010/03/10 14:23:32 dries Exp $
+// $Id: rdf.test,v 1.22 2010/04/22 21:41:09 webchick Exp $
 
 /**
  * @file
@@ -39,7 +39,10 @@ class RdfMappingHookTestCase extends DrupalWebTestCase {
   }
 }
 
-class RdfMarkupTestCase extends DrupalWebTestCase {
+/**
+ * Test RDFa markup generation.
+ */
+class RdfRdfaMarkupTestCase extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
       'name' => 'RDFa markup',
@@ -55,7 +58,7 @@ class RdfMarkupTestCase extends DrupalWebTestCase {
   /**
    * Test rdf_rdfa_attributes().
    */
-  function testDrupalRdfaAtributes() {
+  function testDrupalRdfaAttributes() {
     // Same value as the one in the HTML tag (no callback function).
     $expected_attributes = array(
       'property' => array('dc:title'),
@@ -165,9 +168,15 @@ class RdfMarkupTestCase extends DrupalWebTestCase {
 
     // We only check to make sure that the resource attribute contains '.txt'
     // instead of the full file name because the filename is altered on upload.
-    $file_rel = $this->xpath("//div[contains(@about, 'node/$nid')]//div[contains(@rel, 'rdfs:seeAlso') and contains(@resource, '.txt')]");
+    $file_rel = $this->xpath('//div[contains(@about, :node-uri)]//div[contains(@rel, "rdfs:seeAlso") and contains(@resource, ".txt")]', array(
+      ':node-uri' => 'node/' . $nid,
+    ));
     $this->assertTrue(!empty($file_rel), t('Attribute \'rel\' set on file field. Attribute \'resource\' is also set.'));
-    $image_rel = $this->xpath("//div[contains(@about, 'node/$nid')]//div[contains(@rel, 'rdfs:seeAlso') and contains(@resource, '$image_filename')]//img[contains(@typeof, 'foaf:Image')]");
+    $image_rel = $this->xpath('//div[contains(@about, :node-uri)]//div[contains(@rel, "rdfs:seeAlso") and contains(@resource, :image)]//img[contains(@typeof, "foaf:Image")]', array(
+      ':node-uri' => 'node/' . $nid,
+      ':image' => $image_filename,
+    ));
+
     $this->assertTrue(!empty($image_rel), t('Attribute \'rel\' set on image field. Attribute \'resource\' is also set.'));
   }
 }
@@ -240,7 +249,7 @@ class RdfCrudTestCase extends DrupalWebTestCase {
   }
 }
 
-class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
+class RdfMappingDefinitionTestCase extends TaxonomyWebTestCase {
   public static function getInfo() {
     return array(
       'name' => 'RDF mapping definition functionality',
@@ -259,13 +268,16 @@ class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
    */
   function testAttributesInMarkup1() {
     $node = $this->drupalCreateNode(array('type' => 'blog'));
+    $isoDate = date('c', $node->changed);
+    $url = url('node/' . $node->nid);
     $this->drupalGet('node/' . $node->nid);
 
-    $this->assertRaw('typeof="sioct:Weblog"');
     // Ensure the default bundle mapping for node is used. These attributes come
     // from the node default bundle definition.
-    $this->assertRaw('property="dc:title"');
-    $this->assertRaw('property="dc:date dc:created"');
+    $blog_title = $this->xpath("//meta[@property='dc:title' and @content='$node->title']");
+    $blog_meta = $this->xpath("//div[(@about='$url') and (@typeof='sioct:Weblog')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$isoDate']");
+    $this->assertTrue(!empty($blog_title), t('Property dc:title is present in meta tag.'));
+    $this->assertTrue(!empty($blog_meta), t('RDF type is present on post. Properties dc:date and dc:created are present on post date.'));
   }
 
   /**
@@ -275,13 +287,15 @@ class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
   function testAttributesInMarkup2() {
     $type = $this->drupalCreateContentType(array('type' => 'test_bundle_hook_install'));
     $node = $this->drupalCreateNode(array('type' => 'test_bundle_hook_install'));
+    $isoDate = date('c', $node->changed);
+    $url = url('node/' . $node->nid);
     $this->drupalGet('node/' . $node->nid);
 
-    $this->assertRaw('typeof="foo:mapping_install1 bar:mapping_install2"');
-    // Ensure the default bundle mapping for node is used. These attributes come
-    // from the node default bundle definition.
-    $this->assertRaw('property="dc:title"');
-    $this->assertRaw('property="dc:date dc:created"');
+    // Ensure the mapping defined in rdf_module.test is used.
+    $test_bundle_title = $this->xpath("//meta[@property='dc:title' and @content='$node->title']");
+    $test_bundle_meta = $this->xpath("//div[(@about='$url') and contains(@typeof, 'foo:mapping_install1') and contains(@typeof, 'bar:mapping_install2')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$isoDate']");
+    $this->assertTrue(!empty($test_bundle_title), t('Property dc:title is present in meta tag.'));
+    $this->assertTrue(!empty($test_bundle_meta), t('RDF type is present on post. Properties dc:date and dc:created are present on post date.'));
   }
 
   /**
@@ -291,13 +305,16 @@ class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
   function testAttributesInMarkup3() {
     $type = $this->drupalCreateContentType();
     $node = $this->drupalCreateNode(array('type' => $type->type));
+    $isoDate = date('c', $node->changed);
+    $url = url('node/' . $node->nid);
     $this->drupalGet('node/' . $node->nid);
 
-    $this->assertRaw('typeof="sioc:Item foaf:Document"');
     // Ensure the default bundle mapping for node is used. These attributes come
     // from the node default bundle definition.
-    $this->assertRaw('property="dc:title"');
-    $this->assertRaw('property="dc:date dc:created"');
+    $random_bundle_title = $this->xpath("//meta[@property='dc:title' and @content='$node->title']");
+    $random_bundle_meta = $this->xpath("//div[(@about='$url') and contains(@typeof, 'sioc:Item') and contains(@typeof, 'foaf:Document')]//span[contains(@property, 'dc:date') and contains(@property, 'dc:created') and @datatype='xsd:dateTime' and @content='$isoDate']");
+    $this->assertTrue(!empty($random_bundle_title), t('Property dc:title is present in meta tag.'));
+    $this->assertTrue(!empty($random_bundle_meta), t('RDF type is present on post. Properties dc:date and dc:created are present on post date.'));
   }
 
   /**
@@ -315,11 +332,22 @@ class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
     // page. These attributes come from the user default bundle definition.
     $account_uri = url('user/' . $user2->uid);
     $person_uri = url('user/' . $user2->uid, array('fragment' => 'me'));
-    $user2_profile_about = $this->xpath("//div[@class='profile' and @typeof='sioc:User' and @about='$account_uri']");
+
+    $user2_profile_about = $this->xpath('//div[@class="profile" and @typeof="sioc:UserAccount" and @about=:account-uri]', array(
+      ':account-uri' => $account_uri,
+    ));
     $this->assertTrue(!empty($user2_profile_about), t('RDFa markup found on user profile page'));
-    $user_account_holder = $this->xpath("//meta[contains(@typeof, 'foaf:Person') and @about='$person_uri' and @resource='$account_uri' and contains(@rel, 'foaf:account')]");
-    $this->assertTrue(!empty($user_account_holder), t('URI created for account holder and username set on sioc:User.'));
-    $user_username = $this->xpath("//meta[@about='$account_uri' and contains(@property, 'foaf:name') and @content='$username']");
+
+    $user_account_holder = $this->xpath('//meta[contains(@typeof, "foaf:Person") and @about=:person-uri and @resource=:account-uri and contains(@rel, "foaf:account")]', array(
+      ':person-uri' => $person_uri,
+      ':account-uri' => $account_uri,
+    ));
+    $this->assertTrue(!empty($user_account_holder), t('URI created for account holder and username set on sioc:UserAccount.'));
+
+    $user_username = $this->xpath('//meta[@about=:account-uri and contains(@property, "foaf:name") and @content=:username]', array(
+      ':account-uri' => $account_uri,
+      ':username' => $username,
+    ));
     $this->assertTrue(!empty($user_username), t('foaf:name set on username.'));
 
     // User 2 creates node.
@@ -329,7 +357,9 @@ class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
     $this->drupalGet('node/' . $node->nid);
     // Ensures the default bundle mapping for user is used on the Authored By
     // information on the node.
-    $author_about = $this->xpath("//a[@typeof='sioc:User' and @about='$account_uri' and @property='foaf:name' and contains(@xml:lang, '')]");
+    $author_about = $this->xpath('//a[@typeof="sioc:UserAccount" and @about=:account-uri and @property="foaf:name" and contains(@xml:lang, "")]', array(
+      ':account-uri' => $account_uri,
+    ));
     $this->assertTrue(!empty($author_about), t('RDFa markup found on author information on post. xml:lang on username is set to empty string.'));
   }
 
@@ -337,18 +367,55 @@ class RdfMappingDefinitionTestCase extends DrupalWebTestCase {
    * Creates a random term and ensures the right RDFa markup is used.
    */
   function testTaxonomyTermRdfaAttributes() {
-    $vocabulary = TaxonomyWebTestCase::createVocabulary();
-    $term = TaxonomyWebTestCase::createTerm($vocabulary);
+    $vocabulary = $this->createVocabulary();
+    $term = $this->createTerm($vocabulary);
 
     // Views the term and checks that the RDFa markup is correct.
     $this->drupalGet('taxonomy/term/' . $term->tid);
     $term_url = url('taxonomy/term/' . $term->tid);
     $term_name = $term->name;
-    $term_rdfa_meta = $this->xpath("//meta[@typeof='skos:Concept' and @about='$term_url' and contains(@property, 'rdfs:label') and contains(@property, 'skos:prefLabel') and @content='$term_name']");
+    $term_rdfa_meta = $this->xpath('//meta[@typeof="skos:Concept" and @about=:term-url and contains(@property, "rdfs:label") and contains(@property, "skos:prefLabel") and @content=:term-name]', array(
+      ':term-url' => $term_url,
+      ':term-name' => $term_name,
+    ));
     $this->assertTrue(!empty($term_rdfa_meta), t('RDFa markup found on term page.'));
   }
 }
 
+class RdfCommentAttributesTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'RDF comment mapping',
+      'description' => 'Tests the RDFa markup of comments.',
+      'group' => 'RDF',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp('rdf', 'rdf_test', 'comment');
+    // Enable anonymous posting of content.
+    user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array(
+      'create article content' => TRUE,
+      'access comments' => TRUE,
+      'post comments' => TRUE,
+      'post comments without approval' => TRUE,
+    ));
+  }
+
+  public function testAttributesInTeaser() {
+    $node = $this->drupalCreateNode(array('type' => 'article', 'uid' => 1, 'promote' => 1));
+    $comment = array(
+      'subject' => $this->randomName(),
+      'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(),
+    );
+    $this->drupalPost('comment/reply/' . $node->nid, $comment, t('Save'));
+    $this->drupalGet('');
+    $comment_count_link = $this->xpath('//div[@about=:url]//a[contains(@property, "sioc:num_replies") and @rel=""]', array(':url' => url("node/$node->nid")));
+    $this->assertTrue(!empty($comment_count_link), t('Empty rel attribute found in comment count link.'));
+  }
+
+}
 
 class RdfTrackerAttributesTestCase extends DrupalWebTestCase {
   public static function getInfo() {
@@ -406,19 +473,19 @@ class RdfTrackerAttributesTestCase extends DrupalWebTestCase {
     // Tests whether the about property is applied. This is implicit in the
     // success of the following tests, but making it explicit will make
     // debugging easier in case of failure.
-    $tracker_about = $this->xpath("//tr[@about='$url']");
+    $tracker_about = $this->xpath('//tr[@about=:url]', array(':url' => $url));
     $this->assertTrue(!empty($tracker_about), t('About attribute found on table row for @user content.', array('@user'=> $user)));
 
     // Tests whether the title has the correct property attribute.
-    $tracker_title = $this->xpath("//tr[@about='$url']/td[@property='dc:title' and @datatype='']");
+    $tracker_title = $this->xpath('//tr[@about=:url]/td[@property="dc:title" and @datatype=""]', array(':url' => $url));
     $this->assertTrue(!empty($tracker_title), t('Title property attribute found on @user content.', array('@user'=> $user)));
 
     // Tests whether the relationship between the content and user has been set.
-    $tracker_user = $this->xpath("//tr[@about='$url']//td[contains(@rel, 'sioc:has_creator')]//*[contains(@typeof, 'sioc:User') and contains(@property, 'foaf:name')]");
+    $tracker_user = $this->xpath('//tr[@about=:url]//td[contains(@rel, "sioc:has_creator")]//*[contains(@typeof, "sioc:UserAccount") and contains(@property, "foaf:name")]', array(':url' => $url));
     $this->assertTrue(!empty($tracker_user), t('Typeof and name property attributes found on @user.', array('@user'=> $user)));
     // There should be an about attribute on logged in users and no about
     // attribute for anonymous users.
-    $tracker_user = $this->xpath("//tr[@about='$url']//td[@rel='sioc:has_creator']/*[@about]");
+    $tracker_user = $this->xpath('//tr[@about=:url]//td[@rel="sioc:has_creator"]/*[@about]', array(':url' => $url));
     if ($node->uid == 0) {
       $this->assertTrue(empty($tracker_user), t('No about attribute is present on @user.', array('@user'=> $user)));
     }
@@ -427,23 +494,27 @@ class RdfTrackerAttributesTestCase extends DrupalWebTestCase {
     }
 
     // Tests whether the property has been set for number of comments.
-    $tracker_replies = $this->xpath("//tr[@about='$url']//td[contains(@property, 'sioc:num_replies') and contains(@content, '0') and @datatype='xsd:integer']");
+    $tracker_replies = $this->xpath('//tr[@about=:url]//td[contains(@property, "sioc:num_replies") and contains(@content, "0") and @datatype="xsd:integer"]', array(':url' => $url));
     $this->assertTrue($tracker_replies, t('Num replies property and content attributes found on @user content.', array('@user'=> $user)));
 
     // Tests that the appropriate RDFa markup to annotate the latest activity
     // date has been added to the tracker output before comments have been
     // posted, meaning the latest activity reflects changes to the node itself.
     $isoDate = date('c', $node->changed);
-    $tracker_activity = $this->xpath("//tr[@about='$url']//td[contains(@property, 'dc:modified') and contains(@property, 'sioc:last_activity_date') and contains(@datatype, 'xsd:dateTime') and @content='$isoDate']");
+    $tracker_activity = $this->xpath('//tr[@about=:url]//td[contains(@property, "dc:modified") and contains(@property, "sioc:last_activity_date") and contains(@datatype, "xsd:dateTime") and @content=:date]', array(':url' => $url, ':date' => $isoDate));
     $this->assertTrue(!empty($tracker_activity), t('Latest activity date and changed properties found when there are no comments on @user content. Latest activity date content is correct.', array('@user'=> $user)));
 
     // Tests that the appropriate RDFa markup to annotate the latest activity
     // date has been added to the tracker output after a comment is posted.
-    CommentHelperCase::postComment($node, $this->randomName(), $this->randomName());
+    $comment = array(
+      'subject' => $this->randomName(),
+      'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(),
+    );
+    $this->drupalPost('comment/reply/' . $node->nid, $comment, t('Save'));
     $this->drupalGet('tracker');
 
     // Tests whether the property has been set for number of comments.
-    $tracker_replies = $this->xpath("//tr[@about='$url']//td[contains(@property, 'sioc:num_replies') and contains(@content, '1') and @datatype='xsd:integer']");
+    $tracker_replies = $this->xpath('//tr[@about=:url]//td[contains(@property, "sioc:num_replies") and contains(@content, "1") and @datatype="xsd:integer"]', array(':url' => $url));
     $this->assertTrue($tracker_replies, t('Num replies property and content attributes found on @user content.', array('@user'=> $user)));
 
     // Need to query database directly to obtain last_activity_date because
@@ -453,7 +524,37 @@ class RdfTrackerAttributesTestCase extends DrupalWebTestCase {
       $expected_last_activity_date = $node->changed;
     }
     $isoDate = date('c', $expected_last_activity_date);
-    $tracker_activity = $this->xpath("//tr[@about='$url']//td[@property='sioc:last_activity_date' and @datatype='xsd:dateTime' and @content='$isoDate']");
+    $tracker_activity = $this->xpath('//tr[@about=:url]//td[@property="sioc:last_activity_date" and @datatype="xsd:dateTime" and @content=:date]', array(':url' => $url, ':date' => $isoDate));
     $this->assertTrue(!empty($tracker_activity), t('Latest activity date found when there are comments on @user content. Latest activity date content is correct.', array('@user'=> $user)));
   }
 }
+
+/**
+ * Tests for RDF namespaces declaration with hook_rdf_namespaces().
+ */
+class RdfGetRdfNamespacesTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'RDF namespaces',
+      'description' => 'Test hook_rdf_namespaces() and ensure only "safe" namespaces are returned.',
+      'group' => 'RDF',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('rdf', 'rdf_test');
+  }
+
+  /**
+   * Test getting RDF namesapces.
+   */
+  function testGetRdfNamespaces() {
+    // Get all RDF namespaces.
+    $ns = rdf_get_namespaces();
+
+    $this->assertEqual($ns['owl'], 'http://www.w3.org/2002/07/owl#', t('A prefix declared once is included.'));
+    $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', t('The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.'));
+    $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', t('Two prefixes can be assigned the same namespace.'));
+    $this->assertTrue(!isset($ns['dc']), t('A prefix with conflicting namespaces is discarded.'));
+  }
+}
diff --git a/modules/rdf/tests/rdf_test.info b/modules/rdf/tests/rdf_test.info
index 91237a6a951401327afd1620efaab8af30c6d659..9f59ec3b63aced9811891d6127658ed7dbf19e19 100644
--- a/modules/rdf/tests/rdf_test.info
+++ b/modules/rdf/tests/rdf_test.info
@@ -8,8 +8,8 @@ files[] = rdf_test.install
 files[] = rdf_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/rdf/tests/rdf_test.module b/modules/rdf/tests/rdf_test.module
index 823c1651021ea69788f65faba4c0a683700a5497..8d005c1a3f73aa2de8885b3ce1bdfd57a677d9a7 100644
--- a/modules/rdf/tests/rdf_test.module
+++ b/modules/rdf/tests/rdf_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: rdf_test.module,v 1.3 2010/01/14 06:31:45 webchick Exp $
+// $Id: rdf_test.module,v 1.4 2010/04/22 21:41:09 webchick Exp $
 
 /**
  * @file
@@ -54,3 +54,14 @@ function rdf_test_rdf_mapping() {
     ),
   );
 }
+
+/**
+ * Implements hook_rdf_namespaces().
+ */
+function rdf_test_rdf_namespaces() {
+  return array(
+    'dc'       => 'http://purl.org/conflicting/namespace',
+    'foaf'     => 'http://xmlns.com/foaf/0.1/',
+    'foaf1'    => 'http://xmlns.com/foaf/0.1/',
+  );
+}
diff --git a/modules/search/search.extender.inc b/modules/search/search.extender.inc
index a38eec1f88984fbfc8efd65787de5d428c5d28ce..5c994aa1d5bfb90d18dfc9136951c4ce98042a1e 100644
--- a/modules/search/search.extender.inc
+++ b/modules/search/search.extender.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: search.extender.inc,v 1.3 2010/01/13 23:19:54 dries Exp $
+// $Id: search.extender.inc,v 1.4 2010/04/16 13:53:43 dries Exp $
 
 /**
  * @file
@@ -425,7 +425,8 @@ class SearchQuery extends SelectQueryExtender {
       $i = 0;
       $sum = array_sum($this->multiply);
       foreach ($this->multiply as $total) {
-        $this->scoresArguments['total_' . $i] = $sum;
+        $this->scoresArguments[':total_' . $i] = $sum;
+        $i++;
       }
     }
 
diff --git a/modules/search/search.info b/modules/search/search.info
index c475662f74f229af386a02806c6bb4a1b35b5da5..097adfc11f38c1971242d4370307343f33b92f8e 100644
--- a/modules/search/search.info
+++ b/modules/search/search.info
@@ -12,8 +12,8 @@ files[] = search.test
 files[] = search.extender.inc
 configure = admin/config/search/settings
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/search/search.module b/modules/search/search.module
index 23b9b3c4698b5c9855eadda2eab64cad7d108544..3a511976a2cc1407b434027e31abaef1e507b452 100644
--- a/modules/search/search.module
+++ b/modules/search/search.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: search.module,v 1.340 2010/03/12 02:29:23 dries Exp $
+// $Id: search.module,v 1.345 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -224,7 +224,7 @@ function search_menu() {
     'page arguments' => array('search_admin_settings'),
     'access arguments' => array('administer search'),
     'type' => MENU_NORMAL_ITEM,
-    'weight' => -5,
+    'weight' => -10,
     'file' => 'search.admin.inc',
   );
   $items['admin/config/search/settings/reindex'] = array(
@@ -704,8 +704,9 @@ function search_index($sid, $type, $text) {
       // Unset the link to mark it as processed.
       unset($links[$nid]);
     }
-    else {
-      // Insert the existing link and mark the node for reindexing.
+    elseif ($sid != $nid || $type != 'node') {
+      // Insert the existing link and mark the node for reindexing, but don't
+      // reindex if this is a link in a node pointing to itself.
       db_insert('search_node_links')
         ->fields(array(
           'caption' => $caption,
@@ -867,7 +868,7 @@ function search_get_keys() {
  *   HTML indexing mechanism for searching full text efficiently.
  *
  * If your module needs to provide a more complicated search form, then you need
- * to implement it yourself without hook_search(). In that case, you should
+ * to implement it yourself without hook_search_info(). In that case, you should
  * define it as a local task (tab) under the /search page (e.g. /search/mymodule)
  * so that users can easily find it.
  */
@@ -934,7 +935,8 @@ function search_box($form, &$form_state, $form_id) {
     '#default_value' => '',
     '#attributes' => array('title' => t('Enter the terms you wish to search for.')),
   );
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
   $form['#submit'][] = 'search_box_form_submit';
 
   return $form;
@@ -1076,7 +1078,7 @@ function search_excerpt($keys, $text) {
 
   // If we didn't find anything, return the beginning.
   if (count($ranges) == 0) {
-    return truncate_utf8($text, 256) . ' ...';
+    return truncate_utf8($text, 256, TRUE, TRUE);
   }
 
   // Sort the text ranges by starting position.
diff --git a/modules/search/search.pages.inc b/modules/search/search.pages.inc
index b7f6ba250c1fd3db55784adf8205d171fab3b41f..6e56723634139e5c9956e5487dddf73b1ced3e9c 100644
--- a/modules/search/search.pages.inc
+++ b/modules/search/search.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: search.pages.inc,v 1.17 2010/03/12 02:29:23 dries Exp $
+// $Id: search.pages.inc,v 1.18 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -47,15 +47,14 @@ function search_view($type = 'node') {
 }
 
 /**
- * Theme the listing of search results
+ * Returns HTML for a listing of search results.
  *
  * @param $variables
  *   An associative array containing:
  *   - title: The subject of the listing.
  *   - content: The content of the listing.
  *
- * @return
- *   A string containing the listing output.
+ * @ingroup themeable
  */
 function theme_search_results_listing($variables) {
   $output = '<h2 class="title">' . $variables['title'] . '</h2><div>' . $variables['content'] . '</div>';
diff --git a/modules/search/search.test b/modules/search/search.test
index 6e7f033bbb0fecdcaf1c75c1f0329c6a2e467509..c2402360e9a4fac2ab1b2aa6d70108aaef1b6e36 100644
--- a/modules/search/search.test
+++ b/modules/search/search.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: search.test,v 1.57 2010/03/07 23:14:20 webchick Exp $
+// $Id: search.test,v 1.60 2010/04/26 14:26:46 dries Exp $
 
 // The search index can contain different types of content. Typically the type is 'node'.
 // Here we test with _test_ and _test2_ as the type.
@@ -412,6 +412,45 @@ class SearchRankingTestCase extends DrupalWebTestCase {
       $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
     }
   }
+
+  /**
+   * Verifies that if we combine two rankings, search still works.
+   *
+   * See issue http://drupal.org/node/771596
+   */
+  function testDoubleRankings() {
+    // Login with sufficient privileges.
+    $this->drupalLogin($this->drupalCreateUser(array('post comments without approval', 'create page content')));
+
+    // See testRankings() above - build a node that will rank high for sticky.
+    $settings = array(
+      'type' => 'page', 
+      'title' => array(LANGUAGE_NONE => array(array('value' => 'Drupal rocks'))), 
+      'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
+      'sticky' => 1,
+    );
+
+    $node = $this->drupalCreateNode($settings);
+
+    // Update the search index.
+    module_invoke_all('update_index');
+    search_update_totals();
+
+    // Refresh variables after the treatment.
+    $this->refreshVariables();
+
+    // Set up for ranking sticky and lots of comments; make sure others are
+    // disabled.
+    $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
+    foreach ($node_ranks as $var) {
+      $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0;
+      variable_set('node_rank_' . $var, $value);
+    }
+
+    // Do the search and assert the results.
+    $set = node_search_execute('rocks');
+    $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search double ranking order.');
+  }
 }
 
 class SearchBlockTestCase extends DrupalWebTestCase {
@@ -578,6 +617,98 @@ class SearchCommentTestCase extends DrupalWebTestCase {
   }
 }
 
+/**
+ * Tests that comment count display toggles properly on comment status of node
+ * 
+ * Issue 537278
+ * 
+ * - Nodes with comment status set to Open should always how comment counts
+ * - Nodes with comment status set to Closed should show comment counts
+ *     only when there are comments
+ * - Nodes with comment status set to Hidden should never show comment counts
+ */
+class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
+  protected $searching_user;
+  protected $searchable_nodes;
+  
+  public static function getInfo() {
+    return array(
+      'name' => 'Comment count toggle',
+      'description' => 'Verify that comment count display toggles properly on comment status of node.',
+      'group' => 'Search',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('search');
+
+    // Create searching user.
+    $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'post comments without approval'));
+
+    // Create initial nodes.
+    $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase'))));
+    
+    $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params);
+    $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params);
+    
+    // Login with sufficient privileges.
+    $this->drupalLogin($this->searching_user);
+    
+    // Create a comment array
+    $edit_comment = array();
+    $edit_comment['subject'] = $this->randomName();
+    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
+    $filtered_html_format_id = db_query_range('SELECT format FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'Filtered HTML'))->fetchField();
+    $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;
+    
+    // Post comment to the test node with comment
+    $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save'));
+    
+    // First update the index. This does the initial processing.
+    node_update_index();
+
+    // Then, run the shutdown function. Testing is a unique case where indexing
+    // and searching has to happen in the same request, so running the shutdown
+    // function manually is needed to finish the indexing process.
+    search_update_totals();
+  }
+
+  /**
+   * Verify that comment count display toggles properly on comment status of node
+   */
+  function testSearchCommentCountToggle() {
+    // Search for the nodes by string in the node body.
+    $edit = array(
+      'search_block_form' => "'SearchCommentToggleTestCase'",
+    );
+
+    // Test comment count display for nodes with comment status set to Open
+    $this->drupalPost('', $edit, t('Search'));
+    $this->assertText(t('0 comments'), t('Empty comment count displays for nodes with comment status set to Open'));
+    $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Open'));
+    
+    // Test comment count display for nodes with comment status set to Closed
+    $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED;
+    node_save($this->searchable_nodes['0 comments']);
+    $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED;
+    node_save($this->searchable_nodes['1 comment']);
+    
+    $this->drupalPost('', $edit, t('Search'));
+    $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Closed'));
+    $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Closed'));
+
+    // Test comment count display for nodes with comment status set to Hidden
+    $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN;
+    node_save($this->searchable_nodes['0 comments']);
+    $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN;
+    node_save($this->searchable_nodes['1 comment']);    
+    
+    $this->drupalPost('', $edit, t('Search'));
+    $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Hidden'));
+    $this->assertNoText(t('1 comment'), t('Non-empty comment count does not display for nodes with comment status set to Hidden'));
+  }  
+}
+
 /**
  * Test search_simplify() on every Unicode character.
  */
@@ -610,3 +741,52 @@ class SearchSimplifyTestCase extends DrupalWebTestCase {
     $this->assertIdentical(' ', search_simplify($string), t('Search simplify works for ASCII control characters.'));
   }
 }
+
+/**
+ * Test config page.
+ */
+class SearchConfigSettingsForm extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Config settings form',
+      'description' => 'Verify the search config settings form.',
+      'group' => 'Search',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('search');
+
+    // Login as a user that can create and search content.
+    $this->drupalLogin($this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access')));
+  }
+
+  /**
+   * Verify the search settings form.
+   */
+  function testSearchSettingsPage() {
+    // Add a single piece of content and index it.
+    $node = $this->drupalCreateNode();
+    // Link the node to itself to test that it's only indexed once.
+    $langcode = LANGUAGE_NONE;
+    $body_key = "body[$langcode][0][value]";
+    $edit[$body_key] = l($node->title, 'node/' . $node->nid);
+    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
+
+    node_update_index();
+    search_update_totals();
+
+    // Test that the settings form displays the correct count of items left to index.
+    $this->drupalGet('admin/config/search/settings');
+    $this->assertText(t('There are @count items left to index.', array('@count' => 0)));
+
+    // Test the re-index button.
+    $this->drupalPost('admin/config/search/settings', array(), t('Re-index site'));
+    $this->assertText(t('Are you sure you want to re-index the site'));
+    $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site'));
+    $this->assertText(t('The index will be rebuilt'));
+    $this->drupalGet('admin/config/search/settings');
+    $this->assertText(t('There is 1 item left to index.'));
+  }
+}
+
diff --git a/modules/shortcut/shortcut.admin.inc b/modules/shortcut/shortcut.admin.inc
index 926eafe2edce95316a04c197ed1a85c608a7020f..80d741d4e93d6e727a499a994a7900e4e026deef 100644
--- a/modules/shortcut/shortcut.admin.inc
+++ b/modules/shortcut/shortcut.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: shortcut.admin.inc,v 1.11 2010/03/08 15:45:26 webchick Exp $
+// $Id: shortcut.admin.inc,v 1.13 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -85,7 +85,7 @@ function shortcut_set_switch($form, &$form_state, $account = NULL) {
       'js' => array(drupal_get_path('module', 'shortcut') . '/shortcut.admin.js'),
     );
 
-    $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+    $form['actions'] = array('#type' => 'actions');
     $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Change set'),
@@ -193,7 +193,7 @@ function shortcut_set_add_form($form, &$form_state) {
     '#description' => t('The new set is created by copying items from your default shortcut set.'),
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Create new set'),
@@ -269,7 +269,7 @@ function shortcut_set_customize($form, &$form_state, $shortcut_set) {
     'js' => array(drupal_get_path('module', 'shortcut') . '/shortcut.admin.js'),
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save changes'),
@@ -294,17 +294,14 @@ function shortcut_set_customize_submit($form, &$form_state) {
 }
 
 /**
- * Themes the shortcut set customization form.
+ * Returns HTML for a shortcut set customization form.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An array representing the form.
+ *   - form: A render element representing the form.
  *
- * @return
- *   A themed HTML string representing the content of the form.
- *
- * @ingroup themeable
  * @see shortcut_set_customize()
+ * @ingroup themeable
  */
 function theme_shortcut_set_customize($variables) {
   $form = $variables['form'];
@@ -456,7 +453,7 @@ function _shortcut_link_form_elements($shortcut_link = NULL) {
 
   $form['#validate'][] = 'shortcut_link_edit_validate';
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
@@ -565,11 +562,7 @@ function shortcut_set_edit_form($form, &$form_state, $shortcut_set) {
     '#required' => TRUE,
     '#weight' => -5,
   );
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-    '#weight' => 100,
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
diff --git a/modules/shortcut/shortcut.info b/modules/shortcut/shortcut.info
index 29b1e382179abc2114b8e20196c3d602ef80adfb..ffe1d04e998c4d5a8578ba5207b717315698e1f9 100644
--- a/modules/shortcut/shortcut.info
+++ b/modules/shortcut/shortcut.info
@@ -10,8 +10,8 @@ files[] = shortcut.install
 files[] = shortcut.test
 configure = admin/config/user-interface/shortcut
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php
index 981889c27900a354a1992a797f782cc5cb0eb57d..702eb191344cfb73e9b05b7cb10571dae900668d 100644
--- a/modules/simpletest/drupal_web_test_case.php
+++ b/modules/simpletest/drupal_web_test_case.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: drupal_web_test_case.php,v 1.203 2010/03/12 14:38:37 dries Exp $
+// $Id: drupal_web_test_case.php,v 1.211 2010/04/23 05:46:01 webchick Exp $
 
 /**
  * Base class for Drupal tests.
@@ -566,6 +566,8 @@ class DrupalUnitTestCase extends DrupalTestCase {
     $this->originalPrefix = $db_prefix;
     $this->originalFileDirectory = file_directory_path();
 
+    spl_autoload_register('db_autoload');
+
     // Reset all statics so that test is performed with a clean environment.
     drupal_static_reset();
 
@@ -573,6 +575,9 @@ class DrupalUnitTestCase extends DrupalTestCase {
     $db_prefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
     $conf['file_public_path'] = $this->originalFileDirectory . '/' . $db_prefix;
 
+    // Set user agent to be consistent with web test case.
+    $_SERVER['HTTP_USER_AGENT'] = $db_prefix;
+
     // If locale is enabled then t() will try to access the database and
     // subsequently will fail as the database is not accessible.
     $module_list = module_list();
@@ -1097,7 +1102,8 @@ class DrupalWebTestCase extends DrupalTestCase {
    * is created with the same name as the database prefix.
    *
    * @param ...
-   *   List of modules to enable for the duration of the test.
+   *   List of modules to enable for the duration of the test. This can be
+   *   either a single array or a variable number of string arguments.
    */
   protected function setUp() {
     global $db_prefix, $user, $language, $conf;
@@ -1159,8 +1165,15 @@ class DrupalWebTestCase extends DrupalTestCase {
     // Install the modules specified by the default profile.
     module_enable($profile_details['dependencies'], FALSE);
 
-    // Install modules needed for this test.
-    if ($modules = func_get_args()) {
+    // Install modules needed for this test. This could have been passed in as
+    // either a single array argument or a variable number of string arguments.
+    // @todo Remove this compatibility layer in Drupal 8, and only accept
+    // $modules as a single array argument.
+    $modules = func_get_args();
+    if (isset($modules[0]) && is_array($modules[0])) {
+      $modules = $modules[0];
+    }
+    if ($modules) {
       module_enable($modules, TRUE);
     }
 
@@ -1194,6 +1207,7 @@ class DrupalWebTestCase extends DrupalTestCase {
     variable_set('install_task', 'done');
     variable_set('clean_url', $clean_url_original);
     variable_set('site_mail', 'simpletest@example.com');
+    variable_set('date_default_timezone', date_default_timezone_get());
     // Set up English language.
     unset($GLOBALS['conf']['language_default']);
     $language = language_default();
@@ -1576,22 +1590,32 @@ class DrupalWebTestCase extends DrupalTestCase {
    *     which is likely different than the $path parameter used for retrieving
    *     the initial form. Defaults to 'system/ajax'.
    *   - triggering_element: If the value for the 'path' key is 'system/ajax' or
-   *     another generic AJAX processing path, this needs to be set to the '/'
-   *     separated path to the element within the server's cached $form array.
-   *     The callback for the generic AJAX processing path uses this to find
-   *     the #ajax information for the element, including which specific
-   *     callback to use for processing the request.
+   *     another generic AJAX processing path, this needs to be set to the name
+   *     of the element. If the name doesn't identify the element uniquely, then
+   *     this should instead be an array with a single key/value pair,
+   *     corresponding to the element name and value. The callback for the
+   *     generic AJAX processing path uses this to find the #ajax information
+   *     for the element, including which specific callback to use for
+   *     processing the request.
+   *
+   *   This can also be set to NULL in order to emulate an Internet Explorer
+   *   submission of a form with a single text field, and pressing ENTER in that
+   *   textfield: under these conditions, no button information is added to the
+   *   POST data.
    * @param $options
    *   Options to be forwarded to url().
    * @param $headers
    *   An array containing additional HTTP request headers, each formatted as
    *   "name: value".
-   * @param $form_id
-   *   The optional string identifying the form to be submitted. On some pages
+   * @param $form_html_id
+   *   (optional) HTML ID of the form to be submitted. On some pages
    *   there are many identical forms, so just using the value of the submit
-   *   button is not enough.
+   *   button is not enough. For example: 'trigger-node-presave-assign-form'.
+   *   Note that this is not the Drupal $form_id, but rather the HTML ID of the
+   *   form, which is typically the same thing but with hyphens replacing the
+   *   underscores.
    */
-  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_id = NULL) {
+  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL) {
     $submit_matches = FALSE;
     $ajax = is_array($submit);
     if (isset($path)) {
@@ -1601,8 +1625,8 @@ class DrupalWebTestCase extends DrupalTestCase {
       $edit_save = $edit;
       // Let's iterate over all the forms.
       $xpath = "//form";
-      if (!empty($form_id)) {
-        $xpath .= "[@id='" . drupal_html_id($form_id) . "']";
+      if (!empty($form_html_id)) {
+        $xpath .= "[@id='" . $form_html_id . "']";
       }
       $forms = $this->xpath($xpath);
       foreach ($forms as $form) {
@@ -1622,7 +1646,7 @@ class DrupalWebTestCase extends DrupalTestCase {
 
         // We post only if we managed to handle every field in edit and the
         // submit button matches.
-        if (!$edit && $submit_matches) {
+        if (!$edit && ($submit_matches || !isset($submit))) {
           $post_array = $post;
           if ($upload) {
             // TODO: cURL handles file uploads for us, but the implementation
@@ -1642,8 +1666,21 @@ class DrupalWebTestCase extends DrupalTestCase {
               // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
               $post[$key] = urlencode($key) . '=' . urlencode($value);
             }
-            if ($ajax && isset($submit['triggering_element'])) {
-              $post['ajax_triggering_element'] = 'ajax_triggering_element=' . urlencode($submit['triggering_element']);
+            // For AJAX requests, add '_triggering_element_*' and
+            // 'ajax_html_ids' to the POST data, as ajax.js does.
+            if ($ajax) {
+              if (is_array($submit['triggering_element'])) {
+                // Get the first key/value pair in the array.
+                $post['_triggering_element_value'] = '_triggering_element_value=' . urlencode(reset($submit['triggering_element']));
+                $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode(key($submit['triggering_element']));
+              }
+              else {
+                $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode($submit['triggering_element']);
+              }
+              foreach ($this->xpath('//*[@id]') as $element) {
+                $id = (string) $element['id'];
+                $post[] = urlencode('ajax_html_ids[]') . '=' . urlencode($id);
+              }
             }
             $post = implode('&', $post);
           }
@@ -1666,7 +1703,7 @@ class DrupalWebTestCase extends DrupalTestCase {
       foreach ($edit as $name => $value) {
         $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
       }
-      if (!$ajax) {
+      if (!$ajax && isset($submit)) {
         $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
       }
       $this->fail(t('Found the requested form fields at @path', array('@path' => $path)));
@@ -1674,10 +1711,79 @@ class DrupalWebTestCase extends DrupalTestCase {
   }
 
   /**
-   * Execute a POST request on an AJAX path and JSON decode the result.
+   * Execute an AJAX submission.
+   *
+   * This executes a POST as ajax.js does. It uses the returned JSON data, an
+   * array of commands, to update $this->content using equivalent DOM
+   * manipulation as is used by ajax.js. It also returns the array of commands.
+   *
+   * @see ajax.js
    */
-  protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array()) {
-    return drupal_json_decode($this->drupalPost($path, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers));
+  protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = array()) {
+    // Get the content of the initial page prior to calling drupalPost(), since
+    // drupalPost() replaces $this->content.
+    if (isset($path)) {
+      $this->drupalGet($path, $options);
+    }
+    $content = $this->content;
+    $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id));
+
+    // We need $ajax_settings['wrapper'] to perform DOM manipulation.
+    if (!empty($ajax_settings) && !empty($return)) {
+      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
+      // them.
+      @$dom = DOMDocument::loadHTML($content);
+      foreach ($return as $command) {
+        // @todo ajax.js can process commands other than 'insert' and can
+        //   process commands that include a 'selector', but these are hard to
+        //   emulate with DOMDocument. For now, we only implement 'insert'
+        //   commands that use $ajax_settings['wrapper'].
+        if ($command['command'] == 'insert' && !isset($command['selector'])) {
+          // $dom->getElementById() doesn't work when drupalPostAJAX() is
+          // invoked multiple times for a page, so use XPath instead. This also
+          // sets us up for adding support for $command['selector'], though it
+          // will require transforming a jQuery selector to XPath.
+          $xpath = new DOMXPath($dom);
+          $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0);
+          if ($wrapperNode) {
+            // ajax.js adds an enclosing DIV to work around a Safari bug.
+            $newDom = new DOMDocument();
+            $newDom->loadHTML('<div>' . $command['data'] . '</div>');
+            $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE);
+            $method = isset($command['method']) ? $command['method'] : $ajax_settings['method'];
+            // The "method" is a jQuery DOM manipulation function. Emulate each
+            // one using PHP's DOMNode API.
+            switch ($method) {
+              case 'replaceWith':
+                $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode);
+                break;
+              case 'append':
+                $wrapperNode->appendChild($newNode);
+                break;
+              case 'prepend':
+                // If no firstChild, insertBefore() falls back to appendChild().
+                $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild);
+                break;
+              case 'before':
+                $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode);
+                break;
+              case 'after':
+                // If no nextSibling, insertBefore() falls back to appendChild().
+                $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling);
+                break;
+              case 'html':
+                foreach ($wrapperNode->childNodes as $childNode) {
+                  $wrapperNode->removeChild($childNode);
+                }
+                $wrapperNode->appendChild($newNode);
+                break;
+            }
+          }
+        }
+      }
+      $this->drupalSetContent($dom->saveHTML());
+    }
+    return $return;
   }
 
   /**
@@ -1856,7 +1962,7 @@ class DrupalWebTestCase extends DrupalTestCase {
             break;
           case 'submit':
           case 'image':
-            if ($submit == $value) {
+            if (isset($submit) && $submit == $value) {
               $post[$name] = $value;
               $submit_matches = TRUE;
             }
@@ -1875,6 +1981,48 @@ class DrupalWebTestCase extends DrupalTestCase {
     return $submit_matches;
   }
 
+  /**
+   * Builds an XPath query.
+   *
+   * Builds an XPath query by replacing placeholders in the query by the value
+   * of the arguments.
+   *
+   * XPath 1.0 (the version supported by libxml2, the underlying XML library
+   * used by PHP) doesn't support any form of quotation. This function
+   * simplifies the building of XPath expression.
+   *
+   * @param $xpath
+   *   An XPath query, possibly with placeholders in the form ':name'.
+   * @param $args
+   *   An array of arguments with keys in the form ':name' matching the
+   *   placeholders in the query. The values may be either strings or numeric
+   *   values.
+   * @return
+   *   An XPath query with arguments replaced.
+   */
+  protected function buildXPathQuery($xpath, array $args = array()) {
+    // Replace placeholders.
+    foreach ($args as $placeholder => $value) {
+      // XPath 1.0 doesn't support a way to escape single or double quotes in a
+      // string literal. We split double quotes out of the string, and encode
+      // them separately.
+      if (is_string($value)) {
+        // Explode the text at the quote characters.
+        $parts = explode('"', $value);
+
+        // Quote the parts.
+        foreach ($parts as &$part) {
+          $part = '"' . $part . '"';
+        }
+
+        // Return the string.
+        $value = count($parts) > 1 ? 'concat(' . implode(', \'"\', ', $parts) . ')' : $parts[0];
+      }
+      $xpath = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $xpath);
+    }
+    return $xpath;
+  }
+
   /**
    * Perform an xpath search on the contents of the internal browser. The search
    * is relative to the root element (HTML tag normally) of the page.
@@ -1886,11 +2034,14 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   format and return values see the SimpleXML documentation,
    *   http://us.php.net/manual/function.simplexml-element-xpath.php.
    */
-  protected function xpath($xpath) {
+  protected function xpath($xpath, array $arguments = array()) {
     if ($this->parse()) {
+      $xpath = $this->buildXPathQuery($xpath, $arguments);
       return $this->elements->xpath($xpath);
     }
-    return FALSE;
+    else {
+      return FALSE;
+    }
   }
 
   /**
@@ -1933,7 +2084,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE if the assertion succeeded, FALSE otherwise.
    */
   protected function assertLink($label, $index = 0, $message = '', $group = 'Other') {
-    $links = $this->xpath('//a[text()="' . $label . '"]');
+    $links = $this->xpath('//a[text()=:label]', array(':label' => $label));
     $message = ($message ?  $message : t('Link with label %label found.', array('%label' => $label)));
     return $this->assert(isset($links[$index]), $message, $group);
   }
@@ -1953,7 +2104,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE if the assertion succeeded, FALSE otherwise.
    */
   protected function assertNoLink($label, $message = '', $group = 'Other') {
-    $links = $this->xpath('//a[text()="' . $label . '"]');
+    $links = $this->xpath('//a[text()=:label]', array(':label' => $label));
     $message = ($message ?  $message : t('Link with label %label not found.', array('%label' => $label)));
     return $this->assert(empty($links), $message, $group);
   }
@@ -1974,7 +2125,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE if the assertion succeeded, FALSE otherwise.
    */
   protected function assertLinkByHref($href, $index = 0, $message = '', $group = 'Other') {
-    $links = $this->xpath('//a[contains(@href, "' . $href . '")]');
+    $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href));
     $message = ($message ?  $message : t('Link containing href %href found.', array('%href' => $href)));
     return $this->assert(isset($links[$index]), $message, $group);
   }
@@ -1993,7 +2144,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE if the assertion succeeded, FALSE otherwise.
    */
   protected function assertNoLinkByHref($href, $message = '', $group = 'Other') {
-    $links = $this->xpath('//a[contains(@href, "' . $href . '")]');
+    $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href));
     $message = ($message ?  $message : t('No link containing href %href found.', array('%href' => $href)));
     return $this->assert(empty($links), $message, $group);
   }
@@ -2015,7 +2166,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    */
   protected function clickLink($label, $index = 0) {
     $url_before = $this->getUrl();
-    $urls = $this->xpath('//a[text()="' . $label . '"]');
+    $urls = $this->xpath('//a[text()=:label]', array(':label' => $label));
 
     if (isset($urls[$index])) {
       $url_target = $this->getAbsoluteUrl($urls[$index]['href']);
@@ -2633,7 +2784,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertFieldChecked($id, $message = '') {
-    $elements = $this->xpath('//input[@id="' . $id . '"]');
+    $elements = $this->xpath('//input[@id=:id]', array(':id' => $id));
     return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is checked.', array('@id' => $id)), t('Browser'));
   }
 
@@ -2648,7 +2799,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertNoFieldChecked($id, $message = '') {
-    $elements = $this->xpath('//input[@id="' . $id . '"]');
+    $elements = $this->xpath('//input[@id=:id]', array(':id' => $id));
     return $this->assertTrue(isset($elements[0]) && empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is not checked.', array('@id' => $id)), t('Browser'));
   }
 
@@ -2665,7 +2816,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertOptionSelected($id, $option, $message = '') {
-    $elements = $this->xpath('//select[@id="' . $id . '"]//option[@value="' . $option . '"]');
+    $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option));
     return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['selected']), $message ? $message : t('Option @option for field @id is selected.', array('@option' => $option, '@id' => $id)), t('Browser'));
   }
 
@@ -2682,7 +2833,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   TRUE on pass, FALSE on fail.
    */
   protected function assertNoOptionSelected($id, $option, $message = '') {
-    $elements = $this->xpath('//select[@id="' . $id . '"]//option[@value="' . $option . '"]');
+    $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option));
     return $this->assertTrue(isset($elements[0]) && empty($elements[0]['selected']), $message ? $message : t('Option @option for field @id is not selected.', array('@option' => $option, '@id' => $id)), t('Browser'));
   }
 
@@ -2718,6 +2869,36 @@ class DrupalWebTestCase extends DrupalTestCase {
     return $this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field), '', $message, $group);
   }
 
+  /**
+   * Assert that each HTML ID is used for just a single element.
+   *
+   * @param $message
+   *   Message to display.
+   * @param $group
+   *   The group this message belongs to.
+   * @param $ids_to_skip
+   *   An optional array of ids to skip when checking for duplicates. It is
+   *   always a bug to have duplicate HTML IDs, so this parameter is to enable
+   *   incremental fixing of core code. Whenever a test passes this parameter,
+   *   it should add a "todo" comment above the call to this function explaining
+   *   the legacy bug that the test wishes to ignore and including a link to an
+   *   issue that is working to fix that legacy bug.
+   * @return
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertNoDuplicateIds($message = '', $group = 'Other', $ids_to_skip = array()) {
+    $status = TRUE;
+    foreach ($this->xpath('//*[@id]') as $element) {
+      $id = (string) $element['id'];
+      if (isset($seen_ids[$id]) && !in_array($id, $ids_to_skip)) {
+        $this->fail(t('The HTML ID %id is unique.', array('%id' => $id)), $group);
+        $status = FALSE;
+      }
+      $seen_ids[$id] = TRUE;
+    }
+    return $this->assert($status, $message, $group);
+  }
+
   /**
    * Helper function: construct an XPath for the given set of attributes and value.
    *
@@ -2729,7 +2910,8 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   XPath for specified values.
    */
   protected function constructFieldXpath($attribute, $value) {
-    return '//textarea[@' . $attribute . '="' . $value . '"]|//input[@' . $attribute . '="' . $value . '"]|//select[@' . $attribute . '="' . $value . '"]';
+    $xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]';
+    return $this->buildXPathQuery($xpath, array(':value' => $value));
   }
 
   /**
diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info
index ff5c6001f20fda2ed5f3d2dd60367ef67f43d164..c2d617be202dca3dc1631b337439e9c52655030b 100644
--- a/modules/simpletest/simpletest.info
+++ b/modules/simpletest/simpletest.info
@@ -38,8 +38,8 @@ files[] = tests/unicode.test
 files[] = tests/update.test
 files[] = tests/xmlrpc.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/simpletest.install b/modules/simpletest/simpletest.install
index 2331d1573ed5fb22cc14304ecf13dacde65edd78..2805a9d030c7ccf68c3851003bfb945fafef1246 100644
--- a/modules/simpletest/simpletest.install
+++ b/modules/simpletest/simpletest.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: simpletest.install,v 1.34 2010/01/15 22:46:59 dries Exp $
+// $Id: simpletest.install,v 1.35 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -68,7 +68,7 @@ function simpletest_requirements($phase) {
 
   // SimpleTest currently needs 2 cURL options which are incompatible with
   // having PHP's open_basedir restriction set.
-  // @see http://drupal.org/node/674304.
+  // See http://drupal.org/node/674304.
   $requirements['php_open_basedir'] = array(
     'title' => $t('PHP open_basedir restriction'),
     'value' => $open_basedir ? $t('Enabled') : $t('Disabled'),
diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module
index e3d211c978b225b6c02d73452fa21edfdb303aa7..228a1dfb41d7c56b13ce39cf5ec2c96d795c9d64 100644
--- a/modules/simpletest/simpletest.module
+++ b/modules/simpletest/simpletest.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: simpletest.module,v 1.89 2010/01/15 04:16:57 dries Exp $
+// $Id: simpletest.module,v 1.91 2010/03/30 07:17:19 webchick Exp $
 
 /**
  * @file
@@ -36,6 +36,7 @@ function simpletest_menu() {
     'description' => 'Run tests against Drupal core and your active modules. These tests help assure that your site code is working as designed.',
     'access arguments' => array('administer unit tests'),
     'file' => 'simpletest.pages.inc',
+    'weight' => -5,
   );
   $items['admin/config/development/testing/list'] = array(
     'title' => 'List',
@@ -68,7 +69,7 @@ function simpletest_permission() {
   return array(
     'administer unit tests' => array(
       'title' => t('Administer tests'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
   );
 }
diff --git a/modules/simpletest/simpletest.pages.inc b/modules/simpletest/simpletest.pages.inc
index 04381d907e777b47ae684735e2bf75771f1b78cc..8e46e964ffae0c0909959d293afbe6ed28cfc67b 100644
--- a/modules/simpletest/simpletest.pages.inc
+++ b/modules/simpletest/simpletest.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: simpletest.pages.inc,v 1.25 2010/02/07 05:36:57 webchick Exp $
+// $Id: simpletest.pages.inc,v 1.28 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -58,14 +58,13 @@ function simpletest_test_form($form) {
 }
 
 /**
- * Theme the test list generated by simpletest_test_form() into a table.
+ * Returns HTML for a test list generated by simpletest_test_form() into a table.
  *
  * @param $variables
  *   An associative array containing:
- *   - table: Form array that represent a table.
+ *   - table: A render element representing the table.
  *
- * @return
- *   HTML output.
+ * @ingroup themeable
  */
 function theme_simpletest_test_table($variables) {
   $table = $variables['table'];
@@ -185,7 +184,9 @@ function simpletest_test_form_submit($form, &$form_state) {
   // Get list of tests.
   $tests_list = array();
   foreach ($form_state['values'] as $class_name => $value) {
-    if (class_exists($class_name) && $value === 1) {
+    // Since class_exists() will likely trigger an autoload lookup,
+    // we do the fast check first.
+    if ($value === 1 && class_exists($class_name)) {
       $tests_list[] = $class_name;
     }
   }
@@ -364,9 +365,13 @@ function simpletest_result_form_submit($form, &$form_state) {
 }
 
 /**
- * Add wrapper div with class based on summary status.
+ * Returns HTML for the summary status of a simpletest result.
  *
- * @return HTML output.
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
+ *
+ * @ingroup themeable
  */
 function theme_simpletest_result_summary($variables) {
   $form = $variables['form'];
diff --git a/modules/simpletest/simpletest.test b/modules/simpletest/simpletest.test
index 848239492ddf191859111c277361ab4461259205..e528595e9280d8b87b3a3977a4822ba9086d02e2 100644
--- a/modules/simpletest/simpletest.test
+++ b/modules/simpletest/simpletest.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: simpletest.test,v 1.39 2010/03/03 19:46:26 dries Exp $
+// $Id: simpletest.test,v 1.40 2010/03/31 20:05:06 dries Exp $
 
 class SimpleTestFunctionalTest extends DrupalWebTestCase {
   /**
@@ -281,13 +281,13 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase {
 }
 
 /**
- * Test internal testing framework URL handling.
+ * Test internal testing framework browser.
  */
-class SimpleTestURLTestCase extends DrupalWebTestCase {
+class SimpleTestBrowserTestCase extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
-      'name' => 'SimpleTest URL handling',
-      'description' => 'Test the URL handling in the testing framework.',
+      'name' => 'SimpleTest browser',
+      'description' => 'Test the internal browser of the testing framework.',
       'group' => 'SimpleTest',
     );
   }
@@ -315,6 +315,28 @@ class SimpleTestURLTestCase extends DrupalWebTestCase {
     $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.'));
     $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.'));
   }
+
+  /**
+   * Tests XPath escaping.
+   */
+  function testXPathEscaping() {
+    $testpage = <<< EOF
+<html>
+<body>
+<a href="link1">A "weird" link, just to bother the dumb "XPath 1.0"</a>
+<a href="link2">A second "even more weird" link, in memory of George O'Malley</a>
+</body>
+</html>
+EOF;
+    $this->drupalSetContent($testpage);
+
+    // Matches the first link.
+    $urls = $this->xpath('//a[text()=:text]', array(':text' => 'A "weird" link, just to bother the dumb "XPath 1.0"'));
+    $this->assertEqual($urls[0]['href'], 'link1', 'Match with quotes.');
+
+    $urls = $this->xpath('//a[text()=:text]', array(':text' => 'A second "even more weird" link, in memory of George O\'Malley'));
+    $this->assertEqual($urls[0]['href'], 'link2', 'Match with mixed single and double quotes.');
+  }
 }
 
 class SimpleTestMailCaptureTestCase extends DrupalWebTestCase {
diff --git a/modules/simpletest/tests/actions.test b/modules/simpletest/tests/actions.test
index ed3a1b59cc5e38720dd859f858f8ddff7118c12b..3a098a89435d76e2f2c9d4d0d20a6cfa001d0e88 100644
--- a/modules/simpletest/tests/actions.test
+++ b/modules/simpletest/tests/actions.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: actions.test,v 1.13 2010/01/30 07:59:25 dries Exp $
+// $Id: actions.test,v 1.14 2010/03/26 17:14:45 dries Exp $
 
 class ActionsConfigurationTestCase extends DrupalWebTestCase {
   public static function getInfo() {
@@ -97,7 +97,7 @@ class ActionLoopTestCase extends DrupalWebTestCase {
     // To prevent this test from failing when xdebug is enabled, the maximum
     // recursion level should be kept low enough to prevent the xdebug
     // infinite recursion protection mechanism from aborting the request.
-    // @see http://drupal.org/node/587634.
+    // See http://drupal.org/node/587634.
     variable_set('actions_max_stack', mt_rand(3, 12));
     $this->triggerActions();
   }
diff --git a/modules/simpletest/tests/actions_loop_test.info b/modules/simpletest/tests/actions_loop_test.info
index 8e7fdad47f48d34d383fde5e743c322cdaa88afe..a24f5132d9ba79178f23451b54aac11234faeb47 100644
--- a/modules/simpletest/tests/actions_loop_test.info
+++ b/modules/simpletest/tests/actions_loop_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = actions_loop_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test
index 7417e4fad31b51365ad1d7c6c56633e924f8e957..3fc93e38772c249254150829c986fef16fbd0cec 100644
--- a/modules/simpletest/tests/ajax.test
+++ b/modules/simpletest/tests/ajax.test
@@ -1,9 +1,28 @@
 <?php
-// $Id: ajax.test,v 1.5 2009/12/12 23:36:28 webchick Exp $
+// $Id: ajax.test,v 1.9 2010/04/11 18:33:44 dries Exp $
 
 class AJAXTestCase extends DrupalWebTestCase {
   function setUp() {
-    parent::setUp('ajax_test', 'ajax_forms_test');
+    $modules = func_get_args();
+    if (isset($modules[0]) && is_array($modules[0])) {
+      $modules = $modules[0];
+    }
+    parent::setUp(array_unique(array_merge(array('ajax_test', 'ajax_forms_test'), $modules)));
+  }
+
+  /**
+   * Returns the passed-in commands array without the initial settings command.
+   *
+   * Depending on factors that may be irrelevant to a particular test,
+   * ajax_render() may prepend a settings command. This function allows the test
+   * to only have to concern itself with the commands that were passed to
+   * ajax_render().
+   */
+  protected function discardSettings($commands) {
+    if ($commands[0]['command'] == 'settings') {
+      array_shift($commands);
+    }
+    return $commands;
   }
 }
 
@@ -11,7 +30,7 @@ class AJAXTestCase extends DrupalWebTestCase {
  * Tests primary AJAX framework functions.
  */
 class AJAXFrameworkTestCase extends AJAXTestCase {
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'AJAX framework',
       'description' => 'Performs tests on AJAX framework functions.',
@@ -51,7 +70,7 @@ class AJAXFrameworkTestCase extends AJAXTestCase {
  * Tests AJAX framework commands.
  */
 class AJAXCommandsTestCase extends AJAXTestCase {
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'AJAX commands',
       'description' => 'Performs tests on AJAX framework commands.',
@@ -83,64 +102,63 @@ class AJAXCommandsTestCase extends AJAXTestCase {
     $edit = array();
 
     // Tests the 'after' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'after_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'after' && $command['data'] == 'This will be placed after', "'after' AJAX command issued with correct data");
 
     // Tests the 'alert' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'alert_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'alert' && $command['text'] == 'Alert', "'alert' AJAX Command issued with correct text");
 
     // Tests the 'append' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'append_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'append' && $command['data'] == 'Appended text', "'append' AJAX command issued with correct data");
 
     // Tests the 'before' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'before_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'before' && $command['data'] == 'Before text', "'before' AJAX command issued with correct data");
 
     // Tests the 'changed' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'changed_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed."))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'changed' && $command['selector'] == '#changed_div', "'changed' AJAX command issued with correct selector");
 
     // Tests the 'changed' command using the second argument.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'changed_command_asterisk_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk."))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'changed' && $command['selector'] == '#changed_div' && $command['asterisk'] == '#changed_div_mark_this', "'changed' AJAX command (with asterisk) issued with correct selector");
 
     // Tests the 'css' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'css_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue."))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'css' && $command['selector'] == '#css_div' && $command['argument']['background-color'] == 'blue', "'css' AJAX command issued with correct selector");
 
     // Tests the 'data' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'data_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX data command: Issue command."))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'data' && $command['name'] == 'testkey' && $command['value'] == 'testvalue', "'data' AJAX command issued with correct key and value");
 
     // Tests the 'html' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'html_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector."))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'html' && $command['data'] == 'replacement text', "'html' AJAX command issued with correct data");
 
     // Tests the 'prepend' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'prepend_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'prepend' && $command['data'] == 'prepended text', "'prepend' AJAX command issued with correct data");
 
     // Tests the 'remove' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'remove_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'remove' && $command['selector'] == '#remove_text', "'remove' AJAX command issued with correct command and selector");
 
-
     // Tests the 'restripe' command.
-    $commands = $this->drupalPostAJAX($form_path, $edit, 'restripe_command_example');
-    $command = $commands[1];
+    $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'restripe' command"))));
+    $command = $commands[0];
     $this->assertTrue($command['command'] == 'restripe' && $command['selector'] == '#restripe_table', "'restripe' AJAX command issued with correct selector");
   }
 }
@@ -173,8 +191,8 @@ class AJAXFormValuesTestCase extends AJAXTestCase {
       $edit = array(
         'select' => $item,
       );
-      $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select');
-      $data_command = $commands[2];
+      $commands = $this->discardSettings($this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select'));
+      $data_command = $commands[1];
       $this->assertEqual($data_command['value'], $item);
     }
 
@@ -183,9 +201,128 @@ class AJAXFormValuesTestCase extends AJAXTestCase {
       $edit = array(
         'checkbox' => $item,
       );
-      $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'checkbox');
-      $data_command = $commands[2];
+      $commands = $this->discardSettings($this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'checkbox'));
+      $data_command = $commands[1];
       $this->assertEqual((int) $data_command['value'], (int) $item);
     }
   }
 }
+
+/**
+ * Tests that AJAX-enabled forms work when multiple instances of the same form are on a page.
+ */
+class AJAXMultiFormTestCase extends AJAXTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'AJAX multi form',
+      'description' => 'Tests that AJAX-enabled forms work when multiple instances of the same form are on a page.',
+      'group' => 'AJAX',
+    );
+  }
+
+  function setUp() {
+    parent::setUp(array('form_test'));
+
+    // Create a multi-valued field for 'page' nodes to use for AJAX testing.
+    $field_name = 'field_ajax_test';
+    $field = array(
+      'field_name' => $field_name,
+      'type' => 'text',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+    );
+    field_create_field($field);
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    );
+    field_create_instance($instance);
+
+    // Login a user who can create 'page' nodes.
+    $this->web_user = $this->drupalCreateUser(array('create page content'));
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Test that a page with the 'page_node_form' included twice works correctly.
+   */
+  function testMultiForm() {
+    // HTML IDs for elements within the field are potentially modified with
+    // 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")]',
+    );
+    $button_name = $field_name . '_add_more';
+    $button_value = t('Add another item');
+    $button_xpath_suffix = '//input[@name="' . $button_name . '"]';
+    $field_items_xpath_suffix = '//input[@type="text"]';
+
+    // 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_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.'));
+    }
+    // @todo Legacy bug of duplicate ids for filter-guidelines-FORMAT. See
+    //   http://drupal.org/node/755566.
+    $this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other', array('filter-guidelines-1', 'filter-guidelines-2', 'filter-guidelines-3'));
+
+    // Submit the "add more" button of each form twice. After each corresponding
+    // page update, ensure the same as above. To successfully implement
+    // consecutive AJAX submissions, we need to manage $settings as ajax.js
+    // does for Drupal.settings.
+    preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $this->content, $matches);
+    $settings = drupal_json_decode($matches[1]);
+    foreach ($field_xpaths as $form_id => $field_xpath) {
+      for ($i=0; $i<2; $i++) {
+        $button = $this->xpath($field_xpath . $button_xpath_suffix);
+        $button_id = (string) $button[0]['id'];
+        $commands = $this->drupalPostAJAX(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_id, $settings['ajax'][$button_id]);
+        $settings = array_merge_recursive($settings, $commands[0]['settings']);
+        $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.'));
+        // @todo Legacy bug of duplicate ids for filter-guidelines-FORMAT. See
+        //   http://drupal.org/node/755566.
+        $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other', array('filter-guidelines-1', 'filter-guidelines-2', 'filter-guidelines-3'));
+      }
+    }
+  }
+}
+
+/**
+ * Miscellaneous AJAX tests using ajax_test module.
+ */
+class AJAXElementValidation extends AJAXTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Miscellaneous AJAX tests',
+      'description' => 'Various tests of AJAX behavior',
+      'group' => 'AJAX',
+    );
+  }
+
+  /**
+   * Try to post an AJAX change to a form that has a validated element.
+   *
+   * The drivertext field is AJAX-enabled. An additional field is not, but
+   * is set to be a required field. In this test the required field is not
+   * filled in, and we want to see if the activation of the "drivertext"
+   * AJAX-enabled field fails due to the required field being empty.
+   */
+  function testAJAXElementValidation() {
+    $web_user = $this->drupalCreateUser();
+    $edit = array('drivertext' => t('some dumb text'));
+
+    // Post with 'drivertext' as the triggering element.
+    $post_result = $this->drupalPostAJAX('ajax_validation_test', $edit, 'drivertext');
+    // Look for a validation failure in the resultant JSON.
+    $this->assertNoText(t('Error message'), t("No error message in resultant JSON"));
+    $this->assertText('ajax_forms_test_validation_form_callback invoked', t('The correct callback was invoked'));
+  }
+}
+
diff --git a/modules/simpletest/tests/ajax_forms_test.info b/modules/simpletest/tests/ajax_forms_test.info
index bf9e76174669dea1e42cfa7bea0b953252edb5ac..0b357f5a296714f0c3e21ee701edec583874ed9b 100644
--- a/modules/simpletest/tests/ajax_forms_test.info
+++ b/modules/simpletest/tests/ajax_forms_test.info
@@ -7,8 +7,8 @@ files[] = ajax_forms_test.module
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/ajax_forms_test.module b/modules/simpletest/tests/ajax_forms_test.module
index 4e49881473b80d705f3387e9fd54bc82110a534e..acd9226c221ad2c44b6b25acd995ea26001a8f02 100644
--- a/modules/simpletest/tests/ajax_forms_test.module
+++ b/modules/simpletest/tests/ajax_forms_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: ajax_forms_test.module,v 1.4 2010/03/13 06:55:50 dries Exp $
+// $Id: ajax_forms_test.module,v 1.5 2010/03/26 18:58:12 dries Exp $
 
 /**
  * @file
@@ -24,6 +24,12 @@ function ajax_forms_test_menu() {
     'page arguments' => array('ajax_forms_test_ajax_commands_form'),
     'access callback' => TRUE,
   );
+  $items['ajax_validation_test'] = array(
+    'title' => 'AJAX Validation Test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ajax_forms_test_validation_form'),
+    'access callback' => TRUE,
+  );
   return $items;
 }
 
@@ -340,3 +346,59 @@ function ajax_forms_test_advanced_commands_restripe_callback($form, $form_state)
   $commands[] = ajax_command_restripe('#restripe_table');
   return array('#type' => 'ajax', '#commands' => $commands);
 }
+
+
+
+/**
+ * This form and its related submit and callback functions demonstrate
+ * not validating another form element when a single AJAX element is triggered.
+ *
+ * The "drivertext" element is an AJAX-enabled textfield, free-form.
+ * The "required_field" element is a textfield marked required.
+ *
+ * The correct behavior is that the AJAX-enabled drivertext element should
+ * be able to trigger without causing validation of the "required_field".
+ */
+function ajax_forms_test_validation_form($form, &$form_state) {
+
+  $form['drivertext'] = array(
+    '#title' => t('AJAX-enabled textfield.'),
+    '#description' => t("When this one AJAX-triggers and the spare required field is empty, you should not get an error."),
+    '#type' => 'textfield',
+    '#default_value' => !empty($form_state['values']['drivertext']) ? $form_state['values']['drivertext'] : "",
+    '#ajax' => array(
+      'callback' => 'ajax_forms_test_validation_form_callback',
+      'wrapper' => 'message_area',
+      'method' => 'replace',
+    ),
+    '#suffix' => '<div id="message_area"></div>',
+  );
+
+  $form['spare_required_field'] = array(
+    '#title' => t("Spare Required Field"),
+    '#type' => 'textfield',
+    '#required' => TRUE,
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+  );
+
+  return $form;
+}
+/**
+ * Submit handler for the validation form.
+ */
+function ajax_forms_test_validation_form_submit($form, $form_state) {
+  drupal_set_message(t("Validation form submitted"));
+}
+
+/**
+ * AJAX callback for the 'drivertext' element of the validation form.
+ */
+function ajax_forms_test_validation_form_callback($form, $form_state) {
+  drupal_set_message("ajax_forms_test_validation_form_callback invoked");
+  drupal_set_message(t("Callback: drivertext=%drivertext, spare_required_field=%spare_required_field", array('%drivertext' => $form_state['values']['drivertext'], '%spare_required_field' => $form_state['values']['spare_required_field'])));
+  return '<div id="message_area">ajax_forms_test_validation_form_callback at ' . date('c') . '</div>';
+}
diff --git a/modules/simpletest/tests/ajax_test.info b/modules/simpletest/tests/ajax_test.info
index affc5e5e84e7f7397c42b2366937f3518e0b22db..98b94a798788456809ea5d48f760cfa3b8bf41a7 100644
--- a/modules/simpletest/tests/ajax_test.info
+++ b/modules/simpletest/tests/ajax_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = ajax_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/batch_test.info b/modules/simpletest/tests/batch_test.info
index dd7c58b163e53908712a1de3c5be0cf0bf4fd556..6e0a4adbd9ee015a3db6b89c6062e93699c9a778 100644
--- a/modules/simpletest/tests/batch_test.info
+++ b/modules/simpletest/tests/batch_test.info
@@ -8,8 +8,8 @@ files[] = batch_test.module
 files[] = batch_test.callbacks.inc
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index f5ca48997c1e0e43fec85ae965264e23ddf82f86..8c277567395c88af62497b30be3ebeec0a12e40b 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: common.test,v 1.105 2010/03/03 08:40:25 dries Exp $
+// $Id: common.test,v 1.110 2010/04/22 21:41:09 webchick Exp $
 
 /**
  * @file
@@ -23,6 +23,12 @@ class DrupalAlterTestCase extends DrupalWebTestCase {
   }
 
   function testDrupalAlter() {
+    // This test depends on Garland, so make sure that it is always the current
+    // active theme.
+    global $theme, $base_theme_info;
+    $theme = 'garland';
+    $base_theme_info = array();
+
     $array = array('foo' => 'bar');
     $entity = new stdClass;
     $entity->foo = 'bar';
@@ -317,7 +323,8 @@ class CommonXssUnitTest extends DrupalUnitTestCase {
    * Check that invalid multi-byte sequences are rejected.
    */
   function testInvalidMultiByte() {
-     $text = check_plain("Foo\xC0barbaz");
+     // Ignore PHP 5.3+ invalid multibyte sequence warning.
+     $text = @check_plain("Foo\xC0barbaz");
      $this->assertEqual($text, '', 'check_plain() rejects invalid sequence "Foo\xC0barbaz"');
      $text = check_plain("Fooÿñ");
      $this->assertEqual($text, "Fooÿñ", 'check_plain() accepts valid sequence "Fooÿñ"');
@@ -742,15 +749,15 @@ class DrupalHTMLIdentifierTestCase extends DrupalUnitTestCase {
     $this->assertIdentical(drupal_html_id('invalid,./:@\\^`{Üidentifier'), 'invalididentifier', t('Strip invalid characters.'));
 
     // Verify Drupal coding standards are enforced.
-    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name--1', t('Enforce Drupal coding standards.'));
+    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name-1', t('Enforce Drupal coding standards.'));
 
     // Reset the static cache so we can ensure the unique id count is at zero.
     drupal_static_reset('drupal_html_id');
 
     // Clean up IDs with invalid starting characters.
     $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id', t('Test the uniqueness of IDs #1.'));
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id-2', t('Test the uniqueness of IDs #2.'));
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id-3', t('Test the uniqueness of IDs #3.'));
+    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--2', t('Test the uniqueness of IDs #2.'));
+    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--3', t('Test the uniqueness of IDs #3.'));
   }
 }
 
@@ -1149,6 +1156,7 @@ class JavaScriptTestCase extends DrupalWebTestCase {
     drupal_add_js('(function($){alert("Weight -8 #1");})(jQuery);', array('type' => 'inline', 'scope' => 'footer', 'weight' => -8));
     drupal_add_js('(function($){alert("Weight -8 #2");})(jQuery);', array('type' => 'inline', 'scope' => 'footer', 'weight' => -8));
     drupal_add_js('(function($){alert("Weight -8 #3");})(jQuery);', array('type' => 'inline', 'scope' => 'footer', 'weight' => -8));
+    drupal_add_js('http://example.com/example.js?Weight -5 #1', array('type' => 'external', 'scope' => 'footer', 'weight' => -5));
     drupal_add_js('(function($){alert("Weight -8 #4");})(jQuery);', array('type' => 'inline', 'scope' => 'footer', 'weight' => -8));
     drupal_add_js('(function($){alert("Weight 5 #2");})(jQuery);', array('type' => 'inline', 'scope' => 'footer', 'weight' => 5));
     drupal_add_js('(function($){alert("Weight 0 #3");})(jQuery);', array('type' => 'inline', 'scope' => 'footer'));
@@ -1159,6 +1167,7 @@ class JavaScriptTestCase extends DrupalWebTestCase {
       "-8 #2",
       "-8 #3",
       "-8 #4",
+      "-5 #1", // The external script.
       "0 #1",
       "0 #2",
       "0 #3",
@@ -1766,6 +1775,8 @@ class FormatDateUnitTest extends DrupalWebTestCase {
     $user = user_load($test_user->uid, TRUE);
     $real_language = $language->language;
     $language->language = $user->language;
+    // Simulate a Drupal bootstrap with the logged-in user.
+    date_default_timezone_set(drupal_get_user_timezone());
 
     $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', t('Test a different language.'));
     $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London'), 'Monday, 26-Mar-07 01:00:00 BST', t('Test a different time zone.'));
@@ -1778,6 +1789,8 @@ class FormatDateUnitTest extends DrupalWebTestCase {
     // Restore the original user and language, and enable session saving.
     $user = $real_user;
     $language->language = $real_language;
+    // Restore default time zone.
+    date_default_timezone_set(drupal_get_user_timezone());
     drupal_save_session(TRUE);
   }
 }
@@ -1878,3 +1891,35 @@ class DrupalJSONTest extends DrupalUnitTestCase {
     $this->assertIdentical($source, $json_decoded, t('Encoding structured data to JSON and decoding back results in the original data.'));
   }
 }
+
+/**
+ * Tests for RDF namespaces XML serialization.
+ */
+class DrupalGetRdfNamespacesTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'RDF namespaces XML serialization tests',
+      'description' => 'Confirm that the serialization of RDF namespaces via drupal_get_rdf_namespaces() is output and parsed correctly in the XHTML document.',
+      'group' => 'System',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('rdf', 'rdf_test');
+  }
+
+  /**
+   * Test RDF namespaces.
+   */
+  function testGetRdfNamespaces() {
+    // Fetches the front page and extracts XML namespaces.
+    $this->drupalGet('');
+    $xml = new SimpleXMLElement($this->content);
+    $ns = $xml->getDocNamespaces();
+
+    $this->assertEqual($ns['owl'], 'http://www.w3.org/2002/07/owl#', t('A prefix declared once is displayed.'));
+    $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', t('The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.'));
+    $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', t('Two prefixes can be assigned the same namespace.'));
+    $this->assertTrue(!isset($ns['dc']), t('A prefix with conflicting namespaces is discarded.'));
+  }
+}
diff --git a/modules/simpletest/tests/common_test.info b/modules/simpletest/tests/common_test.info
index e5ef1e551bd47570ed9e84f7865efa247b3d340a..987cd5af7b9998c051b55b99f44541042c5f014b 100644
--- a/modules/simpletest/tests/common_test.info
+++ b/modules/simpletest/tests/common_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = common_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/database_test.info b/modules/simpletest/tests/database_test.info
index 47097355b43b832352a9522b5a44edac203ccc51..ca0f2275d154695017fc0765c0d572f732b03f95 100644
--- a/modules/simpletest/tests/database_test.info
+++ b/modules/simpletest/tests/database_test.info
@@ -8,8 +8,8 @@ files[] = database_test.install
 version = VERSION
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test
index be424d4d85b128563855048c06a4cd0a188fa1ab..018e7fec844f1874870cec3eb01255ed173ce218 100644
--- a/modules/simpletest/tests/database_test.test
+++ b/modules/simpletest/tests/database_test.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: database_test.test,v 1.83 2010/03/07 08:03:45 webchick Exp $
+// $Id: database_test.test,v 1.87 2010/04/11 18:33:44 dries Exp $
 
 /**
  * Dummy class for fetching into a class.
@@ -2247,7 +2247,7 @@ class DatabaseTaggingTestCase extends DatabaseTestCase {
 /**
  * Select alter tests.
  *
- * @see database_test_query_alter().
+ * @see database_test_query_alter()
  */
 class DatabaseAlterTestCase extends DatabaseTestCase {
 
@@ -2331,7 +2331,7 @@ class DatabaseAlterTestCase extends DatabaseTestCase {
 /**
  * Select alter tests, part 2.
  *
- * @see database_test_query_alter().
+ * @see database_test_query_alter()
  */
 class DatabaseAlter2TestCase extends DatabaseTestCase {
 
@@ -2434,11 +2434,11 @@ class DatabaseRegressionTestCase extends DatabaseTestCase {
   }
 
   /**
-   * Test the db_column_exists() function.
+   * Test the db_field_exists() function.
    */
-  function testDBColumnExists() {
-    $this->assertIdentical(TRUE, db_column_exists('node', 'nid'), t('Returns true for existent column.'));
-    $this->assertIdentical(FALSE, db_column_exists('node', 'nosuchcolumn'), t('Returns false for nonexistent column.'));
+  function testDBFieldExists() {
+    $this->assertIdentical(TRUE, db_field_exists('node', 'nid'), t('Returns true for existent column.'));
+    $this->assertIdentical(FALSE, db_field_exists('node', 'nosuchcolumn'), t('Returns false for nonexistent column.'));
   }
 
   /**
@@ -2908,6 +2908,7 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
    */
   protected function transactionOuterLayer($suffix, $rollback = FALSE) {
     $connection = Database::getConnection();
+    $depth = $connection->transactionDepth();
     $txn = db_transaction();
 
     // Insert a single row into the testing table.
@@ -2925,6 +2926,13 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
     $this->transactionInnerLayer($suffix, $rollback);
 
     $this->assertTrue($connection->inTransaction(), t('In transaction after calling nested transaction.'));
+
+    if ($rollback) {
+      // Roll back the transaction, if requested.
+      // This rollback should propagate to the last savepoint.
+      $txn->rollback();
+      $this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().'));
+    }
   }
 
   /**
@@ -2939,12 +2947,18 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
   protected function transactionInnerLayer($suffix, $rollback = FALSE) {
     $connection = Database::getConnection();
 
+    $this->assertTrue($connection->inTransaction(), t('In transaction in nested transaction.'));
+
+    $depth = $connection->transactionDepth();
     // Start a transaction. If we're being called from ->transactionOuterLayer,
     // then we're already in a transaction. Normally, that would make starting
     // a transaction here dangerous, but the database API handles this problem
     // for us by tracking the nesting and avoiding the danger.
     $txn = db_transaction();
 
+    $depth2 = $connection->transactionDepth();
+    $this->assertTrue($depth < $depth2, t('Transaction depth is has increased with new transaction.'));
+
     // Insert a single row into the testing table.
     db_insert('test')
       ->fields(array(
@@ -2957,9 +2971,9 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
 
     if ($rollback) {
       // Roll back the transaction, if requested.
-      // This rollback should propagate to the the outer transaction, if present.
+      // This rollback should propagate to the last savepoint.
       $txn->rollback();
-      $this->assertTrue($txn->willRollback(), t('Transaction is scheduled to roll back after calling rollback().'));
+      $this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().'));
     }
   }
 
@@ -3146,7 +3160,7 @@ class DatabaseExtraTypesTestCase extends DrupalWebTestCase {
  * Check the sequences API.
  */
 class DatabaseNextIdCase extends DrupalWebTestCase {
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => t('Sequences API'),
       'description' => t('Test the secondary sequences API.'),
@@ -3173,7 +3187,7 @@ class DatabaseNextIdCase extends DrupalWebTestCase {
  * Tests the empty pseudo-statement class.
  */
 class DatabaseEmptyStatementTestCase extends DrupalWebTestCase {
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => t('Empty statement'),
       'description' => t('Test the empty pseudo-statement class.'),
diff --git a/modules/simpletest/tests/entity_cache_test.info b/modules/simpletest/tests/entity_cache_test.info
new file mode 100644
index 0000000000000000000000000000000000000000..c0b8b31c53f60b3a963c862db7430ec5c84bbff6
--- /dev/null
+++ b/modules/simpletest/tests/entity_cache_test.info
@@ -0,0 +1,15 @@
+; $Id: entity_cache_test.info,v 1.1 2010/04/18 15:01:56 webchick Exp $
+name = "Entity cache test"
+description = "Support module for testing entity cache."
+package = Testing
+version = VERSION
+core = 7.x
+files[] = entity_cache_test.module
+dependencies[] = entity_cache_test_dependency
+hidden = TRUE
+
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
+project = "drupal"
+datestamp = "1272318008"
+
diff --git a/modules/simpletest/tests/entity_cache_test.module b/modules/simpletest/tests/entity_cache_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..acafca109d0acc1fe41291ae108f722df0d6fa48
--- /dev/null
+++ b/modules/simpletest/tests/entity_cache_test.module
@@ -0,0 +1,22 @@
+<?php
+// $Id: entity_cache_test.module,v 1.1 2010/04/18 15:01:56 webchick Exp $
+
+/**
+ * @file
+ * Helper module for entity cache tests.
+ */
+
+/**
+ * Implements hook_watchdog().
+ *
+ * This function is called during module_enable() and tries to access entity
+ * information provided by the module this one depends on. The information is
+ * stored in a temporary system variable and is later analyzed in the test
+ * case.
+ *
+ * @see EnableDisableTestCase::testEntityCache()
+ */
+function entity_cache_test_watchdog() {
+  $info = entity_get_info('entity_cache_test');
+  variable_set('entity_cache_test', $info);
+}
diff --git a/modules/simpletest/tests/entity_cache_test_dependency.info b/modules/simpletest/tests/entity_cache_test_dependency.info
new file mode 100644
index 0000000000000000000000000000000000000000..0888277d55f6b10c39db81ee86bd6055b9a015cd
--- /dev/null
+++ b/modules/simpletest/tests/entity_cache_test_dependency.info
@@ -0,0 +1,14 @@
+; $Id: entity_cache_test_dependency.info,v 1.1 2010/04/18 15:01:56 webchick Exp $
+name = "Entity cache test dependency"
+description = "Support dependency module for testing entity cache."
+package = Testing
+version = VERSION
+core = 7.x
+files[] = entity_cache_test_dependency.module
+hidden = TRUE
+
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
+project = "drupal"
+datestamp = "1272318008"
+
diff --git a/modules/simpletest/tests/entity_cache_test_dependency.module b/modules/simpletest/tests/entity_cache_test_dependency.module
new file mode 100644
index 0000000000000000000000000000000000000000..387921e80d1d277ed6541c881f133cb26701585a
--- /dev/null
+++ b/modules/simpletest/tests/entity_cache_test_dependency.module
@@ -0,0 +1,19 @@
+<?php
+// $Id: entity_cache_test_dependency.module,v 1.1 2010/04/18 15:01:56 webchick Exp $
+
+/**
+ * @file
+ * Helper module for entity cache tests.
+ */
+
+/**
+ * Implements hook_entity_info().
+ */
+function entity_cache_test_dependency_entity_info() {
+  return array(
+    'entity_cache_test' => array(
+      'label' => 'Entity Cache Test',
+      'base table' => 'entity_cache_test',
+    ),
+  );
+}
diff --git a/modules/simpletest/tests/error_test.info b/modules/simpletest/tests/error_test.info
index e53a7605e83be6d7564b9e6857a9fb5d2811a689..324a296e1473afb66aa4fa68a70ab731d99d487a 100644
--- a/modules/simpletest/tests/error_test.info
+++ b/modules/simpletest/tests/error_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = error_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/file.test b/modules/simpletest/tests/file.test
index 905ac3a754834a2dbfcccc80a6a5bfcd011c94c0..971aa2522e4f82e2691d38bffd444408996cb2f1 100644
--- a/modules/simpletest/tests/file.test
+++ b/modules/simpletest/tests/file.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: file.test,v 1.51 2010/01/29 22:40:41 dries Exp $
+// $Id: file.test,v 1.53 2010/04/11 18:33:44 dries Exp $
 
 /**
  *  @file
@@ -194,7 +194,7 @@ class FileTestCase extends DrupalWebTestCase {
     $file->status = 0;
     // Write the record directly rather than calling file_save() so we don't
     // invoke the hooks.
-    $this->assertNotIdentical(drupal_write_record('file', $file), FALSE, t('The file was added to the database.'), 'Create test file');
+    $this->assertNotIdentical(drupal_write_record('file_managed', $file), FALSE, t('The file was added to the database.'), 'Create test file');
 
     return $file;
   }
@@ -289,21 +289,21 @@ class FileSpaceUsedTest extends FileTestCase {
 
     // Create records for a couple of users with different sizes.
     $file = array('uid' => 2, 'uri' => 'public://example1.txt', 'filesize' => 50, 'status' => FILE_STATUS_PERMANENT);
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
     $file = array('uid' => 2, 'uri' => 'public://example2.txt', 'filesize' => 20, 'status' => FILE_STATUS_PERMANENT);
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
     $file = array('uid' => 3, 'uri' => 'public://example3.txt', 'filesize' => 100, 'status' => FILE_STATUS_PERMANENT);
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
     $file = array('uid' => 3, 'uri' => 'public://example4.txt', 'filesize' => 200, 'status' => FILE_STATUS_PERMANENT);
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
 
     // Now create some with other statuses. These values were chosen arbitrarily
     // for the sole purpose of testing that bitwise operators were used
     // correctly on the field.
     $file = array('uid' => 2, 'uri' => 'public://example5.txt', 'filesize' => 1, 'status' => 2 | 8);
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
     $file = array('uid' => 3, 'uri' => 'public://example6.txt', 'filesize' => 3, 'status' => 2 | 4);
-    drupal_write_record('file', $file);
+    drupal_write_record('file_managed', $file);
   }
 
   /**
@@ -560,7 +560,7 @@ class FileSaveUploadTest extends FileHookTestCase {
     $this->image = current($this->drupalGetTestFiles('image'));
     $this->assertTrue(is_file($this->image->uri), t("The file we're going to upload exists."));
 
-    $this->maxFidBefore = db_query('SELECT MAX(fid) AS fid FROM {file}')->fetchField();
+    $this->maxFidBefore = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
 
     // Upload with replace to gurantee there's something there.
     $edit = array(
@@ -581,7 +581,7 @@ class FileSaveUploadTest extends FileHookTestCase {
    * Test the file_save_upload() function.
    */
   function testNormal() {
-    $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file}')->fetchField();
+    $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
     $this->assertTrue($max_fid_after > $this->maxFidBefore, t('A new file was created.'));
     $file1 = file_load($max_fid_after);
     $this->assertTrue($file1, t('Loaded the file.'));
@@ -592,13 +592,13 @@ class FileSaveUploadTest extends FileHookTestCase {
     file_test_reset();
 
     // Upload a second file.
-    $max_fid_before = db_query('SELECT MAX(fid) AS fid FROM {file}')->fetchField();
+    $max_fid_before = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
     $image2 = current($this->drupalGetTestFiles('image'));
     $edit = array('files[file_test_upload]' => drupal_realpath($image2->uri));
     $this->drupalPost('file-test/upload', $edit, t('Submit'));
     $this->assertResponse(200, t('Received a 200 response for posted test file.'));
     $this->assertRaw(t('You WIN!'));
-    $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file}')->fetchField();
+    $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
 
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(array('validate', 'insert'));
@@ -1680,7 +1680,7 @@ class FileSaveTest extends FileHookTestCase {
 
     $this->assertNotNull($saved_file, t("Saving the file should give us back a file object."), 'File');
     $this->assertTrue($saved_file->fid > 0, t("A new file ID is set when saving a new file to the database."), 'File');
-    $loaded_file = db_query('SELECT * FROM {file} f WHERE f.fid = :fid', array(':fid' => $saved_file->fid))->fetch(PDO::FETCH_OBJ);
+    $loaded_file = db_query('SELECT * FROM {file_managed} f WHERE f.fid = :fid', array(':fid' => $saved_file->fid))->fetch(PDO::FETCH_OBJ);
     $this->assertNotNull($loaded_file, t("Record exists in the database."));
     $this->assertEqual($loaded_file->status, $file->status, t("Status was saved correctly."));
     $this->assertEqual($saved_file->filesize, filesize($file->uri), t("File size was set correctly."), 'File');
@@ -1697,7 +1697,7 @@ class FileSaveTest extends FileHookTestCase {
 
     $this->assertEqual($resaved_file->fid, $saved_file->fid, t("The file ID of an existing file is not changed when updating the database."), 'File');
     $this->assertTrue($resaved_file->timestamp >= $saved_file->timestamp, t("Timestamp didn't go backwards."), 'File');
-    $loaded_file = db_query('SELECT * FROM {file} f WHERE f.fid = :fid', array(':fid' => $saved_file->fid))->fetch(PDO::FETCH_OBJ);
+    $loaded_file = db_query('SELECT * FROM {file_managed} f WHERE f.fid = :fid', array(':fid' => $saved_file->fid))->fetch(PDO::FETCH_OBJ);
     $this->assertNotNull($loaded_file, t("Record still exists in the database."), 'File');
     $this->assertEqual($loaded_file->status, $saved_file->status, t("Status was saved correctly."));
   }
@@ -1946,10 +1946,13 @@ class FileDownloadTest extends FileTestCase {
   function testFileCreateUrl() {
     global $base_url;
 
-    $basename = " -._~!$'\"()*@[]?&+%#,;=:\n\x00" . // "Special" ASCII characters.
+    // Tilde (~) is excluded from this test because it is encoded by
+    // rawurlencode() in PHP 5.2 but not in PHP 5.3, as per RFC 3986.
+    // @see http://www.php.net/manual/en/function.rawurlencode.php#86506
+    $basename = " -._!$'\"()*@[]?&+%#,;=:\n\x00" . // "Special" ASCII characters.
       "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
       "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
-    $basename_encoded = '%20-._%7E%21%24%27%22%28%29%2A%40%5B%5D%3F%26%2B%25%23%2C%3B%3D%3A__' .
+    $basename_encoded = '%20-._%21%24%27%22%28%29%2A%40%5B%5D%3F%26%2B%25%23%2C%3B%3D%3A__' .
       '%2523%2525%2526%252B%252F%253F' .
       '%C3%A9%C3%B8%C3%AF%D0%B2%CE%B2%E4%B8%AD%E5%9C%8B%E6%9B%B8%DB%9E';
 
diff --git a/modules/simpletest/tests/file_test.info b/modules/simpletest/tests/file_test.info
index d29efd7b3e9d8e3117d2e882f6e75f901c4dd1ed..53fa3ce897b8051982db7281c4399ebd289eb95d 100644
--- a/modules/simpletest/tests/file_test.info
+++ b/modules/simpletest/tests/file_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = file_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/file_test.module b/modules/simpletest/tests/file_test.module
index 2f761054cb0702e0405c575ea8b59d9a61b7060e..8c531f2d0f20508cfd86bf3d849c49f68933bb90 100644
--- a/modules/simpletest/tests/file_test.module
+++ b/modules/simpletest/tests/file_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: file_test.module,v 1.20 2010/01/29 01:59:32 webchick Exp $
+// $Id: file_test.module,v 1.21 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -99,7 +99,8 @@ function _file_test_form_submit(&$form, &$form_state) {
 /**
  * Reset/initialize the history of calls to the file_* hooks.
  *
- * @see the getter/setter functions file_test_get_calls() and file_test_reset().
+ * @see file_test_get_calls() 
+ * @see file_test_reset()
  */
 function file_test_reset() {
   // Keep track of calls to these hooks
@@ -116,7 +117,7 @@ function file_test_reset() {
   );
   variable_set('file_test_results', $results);
 
-  // These hooks will return these values, @see file_test_set_return().
+  // These hooks will return these values, see file_test_set_return().
   $return = array(
     'validate' => array(),
     'download' => NULL,
@@ -132,9 +133,12 @@ function file_test_reset() {
  * @param $op
  *   One of the hook_file_* operations: 'load', 'validate', 'download',
  *   'references', 'insert', 'update', 'copy', 'move', 'delete'.
+ *
  * @return
  *   Array of the parameters passed to each call.
- * @see _file_test_log_call() and file_test_reset()
+ *
+ * @see _file_test_log_call()
+ * @see file_test_reset()
  */
 function file_test_get_calls($op) {
   $results = variable_get('file_test_results', array());
@@ -161,7 +165,9 @@ function file_test_get_all_calls() {
  *   'references', 'insert', 'update', 'copy', 'move', 'delete'.
  * @param $args
  *   Values passed to hook.
- * @see file_test_get_calls() and file_test_reset()
+ *
+ * @see file_test_get_calls()
+ * @see file_test_reset()
  */
 function _file_test_log_call($op, $args) {
   $results = variable_get('file_test_results', array());
@@ -174,9 +180,12 @@ function _file_test_log_call($op, $args) {
  *
  * @param $op
  *   One of the hook_file_[validate,download,references] operations.
+ *
  * @return
  *   Value set by file_test_set_return().
-* @see file_test_set_return() and file_test_reset().
+ *
+ * @see file_test_set_return()
+ * @see file_test_reset()
  */
 function _file_test_get_return($op) {
   $return = variable_get('file_test_return', array($op => NULL));
@@ -190,7 +199,9 @@ function _file_test_get_return($op) {
  *   One of the hook_file_[validate,download,references] operations.
  * @param $value
  *   Value for the hook to return.
- * @see _file_test_get_return() and file_test_reset().
+ *
+ * @see _file_test_get_return()
+ * @see file_test_reset()
  */
 function file_test_set_return($op, $value) {
   $return = variable_get('file_test_return', array());
diff --git a/modules/simpletest/tests/filter_test.info b/modules/simpletest/tests/filter_test.info
index 74a55bb6ddefd051f056fd6fada638cb850b80d7..965c1eb8124229a4741598f7dc809ad839be9fbd 100644
--- a/modules/simpletest/tests/filter_test.info
+++ b/modules/simpletest/tests/filter_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = filter_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test
index 5b3535dca3576bc0eb7cab29ad05a2b937261b27..cf4765fdb980f4a4c424d21fd443621023f5273e 100644
--- a/modules/simpletest/tests/form.test
+++ b/modules/simpletest/tests/form.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: form.test,v 1.39 2010/03/12 14:38:37 dries Exp $
+// $Id: form.test,v 1.48 2010/04/11 19:00:27 dries Exp $
 
 /**
  * @file
@@ -123,7 +123,7 @@ class FormsTestCase extends DrupalWebTestCase {
   /**
    * Test default value handling for checkboxes.
    *
-   * @see _form_test_checkbox().
+   * @see _form_test_checkbox()
    */
   function testCheckboxProcessing() {
     // First, try to submit without the required checkbox.
@@ -153,26 +153,57 @@ class FormsTestCase extends DrupalWebTestCase {
   /**
    * Test handling of disabled elements.
    *
-   * @see _form_test_disabled_elements().
+   * @see _form_test_disabled_elements()
    */
   function testDisabledElements() {
-    // Submit the form, and fetch the default values.
-    $this->drupalPost('form-test/disabled-elements', array(), t('Submit'));
-    $returned_values = drupal_json_decode($this->content);
-
-    // Get the default value from the form.
+    // Get the raw form in its original state.
     $form_state = array();
     $form = _form_test_disabled_elements(array(), $form_state);
 
+    // Build a submission that tries to hijack the form by submitting input for
+    // elements that are disabled.
+    $edit = array();
     foreach (element_children($form) as $key) {
-      if (isset($form[$key]['#default_value'])) {
-        $expected_value = $form[$key]['#default_value'];
+      if (isset($form[$key]['#test_hijack_value'])) {
+        if (is_array($form[$key]['#test_hijack_value'])) {
+          foreach ($form[$key]['#test_hijack_value'] as $subkey => $value) {
+            $edit[$key . '[' . $subkey . ']'] = $value;
+          }
+        }
+        else {
+          $edit[$key] = $form[$key]['#test_hijack_value'];
+        }
+      }
+    }
 
-        if ($key == 'checkboxes_multiple') {
-          // Checkboxes values are not filtered out.
-          $returned_values[$key] = array_filter($returned_values[$key]);
+    // Submit the form with no input, as the browser does for disabled elements,
+    // and fetch the $form_state['values'] that is passed to the submit handler.
+    $this->drupalPost('form-test/disabled-elements', array(), t('Submit'));
+    $returned_values['normal'] = drupal_json_decode($this->content);
+
+    // Do the same with input, as could happen if JavaScript un-disables an
+    // element. drupalPost() emulates a browser by not submitting input for
+    // disabled elements, so we need to un-disable those elements first.
+    $this->drupalGet('form-test/disabled-elements');
+    foreach ($this->xpath('//*[@disabled]') as $element) {
+      unset($element['disabled']);
+    }
+    $this->drupalPost(NULL, $edit, t('Submit'));
+    $returned_values['hijacked'] = drupal_json_decode($this->content);
+
+    // Ensure that the returned values match the form's default values in both
+    // cases.
+    foreach ($returned_values as $type => $values) {
+      foreach (element_children($form) as $key) {
+        if (isset($form[$key]['#default_value'])) {
+          $expected_value = $form[$key]['#default_value'];
+
+          if ($key == 'checkboxes_multiple') {
+            // Checkboxes values are not filtered out.
+            $values[$key] = array_filter($values[$key]);
+          }
+          $this->assertIdentical($expected_value, $values[$key], t('Default value for %type: expected %expected, returned %returned.', array('%type' => $key, '%expected' => var_export($expected_value, TRUE), '%returned' => var_export($values[$key], TRUE))));
         }
-        $this->assertIdentical($expected_value, $returned_values[$key], t('Default value for %type: expected %expected, returned %returned.', array('%type' => $key, '%expected' => var_export($expected_value, TRUE), '%returned' => var_export($returned_values[$key], TRUE))));
       }
     }
   }
@@ -305,6 +336,9 @@ class FormsElementsLabelsTestCase extends DrupalWebTestCase {
     $elements = $this->xpath('//input[@id="edit-form-textfield-test-title-after"]/following-sibling::label[@for="edit-form-textfield-test-title-after" and @class="option"]');
     $this->assertTrue(isset($elements[0]), t("Label after field and label option class correct for text field."));
 
+    $elements = $this->xpath('//input[@id="edit-form-textfield-test-title-invisible"]/following-sibling::label[@for="edit-form-textfield-test-title-invisible" and @class="element-invisible"]');
+    $this->assertTrue(isset($elements[0]), t("Label after field and label class is element-invisible."));
+
     $elements = $this->xpath('//label[@for="edit-form-textfield-test-title-no-show"]');
     $this->assertFalse(isset($elements[0]), t("No label tag when title set not to display."));
   }
@@ -783,6 +817,56 @@ class FormsRebuildTestCase extends DrupalWebTestCase {
     $this->assertNoFieldChecked('edit-checkbox-2-default-off', t('A newly added checkbox was initialized with a default unchecked state.'));
     $this->assertFieldById('edit-text-2', 'DEFAULT 2', t('A newly added textfield was initialized with its default value.'));
   }
+
+  /**
+   * Tests that a form's action is retained after an AJAX submission.
+   *
+   * The 'action' attribute of a form should not change after an AJAX submission
+   * followed by a non-AJAX submission, which triggers a validation error.
+   */
+  function testPreserveFormActionAfterAJAX() {
+    // Create a multi-valued field for 'page' nodes to use for AJAX testing.
+    $field_name = 'field_ajax_test';
+    $field = array(
+      'field_name' => $field_name,
+      'type' => 'text',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+    );
+    field_create_field($field);
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    );
+    field_create_instance($instance);
+
+    // Log in a user who can create 'page' nodes.
+    $this->web_user = $this->drupalCreateUser(array('create page content'));
+    $this->drupalLogin($this->web_user);
+
+    // Get the form for adding a 'page' node. Submit an "add another item" AJAX
+    // submission and verify it worked by ensuring the updated page has two text
+    // field items in the field for which we just added an item.
+    $this->drupalGet('node/add/page');
+    preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $this->content, $matches);
+    $settings = drupal_json_decode($matches[1]);
+    $button = $this->xpath('//input[@name="field_ajax_test_add_more"]');
+    $button_id = (string) $button[0]['id'];
+    $this->drupalPostAJAX(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'page-node-form', $settings['ajax'][$button_id]);
+    $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test")]//input[@type="text"]')) == 2, t('AJAX submission succeeded.'));
+
+    // Submit the form with the non-AJAX "Save" button, leaving the title field
+    // blank to trigger a validation error, and ensure that a validation error
+    // occurred, because this test is for testing what happens when a form is
+    // re-rendered without being re-built, which is what happens when there's
+    // a validation error.
+    $this->drupalPost(NULL, array(), t('Save'));
+    $this->assertText('Title field is required.', t('Non-AJAX submission correctly triggered a validation error.'));
+
+    // Ensure that the form's action is correct.
+    $forms = $this->xpath('//form[contains(@class, "node-page-form")]');
+    $this->assert(count($forms) == 1 && $forms[0]['action'] == url('node/add/page'), t('Re-rendered form contains the correct action value.'));
+  }
 }
 
 /**
@@ -790,7 +874,7 @@ class FormsRebuildTestCase extends DrupalWebTestCase {
  */
 class FormsProgrammaticTestCase extends DrupalWebTestCase {
 
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Programmatic form submissions',
       'description' => 'Test the programmatic form submission behavior.',
@@ -849,3 +933,140 @@ class FormsProgrammaticTestCase extends DrupalWebTestCase {
     }
   }
 }
+
+/**
+ * Test that FAPI correctly determines $form_state['clicked_button'].
+ */
+class FormsClickedButtonTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Form clicked button determination',
+      'description' => 'Test the determination of $form_state[\'clicked_button\'].',
+      'group' => 'Form API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('form_test');
+  }
+
+  /**
+   * Test the determination of $form_state['clicked_button'] when no button
+   * information is included in the POST data, as is sometimes the case when
+   * the ENTER key is pressed in a textfield in Internet Explorer.
+   */
+  function testNoButtonInfoInPost() {
+    $path = 'form-test/clicked-button';
+    $edit = array();
+    $form_id = 'form-test-clicked-button';
+
+    // Ensure submitting a form with no buttons results in no
+    // $form_state['clicked_button'] and the form submit handler not running.
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path, $edit, NULL, array(), array(), $form_id);
+    $this->assertText('There is no clicked button.', t('$form_state[\'clicked_button\'] set to NULL.'));
+    $this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.'));
+
+    // Ensure submitting a form with one or more submit buttons results in
+    // $form_state['clicked_button'] being set to the first one the user has
+    // access to. An argument with 'r' in it indicates a restricted
+    // (#access=FALSE) button.
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path . '/s', $edit, NULL, array(), array(), $form_id);
+    $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to only button.'));
+    $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path . '/s/s', $edit, NULL, array(), array(), $form_id);
+    $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+    $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path . '/rs/s', $edit, NULL, array(), array(), $form_id);
+    $this->assertText('The clicked button is button2.', t('$form_state[\'clicked_button\'] set to first available button.'));
+    $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
+
+    // Ensure submitting a form with buttons of different types results in
+    // $form_state['clicked_button'] being set to the first button, regardless
+    // of type. For the FAPI 'button' type, this should result in the submit
+    // handler not executing. The types are 's'(ubmit), 'b'(utton), and
+    // 'i'(mage_button).
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path . '/s/b/i', $edit, NULL, array(), array(), $form_id);
+    $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+    $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path . '/b/s/i', $edit, NULL, array(), array(), $form_id);
+    $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+    $this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.'));
+    drupal_static_reset('drupal_html_id');
+    $this->drupalPost($path . '/i/s/b', $edit, NULL, array(), array(), $form_id);
+    $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+    $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
+  }
+}
+
+
+/**
+ * Tests rebuilding of arbitrary forms by altering them.
+ */
+class FormsArbitraryRebuildTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Rebuild arbitrary forms',
+      'description' => 'Tests altering forms to be rebuilt so there are multiple steps.',
+      'group' => 'Form API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('form_test');
+    // Auto-create a field for testing.
+    $field = array(
+      'field_name' => 'test_multiple',
+      'type' => 'text',
+      'cardinality' => -1,
+      'translatable' => FALSE,
+    );
+    field_create_field($field);
+
+    $instance = array(
+      'entity_type' => 'node',
+      'field_name' => 'test_multiple',
+      'bundle' => 'page',
+      'label' => 'Test a multiple valued field',
+      'widget' => array(
+        'type' => 'text_textfield',
+        'weight' => 0,
+      ),
+    );
+    field_create_instance($instance);
+  }
+
+  /**
+   * Tests a basic rebuild with the user registration form.
+   */
+  function testUserRegistrationRebuild() {
+    $edit = array(
+      'name' => 'foo',
+      'mail' => 'bar@example.com',
+    );
+    $this->drupalPost('user/register', $edit, 'Rebuild');
+    $this->assertText('Form rebuilt.');
+    $this->assertFieldByName('name', 'foo', 'Entered user name has been kept.');
+    $this->assertFieldByName('mail', 'bar@example.com', 'Entered mail address has been kept.');
+  }
+
+  /**
+   * Tests a rebuild caused by a multiple value field.
+   */
+  function testUserRegistrationMultipleField() {
+    $edit = array(
+      'name' => 'foo',
+      'mail' => 'bar@example.com',
+    );
+    $this->drupalPost('user/register', $edit, t('Add another item'), array('query' => array('field' => TRUE)));
+    $this->assertText('Test a multiple valued field', 'Form has been rebuilt.');
+    $this->assertFieldByName('name', 'foo', 'Entered user name has been kept.');
+    $this->assertFieldByName('mail', 'bar@example.com', 'Entered mail address has been kept.');
+  }
+}
diff --git a/modules/simpletest/tests/form_test.info b/modules/simpletest/tests/form_test.info
index 97cbe90a3ee19a82e4a0697ba1c423a623f78ca8..3c81f8f8dde586f465d1a7c7e7ae44c8723d5d4a 100644
--- a/modules/simpletest/tests/form_test.info
+++ b/modules/simpletest/tests/form_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = form_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module
index 9f44497dab7026c3291c6d62f7d27640f887a733..9f8094c4aefb0c389920dbfeb3457637ec520579 100644
--- a/modules/simpletest/tests/form_test.module
+++ b/modules/simpletest/tests/form_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: form_test.module,v 1.30 2010/03/07 07:49:26 webchick Exp $
+// $Id: form_test.module,v 1.37 2010/04/11 19:00:27 dries Exp $
 
 /**
  * @file
@@ -118,6 +118,26 @@ function form_test_menu() {
     'type' => MENU_CALLBACK,
   );
 
+  $items['form-test/clicked-button'] = array(
+    'title' => 'Clicked button test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_clicked_button'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+
+  if (module_exists('node')) {
+    $items['form-test/two-instances-of-same-form'] = array(
+      'title' => 'AJAX test with two form instances',
+      'page callback' => 'form_test_two_instances',
+      'access callback' => 'node_access',
+      'access arguments' => array('create', 'page'),
+      'file path' => drupal_get_path('module', 'node'),
+      'file' => 'node.pages.inc',
+      'type' => MENU_CALLBACK,
+    );
+  }
+
   return $items;
 }
 
@@ -378,7 +398,7 @@ function _form_test_tableselect_js_select_form($form, $form_state, $action) {
  * request parameter "cache" the form can be tested with caching enabled, as
  * it would be the case, if the form would contain some #ajax callbacks.
  *
- * @see form_test_storage_form_submit().
+ * @see form_test_storage_form_submit()
  */
 function form_test_storage_form($form, &$form_state) {
   if ($form_state['rebuild']) {
@@ -488,10 +508,10 @@ function form_test_storage_form_submit($form, &$form_state) {
   $form_state['redirect'] = 'node';
 }
 
- /**
+/**
  * A form for testing form labels and required marks.
  */
-function form_label_test_form(&$form_state) {
+function form_label_test_form() {
   $form['form_checkboxes_test'] = array(
     '#type' => 'checkboxes',
     '#title' => t('Checkboxes test'),
@@ -536,6 +556,11 @@ function form_label_test_form(&$form_state) {
     '#title' => t('Textfield test for title after element'),
     '#title_display' => 'after',
   );
+  $form['form_textfield_test_title_invisible'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Textfield test for invisible title'),
+    '#title_display' => 'invisible',
+  );
   // Textfield test for title set not to display
   $form['form_textfield_test_title_no_show'] = array(
     '#type' => 'textfield',
@@ -681,6 +706,7 @@ function _form_test_disabled_elements($form, &$form_state) {
       '#type' => $type,
       '#title' => $type,
       '#default_value' => $type,
+      '#test_hijack_value' => 'HIJACK',
       '#disabled' => TRUE,
     );
   }
@@ -696,6 +722,9 @@ function _form_test_disabled_elements($form, &$form_state) {
       ),
       '#multiple' => TRUE,
       '#default_value' => array('test_2' => 'test_2'),
+      // The keys of #test_hijack_value need to match the #name of the control.
+      // @see FormsTestCase::testDisabledElements()
+      '#test_hijack_value' => $type == 'select' ? array('' => 'test_1') : array('test_1' => 'test_1'),
       '#disabled' => TRUE,
     );
   }
@@ -711,6 +740,7 @@ function _form_test_disabled_elements($form, &$form_state) {
       ),
       '#multiple' => FALSE,
       '#default_value' => 'test_2',
+      '#test_hijack_value' => 'test_1',
       '#disabled' => TRUE,
     );
   }
@@ -722,6 +752,7 @@ function _form_test_disabled_elements($form, &$form_state) {
       '#title' => $type . ' (unchecked)',
       '#return_value' => 1,
       '#default_value' => 0,
+      '#test_hijack_value' => 1,
       '#disabled' => TRUE,
     );
     $form[$type . '_checked'] = array(
@@ -729,6 +760,7 @@ function _form_test_disabled_elements($form, &$form_state) {
       '#title' => $type . ' (checked)',
       '#return_value' => 1,
       '#default_value' => 1,
+      '#test_hijack_value' => NULL,
       '#disabled' => TRUE,
     );
   }
@@ -738,6 +770,7 @@ function _form_test_disabled_elements($form, &$form_state) {
     '#type' => 'weight',
     '#title' => 'weight',
     '#default_value' => 10,
+    '#test_hijack_value' => 5,
     '#disabled' => TRUE,
   );
 
@@ -751,6 +784,11 @@ function _form_test_disabled_elements($form, &$form_state) {
       'month' => 11,
       'year' => 1978,
     ),
+    '#test_hijack_value' => array(
+      'day' => 20,
+      'month' => 12,
+      'year' => 1979,
+    ),
   );
 
   $form['submit'] = array(
@@ -922,3 +960,125 @@ function form_test_programmatic_form_validate($form, &$form_state) {
 function form_test_programmatic_form_submit($form, &$form_state) {
   $form_state['storage']['programmatic_form_submit'] = $form_state['values']['submitted_field'];
 }
+
+/**
+ * Form builder to test button click detection.
+ */
+function form_test_clicked_button($form, &$form_state) {
+  // A single text field. In IE, when a form has only one non-button input field
+  // and the ENTER key is pressed while that field has focus, the form is
+  // submitted without any information identifying the button responsible for
+  // the submission. In other browsers, the form is submitted as though the
+  // first button were clicked.
+  $form['text'] = array(
+    '#title' => 'Text',
+    '#type' => 'textfield',
+  );
+
+  // Loop through each path argument, addding buttons based on the information
+  // in the argument. For example, if the path is
+  // form-test/clicked-button/s/i/rb, then 3 buttons are added: a 'submit', an
+  // 'image_button', and a 'button' with #access=FALSE. This enables form.test
+  // to test a variety of combinations.
+  $i=0;
+  $args = array_slice(arg(), 2);
+  foreach ($args as $arg) {
+    $name = 'button' . ++$i;
+    // 's', 'b', or 'i' in the argument define the button type wanted.
+    if (strpos($arg, 's') !== FALSE) {
+      $type = 'submit';
+    }
+    elseif (strpos($arg, 'b') !== FALSE) {
+      $type = 'button';
+    }
+    elseif (strpos($arg, 'i') !== FALSE) {
+      $type = 'image_button';
+    }
+    else {
+      $type = NULL;
+    }
+    if (isset($type)) {
+      $form[$name] = array(
+        '#type' => $type,
+        '#name' => $name,
+      );
+      // Image buttons need a #src; the others need a #value.
+      if ($type == 'image_button') {
+        $form[$name]['#src'] = 'misc/druplicon.png';
+      }
+      else {
+        $form[$name]['#value'] = $name;
+      }
+      // 'r' for restricted, so we can test that button click detection code
+      // correctly takes #access security into account.
+      if (strpos($arg, 'r') !== FALSE) {
+        $form[$name]['#access'] = FALSE;
+      }
+    }
+  }
+
+  return $form;
+}
+
+/**
+ * Form validation handler for the form_test_clicked_button() form.
+ */
+function form_test_clicked_button_validate($form, &$form_state) {
+  if (isset($form_state['clicked_button'])) {
+    drupal_set_message(t('The clicked button is %name.', array('%name' => $form_state['clicked_button']['#name'])));
+  }
+  else {
+    drupal_set_message('There is no clicked button.');
+  }
+}
+
+/**
+ * Form submit handler for the form_test_clicked_button() form.
+ */
+function form_test_clicked_button_submit($form, &$form_state) {
+  drupal_set_message('Submit handler for form_test_clicked_button executed.');
+}
+
+
+/**
+ * Implements hook_form_FORM_ID_alter() for the registration form.
+ */
+function form_test_form_user_register_form_alter(&$form, &$form_state) {
+  $form['test_rebuild'] = array(
+    '#type' => 'submit',
+    '#value' => t('Rebuild'),
+    '#submit' => array('form_test_user_register_form_rebuild'),
+  );
+  // If requested, add the test field by attaching the node page form.
+  if (!empty($_REQUEST['field'])) {
+    $node = (object)array('type' => 'page');
+    field_attach_form('node', $node, $form, $form_state);
+    // The form API requires the builder function to set rebuilding, so do so.
+    $form['#builder_function'] = 'form_test_user_register_form_rebuild';
+  }
+}
+
+/**
+ * Submit callback that just lets the form rebuild.
+ */
+function form_test_user_register_form_rebuild($form, &$form_state) {
+  drupal_set_message('Form rebuilt.');
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Menu callback that returns two instances of the node form.
+ */
+function form_test_two_instances() {
+  global $user;
+  $node1 = (object) array(
+    'uid' => $user->uid,
+    'name' => (isset($user->name) ? $user->name : ''),
+    'type' => 'page',
+    'language' => LANGUAGE_NONE,
+  );
+  $node2 = clone($node1);
+  $return['node_form_1'] = drupal_get_form('page_node_form', $node1);
+  $return['node_form_2'] = drupal_get_form('page_node_form', $node2);
+  return $return;
+}
diff --git a/modules/simpletest/tests/image.test b/modules/simpletest/tests/image.test
index 2ddcfd8d98fe85e2956a30cc3c5f6f046d84d093..d1b7fc79c943d68800df24f2f94e0d8418c075fe 100644
--- a/modules/simpletest/tests/image.test
+++ b/modules/simpletest/tests/image.test
@@ -1,9 +1,9 @@
 <?php
-// $Id: image.test,v 1.12 2009/12/28 14:00:01 dries Exp $
+// $Id: image.test,v 1.13 2010/04/23 05:10:35 webchick Exp $
 
 /**
  * @file
- * Unit tests for the Drupal Form API.
+ * Tests for core image handling API.
  */
 
 /**
diff --git a/modules/simpletest/tests/image_test.info b/modules/simpletest/tests/image_test.info
index f6d301dc4beb775d85545b0cf9b4611a8c531176..99aeaff517b2ae0ac5e7456ce623084c6946e769 100644
--- a/modules/simpletest/tests/image_test.info
+++ b/modules/simpletest/tests/image_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = image_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/image_test.module b/modules/simpletest/tests/image_test.module
index ff289a8a108fc57dde77487b447697546ac415e0..6f88ef316fca9b2687dafba0dfd7693d23fdf73a 100644
--- a/modules/simpletest/tests/image_test.module
+++ b/modules/simpletest/tests/image_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: image_test.module,v 1.5 2009/12/04 16:49:47 dries Exp $
+// $Id: image_test.module,v 1.6 2010/03/26 17:14:45 dries Exp $
 
 /**
  * @file
@@ -25,7 +25,7 @@ function image_test_image_toolkits() {
 /**
  * Reset/initialize the history of calls to the toolkit functions.
  *
- * @see image_test_get_all_calls().
+ * @see image_test_get_all_calls()
  */
 function image_test_reset() {
   // Keep track of calls to these operations
@@ -62,6 +62,7 @@ function image_test_get_all_calls() {
  *   'settings', 'resize', 'rotate', 'crop', 'desaturate'.
  * @param $args
  *   Values passed to hook.
+ *
  * @see image_test_get_all_calls()
  * @see image_test_reset()
  */
diff --git a/modules/simpletest/tests/mail.test b/modules/simpletest/tests/mail.test
index 0eee1a581919130ba7c277aaf4bcd02b5e43210d..f5cb76a577c948180a48b9e40b0d1adbca838f3c 100644
--- a/modules/simpletest/tests/mail.test
+++ b/modules/simpletest/tests/mail.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: mail.test,v 1.2 2009/10/16 03:01:54 dries Exp $
+// $Id: mail.test,v 1.3 2010/04/11 18:33:44 dries Exp $
 
 /**
  * Test the Drupal mailing system.
@@ -13,7 +13,7 @@ class MailTestCase extends DrupalWebTestCase implements MailSystemInterface {
    */
   private static $sent_message;
 
-  function getInfo() {
+  public static function getInfo() {
     return array(
       'name' => 'Mail system',
       'description' => 'Performs tests on the pluggable mailing framework.',
diff --git a/modules/simpletest/tests/menu.test b/modules/simpletest/tests/menu.test
index 53ee3b7c7876a740488df6e906b44bbc47f57cea..0f29b0ff7470686408adfa5d354f5ed536306335 100644
--- a/modules/simpletest/tests/menu.test
+++ b/modules/simpletest/tests/menu.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: menu.test,v 1.28 2010/01/30 03:38:22 webchick Exp $
+// $Id: menu.test,v 1.29 2010/04/26 14:06:23 dries Exp $
 
 /**
  * @file
@@ -51,6 +51,15 @@ class MenuRouterTestCase extends DrupalWebTestCase {
     $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
   }
 
+  /**
+   * Test that 'page callback', 'file' and 'file path' keys are properly
+   * inherited from parent menu paths.
+   */
+  function testFileInheritance() {
+    $this->drupalGet('admin/config/development/file-inheritance');
+    $this->assertText('File inheritance test description', t('File inheritance works.'));
+  }
+
   /**
    * Test path containing "exotic" characters.
    */
diff --git a/modules/simpletest/tests/menu_test.info b/modules/simpletest/tests/menu_test.info
index da6dab0c402a46eace16ac21d2fca29caafa40a1..9a39315f8a4a4ef7e69a79d4e35ea41f3277f01f 100644
--- a/modules/simpletest/tests/menu_test.info
+++ b/modules/simpletest/tests/menu_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = menu_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/menu_test.module b/modules/simpletest/tests/menu_test.module
index a51d9fc8b60b7b6cc7520ea7db1f1f2fb92f966f..14699ae0d2aa5838bbf4f9fe45a6c3df7be4a3a4 100644
--- a/modules/simpletest/tests/menu_test.module
+++ b/modules/simpletest/tests/menu_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: menu_test.module,v 1.13 2010/01/30 03:38:22 webchick Exp $
+// $Id: menu_test.module,v 1.14 2010/04/26 14:06:23 dries Exp $
 
 /**
  * @file
@@ -173,6 +173,22 @@ function menu_test_menu() {
     'context' => MENU_CONTEXT_NONE,
   );
 
+  // File inheritance tests. This menu item should inherit the page callback
+  // system_admin_menu_block_page() and therefore render its children as links
+  // on the page.
+  $items['admin/config/development/file-inheritance'] = array(
+    'title' => 'File inheritance',
+    'description' => 'Test file inheritance',
+    'access arguments' => array('access content'),
+  );
+  $items['admin/config/development/file-inheritance/inherit'] = array(
+    'title' => 'Inherit',
+    'description' => 'File inheritance test description',
+    'page callback' => 'menu_test_callback',
+    'access arguments' => array('access content'),
+    'type' => MENU_LOCAL_TASK,
+  );
+
   return $items;
 }
 
diff --git a/modules/simpletest/tests/module.test b/modules/simpletest/tests/module.test
index 1a4753b0642ccd87b00ae1711ff8644a63796c96..a9b16c493022add02f6f9dcc98b5b773c223341d 100644
--- a/modules/simpletest/tests/module.test
+++ b/modules/simpletest/tests/module.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: module.test,v 1.20 2010/02/26 18:31:29 dries Exp $
+// $Id: module.test,v 1.21 2010/04/22 18:56:31 dries Exp $
 
 /**
  * @file
@@ -98,6 +98,17 @@ class ModuleUnitTest extends DrupalWebTestCase {
     cache_clear_all('module_implements', 'cache_bootstrap');
     $this->drupalGet('');
     $this->assertTrue(cache_get('module_implements', 'cache_bootstrap'), t('The module implements cache is populated after requesting a page.'));
+
+    // Make sure group include files are detected properly even when the file is
+    // already loaded when the cache is rebuilt.
+    // For that activate the module_test which provides the file to load.
+    module_enable(array('module_test'));
+
+    module_load_include('inc', 'module_test', 'module_test.file');
+    $modules = module_implements('test_hook');
+    $static = drupal_static('module_implements');
+    $this->assertTrue(in_array('module_test', $modules), 'Hook found.');
+    $this->assertEqual($static['test_hook']['module_test'], 'file', 'Include file detected.');
   }
 
   /**
diff --git a/modules/simpletest/tests/module_test.file.inc b/modules/simpletest/tests/module_test.file.inc
new file mode 100644
index 0000000000000000000000000000000000000000..ebdaf1f3874847cf5d8a1d5a4091fedb99ee729c
--- /dev/null
+++ b/modules/simpletest/tests/module_test.file.inc
@@ -0,0 +1,14 @@
+<?php
+// $Id: module_test.file.inc,v 1.1 2010/04/22 18:56:53 dries Exp $
+
+/**
+ * @file
+ * A file to test module_implements() loading includes.
+ */
+
+/**
+ * Implements hook_test_hook().
+ */
+function module_test_test_hook() {
+
+}
\ No newline at end of file
diff --git a/modules/simpletest/tests/module_test.info b/modules/simpletest/tests/module_test.info
index 276e5184e01dc333953e28320f900a3c1903a813..482d10f071193009986e44ffff1c1ca1b8fe876d 100644
--- a/modules/simpletest/tests/module_test.info
+++ b/modules/simpletest/tests/module_test.info
@@ -1,14 +1,15 @@
-; $Id: module_test.info,v 1.1 2009/07/01 08:39:55 dries Exp $
+; $Id: module_test.info,v 1.2 2010/04/22 18:56:31 dries Exp $
 name = "Module test"
 description = "Support module for module system testing."
 package = Testing
 version = VERSION
 core = 7.x
 files[] = module_test.module
+files[] = module_test.file.inc
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/module_test.module b/modules/simpletest/tests/module_test.module
index c074baa3c433a35157e51b14b2c9251fa318e62c..c84a45758697feedce727aa06b676037fa500bee 100644
--- a/modules/simpletest/tests/module_test.module
+++ b/modules/simpletest/tests/module_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: module_test.module,v 1.5 2010/01/30 07:59:25 dries Exp $
+// $Id: module_test.module,v 1.6 2010/04/22 18:56:31 dries Exp $
 
 /**
  * Implements hook_permission().
@@ -37,3 +37,13 @@ function module_test_system_info_alter(&$info, $file, $type) {
     }
   }
 }
+
+/**
+ * Implements hook_hook_info().
+ */
+function module_test_hook_info() {
+  $hooks['test_hook'] = array(
+    'group' => 'file',
+  );
+  return $hooks;
+}
diff --git a/modules/simpletest/tests/path.test b/modules/simpletest/tests/path.test
index 39f64716215e7f458a3836eae3b708f0021d1c41..5fb8ced72e6f3c1664a89e5f9357dc9c6f7bf00f 100644
--- a/modules/simpletest/tests/path.test
+++ b/modules/simpletest/tests/path.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: path.test,v 1.4 2010/01/29 22:40:41 dries Exp $
+// $Id: path.test,v 1.5 2010/04/06 19:49:03 dries Exp $
 
 /**
  * @file
@@ -177,11 +177,10 @@ class UrlAlterFunctionalTest extends DrupalWebTestCase {
     $this->assertUrlInboundAlter("user/$uid", "user/$uid");
     $this->assertUrlOutboundAlter("user/$uid", "user/$uid");
 
-    // Test that 'forum' is altered to 'community' correctly.
+    // Test that 'forum' is altered to 'community' correctly, both at the root
+    // level and for a specific existing forum.
     $this->assertUrlInboundAlter('community', 'forum');
     $this->assertUrlOutboundAlter('forum', 'community');
-
-    // Add a forum to test url altering.
     $forum_vid = db_query("SELECT vid FROM {taxonomy_vocabulary} WHERE module = 'forum'")->fetchField();
     $tid = db_insert('taxonomy_term_data')
       ->fields(array(
@@ -189,15 +188,8 @@ class UrlAlterFunctionalTest extends DrupalWebTestCase {
         'vid' => $forum_vid,
       ))
       ->execute();
-
-    // Test that a existing forum URL is altered.
     $this->assertUrlInboundAlter("community/$tid", "forum/$tid");
-    $this->assertUrlOutboundAlter("taxonomy/term/$tid", "community/$tid");
-
-    // Test that a non-existant forum URL is not altered.
-    $tid++;
-    $this->assertUrlInboundAlter("taxonomy/term/$tid", "taxonomy/term/$tid");
-    $this->assertUrlOutboundAlter("taxonomy/term/$tid", "taxonomy/term/$tid");
+    $this->assertUrlOutboundAlter("forum/$tid", "community/$tid");
   }
 
   /**
diff --git a/modules/simpletest/tests/schema.test b/modules/simpletest/tests/schema.test
index 3076ee7c5b5ce25da8ad980b3492045f52bb95a3..c8c297e3eb5952a22efd9591de6080727f5d1ad2 100644
--- a/modules/simpletest/tests/schema.test
+++ b/modules/simpletest/tests/schema.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: schema.test,v 1.18 2010/03/01 11:30:37 dries Exp $
+// $Id: schema.test,v 1.19 2010/03/28 11:45:11 dries Exp $
 
 /**
  * @file
@@ -170,7 +170,7 @@ class SchemaTestCase extends DrupalWebTestCase {
 
     // Finally, check each column and try to insert invalid values into them.
     foreach($table_spec['fields'] as $column_name => $column_spec) {
-      $this->assertTrue(db_column_exists($table_name, $column_name), t('Unsigned @type column was created.', array('@type' => $column_spec['type'])));
+      $this->assertTrue(db_field_exists($table_name, $column_name), t('Unsigned @type column was created.', array('@type' => $column_spec['type'])));
       $this->assertFalse($this->tryUnsignedInsert($table_name, $column_name), t('Unsigned @type column rejected a negative value.', array('@type' => $column_spec['type'])));
     }
   }
diff --git a/modules/simpletest/tests/session_test.info b/modules/simpletest/tests/session_test.info
index 207e1af55a5559a5b3129dced725bbd243dd7d43..d095ad61670e0687a7553d636f6e98137893a5b4 100644
--- a/modules/simpletest/tests/session_test.info
+++ b/modules/simpletest/tests/session_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = session_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/system_dependencies_test.info b/modules/simpletest/tests/system_dependencies_test.info
index 197105314be834268313b30142cc4615bc9b4764..abbb29d6b31f2b1e1ab65bf21c062150fe648aa1 100644
--- a/modules/simpletest/tests/system_dependencies_test.info
+++ b/modules/simpletest/tests/system_dependencies_test.info
@@ -8,8 +8,8 @@ files[] = system_dependencies_test.module
 hidden = TRUE
 dependencies[] = _missing_dependency
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/system_test.info b/modules/simpletest/tests/system_test.info
index f38519c4cf576e6ca7184a8fc294a7a1b156461b..a48c86a3278ca52365aae66442bffff55abac451 100644
--- a/modules/simpletest/tests/system_test.info
+++ b/modules/simpletest/tests/system_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = system_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/taxonomy_test.info b/modules/simpletest/tests/taxonomy_test.info
index f768607c3ef456f866c6a495deeb99038dd78863..4a252be84965314f70ba21956b594066b6a123ad 100644
--- a/modules/simpletest/tests/taxonomy_test.info
+++ b/modules/simpletest/tests/taxonomy_test.info
@@ -8,8 +8,8 @@ files[] = taxonomy_test.module
 hidden = TRUE
 dependencies[] = taxonomy
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/theme.test b/modules/simpletest/tests/theme.test
index 847376086db8043b7947388ae3c73e3513b6cffe..f69c2af419443c3c9cbe4521c13449fb0da044db 100644
--- a/modules/simpletest/tests/theme.test
+++ b/modules/simpletest/tests/theme.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: theme.test,v 1.14 2010/03/21 04:05:24 webchick Exp $
+// $Id: theme.test,v 1.15 2010/04/22 22:33:29 dries Exp $
 
 /**
  * @file
@@ -110,15 +110,22 @@ class ThemeTableUnitTest extends DrupalWebTestCase {
   }
 
   /**
-   * Tests that the table header is printed even if there are no rows. And that
-   * the empty text is displayed correctly.
+   * Tests that the table header is printed correctly even if there are no rows,
+   * and that the empty text is displayed correctly.
    */
   function testThemeTableWithEmptyMessage() {
-    $header = array(t('Header 1'), t('Header 2'));
+    $header = array(
+      t('Header 1'),
+      array(
+        'data' => t('Header 2'),
+        'colspan' => 2,
+      ),
+    );
     $this->content = theme('table', array('header' => $header, 'rows' => array(), 'empty' => t('No strings available.')));
-    $this->assertRaw('<tr class="odd"><td colspan="2" class="empty message">No strings available.</td>', t('Correct colspan was set on empty message.'));
+    $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">No strings available.</td>', t('Correct colspan was set on empty message.'));
     $this->assertRaw('<thead><tr><th>Header 1</th>', t('Table header was printed.'));
   }
+
 }
 
 /**
diff --git a/modules/simpletest/tests/theme_test.info b/modules/simpletest/tests/theme_test.info
index 09ab5e1c61bd826320b08df259b4c0c5e05c72bf..b08461b67e9e079a3b3f9fb0072c1b6e28e764c4 100644
--- a/modules/simpletest/tests/theme_test.info
+++ b/modules/simpletest/tests/theme_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = theme_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/update_test_1.info b/modules/simpletest/tests/update_test_1.info
index 69a58e345848f5b244becda6e85a93adf3f0209c..3a8d9c3254c3be83d194bfcb16e078bbd6a63637 100644
--- a/modules/simpletest/tests/update_test_1.info
+++ b/modules/simpletest/tests/update_test_1.info
@@ -8,8 +8,8 @@ files[] = update_test_1.module
 files[] = update_test_1.install
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/update_test_2.info b/modules/simpletest/tests/update_test_2.info
index f6c800ad56c4b2f91a29f28443053132d57056cb..c1c6c4d4c4c6a7db70e91f4d351871cbfb51061a 100644
--- a/modules/simpletest/tests/update_test_2.info
+++ b/modules/simpletest/tests/update_test_2.info
@@ -8,8 +8,8 @@ files[] = update_test_2.module
 files[] = update_test_2.install
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/update_test_3.info b/modules/simpletest/tests/update_test_3.info
index da62e4ef9c44f9baa11c76e27e6ca7a749cd524c..a95f31dad36a7c16d5530fe5126947c7ad996184 100644
--- a/modules/simpletest/tests/update_test_3.info
+++ b/modules/simpletest/tests/update_test_3.info
@@ -8,8 +8,8 @@ files[] = update_test_3.module
 files[] = update_test_3.install
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/url_alter_test.info b/modules/simpletest/tests/url_alter_test.info
index 2c6c0f43aea94035be3074abcc77992ddaa6274f..f24af5268a4c76d8a39e925186c9da8449145bf7 100644
--- a/modules/simpletest/tests/url_alter_test.info
+++ b/modules/simpletest/tests/url_alter_test.info
@@ -8,8 +8,8 @@ files[] = url_alter_test.module
 files[] = url_alter_test.install
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/simpletest/tests/xmlrpc.test b/modules/simpletest/tests/xmlrpc.test
index 2ded83760ce86e0f9f645aff49d46dfb6df940bf..42288250ce8e793a5eba42bb68893581f6757af4 100644
--- a/modules/simpletest/tests/xmlrpc.test
+++ b/modules/simpletest/tests/xmlrpc.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: xmlrpc.test,v 1.15 2009/12/02 00:31:47 dries Exp $
+// $Id: xmlrpc.test,v 1.16 2010/03/31 15:56:53 dries Exp $
 
 /**
  * Perform basic XML-RPC tests that do not require addition callbacks.
@@ -41,6 +41,34 @@ class XMLRPCBasicTestCase extends DrupalWebTestCase {
 
     $this->assertEqual($count, count($minimum), 'system.listMethods returned at least the minimum listing');
   }
+
+  /**
+   * Ensure that XML-RPC correctly handles invalid messages when parsing.
+   */
+  protected function testInvalidMessageParsing() {
+    $invalid_messages = array(
+      array(
+        'message' => xmlrpc_message(''),
+        'assertion' => t('Empty message correctly rejected during parsing.'),
+      ),
+      array(
+        'message' => xmlrpc_message('<?xml version="1.0" encoding="ISO-8859-1"?>'),
+        'assertion' => t('Empty message with XML declaration correctly rejected during parsing.'),
+      ),
+      array(
+        'message' => xmlrpc_message('<?xml version="1.0"?><params><param><value><string>value</string></value></param></params>'),
+        'assertion' => t('Non-empty message without a valid message type is rejected during parsing.'),
+      ),
+      array(
+        'message' => xmlrpc_message('<methodResponse><params><param><value><string>value</string></value></param></methodResponse>'),
+        'assertion' => t('Non-empty malformed message is rejected during parsing.'),
+      ),
+    );
+
+    foreach ($invalid_messages as $assertion) {
+      $this->assertFalse(xmlrpc_message_parse($assertion['message']), $assertion['assertion']);
+    }
+  }
 }
 
 class XMLRPCValidator1IncTestCase extends DrupalWebTestCase {
diff --git a/modules/simpletest/tests/xmlrpc_test.info b/modules/simpletest/tests/xmlrpc_test.info
index 02a2f05a511d574a0e7c78b83c768d345488517f..4537cb49369b1644b3e3a0845bc1925e4581c1cd 100644
--- a/modules/simpletest/tests/xmlrpc_test.info
+++ b/modules/simpletest/tests/xmlrpc_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = xmlrpc_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/statistics/statistics.info b/modules/statistics/statistics.info
index 9d77f92ca1273506a7ddb505279604de35f27a87..8329f376c37b55fcbd2b80eb7ff37ec216b8760b 100644
--- a/modules/statistics/statistics.info
+++ b/modules/statistics/statistics.info
@@ -12,8 +12,8 @@ files[] = statistics.test
 files[] = statistics.tokens.inc
 configure = admin/config/system/statistics
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/statistics/statistics.module b/modules/statistics/statistics.module
index 7824f81f59bba3a7b74f1cec09c053e16039bc6d..16b14fdd4e0a95f73a410a918a89746812ea5f71 100644
--- a/modules/statistics/statistics.module
+++ b/modules/statistics/statistics.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: statistics.module,v 1.332 2010/03/12 15:56:29 dries Exp $
+// $Id: statistics.module,v 1.333 2010/03/30 07:17:19 webchick Exp $
 
 /**
  * @file
@@ -174,6 +174,7 @@ function statistics_menu() {
     'page arguments' => array('statistics_settings_form'),
     'access arguments' => array('administer statistics'),
     'file' => 'statistics.admin.inc',
+    'weight' => -15,
   );
   $items['user/%user/track/navigation'] = array(
     'title' => 'Track page visits',
diff --git a/modules/statistics/statistics.test b/modules/statistics/statistics.test
index 5b5eaf91e1ef5e30fc2435291678d9d3be0b20c4..1b25a33ac68e1921bf3fedb6b9d017b556314d2a 100644
--- a/modules/statistics/statistics.test
+++ b/modules/statistics/statistics.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: statistics.test,v 1.16 2010/01/30 07:59:25 dries Exp $
+// $Id: statistics.test,v 1.17 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * Sets up a base class for the Statistics module.
@@ -312,3 +312,47 @@ class StatisticsAdminTestCase extends DrupalWebTestCase {
     $this->assertFalse($result, t('Daycounter is zero.'));
   }
 }
+
+/**
+ * Test statistics token replacement in strings.
+ */
+class StatisticsTokenReplaceTestCase extends StatisticsTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Statistics token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check statistics token replacement.',
+      'group' => 'Statistics',
+    );
+  }
+
+  /**
+   * Creates a node, then tests the statistics tokens generated from it.
+   */
+  function testStatisticsTokenReplacement() {
+    global $language;
+
+    // Create user and node.
+    $user = $this->drupalCreateUser(array('create page content'));
+    $this->drupalLogin($user);
+    $node = $this->drupalCreateNode(array('type' => 'page', 'uid' => $user->uid));
+
+    // Hit the node.
+    $this->drupalGet('node/' . $node->nid);
+    $statistics = statistics_get($node->nid);
+
+    // Generate and test tokens.
+    $tests = array();
+    $tests['[node:total-count]'] = 1;
+    $tests['[node:day-count]'] = 1;
+    $tests['[node:last-view]'] = format_date($statistics['timestamp']);
+    $tests['[node:last-view:short]'] = format_date($statistics['timestamp'], 'short');
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('node' => $node), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Statistics token %token replaced.', array('%token' => $input)));
+    }
+  }
+}
diff --git a/modules/statistics/statistics.tokens.inc b/modules/statistics/statistics.tokens.inc
index 942e3adb58a869589671b29cc98a433cff49e9b2..03d7ac8146a0ec5b4a9c8791fce3d43ed0c6b79b 100644
--- a/modules/statistics/statistics.tokens.inc
+++ b/modules/statistics/statistics.tokens.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: statistics.tokens.inc,v 1.3 2009/12/04 16:49:47 dries Exp $
+// $Id: statistics.tokens.inc,v 1.4 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -10,11 +10,11 @@
  * Implements hook_token_info().
  */
 function statistics_token_info() {
-  $node['views'] = array(
+  $node['total-count'] = array(
     'name' => t("Number of views"),
     'description' => t("The number of visitors who have read the node."),
   );
-  $node['day-views'] = array(
+  $node['day-count'] = array(
     'name' => t("Views today"),
     'description' => t("The number of visitors who have read the node today."),
   );
@@ -40,13 +40,13 @@ function statistics_tokens($type, $tokens, array $data = array(), array $options
     $node = $data['node'];
 
     foreach ($tokens as $name => $original) {
-      if ($name == 'views') {
+      if ($name == 'total-count') {
         $statistics = statistics_get($node->nid);
-        $replacements[$original] = $statistics['totalviews'];
+        $replacements[$original] = $statistics['totalcount'];
       }
-      elseif ($name == 'views-today') {
+      elseif ($name == 'day-count') {
         $statistics = statistics_get($node->nid);
-        $replacements[$original] = $statistics['dayviews'];
+        $replacements[$original] = $statistics['daycount'];
       }
       elseif ($name == 'last-view') {
         $statistics = statistics_get($node->nid);
diff --git a/modules/syslog/syslog.info b/modules/syslog/syslog.info
index 7aa3ef5251a49727fb16880b09ade9df5f224c29..f5891b8f764582610d44f8f229f8b5004a824443 100644
--- a/modules/syslog/syslog.info
+++ b/modules/syslog/syslog.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = syslog.module
 files[] = syslog.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/syslog/syslog.module b/modules/syslog/syslog.module
index 4279c036d9660dc513b96e453d4e4cf0b49bdba1..abe6e22c5883876f1302606ad5db286437a38110 100644
--- a/modules/syslog/syslog.module
+++ b/modules/syslog/syslog.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: syslog.module,v 1.33 2010/01/27 11:29:51 dries Exp $
+// $Id: syslog.module,v 1.35 2010/04/21 14:27:03 dries Exp $
 
 /**
  * @file
@@ -46,6 +46,12 @@ function syslog_form_system_logging_settings_alter(&$form, &$form_state) {
       '#options'       => syslog_facility_list(),
       '#description'   => t('Depending on the system configuration, Syslog and other logging tools use this code to identify or filter messages from within the entire system log.') . $help,
      );
+    $form['syslog_format'] = array(
+      '#type'          => 'textarea',
+      '#title'         => t('Syslog format'),
+      '#default_value' => variable_get('syslog_format', '!base_url|!timestamp|!type|!ip|!request_uri|!referer|!uid|!link|!message'),
+      '#description'   => t('Specify the format of the syslog entry. Available variables are: <dl><dt><code>!base_url</code></dt><dd>Base URL of the site.</dd><dt><code>!timestamp</code></dt><dd>Unix timestamp of the log entry.</dd><dt><code>!type</code></dt><dd>The category to which this message belongs.</dd><dt><code>!ip</code></dt><dd>IP address of the user triggering the message.</dd><dt><code>!request_uri</code></dt><dd>The requested URI.</dd><dt><code>!referer</code></dt><dd>HTTP Referer if available.</dd><dt><code>!uid</code></dt><dd>User ID.</dd><dt><code>!link</code></dt><dd>A link to associate with the message.</dd><dt><code>!message</code></dt><dd>The message to store in the log.</dd></dl>'),
+    );
     $form['actions']['#weight'] = 1;
   }
 }
@@ -72,6 +78,8 @@ function syslog_facility_list() {
  * Implements hook_watchdog().
  */
 function syslog_watchdog(array $log_entry) {
+  global $base_url;
+
   $log_init = &drupal_static(__FUNCTION__, FALSE);
 
   if (!$log_init) {
@@ -80,38 +88,17 @@ function syslog_watchdog(array $log_entry) {
     openlog('drupal', LOG_NDELAY, variable_get('syslog_facility', $default_facility));
   }
 
-  syslog($log_entry['severity'], theme('syslog_format', array('entry' => $log_entry)));
-}
-
-/**
- * Implements hook_theme().
- */
-function syslog_theme() {
-  return array(
-    'syslog_format' => array(
-      'variables' => array('entry' => NULL),
-    ),
-  );
-}
-
-/**
- * Format a system log entry.
- *
- * @ingroup themeable
- */
-function theme_syslog_format($variables) {
-  $entry = $variables['entry'];
-  global $base_url;
-
-  $message  = $base_url;
-  $message .= '|' . $entry['timestamp'];
-  $message .= '|' . $entry['type'];
-  $message .= '|' . $entry['ip'];
-  $message .= '|' . $entry['request_uri'];
-  $message .= '|' . $entry['referer'];
-  $message .= '|' . $entry['user']->uid;
-  $message .= '|' . strip_tags($entry['link']);
-  $message .= '|' . strip_tags(is_null($entry['variables']) ? $entry['message'] : strtr($entry['message'], $entry['variables']));
+  $message = strtr(variable_get('syslog_format', '!base_url|!timestamp|!type|!ip|!request_uri|!referer|!uid|!link|!message'), array(
+    '!base_url'    => $base_url,
+    '!timestamp'   => $log_entry['timestamp'],
+    '!type'        => $log_entry['type'],
+    '!ip'          => $log_entry['ip'],
+    '!request_uri' => $log_entry['request_uri'],
+    '!referer'     => $log_entry['referer'],
+    '!uid'         => $log_entry['user']->uid,
+    '!link'        => strip_tags($log_entry['link']),
+    '!message'     => strip_tags(is_null($log_entry['variables']) ? $log_entry['message'] : strtr($log_entry['message'], $log_entry['variables'])),
+  ));
 
-  return $message;
+  syslog($log_entry['severity'], $message);
 }
diff --git a/modules/syslog/syslog.test b/modules/syslog/syslog.test
index 59cc3962934e340ff438bbc5b26ad5e91f3587be..7d41b6e75833cc92df7c6b23039f733da29f2f68 100644
--- a/modules/syslog/syslog.test
+++ b/modules/syslog/syslog.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: syslog.test,v 1.11 2009/11/10 17:27:54 webchick Exp $
+// $Id: syslog.test,v 1.12 2010/03/31 20:05:06 dries Exp $
 
 class SyslogTestCase extends DrupalWebTestCase {
   public static function getInfo() {
@@ -29,7 +29,7 @@ class SyslogTestCase extends DrupalWebTestCase {
 
       $this->drupalGet('admin/config/development/logging');
       if ($this->parse()) {
-        $field = $this->xpath('//option[@value="' . LOG_LOCAL6 . '"]'); // Should be one field.
+        $field = $this->xpath('//option[@value=:value]', array(':value' => LOG_LOCAL6)); // Should be one field.
         $this->assertTrue($field[0]['selected'] == 'selected', t('Facility value saved.'));
       }
     }
diff --git a/modules/system/system-behavior.css b/modules/system/system-behavior.css
index 9f42ebf88dad1818c9d792fa00ce03f3206700e0..66e71a89baebb934e76ab327ec5ffedc876196dd 100644
--- a/modules/system/system-behavior.css
+++ b/modules/system/system-behavior.css
@@ -1,4 +1,4 @@
-/* $Id: system-behavior.css,v 1.6 2010/03/09 11:45:37 dries Exp $ */
+/* $Id: system-behavior.css,v 1.8 2010/04/11 18:45:47 dries Exp $ */
 
 /**
  * Autocomplete
@@ -43,38 +43,30 @@ html.js fieldset.collapsed {
   border-bottom-width: 0;
   border-left-width: 0;
   border-right-width: 0;
-  margin-bottom: 0;
   height: 1em;
 }
 html.js fieldset.collapsed .fieldset-wrapper {
   display: none;
 }
-html.js fieldset.collapsible .fieldset-legend {
+fieldset.collapsible {
+  position: relative;
+}
+fieldset.collapsible .fieldset-legend {
   display: block;
+}
+html.js fieldset.collapsible .fieldset-legend {
   padding-left: 15px; /* LTR */
-  background: url(../../misc/menu-expanded.png) 5px 75% no-repeat; /* LTR */
+  background: url(../../misc/menu-expanded.png) 5px 65% no-repeat; /* LTR */
 }
 html.js fieldset.collapsed .fieldset-legend {
   background-image: url(../../misc/menu-collapsed.png); /* LTR */
   background-position: 5px 50%; /* LTR */
 }
-html.js fieldset.collapsible legend span.summary {
+.fieldset-legend span.summary {
   font-size: 0.9em;
   color: #999;
   margin-left: 0.5em;
 }
-/* IE6: Fix due to '* html' (breaks Konqueror otherwise). */
-* html.js fieldset.collapsed table * {
-  display: inline;
-}
-/* Safari 2: Tables in collapsible fieldsets disappear due to tableheader.js. */
-html.js fieldset.collapsible {
-  position: relative;
-}
-/* Avoid jumping around due to margins collapsing into collapsible fieldset border */
-html.js fieldset.collapsible .fieldset-wrapper {
-  overflow: auto;
-}
 
 /**
  * Resizable textareas
diff --git a/modules/system/system-messages.css b/modules/system/system-messages.css
new file mode 100644
index 0000000000000000000000000000000000000000..90d43d07df5994a2e4b5fbd3725940e59515f173
--- /dev/null
+++ b/modules/system/system-messages.css
@@ -0,0 +1,36 @@
+/* $Id */
+
+div.messages {
+  background-color: #dfd;
+  color: #000;
+  margin-bottom: 0.25em;
+  padding: 0.25em 0.5em;
+}
+
+.error {
+  color: #e55;
+}
+
+div.error,
+tr.error {
+  background-color: #fcc;
+}
+
+.warning {
+  color: #e09010;
+}
+
+div.warning,
+tr.warning {
+  background-color: #fcfca7;
+}
+
+.ok {
+  color: #008000;
+}
+
+div.ok,
+tr.ok {
+  background-color: #dfd;
+  color: #020;
+}
\ No newline at end of file
diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc
index 91f9b355fdeb675cb04519457f32e3341052b478..cf6fd312dfb8ae8da5a7c3d3fc252a5cda80978a 100644
--- a/modules/system/system.admin.inc
+++ b/modules/system/system.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.admin.inc,v 1.262 2010/03/07 07:36:27 webchick Exp $
+// $Id: system.admin.inc,v 1.270 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -210,7 +210,7 @@ function _system_theme_list() {
  */
 function system_themes_page() {
   // Get current list of themes.
-  $themes =& _system_theme_list();
+  $themes = _system_theme_list();
 
   $theme_default = variable_get('theme_default', 'garland');
   $theme_groups  = array();
@@ -312,7 +312,7 @@ function system_themes_page() {
   );
   if (!empty($theme_groups['disabled'])) {
     $theme_group_titles['disabled'] = format_plural(count($theme_groups['disabled']), 'Disabled theme', 'Disabled themes');
-  };
+  }
 
   uasort($theme_groups['enabled'], 'system_sort_themes');
   drupal_alter('system_themes_page', $theme_groups);
@@ -345,7 +345,7 @@ function system_themes_admin_form($form, &$form_state, $theme_options) {
     '#title' => t('Use the administration theme when editing or creating content'),
     '#default_value' => variable_get('node_admin_theme', '0'),
   );
-  $form['admin_theme']['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['admin_theme']['actions'] = array('#type' => 'actions');
   $form['admin_theme']['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save configuration'),
@@ -369,7 +369,7 @@ function system_theme_enable() {
   if (isset($_REQUEST['theme']) && isset($_REQUEST['token']) && drupal_valid_token($_REQUEST['token'], 'system-theme-operation-link')) {
     $theme = $_REQUEST['theme'];
     // Get current list of themes.
-    $themes =& _system_theme_list();
+    $themes = _system_theme_list();
 
     // Check if the specified theme is one recognized by the system.
     if (!empty($themes[$theme])) {
@@ -391,7 +391,7 @@ function system_theme_disable() {
   if (isset($_REQUEST['theme']) && isset($_REQUEST['token']) && drupal_valid_token($_REQUEST['token'], 'system-theme-operation-link')) {
     $theme = $_REQUEST['theme'];
     // Get current list of themes.
-    $themes =& _system_theme_list();
+    $themes = _system_theme_list();
 
     // Check if the specified theme is one recognized by the system.
     if (!empty($themes[$theme])) {
@@ -419,7 +419,7 @@ function system_theme_default() {
   if (isset($_REQUEST['theme']) && isset($_REQUEST['token']) && drupal_valid_token($_REQUEST['token'], 'system-theme-operation-link')) {
     $theme = $_REQUEST['theme'];
     // Get current list of themes.
-    $themes =& _system_theme_list();
+    $themes = _system_theme_list();
 
     // Check if the specified theme is one recognized by the system.
     if (!empty($themes[$theme])) {
@@ -950,7 +950,7 @@ function system_modules($form, $form_state = array()) {
     );
   }
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save configuration'),
@@ -1238,7 +1238,9 @@ function system_modules_submit($form, &$form_state) {
 
   // Clear all caches.
   registry_rebuild();
+  system_rebuild_theme_data();
   drupal_theme_rebuild();
+  cache_clear_all('system_list', 'cache_bootstrap');
   node_types_rebuild();
   menu_rebuild();
   cache_clear_all('schema', 'cache');
@@ -1305,7 +1307,7 @@ function system_modules_uninstall($form, $form_state = NULL) {
       '#type' => 'checkboxes',
       '#options' => $options,
     );
-    $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+    $form['actions'] = array('#type' => 'actions');
     $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Uninstall'),
@@ -1437,7 +1439,8 @@ function system_ip_blocking_form($form, $form_state, $default_ip) {
     '#default_value' => $default_ip,
     '#description' => t('Enter a valid IP address.'),
   );
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
   );
@@ -1656,7 +1659,7 @@ function system_performance_settings() {
   $form['bandwidth_optimization'] = array(
     '#type' => 'fieldset',
     '#title' => t('Bandwidth optimization'),
-    '#description' => t('External resources  can be optimized automatically, which can reduce both the size and number of requests made to your website.') . $disabled_message,
+    '#description' => t('External resources can be optimized automatically, which can reduce both the size and number of requests made to your website.') . $disabled_message,
   );
 
   $js_hide = $cache == CACHE_DISABLED ? ' class="js-hide"' : '';
@@ -1945,7 +1948,8 @@ function system_date_time_settings() {
       foreach ($formats as $f => $format) {
         $choices[$f] = format_date(REQUEST_TIME, 'custom', $f);
       }
-      $default = variable_get('date_format_' . $type, array_shift(array_keys($formats)));
+      reset($formats);
+      $default = variable_get('date_format_' . $type, key($formats));
 
       // Get date type info for this date type.
       $type_info = system_get_date_types($type);
@@ -1979,7 +1983,11 @@ function system_date_time_settings() {
 }
 
 /**
- * Theme function for date settings form.
+ * Returns HTML for the date settings form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -2069,7 +2077,8 @@ function system_add_date_format_type_form($form, &$form_state) {
     '#required' => TRUE,
   );
 
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Add date type'),
   );
@@ -2267,12 +2276,12 @@ function system_batch_page() {
 }
 
 /**
- * This function formats an administrative block for display.
+ * Returns HTML for an administrative block for display.
  *
  * @param $variables
  *   An associative array containing:
- *   - block: An array containing information about the block. It should
- *     include a 'title', a 'description' and a formatted 'content'.
+ *   - block: An array containing information about the block. It should include
+ *     a 'title', a 'description' and a formatted 'content'.
  *
  * @ingroup themeable
  */
@@ -2312,7 +2321,7 @@ EOT;
 }
 
 /**
- * This function formats the content of an administrative block.
+ * Returns HTML for the content of an administrative block.
  *
  * @param $variables
  *   An associative array containing:
@@ -2347,14 +2356,14 @@ function theme_admin_block_content($variables) {
 }
 
 /**
- * This function formats an administrative page for viewing.
+ * Returns HTML for an administrative page.
  *
  * @param $variables
  *   An associative array containing:
  *   - blocks: An array of blocks to display. Each array should include a
- *     'title', a 'description', a formatted 'content' and a
- *     'position' which will control which container it will be
- *     in. This is usually 'left' or 'right'.
+ *     'title', a 'description', a formatted 'content' and a 'position' which
+ *     will control which container it will be in. This is usually 'left' or
+ *     'right'.
  *
  * @ingroup themeable
  */
@@ -2390,7 +2399,7 @@ function theme_admin_page($variables) {
 }
 
 /**
- * Theme output of the dashboard page.
+ * Returns HTML for the output of the dashboard page.
  *
  * @param $variables
  *   An associative array containing:
@@ -2407,11 +2416,11 @@ function theme_system_admin_by_module($variables) {
   $flip = array('left' => 'right', 'right' => 'left');
   $position = 'left';
 
-  // Iterate over all modules
+  // Iterate over all modules.
   foreach ($menu_items as $module => $block) {
     list($description, $items) = $block;
 
-    // Output links
+    // Output links.
     if (count($items)) {
       $block = array();
       $block['title'] = $module;
@@ -2442,7 +2451,7 @@ function theme_system_admin_by_module($variables) {
 }
 
 /**
- * Theme requirements status report.
+ * Returns HTML for the status report.
  *
  * @param $variables
  *   An associative array containing:
@@ -2483,11 +2492,11 @@ function theme_status_report($variables) {
 }
 
 /**
- * Theme callback for the modules form.
+ * Returns HTML for the modules form.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An associative array containing the structure of the form.
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -2530,32 +2539,26 @@ function theme_system_modules_fieldset($variables) {
 }
 
 /**
- * Themes an incompatible message.
- *
- * @ingroup themeable
+ * Returns HTML for a message about incompatible modules.
  *
  * @param $variables
  *   An associative array containing:
  *   - message: The form array representing the currently disabled modules.
  *
- * @return
- *   An HTML string for the message.
+ * @ingroup themeable
  */
 function theme_system_modules_incompatible($variables) {
   return '<div class="incompatible">' . $variables['message'] . '</div>';
 }
 
 /**
- * Themes a table of currently disabled modules.
- *
- * @ingroup themeable
+ * Returns HTML for a table of currently disabled modules.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: The form array representing the currently disabled modules.
+ *   - form: A render element representing the form.
  *
- * @return
- *   An HTML string representing the table.
+ * @ingroup themeable
  */
 function theme_system_modules_uninstall($variables) {
   $form = $variables['form'];
@@ -2588,7 +2591,7 @@ function theme_system_modules_uninstall($variables) {
 }
 
 /**
- * Theme function for the system themes form.
+ * Returns HTML for the Appearance page.
  *
  * @param $variables
  *   An associative array containing:
@@ -2778,7 +2781,7 @@ function system_configure_date_formats_form($form, &$form_state, $dfid = 0) {
     ),
   );
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['update'] = array(
     '#type' => 'submit',
     '#value' => ($dfid ? t('Save format') : t('Add format')),
@@ -2905,7 +2908,7 @@ function system_actions_manage_form($form, &$form_state, $options = array()) {
     '#options' => $options,
     '#description' => '',
   );
-  $form['parent']['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['parent']['actions'] = array('#type' => 'actions');
   $form['parent']['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Create'),
@@ -3002,7 +3005,7 @@ function system_actions_configure($form, &$form_state, $action = NULL) {
     '#type' => 'hidden',
     '#value' => '1',
   );
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
diff --git a/modules/system/system.api.php b/modules/system/system.api.php
index c9d1cfcc6a1b081917f3877211455fb9e72bd4e7..3bb8824ea6e7bbdb7e033fd1f0fb2a7b10f94f04 100644
--- a/modules/system/system.api.php
+++ b/modules/system/system.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.api.php,v 1.142 2010/03/11 21:23:06 dries Exp $
+// $Id: system.api.php,v 1.157 2010/04/26 14:33:54 dries Exp $
 
 /**
  * @file
@@ -64,41 +64,46 @@ function hook_hook_info() {
  *     entity type's base table.
  *   - static cache: (used by DrupalDefaultEntityController) FALSE to disable
  *     static caching of entities during a page request. Defaults to TRUE.
+ *   - field cache: (used by Field API loading and saving of field data) FALSE
+ *     to disable Field API's persistent cache of field data. Only recommended
+ *     if a higher level persistent cache is available for the entity type.
+ *     Defaults to TRUE.
  *   - load hook: The name of the hook which should be invoked by
  *     DrupalDefaultEntityController:attachLoad(), for example 'node_load'.
  *   - uri callback: A function taking an entity as argument and returning the
  *     uri elements of the entity, e.g. 'path' and 'options'. The actual entity
  *     uri can be constructed by passing these elements to url().
  *   - fieldable: Set to TRUE if you want your entity type to be fieldable.
- *   - object keys: An array describing how the Field API can extract the
+ *   - entity keys: An array describing how the Field API can extract the
  *     information it needs from the objects of the type. Elements:
  *     - id: The name of the property that contains the primary id of the
- *       object. Every object passed to the Field API must have this property
- *       and its value must be numeric.
+ *       entity. Every entity object passed to the Field API must have this
+ *       property and its value must be numeric.
  *     - revision: The name of the property that contains the revision id of
- *       the object. The Field API assumes that all revision ids are unique
- *       across all objects of a type.
- *       This element can be omitted if the objects of this type are not
- *       versionable.
+ *       the entity. The Field API assumes that all revision ids are unique
+ *       across all entities of a type. This entry can be omitted if the
+ *       entities of this type are not versionable.
  *     - bundle: The name of the property that contains the bundle name for the
- *       object. The bundle name defines which set of fields are attached to
- *       the object (e.g. what nodes call "content type").
- *       This element can be omitted if this type has no bundles (all objects
- *       have the same fields).
+ *       entity. The bundle name defines which set of fields are attached to
+ *       the entity (e.g. what nodes call "content type"). This entry can be
+ *       omitted if this entity type exposes a single bundle (all entities have
+ *       the same collection of fields). The name of this single bundle will be
+ *       the same as the entity type.
  *   - bundle keys: An array describing how the Field API can extract the
  *     information it needs from the bundle objects for this type (e.g
- *     $vocabulary objects for terms; not applicable for nodes).
- *     This element can be omitted if this type's bundles do not exist as
- *     standalone objects. Elements:
+ *     $vocabulary objects for terms; not applicable for nodes). This entry can
+ *     be omitted if this type's bundles do not exist as standalone objects.
+ *     Elements:
  *     - bundle: The name of the property that contains the name of the bundle
  *       object.
- *   - cacheable: A boolean indicating whether Field API should cache
- *     loaded fields for each object, reducing the cost of
- *     field_attach_load().
- *   - bundles: An array describing all bundles for this object type.
- *     Keys are bundles machine names, as found in the objects' 'bundle'
- *     property (defined in the 'object keys' entry above). Elements:
+ *   - bundles: An array describing all bundles for this object type. Keys are
+ *     bundles machine names, as found in the objects' 'bundle' property
+ *     (defined in the 'entity keys' entry above). Elements:
  *     - label: The human-readable name of the bundle.
+ *     - uri callback: Same as the 'uri callback' key documented above for the
+ *       entity type, but for the bundle only. When determining the URI of an
+ *       entity, if a 'uri callback' is defined for both the entity type and
+ *       the bundle, the one for the bundle is used.
  *     - admin: An array of information that allows Field UI pages to attach
  *       themselves to the existing administration pages for the bundle.
  *       Elements:
@@ -131,7 +136,7 @@ function hook_entity_info() {
       'revision table' => 'node_revision',
       'path callback' => 'node_path',
       'fieldable' => TRUE,
-      'object keys' => array(
+      'entity keys' => array(
         'id' => 'nid',
         'revision' => 'vid',
         'bundle' => 'type',
@@ -139,8 +144,6 @@ function hook_entity_info() {
       'bundle keys' => array(
         'bundle' => 'type',
       ),
-      // Node.module handles its own caching.
-      // 'cacheable' => FALSE,
       'bundles' => array(),
       'view modes' => array(
         'full' => array(
@@ -396,13 +399,14 @@ function hook_cron_queue_info() {
 /**
  * Alter cron queue information before cron runs.
  *
- * Called by drupal_run_cron() to allow modules to alter cron queue settings
+ * Called by drupal_cron_run() to allow modules to alter cron queue settings
  * before any jobs are processesed.
  *
  * @param array $queues
  *   An array of cron queue information.
  *
  *  @see hook_cron_queue_info()
+ *  @see drupal_cron_run()
  */
 function hook_cron_queue_info_alter(&$queues) {
   // This site has many feeds so let's spend 90 seconds on each cron run
@@ -503,6 +507,7 @@ function hook_exit($destination = NULL) {
  *
  * @param $javascript
  *   An array of all JavaScript being presented on the page.
+ *
  * @see drupal_add_js()
  * @see drupal_get_js()
  * @see drupal_js_defaults()
@@ -622,6 +627,7 @@ function hook_library_alter(&$libraries, $module) {
  *
  * @param $css
  *   An array of all CSS items (files and inline CSS) being requested on the page.
+ *
  * @see drupal_add_css()
  * @see drupal_get_css()
  */
@@ -635,6 +641,7 @@ function hook_css_alter(&$css) {
  *
  * @param $commands
  *   An array of all commands that will be sent to the user.
+ *
  * @see ajax_render()
  */
 function hook_ajax_render_alter($commands) {
@@ -778,7 +785,7 @@ function hook_form_alter(&$form, &$form_state, $form_id) {
  * @param $form_state
  *   A keyed array containing the current state of the form.
  *
- * @see drupal_prepare_form().
+ * @see drupal_prepare_form()
  */
 function hook_form_FORM_ID_alter(&$form, &$form_state) {
   // Modification for the form with the given form ID goes here. For example, if
@@ -976,6 +983,32 @@ function hook_mail_alter(&$message) {
   }
 }
 
+/**
+ * Alter the registry of modules implementing a hook.
+ *
+ * This hook is invoked during module_implements(). A module may implement this
+ * hook in order to reorder the implementing modules, which are otherwise
+ * ordered by the module's system weight.
+ *
+ * @param &$implementations
+ *   An array keyed by the module's name. The value of each item corresponds
+ *   to a $group, which is usually FALSE, unless the implementation is in a
+ *   file named $module.$group.inc.
+ * @param $hook
+ *   The name of the module hook being implemented.
+ */
+function hook_module_implements_alter(&$implementations, $hook) {
+  if ($hook == 'rdf_mapping') {
+    // Move my_module_rdf_mapping() to the end of the list. module_implements()
+    // iterates through $implementations with a foreach loop which PHP iterates
+    // in the order that the items were added, so to move an item to the end of
+    // the array, we remove it and then add it.
+    $group = $implementations['my_module'];
+    unset($implementations['my_module']);
+    $implementations['my_module'] = $group;
+  }
+}
+
 /**
  * Alter the information parsed from module and theme .info files
  *
@@ -1012,13 +1045,22 @@ function hook_system_info_alter(&$info, $file, $type) {
  * For a detailed usage example, see page_example.module.
  *
  * @return
- *   An array of which permission names are the keys and their corresponding
- *   values are descriptions of each permission.
- *   The permission names (keys of the array) must not be wrapped with
- *   the t() function, since the string extractor takes care of
- *   extracting permission names defined in the perm hook for
- *   translation. The permission descriptions (values of the array)
- *   should be wrapped in the t() function so they can be translated.
+ *   An array whose keys are permission names and whose corresponding values
+ *   are arrays containing the following key-value pairs:
+ *   - title: The human-readable name of the permission, to be shown on the
+ *     permission administration page. This should be wrapped in the t()
+ *     function so it can be translated.
+ *   - description: (optional) A description of what the permission does. This
+ *     should be wrapped in the t() function so it can be translated.
+ *   - restrict access: (optional) A boolean which can be set to TRUE to
+ *     indicate that site administrators should restrict access to this
+ *     permission to trusted users. This should be used for permissions that
+ *     have inherent security risks across a variety of potential use cases
+ *     (for example, the "administer filters" and "bypass node access"
+ *     permissions provided by Drupal core). When set to TRUE, a standard
+ *     warning message defined in user_admin_permissions() will be associated
+ *     with the permission and displayed with it on the permission
+ *     administration page. Defaults to FALSE.
  */
 function hook_permission() {
   return array(
@@ -1413,7 +1455,7 @@ function hook_mail($key, &$message, $params) {
     '%username' => format_username($account),
   );
   if ($context['hook'] == 'taxonomy') {
-    $entity = $params['object'];
+    $entity = $params['entity'];
     $vocabulary = taxonomy_vocabulary_load($entity->vid);
     $variables += array(
       '%term_name' => $entity->name,
@@ -1724,11 +1766,11 @@ function hook_file_move($file, $source) {
  * @see upload_file_references()
  */
 function hook_file_references($file) {
-  // If upload.module is still using a file, do not let other modules delete it.
-  $file_used = (bool) db_query_range('SELECT 1 FROM {upload} WHERE fid = :fid', 0, 1, array(':fid' => $file->fid))->fetchField();
+  // If user.module is still using a file, do not let other modules delete it.
+  $file_used = (bool) db_query_range('SELECT 1 FROM {user} WHERE pictire = :fid', 0, 1, array(':fid' => $file->fid))->fetchField();
   if ($file_used) {
     // Return the name of the module and how many references it has to the file.
-    return array('upload' => $count);
+    return array('user' => 1);
   }
 }
 
@@ -1769,7 +1811,7 @@ function hook_file_download($uri) {
   if (!file_prepare_directory($uri)) {
     $uri = FALSE;
   }
-  $result = db_query("SELECT f.* FROM {file} f INNER JOIN {upload} u ON f.fid = u.fid WHERE uri = :uri", array('uri' => $uri));
+  $result = db_query("SELECT f.* FROM {file_managed} f INNER JOIN {upload} u ON f.fid = u.fid WHERE uri = :uri", array('uri' => $uri));
   foreach ($result as $file) {
     if (!user_access('view uploaded files')) {
       return -1;
@@ -2730,7 +2772,7 @@ function hook_actions_delete($aid) {
  * Called by actions_list() to allow modules to alter the return values from
  * implementations of hook_action_info().
  *
- * @see trigger_example_action_info_alter().
+ * @see trigger_example_action_info_alter()
  */
 function hook_action_info_alter(&$actions) {
   $actions['node_unpublish_action']['label'] = t('Unpublish and remove from public view.');
@@ -3178,6 +3220,34 @@ function hook_token_info() {
   );
 }
 
+/**
+ * Alter batch information before a batch is processed.
+ *
+ * Called by batch_process() to allow modules to alter a batch before it is
+ * processed.
+ *
+ * @param $batch
+ *   The associative array of batch information. See batch_set() for details on
+ *   what this could contain.
+ *
+ * @see batch_set()
+ * @see batch_process()
+ *
+ * @ingroup batch
+ */
+function hook_batch_alter(&$batch) {
+  // If the current page request is inside the overlay, add ?render=overlay to
+  // the success callback URL, so that it appears correctly within the overlay.
+  if (overlay_get_mode() == 'child') {
+    if (isset($batch['url_options']['query'])) {
+      $batch['url_options']['query']['render'] = 'overlay';
+    }
+    else {
+      $batch['url_options']['query'] = array('render' => 'overlay');
+    }
+  }
+}
+
 /**
  * Alter the metadata about available placeholder tokens and token types.
  *
@@ -3215,9 +3285,56 @@ function hook_token_info_alter(&$data) {
  * @see _country_get_predefined_list()
  */
 function hook_countries_alter(&$countries) {
-  // Quebec has seceded from Canada. Add to country list.
-  $countries['QC'] = 'Quebec';
+  // Elbonia is now independent, so add it to the country list.
+  $countries['EB'] = 'Elbonia';
 }
+
+/**
+ * Provide information on available file transfer backends.
+ *
+ * File transfer backends are used by modules to transfer files from remote
+ * locations to Drupal sites. For instance, update.module uses a file transfer
+ * backend to download new versions of modules and themes from drupal.org.
+ *
+ * @return
+ *   An associative array of information about the file transfer backend(s).
+ *   being provided. This array can contain the following keys:
+ *   - title: Title of the backend to be shown to the end user.
+ *   - class: Name of the PHP class which implements this backend.
+ *   - settings_form: An optional callback function that provides additional
+ *     configuration information required by this backend (for instance a port
+ *     number.)
+ *   - weight: Controls what order the backends are presented to the user.
+ *
+ * @see authorize.php
+ * @see FileTransfer
+ */
+function hook_filetransfer_backends() {
+  $backends = array();
+
+  // This is the default, will be available on most systems.
+  if (function_exists('ftp_connect')) {
+    $backends['ftp'] = array(
+      'title' => t('FTP'),
+      'class' => 'FileTransferFTP',
+      'settings_form' => 'system_filetransfer_backend_form_ftp',
+      'weight' => 0,
+    );
+  }
+
+  // SSH2 lib connection is only available if the proper PHP extension is
+  // installed.
+  if (function_exists('ssh2_connect')) {
+    $backends['ssh'] = array(
+      'title' => t('SSH'),
+      'class' => 'FileTransferSSH',
+      'settings_form' => 'system_filetransfer_backend_form_ssh',
+      'weight' => 20,
+    );
+  }
+  return $backends;
+}
+
 /**
  * @} End of "addtogroup hooks".
  */
diff --git a/modules/system/system.css b/modules/system/system.css
index 85580224249e23aa3753df1f92f403a129a9e77c..a426d80b49d2a73b231a10e8d99e89e7b169bbe6 100644
--- a/modules/system/system.css
+++ b/modules/system/system.css
@@ -1,4 +1,4 @@
-/* $Id: system.css,v 1.73 2010/01/18 17:29:28 dries Exp $ */
+/* $Id: system.css,v 1.74 2010/04/01 14:47:16 dries Exp $ */
 
 /*
 ** HTML elements
@@ -64,39 +64,6 @@ thead th {
 .breadcrumb {
   padding-bottom: .5em
 }
-.error {
-  color: #e55;
-}
-div.error {
-  border: 1px solid #d77;
-}
-div.error, tr.error {
-  background: #fcc;
-  color: #200;
-  padding: 2px;
-}
-.warning {
-  color: #e09010;
-}
-div.warning {
-  border: 1px solid #f0c020;
-}
-div.warning, table tr.warning {
-  background: #ffd;
-  color: #220;
-  padding: 2px;
-}
-.ok {
-  color: #008000;
-}
-div.ok {
-  border: 1px solid #00aa00;
-}
-div.ok, tr.ok {
-  background: #dfd;
-  color: #020;
-  padding: 2px;
-}
 .item-list .icon {
   color: #555;
   float: right; /* LTR */
diff --git a/modules/system/system.info b/modules/system/system.info
index 68f6d24ffde5ad7c77908307a4f9bdaa4ac78cd1..96ccc8132a0e5f7ff2891c1ed8babe038d6d6524 100644
--- a/modules/system/system.info
+++ b/modules/system/system.info
@@ -18,8 +18,8 @@ files[] = system.mail.inc
 required = TRUE
 configure = admin/config/system
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/system/system.install b/modules/system/system.install
index 8404f88c79666e09ec0872291262edd10fcaf975..42922102c3021d7b9e04906c456ecf8fa4298238 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.install,v 1.457 2010/03/20 15:06:51 dries Exp $
+// $Id: system.install,v 1.461 2010/04/10 17:30:15 dries Exp $
 
 /**
  * @file
@@ -379,7 +379,7 @@ function system_requirements($phase) {
         'title' => $t('HTTP request status'),
         'value' => $t('Fails'),
         'severity' => REQUIREMENT_ERROR,
-        'description' => $t('Your system or network configuration does not allow Drupal to access web pages, resulting in reduced functionality. This could be due to your webserver configuration or PHP settings, and should be resolved in order to download information about available updates, fetch aggregator feeds, sign in via OpenID, or use other network-dependent services.  If you are certain that Drupal can access web pages but you are still seeing this message, you may add <code>$conf[\'drupal_http_request_fails\'] = FALSE;</code> to the bottom of your settings.php file.'),
+        'description' => $t('Your system or network configuration does not allow Drupal to access web pages, resulting in reduced functionality. This could be due to your webserver configuration or PHP settings, and should be resolved in order to download information about available updates, fetch aggregator feeds, sign in via OpenID, or use other network-dependent services. If you are certain that Drupal can access web pages but you are still seeing this message, you may add <code>$conf[\'drupal_http_request_fails\'] = FALSE;</code> to the bottom of your settings.php file.'),
       );
     }
   }
@@ -692,7 +692,7 @@ function system_schema() {
     'primary key' => array('type', 'language'),
   );
 
-  $schema['file'] = array(
+  $schema['file_managed'] = array(
     'description' => 'Stores information for uploaded files.',
     'fields' => array(
       'fid' => array(
@@ -983,10 +983,10 @@ function system_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
-      'file' => array(
+      'include_file' => array(
         'description' => 'The file to include for this element, usually the page callback function lives in this file.',
         'type' => 'text',
-        'size' => 'medium'
+        'size' => 'medium',
       ),
     ),
     'indexes' => array(
@@ -1549,7 +1549,7 @@ function system_update_6050() {
  */
 function system_update_6051() {
 
-  if (!db_column_exists('users', 'signature_format')) {
+  if (!db_field_exists('users', 'signature_format')) {
 
     // Set future text formats to FILTER_FORMAT_DEFAULT to ensure a safe default
     // when incompatible modules insert into the users table. An actual format
@@ -2068,6 +2068,7 @@ function system_update_7030() {
 
 /**
  * Removed in favour of Drupal 6 backport.
+ *
  * @see system_update_6052()
  */
 function system_update_7031() {
@@ -2093,7 +2094,7 @@ function system_update_7033() {
 }
 
 /**
- * Migrate the file_downloads setting and create the new {file} table.
+ * Migrate the file_downloads setting and create the new {file_managed} table.
  */
 function system_update_7034() {
   $files_directory = variable_get('file_directory_path', NULL);
@@ -2111,7 +2112,7 @@ function system_update_7034() {
   }
   variable_del('file_downloads');
 
-  $schema['file'] = array(
+  $schema['file_managed'] = array(
     'description' => 'Stores information for uploaded files.',
     'fields' => array(
       'fid' => array(
@@ -2180,11 +2181,11 @@ function system_update_7034() {
     'primary key' => array('fid'),
   );
 
-  db_create_table('file', $schema['file']);
+  db_create_table('file_managed', $schema['file_managed']);
 }
 
 /**
- * Migrate upload module files to the new {file} table.
+ * Migrate upload module files to the new {file_managed} table.
  */
 function system_update_7035() {
   if (!db_table_exists('upload')) {
@@ -2204,7 +2205,7 @@ function system_update_7035() {
   foreach ($result as $file) {
     $file['uri'] = $scheme . str_replace($basename, '', $file['uri']);
     $file['uri'] = file_stream_wrapper_uri_normalize($file['uri']);
-    db_insert('file')->fields($file)->execute();
+    db_insert('file_managed')->fields($file)->execute();
     $fids[] = $file['fid'];
   }
   // TODO: delete the found fids from {files}?
@@ -2229,8 +2230,8 @@ function system_update_7036() {
   }
   $insert->execute();
 
-  // Remove obsolete variable 'site_offline_message'.
-  // @see update_fix_d7_requirements().
+  // Remove obsolete variable 'site_offline_message'. See
+  // update_fix_d7_requirements().
   variable_del('site_offline_message');
 }
 
@@ -2389,6 +2390,13 @@ function system_update_7051() {
   db_change_field('blocked_ips', 'ip', 'ip', array('description' => 'IP address', 'type' => 'varchar', 'length' => 40, 'not null' => TRUE, 'default' => ''));
 }
 
+/**
+ * Rename file to include_file in {menu_router} table.
+ */
+function system_update_7052() {
+  db_change_field('menu_router', 'file', 'include_file', array('type' => 'text', 'size' => 'medium'));
+}
+
 /**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
diff --git a/modules/system/system.module b/modules/system/system.module
index a51834a4cf91495701ff038eec0018ecd68abcd3..0819e63ef217248d240e48ab7ac4c537c2405566 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.module,v 1.903 2010/03/21 03:47:32 webchick Exp $
+// $Id: system.module,v 1.923 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -47,12 +47,16 @@ define('DRUPAL_OPTIONAL', 1);
 define('DRUPAL_REQUIRED', 2);
 
 /**
- * Return only visible regions. @see system_region_list().
+ * Return only visible regions.
+ *
+ * @see system_region_list()
  */
 define('REGIONS_VISIBLE', 'visible');
 
 /**
- * Return all visible regions. @see system_region_list().
+ * Return all regions.
+ *
+ * @see system_region_list()
  */
 define('REGIONS_ALL', 'all');
 
@@ -207,7 +211,7 @@ function system_permission() {
     ),
     'administer site configuration' => array(
       'title' => t('Administer site configuration'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
     'administer themes' => array(
       'title' => t('Administer themes'),
@@ -256,8 +260,8 @@ function system_entity_info() {
   return array(
     'file' => array(
       'label' => t('File'),
-      'base table' => 'file',
-      'object keys' => array(
+      'base table' => 'file_managed',
+      'entity keys' => array(
         'id' => 'fid',
       ),
       'static cache' => FALSE,
@@ -280,12 +284,6 @@ function system_element_info() {
     '#theme' => 'page',
     '#theme_wrappers' => array('html'),
   );
-  $types['list'] = array(
-    '#title' => '',
-    '#list_type' => 'ul',
-    '#attributes' => array(),
-    '#items' => array(),
-  );
   // By default, we don't want AJAX commands being rendered in the context of an
   // HTML page, so we don't provide defaults for #theme or #theme_wrappers.
   // However, modules can set these properties (for example, to provide an HTML
@@ -314,6 +312,7 @@ function system_element_info() {
     '#name' => 'op',
     '#button_type' => 'submit',
     '#executes_submit_callback' => TRUE,
+    '#limit_validation_errors' => FALSE,
     '#process' => array('ajax_process_form'),
     '#theme_wrappers' => array('button'),
   );
@@ -322,6 +321,7 @@ function system_element_info() {
     '#name' => 'op',
     '#button_type' => 'submit',
     '#executes_submit_callback' => FALSE,
+    '#limit_validation_errors' => FALSE,
     '#process' => array('ajax_process_form'),
     '#theme_wrappers' => array('button'),
   );
@@ -329,6 +329,7 @@ function system_element_info() {
     '#input' => TRUE,
     '#button_type' => 'submit',
     '#executes_submit_callback' => TRUE,
+    '#limit_validation_errors' => FALSE,
     '#process' => array('ajax_process_form'),
     '#return_value' => TRUE,
     '#has_garbage_value' => TRUE,
@@ -471,6 +472,11 @@ function system_element_info() {
     '#theme_wrappers' => array('container'),
     '#process' => array('form_process_container'),
   );
+  $types['actions'] = array(
+    '#theme_wrappers' => array('container'),
+    '#process' => array('form_process_actions', 'form_process_container'),
+    '#weight' => 100,
+  );
 
   $types['token'] = array(
     '#input' => TRUE,
@@ -673,6 +679,7 @@ function system_menu() {
     'page callback' => 'system_ip_blocking',
     'access arguments' => array('block IP addresses'),
     'file' => 'system.admin.inc',
+    'weight' => 10,
   );
   $items['admin/config/people/ip-blocking/delete/%blocked_ip'] = array(
     'title' => 'Delete IP address',
@@ -688,7 +695,7 @@ function system_menu() {
     'title' => 'Media',
     'description' => 'Media tools.',
     'position' => 'left',
-    'weight' => 10,
+    'weight' => -10,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -699,7 +706,7 @@ function system_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('system_file_system_settings'),
     'access arguments' => array('administer site configuration'),
-    'weight' => 10,
+    'weight' => -10,
     'file' => 'system.admin.inc',
   );
     $items['admin/config/media/image-toolkit'] = array(
@@ -716,6 +723,8 @@ function system_menu() {
   $items['admin/config/services'] = array(
     'title' => 'Web services',
     'description' => 'Tools related to web services.',
+    'position' => 'right',
+    'weight' => 0,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -733,8 +742,8 @@ function system_menu() {
   $items['admin/config/development'] = array(
     'title' => 'Development',
     'description' => 'Development tools.',
-    'position' => 'left',
-    'weight' => 10,
+    'position' => 'right',
+    'weight' => -10,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -745,8 +754,8 @@ function system_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('system_site_maintenance_mode'),
     'access arguments' => array('administer site configuration'),
-    'weight' => 50,
     'file' => 'system.admin.inc',
+    'weight' => -10,
   );
   $items['admin/config/development/performance'] = array(
     'title' => 'Performance',
@@ -754,8 +763,8 @@ function system_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('system_performance_settings'),
     'access arguments' => array('administer site configuration'),
-    'weight' => -10,
     'file' => 'system.admin.inc',
+    'weight' => -20,
   );
   $items['admin/config/development/logging'] = array(
     'title' => 'Logging and errors',
@@ -763,8 +772,8 @@ function system_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('system_logging_settings'),
     'access arguments' => array('administer site configuration'),
-    'weight' => 60,
     'file' => 'system.admin.inc',
+    'weight' => -15,
   );
 
   // Regional and date settings.
@@ -772,7 +781,7 @@ function system_menu() {
     'title' => 'Regional and language',
     'description' => 'Regional settings, localization and translation.',
     'position' => 'left',
-    'weight' => -7,
+    'weight' => -5,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -783,7 +792,7 @@ function system_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('system_regional_settings'),
     'access arguments' => array('administer site configuration'),
-    'weight' => 50,
+    'weight' => -20,
     'file' => 'system.admin.inc',
   );
   $items['admin/config/regional/date-time'] = array(
@@ -792,7 +801,7 @@ function system_menu() {
     'page callback' => 'drupal_get_form',
     'page arguments' => array('system_date_time_settings'),
     'access arguments' => array('administer site configuration'),
-    'weight' => 60,
+    'weight' => -15,
     'file' => 'system.admin.inc',
   );
   $items['admin/config/regional/date-time/types'] = array(
@@ -873,6 +882,8 @@ function system_menu() {
   $items['admin/config/search'] = array(
     'title' => 'Search and metadata',
     'description' => 'Local site search, metadata and SEO.',
+    'position' => 'left',
+    'weight' => -10,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -884,6 +895,7 @@ function system_menu() {
     'page arguments' => array('system_clean_url_settings'),
     'access arguments' => array('administer site configuration'),
     'file' => 'system.admin.inc',
+    'weight' => 5,
   );
   $items['admin/config/search/clean-urls/check'] = array(
     'title' => 'Clean URL check',
@@ -899,7 +911,7 @@ function system_menu() {
     'title' => 'System',
     'description' => 'General system related configuration.',
     'position' => 'right',
-    'weight' => -5,
+    'weight' => -20,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -950,7 +962,7 @@ function system_menu() {
     'page arguments' => array('system_site_information_settings'),
     'access arguments' => array('administer site configuration'),
     'file' => 'system.admin.inc',
-    'weight' => -10,
+    'weight' => -20,
   );
 
   // Additional categories
@@ -961,11 +973,13 @@ function system_menu() {
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
-    'weight' => -5,
+    'weight' => -15,
   );
   $items['admin/config/workflow'] = array(
     'title' => 'Workflow',
     'description' => 'Content workflow, editorial workflow tools.',
+    'position' => 'right',
+    'weight' => 5,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -973,8 +987,8 @@ function system_menu() {
   $items['admin/config/content'] = array(
     'title' => 'Content authoring',
     'description' => 'Settings related to formatting and authoring content.',
-    'position' => 'right',
-    'weight' => 5,
+    'position' => 'left',
+    'weight' => -15,
     'page callback' => 'system_admin_menu_block_page',
     'access arguments' => array('access administration pages'),
     'file' => 'system.admin.inc',
@@ -1078,7 +1092,7 @@ function system_library() {
   $libraries['jquery-bbq'] = array(
     'title' => 'jQuery BBQ',
     'website' => 'http://benalman.com/projects/jquery-bbq-plugin/',
-    'version' => '1.0.2',
+    'version' => '1.2.1',
     'js' => array(
       'misc/jquery.ba-bbq.js' => array(),
     ),
@@ -1798,6 +1812,7 @@ function system_init() {
   drupal_add_css(drupal_get_path('module', 'system') . '/system.css', array('weight' => CSS_SYSTEM));
   drupal_add_css(drupal_get_path('module', 'system') . '/system-behavior.css', array('weight' => CSS_SYSTEM));
   drupal_add_css(drupal_get_path('module', 'system') . '/system-menus.css', array('weight' => CSS_SYSTEM));
+  drupal_add_css(drupal_get_path('module', 'system') . '/system-messages.css', array('weight' => CSS_SYSTEM));
 
 
   // Ignore slave database servers for this request.
@@ -2168,6 +2183,7 @@ function system_update_files_database(&$files, $type) {
  *
  * @param $type
  *   Either 'module' or 'theme'.
+ *
  * @return
  *   An associative array of module or theme information keyed by name.
  *
@@ -2294,128 +2310,112 @@ function _system_update_bootstrap_status() {
  *   An associative array of themes information.
  */
 function _system_rebuild_theme_data() {
-  $themes_info = &drupal_static(__FUNCTION__, array());
-
-  if (empty($themes_info)) {
-    // Find themes
-    $themes = drupal_system_listing('/\.info$/', 'themes');
-    // Find theme engines
-    $engines = drupal_system_listing('/\.engine$/', 'themes/engines');
-
-    // Set defaults for theme info.
-    $defaults = array(
-      'regions' => array(
-        'sidebar_first' => 'Left sidebar',
-        'sidebar_second' => 'Right sidebar',
-        'content' => 'Content',
-        'header' => 'Header',
-        'footer' => 'Footer',
-        'highlight' => 'Highlighted content',
-        'help' => 'Help',
-        'page_top' => 'Page top',
-        'page_bottom' => 'Page bottom',
-      ),
-      'description' => '',
-      'features' => array(
-        'comment_user_picture',
-        'comment_user_verification',
-        'favicon',
-        'logo',
-        'name',
-        'node_user_picture',
-        'slogan',
-        'main_menu',
-        'secondary_menu',
-      ),
-      'screenshot' => 'screenshot.png',
-      'php' => DRUPAL_MINIMUM_PHP,
-    );
+  // Find themes
+  $themes = drupal_system_listing('/\.info$/', 'themes');
+  // Find theme engines
+  $engines = drupal_system_listing('/\.engine$/', 'themes/engines');
 
-    $sub_themes = array();
-    // Read info files for each theme
-    foreach ($themes as $key => $theme) {
-      $themes[$key]->filename = $theme->uri;
-      $themes[$key]->info = drupal_parse_info_file($theme->uri) + $defaults;
+  // Set defaults for theme info.
+  $defaults = array(
+    'regions' => array(
+      'sidebar_first' => 'Left sidebar',
+      'sidebar_second' => 'Right sidebar',
+      'content' => 'Content',
+      'header' => 'Header',
+      'footer' => 'Footer',
+      'highlight' => 'Highlighted content',
+      'help' => 'Help',
+      'page_top' => 'Page top',
+      'page_bottom' => 'Page bottom',
+    ),
+    'description' => '',
+    'features' => _system_default_theme_features(),
+    'screenshot' => 'screenshot.png',
+    'php' => DRUPAL_MINIMUM_PHP,
+  );
 
-      // Invoke hook_system_info_alter() to give installed modules a chance to
-      // modify the data in the .info files if necessary.
-      $type = 'theme';
-      drupal_alter('system_info', $themes[$key]->info, $themes[$key], $type);
+  $sub_themes = array();
+  // Read info files for each theme
+  foreach ($themes as $key => $theme) {
+    $themes[$key]->filename = $theme->uri;
+    $themes[$key]->info = drupal_parse_info_file($theme->uri) + $defaults;
 
-      if (!empty($themes[$key]->info['base theme'])) {
-        $sub_themes[] = $key;
-      }
-      if (empty($themes[$key]->info['engine'])) {
-        $filename = dirname($themes[$key]->uri) . '/' . $themes[$key]->name . '.theme';
-        if (file_exists($filename)) {
-          $themes[$key]->owner = $filename;
-          $themes[$key]->prefix = $key;
-        }
+    // Invoke hook_system_info_alter() to give installed modules a chance to
+    // modify the data in the .info files if necessary.
+    $type = 'theme';
+    drupal_alter('system_info', $themes[$key]->info, $themes[$key], $type);
+
+    if (!empty($themes[$key]->info['base theme'])) {
+      $sub_themes[] = $key;
+    }
+    if (empty($themes[$key]->info['engine'])) {
+      $filename = dirname($themes[$key]->uri) . '/' . $themes[$key]->name . '.theme';
+      if (file_exists($filename)) {
+        $themes[$key]->owner = $filename;
+        $themes[$key]->prefix = $key;
       }
-      else {
-        $engine = $themes[$key]->info['engine'];
-        if (isset($engines[$engine])) {
-          $themes[$key]->owner = $engines[$engine]->uri;
-          $themes[$key]->prefix = $engines[$engine]->name;
-          $themes[$key]->template = TRUE;
-        }
+    }
+    else {
+      $engine = $themes[$key]->info['engine'];
+      if (isset($engines[$engine])) {
+        $themes[$key]->owner = $engines[$engine]->uri;
+        $themes[$key]->prefix = $engines[$engine]->name;
+        $themes[$key]->template = TRUE;
       }
+    }
 
-      // Give the stylesheets proper path information.
-      $pathed_stylesheets = array();
-      if (isset($themes[$key]->info['stylesheets'])) {
-        foreach ($themes[$key]->info['stylesheets'] as $media => $stylesheets) {
-          foreach ($stylesheets as $stylesheet) {
-            $pathed_stylesheets[$media][$stylesheet] = dirname($themes[$key]->uri) . '/' . $stylesheet;
-          }
+    // Give the stylesheets proper path information.
+    $pathed_stylesheets = array();
+    if (isset($themes[$key]->info['stylesheets'])) {
+      foreach ($themes[$key]->info['stylesheets'] as $media => $stylesheets) {
+        foreach ($stylesheets as $stylesheet) {
+          $pathed_stylesheets[$media][$stylesheet] = dirname($themes[$key]->uri) . '/' . $stylesheet;
         }
       }
-      $themes[$key]->info['stylesheets'] = $pathed_stylesheets;
+    }
+    $themes[$key]->info['stylesheets'] = $pathed_stylesheets;
 
-      // Give the scripts proper path information.
-      $scripts = array();
-      if (isset($themes[$key]->info['scripts'])) {
-        foreach ($themes[$key]->info['scripts'] as $script) {
-          $scripts[$script] = dirname($themes[$key]->uri) . '/' . $script;
-        }
-      }
-      $themes[$key]->info['scripts'] = $scripts;
-      // Give the screenshot proper path information.
-      if (!empty($themes[$key]->info['screenshot'])) {
-        $themes[$key]->info['screenshot'] = dirname($themes[$key]->uri) . '/' . $themes[$key]->info['screenshot'];
+    // Give the scripts proper path information.
+    $scripts = array();
+    if (isset($themes[$key]->info['scripts'])) {
+      foreach ($themes[$key]->info['scripts'] as $script) {
+        $scripts[$script] = dirname($themes[$key]->uri) . '/' . $script;
       }
     }
+    $themes[$key]->info['scripts'] = $scripts;
+    // Give the screenshot proper path information.
+    if (!empty($themes[$key]->info['screenshot'])) {
+      $themes[$key]->info['screenshot'] = dirname($themes[$key]->uri) . '/' . $themes[$key]->info['screenshot'];
+    }
+  }
 
-    // Now that we've established all our master themes, go back and fill in
-    // data for subthemes.
-    foreach ($sub_themes as $key) {
-      $themes[$key]->base_themes = system_find_base_themes($themes, $key);
-      // Don't proceed if there was a problem with the root base theme.
-      if (!current($themes[$key]->base_themes)) {
-        continue;
-      }
-      $base_key = key($themes[$key]->base_themes);
-      foreach (array_keys($themes[$key]->base_themes) as $base_theme) {
-        $themes[$base_theme]->sub_themes[$key] = $themes[$key]->info['name'];
+  // Now that we've established all our master themes, go back and fill in data
+  // for subthemes.
+  foreach ($sub_themes as $key) {
+    $themes[$key]->base_themes = system_find_base_themes($themes, $key);
+    // Don't proceed if there was a problem with the root base theme.
+    if (!current($themes[$key]->base_themes)) {
+      continue;
+    }
+    $base_key = key($themes[$key]->base_themes);
+    foreach (array_keys($themes[$key]->base_themes) as $base_theme) {
+      $themes[$base_theme]->sub_themes[$key] = $themes[$key]->info['name'];
+    }
+    // Copy the 'owner' and 'engine' over if the top level theme uses a theme
+    // engine.
+    if (isset($themes[$base_key]->owner)) {
+      if (isset($themes[$base_key]->info['engine'])) {
+        $themes[$key]->info['engine'] = $themes[$base_key]->info['engine'];
+        $themes[$key]->owner = $themes[$base_key]->owner;
+        $themes[$key]->prefix = $themes[$base_key]->prefix;
       }
-      // Copy the 'owner' and 'engine' over if the top level theme uses a
-      // theme engine.
-      if (isset($themes[$base_key]->owner)) {
-        if (isset($themes[$base_key]->info['engine'])) {
-          $themes[$key]->info['engine'] = $themes[$base_key]->info['engine'];
-          $themes[$key]->owner = $themes[$base_key]->owner;
-          $themes[$key]->prefix = $themes[$base_key]->prefix;
-        }
-        else {
-          $themes[$key]->prefix = $key;
-        }
+      else {
+        $themes[$key]->prefix = $key;
       }
     }
-
-    $themes_info = $themes;
   }
 
-  return $themes_info;
+  return $themes;
 }
 
 /**
@@ -2432,6 +2432,23 @@ function system_rebuild_theme_data() {
   return $themes;
 }
 
+/**
+ * Returns an array of default theme features.
+ */
+function _system_default_theme_features() {
+  return array(
+    'logo',
+    'favicon',
+    'name',
+    'slogan',
+    'node_user_picture',
+    'comment_user_picture',
+    'comment_user_verification',
+    'main_menu',
+    'secondary_menu',
+  );
+}
+
 /**
  * Find all the base themes for the specified theme.
  *
@@ -2494,7 +2511,7 @@ function system_region_list($theme_key, $show = REGIONS_ALL) {
       return $list[$theme_key][$show];
     }
     $info = $themes[$theme_key]->info;
-    // If requested, suppress hidden regions. @see block_admin_display_form().
+    // If requested, suppress hidden regions. See block_admin_display_form().
     foreach ($info['regions'] as $name => $label) {
       if ($show == REGIONS_ALL || !isset($info['regions_hidden']) || !in_array($name, $info['regions_hidden'])) {
         $list[$theme_key][$show][$name] = $label;
@@ -2550,20 +2567,20 @@ function _system_settings_form_automatic_defaults($form) {
 /**
  * Add default buttons to a form and set its prefix.
  *
- * @ingroup forms
- * @see system_settings_form_submit()
  * @param $form
  *   An associative array containing the structure of the form.
  * @param $automatic_defaults
  *   Automatically load the saved values for each field from the system variables
  *   (defaults to TRUE).
+ *
  * @return
  *   The form structure.
+ *
+ * @see system_settings_form_submit()
+ * @ingroup forms
  */
 function system_settings_form($form, $automatic_defaults = TRUE) {
-  $form['actions']['#type'] = 'container';
-  $form['actions']['#attributes']['class'][] = 'form-actions';
-  $form['actions']['#weight'] = 100;
+  $form['actions']['#type'] = 'actions';
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
 
   if ($automatic_defaults) {
@@ -2671,13 +2688,13 @@ function confirm_form($form, $question, $path, $description = NULL, $yes = NULL,
 
   drupal_set_title($question, PASS_THROUGH);
 
-  $form['#attributes'] = array('class' => array('confirmation'));
+  $form['#attributes']['class'][] = 'confirmation';
   $form['description'] = array('#markup' => $description);
   $form[$name] = array('#type' => 'hidden', '#value' => 1);
 
   $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions', 'container-inline')),
+    '#type' => 'actions',
+    '#attributes' => array('class' => array('container-inline')),
   );
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => $yes ? $yes : t('Confirm'));
   $form['actions']['cancel'] = array('#markup' => $cancel);
@@ -2689,11 +2706,15 @@ function confirm_form($form, $question, $path, $description = NULL, $yes = NULL,
 }
 
 /**
- * Determine if a user is in compact mode.
+ * Determines if the current user is in compact mode.
+ *
+ * @return
+ *   TRUE when in compact mode, FALSE when in expanded mode.
  */
 function system_admin_compact_mode() {
-  global $user;
-  return (isset($user->admin_compact_mode)) ? $user->admin_compact_mode : variable_get('admin_compact_mode', FALSE);
+  // PHP converts dots into underscores in cookie names to avoid problems with
+  // its parser, so we use a converted cookie name.
+  return isset($_COOKIE['Drupal_visitor_admin_compact_mode']) ? $_COOKIE['Drupal_visitor_admin_compact_mode'] : variable_get('admin_compact_mode', FALSE);
 }
 
 /**
@@ -2703,8 +2724,7 @@ function system_admin_compact_mode() {
  *   Valid values are 'on' and 'off'.
  */
 function system_admin_compact_page($mode = 'off') {
-  global $user;
-  user_save($user, array('admin_compact_mode' => ($mode == 'on')));
+  user_cookie_save(array('admin_compact_mode' => ($mode == 'on')));
   drupal_goto();
 }
 
@@ -2764,8 +2784,8 @@ function system_cron() {
 
   // Remove temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
   // Use separate placeholders for the status to avoid a bug in some versions
-  // of PHP. See http://drupal.org/node/352956
-  $result = db_query('SELECT fid FROM {file} WHERE status & :permanent1 <> :permanent2 AND timestamp < :timestamp', array(
+  // of PHP. See http://drupal.org/node/352956.
+  $result = db_query('SELECT fid FROM {file_managed} WHERE status & :permanent1 <> :permanent2 AND timestamp < :timestamp', array(
     ':permanent1' => FILE_STATUS_PERMANENT,
     ':permanent2' => FILE_STATUS_PERMANENT,
     ':timestamp' => REQUEST_TIME - DRUPAL_MAXIMUM_TEMP_FILE_AGE
@@ -2846,12 +2866,14 @@ function system_action_info() {
 /**
  * Return a form definition so the Send email action can be configured.
  *
- * @see system_send_email_action_validate()
- * @see system_send_email_action_submit()
  * @param $context
  *   Default values (if we are editing an existing action instance).
+ *
  * @return
  *   Form definition.
+ *
+ * @see system_send_email_action_validate()
+ * @see system_send_email_action_submit()
  */
 function system_send_email_action_form($context) {
   // Set default values for form.
@@ -2942,7 +2964,16 @@ function system_send_email_action($entity, $context) {
 
   $recipient = token_replace($context['recipient'], $context);
 
-  $language = user_preferred_language($account);
+  // If the recipient is a registered user with a language preference, use
+  // the recipient's preferred language. Otherwise, use the system default
+  // language.
+  $recipient_account = user_load_by_mail($recipient);
+  if ($recipient_account) {
+    $language = user_preferred_language($recipient_account);
+  }
+  else {
+    $language = language_default();
+  }
   $params = array('context' => $context);
 
   if (drupal_mail('system', 'action_send_email', $recipient, $language, $params)) {
@@ -3110,16 +3141,16 @@ function system_timezone($abbreviation = '', $offset = -1, $is_daylight_saving_t
 }
 
 /**
- * Format the Powered by Drupal text.
+ * Returns HTML for the Powered by Drupal text.
  *
  * @ingroup themeable
  */
-function theme_system_powered_by($variables) {
+function theme_system_powered_by() {
   return '<span>' . t('Powered by <a href="@poweredby">Drupal</a>', array('@poweredby' => 'http://drupal.org')) . '</span>';
 }
 
 /**
- * Display the link to show or hide inline help descriptions.
+ * Returns HTML for a link to show or hide inline help descriptions.
  *
  * @ingroup themeable
  */
@@ -3193,7 +3224,7 @@ function system_retrieve_file($url, $destination = NULL, $managed = FALSE, $repl
   }
   $result = drupal_http_request($url);
   if ($result->code != 200) {
-    drupal_set_message(t('HTTP error @errorcode occured when trying to fetch @remote.', array('@errorcode' => $result->code, '@remote' => $url)), 'error');
+    drupal_set_message(t('HTTP error @errorcode occurred when trying to fetch @remote.', array('@errorcode' => $result->code, '@remote' => $url)), 'error');
     return FALSE;
   }
   $local = $managed ? file_save_data($result->data, $path, $replace) : file_unmanaged_save_data($result->data, $path, $replace);
@@ -3599,14 +3630,15 @@ function system_archiver_info() {
 }
 
 /**
- * Theme confirmation forms.
+ * Returns HTML for a confirmation form.
  *
  * By default this does not alter the appearance of a form at all,
  * but is provided as a convenience for themers.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An associative array containing the structure of the form.
+ *   - form: A render element representing the form.
+ *
  * @ingroup themeable
  */
 function theme_confirm_form($variables) {
@@ -3614,14 +3646,15 @@ function theme_confirm_form($variables) {
 }
 
 /**
- * Theme function for the system settings form.
+ * Returns HTML for a system settings form.
  *
  * By default this does not alter the appearance of a form at all,
  * but is provided as a convenience for themers.
  *
  * @param $variables
  *   An associative array containing:
- *   - form: An associative array containing the structure of the form.
+ *   - form: A render element representing the form.
+ *
  * @ingroup themeable
  */
 function theme_system_settings_form($variables) {
diff --git a/modules/system/system.queue.inc b/modules/system/system.queue.inc
index cc12d2557915ccbc3422d6b8b184c1b09d3abf2b..259830d690308b0b853c66fc693fc747a3121b57 100644
--- a/modules/system/system.queue.inc
+++ b/modules/system/system.queue.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.queue.inc,v 1.10 2010/01/09 02:51:09 webchick Exp $
+// $Id: system.queue.inc,v 1.12 2010/04/26 13:02:40 dries Exp $
 
 /**
  * @file
@@ -22,7 +22,7 @@
  * long you want to have a lease for working on that item. When finished
  * processing, the item needs to be deleted by calling
  * DrupalQueueInterface::deleteItem(). If the consumer dies, the item will be
- * made available again by the DrapalQueueInterface implementation once the
+ * made available again by the DrupalQueueInterface implementation once the
  * lease expires. Another consumer will then be able to receive it when calling
  * DrupalQueueInterface::claimItem().
  *
@@ -62,7 +62,10 @@ class DrupalQueue {
   public static function get($name) {
     static $queues;
     if (!isset($queues[$name])) {
-      $class = variable_get('queue_module_' . $name, 'System') . 'Queue';
+      $class = variable_get('queue_class_' . $name, NULL);
+      if (!$class) {
+        $class = variable_get('queue_default_class', 'SystemQueue');
+      }
       $queues[$name] = new $class($name);
     }
     return $queues[$name];
diff --git a/modules/system/system.test b/modules/system/system.test
index 42bb46bbf50183b139c3833724d7f4607ab08e2b..273244bea273274003e261cfa83f1368a6943da9 100644
--- a/modules/system/system.test
+++ b/modules/system/system.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.test,v 1.117 2010/03/12 14:33:02 dries Exp $
+// $Id: system.test,v 1.124 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * Helper class for module test cases.
@@ -24,7 +24,7 @@ class ModuleTestCase extends DrupalWebTestCase {
    *   specified base table. Defaults to TRUE.
    */
   function assertTableCount($base_table, $count = TRUE) {
-    $tables = db_find_tables(Database::getConnection()->prefixTables('{' . $base_table . '}') . '%');
+    $tables = db_find_tables($base_table . '%');
 
     if ($count) {
       return $this->assertTrue($tables, t('Tables matching "@base_table" found.', array('@base_table' => $base_table)));
@@ -157,6 +157,16 @@ class EnableDisableTestCase extends ModuleTestCase {
     $this->drupalPost('admin/modules', $edit, t('Save configuration'));
     $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.'));
   }
+
+  /**
+   * Tests entity cache after enabling a module with a dependency on an enitity
+   * providing module.
+   */
+  function testEntityCache() {
+    module_enable(array('entity_cache_test'));
+    $info = variable_get('entity_cache_test');
+    $this->assertNotNull($info, t('Entity information must not be NULL'));
+  }
 }
 
 /**
@@ -469,7 +479,7 @@ class CronRunTestCase extends DrupalWebTestCase {
   function testTempFileCleanup() {
     // Temporary file that is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
     $temp_old = file_save_data('');
-    db_update('file')
+    db_update('file_managed')
       ->fields(array(
         'status' => 0,
         'timestamp' => 1,
@@ -480,7 +490,7 @@ class CronRunTestCase extends DrupalWebTestCase {
 
     // Temporary file that is less than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
     $temp_new = file_save_data('');
-    db_update('file')
+    db_update('file_managed')
       ->fields(array('status' => 0))
       ->condition('fid', $temp_new->fid)
       ->execute();
@@ -488,7 +498,7 @@ class CronRunTestCase extends DrupalWebTestCase {
 
     // Permanent file that is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
     $perm_old = file_save_data('');
-    db_update('file')
+    db_update('file_managed')
       ->fields(array('timestamp' => 1))
       ->condition('fid', $temp_old->fid)
       ->execute();
@@ -1480,6 +1490,77 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
     $generated = token_generate('node', $raw_tokens, array('node' => $node), array('sanitize' => FALSE));
     $this->assertFalse(strcmp($generated['[node:title]'], $node->title), t('Unsanitized token generated properly.'));
   }
+
+  /**
+   * Tests the generation of all system site information tokens.
+   */
+  function testSystemSiteTokenReplacement() {
+    global $language;
+    $url_options = array(
+      'absolute' => TRUE,
+      'language' => $language,
+    );
+
+    // Set a few site variables.
+    variable_set('site_name', '<strong>Drupal<strong>');
+    variable_set('site_slogan', '<blink>Slogan</blink>');
+    variable_set('site_mission', '<em>Mission</em>');
+
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[site:name]'] = check_plain(variable_get('site_name', 'Drupal'));
+    $tests['[site:slogan]'] = check_plain(variable_get('site_slogan', ''));
+    $tests['[site:mission]'] = filter_xss(variable_get('site_mission', ''));
+    $tests['[site:mail]'] = 'simpletest@example.com';
+    $tests['[site:url]'] = url('<front>', $url_options);
+    $tests['[site:url-brief]'] = preg_replace('!^https?://!', '', url('<front>', $url_options));
+    $tests['[site:login-url]'] = url('user', $url_options);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array(), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized system site information token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[site:name]'] = variable_get('site_name', 'Drupal');
+    $tests['[site:slogan]'] = variable_get('site_slogan', '');
+    $tests['[site:mission]'] = variable_get('site_mission', '');
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array(), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized system site information token %token replaced.', array('%token' => $input)));
+    }
+  }
+
+  /**
+   * Tests the generation of all system date tokens.
+   */
+  function testSystemDateTokenReplacement() {
+    global $language;
+
+    // Set time to one hour before request.
+    $date = REQUEST_TIME - 3600;
+
+    // Generate and test tokens.
+    $tests = array();
+    $tests['[date:short]'] = format_date($date, 'short', '', NULL, $language->language);
+    $tests['[date:medium]'] = format_date($date, 'medium', '', NULL, $language->language);
+    $tests['[date:long]'] = format_date($date, 'long', '', NULL, $language->language);
+    $tests['[date:custom:m/j/Y]'] = format_date($date, 'custom', 'm/j/Y', NULL, $language->language);
+    $tests['[date:since]'] = format_interval((REQUEST_TIME - $date), 2, $language->language);
+    $tests['[date:raw]'] = filter_xss($date);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('date' => $date), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Date token %token replaced.', array('%token' => $input)));
+    }
+  }
 }
 
 class InfoFileParserTestCase extends DrupalUnitTestCase {
@@ -1604,6 +1685,20 @@ class UpdateScriptFunctionalTest extends DrupalWebTestCase {
     $this->drupalGet($this->update_url, array('external' => TRUE));
     $this->assertResponse(200);
   }
+
+  /**
+   * Tests the effect of using the update script on the theme system.
+   */
+  function testThemeSystem() {
+    // Since visiting update.php triggers a rebuild of the theme system from an
+    // unusual maintenance mode environment, we check that this rebuild did not
+    // put any incorrect information about the themes into the database.
+    $original_theme_data = db_query("SELECT * FROM {system} WHERE type = 'theme' ORDER BY name")->fetchAll();
+    $this->drupalLogin($this->update_user);
+    $this->drupalGet($this->update_url, array('external' => TRUE));
+    $final_theme_data = db_query("SELECT * FROM {system} WHERE type = 'theme' ORDER BY name")->fetchAll();
+    $this->assertEqual($original_theme_data, $final_theme_data, t('Visiting update.php does not alter the information about themes stored in the database.'));
+  }
 }
 
 /**
@@ -1705,7 +1800,7 @@ class ShutdownFunctionsTest extends DrupalWebTestCase {
   }
 
   /**
-   * Test flood control mechanism clean-up.
+   * Test shutdown functions.
    */
   function testShutdownFunctions() {
     $arg1 = $this->randomName();
@@ -1715,3 +1810,41 @@ class ShutdownFunctionsTest extends DrupalWebTestCase {
     $this->assertText(t('Second shutdown function, arg1 : @arg1, arg2: @arg2', array('@arg1' => $arg1, '@arg2' => $arg2)));
   }
 }
+
+/**
+ * Functional tests compact mode.
+ */
+class CompactModeTest extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Compact mode',
+      'description' => 'Tests compact mode functionality.',
+      'group' => 'System',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    $admin_user = $this->drupalCreateUser(array('access administration pages'));
+    $this->drupalLogin($admin_user);
+  }
+
+  /**
+   * Test compact mode.
+   */
+  function testCompactMode() {
+    $this->drupalGet('admin/compact/on');
+    $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode turns on.'));
+    $this->drupalGet('admin/compact/on');
+    $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode remains on after a repeat call.'));
+    $this->drupalGet('');
+    $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode persists on new requests.'));
+
+    $this->drupalGet('admin/compact/off');
+    $this->assertEqual($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'deleted', t('Compact mode turns off.'));
+    $this->drupalGet('admin/compact/off');
+    $this->assertEqual($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'deleted', t('Compact mode remains off after a repeat call.'));
+    $this->drupalGet('');
+    $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode persists on new requests.'));
+  }
+}
diff --git a/modules/system/system.tokens.inc b/modules/system/system.tokens.inc
index 13f86b07290b629dc4fc8c8bc4ddb5a45af3f734..2c1284311f1f9cb6b71cc751423f9f672ce6c025 100644
--- a/modules/system/system.tokens.inc
+++ b/modules/system/system.tokens.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.tokens.inc,v 1.8 2010/01/30 07:59:25 dries Exp $
+// $Id: system.tokens.inc,v 1.9 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -76,7 +76,7 @@ function system_token_info() {
   );
   $date['since'] = array(
     'name' => t("Time-since"),
-    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
+    'description' => t("A date in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
   );
   $date['raw'] = array(
     'name' => t("Raw timestamp"),
@@ -93,10 +93,6 @@ function system_token_info() {
     'name' => t("User ID"),
     'description' => t("The unique ID of the user who owns the file."),
   );
-  $file['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node the file is attached to."),
-  );
   $file['name'] = array(
     'name' => t("File name"),
     'description' => t("The name of the file on disk."),
@@ -107,7 +103,7 @@ function system_token_info() {
   );
   $file['path'] = array(
     'name' => t("Path"),
-    'description' => t("The location of the file on disk."),
+    'description' => t("The location of the file relative to Drupal root."),
   );
   $file['mime'] = array(
     'name' => t("MIME type"),
@@ -117,7 +113,7 @@ function system_token_info() {
     'name' => t("File size"),
     'description' => t("The size of the file, in kilobytes."),
   );
-  $file['path'] = array(
+  $file['url'] = array(
     'name' => t("URL"),
     'description' => t("The web-accessible URL for the file."),
   );
@@ -126,11 +122,6 @@ function system_token_info() {
     'description' => t("The date the file was most recently changed."),
     'type' => 'date',
   );
-  $file['node'] = array(
-    'name' => t("Node"),
-    'description' => t("The node the file is attached to."),
-    'type' => 'date',
-  );
   $file['owner'] = array(
     'name' => t("Owner"),
     'description' => t("The user who originally uploaded the file."),
@@ -155,6 +146,7 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
   if (isset($language)) {
     $url_options['language'] = $language;
   }
+  $langcode = (isset($language) ? $language->language : NULL);
   $sanitize = !empty($options['sanitize']);
 
   $replacements = array();
@@ -203,14 +195,9 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
     else {
       $date = $data['date'];
     }
-    $langcode = (isset($language) ? $language->language : NULL);
 
     foreach ($tokens as $name => $original) {
       switch ($name) {
-        case 'raw':
-          $replacements[$original] = filter_xss($date);
-          break;
-
         case 'short':
           $replacements[$original] = format_date($date, 'short', '', NULL, $langcode);
           break;
@@ -226,6 +213,10 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
         case 'since':
           $replacements[$original] = format_interval((REQUEST_TIME - $date), 2, $langcode);
           break;
+
+        case 'raw':
+          $replacements[$original] = filter_xss($date);
+          break;
       }
     }
 
@@ -250,10 +241,6 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
           $replacements[$original] = $file->uid;
           break;
 
-        case 'nid':
-          $replacements[$original] = $file->nid;
-          break;
-
         // Essential file data
         case 'name':
           $replacements[$original] = $sanitize ? check_plain($file->filename) : $file->filename;
@@ -264,7 +251,7 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
           break;
 
         case 'path':
-          $replacements[$original] = $sanitize ? filter_xss($file->filepath) : $file->filepath;
+          $replacements[$original] = $sanitize ? filter_xss($file->uri) : $file->uri;
           break;
 
         case 'mime':
@@ -276,39 +263,27 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a
           break;
 
         case 'url':
-          $replacements[$original] = url(file_create_url($file->filepath), $url_options);
+          $replacements[$original] = url(file_create_url($file->uri), $url_options);
           break;
 
         // These tokens are default variations on the chained tokens handled below.
-        case 'node':
-          if ($nid = $file->nid) {
-            $node = node_load($file->nid);
-            $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
-          }
-          break;
-
         case 'timestamp':
-          $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
+          $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, $langcode);
           break;
 
         case 'owner':
           $account = user_load($file->uid);
-          $replacements[$original] = $sanitize ? filter_xss($user->name) : $user->name;
+          $replacements[$original] = $sanitize ? filter_xss($account->name) : $account->name;
           break;
       }
     }
 
-    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
-      $node = node_load($file->nid);
-      $replacements += token_generate('node', $node_tokens, array('node' => $node), $language, $sanitize);
-    }
-
     if ($date_tokens = token_find_with_prefix($tokens, 'timestamp')) {
-      $replacements += token_generate('date', $date_tokens, array('date' => $file->timestamp), $language, $sanitize);
+      $replacements += token_generate('date', $date_tokens, array('date' => $file->timestamp), $options);
     }
 
     if (($owner_tokens = token_find_with_prefix($tokens, 'owner')) && $account = user_load($file->uid)) {
-      $replacements += token_generate('user', $owner_tokens, array('user' => $account), $language, $sanitize);
+      $replacements += token_generate('user', $owner_tokens, array('user' => $account), $options);
     }
   }
 
diff --git a/modules/system/system.updater.inc b/modules/system/system.updater.inc
index d990c3a727f31b41a31f31172662dd196aab5653..4e5b3f227219bac963c5c3d143ee5c5bea952941 100644
--- a/modules/system/system.updater.inc
+++ b/modules/system/system.updater.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: system.updater.inc,v 1.5 2010/01/30 07:59:25 dries Exp $
+// $Id: system.updater.inc,v 1.7 2010/04/10 09:49:49 dries Exp $
 
 /**
  * @file
@@ -81,10 +81,8 @@ class ModuleUpdater extends Updater implements DrupalUpdaterInterface {
   }
 
   public function postUpdateTasks() {
-    // @todo: If there are schema updates.
-    return array(
-      l(t('Run database updates for !project', array('!project' => $this->title)), 'update.php'),
-    );
+    // We don't want to check for DB updates here, we do that once for all
+    // updated modules on the landing page.
   }
 
 }
@@ -135,8 +133,7 @@ class ThemeUpdater extends Updater implements DrupalUpdaterInterface {
   public function postInstall() {
     // Update the system table.
     clearstatcache();
-    drupal_static_reset('_system_rebuild_theme_data');
-    _system_rebuild_theme_data();
+    system_rebuild_theme_data();
 
     // Active the theme
     db_update('system')
diff --git a/modules/system/theme.api.php b/modules/system/theme.api.php
index d0d85e879e0e59b9eade839270353573d54876fa..14b9ffe73d5d846aef6087545afc097a742b257d 100644
--- a/modules/system/theme.api.php
+++ b/modules/system/theme.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: theme.api.php,v 1.1 2010/02/27 09:24:09 dries Exp $
+// $Id: theme.api.php,v 1.2 2010/04/24 07:11:07 dries Exp $
 
 /**
  * @defgroup themeable Default theme implementations
@@ -93,3 +93,30 @@ function hook_form_system_theme_settings_alter(&$form, &$form_state) {
     '#description'   => t('Show a trail of links from the homepage to the current page.'),
   );
 }
+
+/**
+ * Respond to themes being enabled.
+ *
+ * @param array $theme_list
+ *   Array containing the names of the themes being enabled.
+ *
+ * @see theme_enable()
+ */
+function hook_themes_enabled($theme_list) {
+  foreach ($theme_list as $theme) {
+    block_theme_initialize($theme);
+  }
+}
+
+/**
+ * Respond to themes being disabled.
+ *
+ * @param array $theme_list
+ *   Array containing the names of the themes being disabled.
+ *
+ * @see theme_disable()
+ */
+function hook_themes_disabled($theme_list) {
+ // Clear all update module caches.
+  _update_cache_clear();
+}
diff --git a/modules/taxonomy/taxonomy.admin.inc b/modules/taxonomy/taxonomy.admin.inc
index e5f3be93aa2552206b657507e9706ac06e24e709..2a218661d0be884512831dd0d7b5a119383ba476 100644
--- a/modules/taxonomy/taxonomy.admin.inc
+++ b/modules/taxonomy/taxonomy.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: taxonomy.admin.inc,v 1.96 2010/03/18 06:36:39 dries Exp $
+// $Id: taxonomy.admin.inc,v 1.102 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -20,15 +20,16 @@ function taxonomy_overview_vocabularies($form) {
     $form[$vocabulary->vid]['#vocabulary'] = $vocabulary;
     $form[$vocabulary->vid]['name'] = array('#markup' => check_plain($vocabulary->name));
     $form[$vocabulary->vid]['weight'] = array('#type' => 'weight', '#delta' => 10, '#default_value' => $vocabulary->weight);
-    $form[$vocabulary->vid]['edit'] = array('#type' => 'link', '#title' => t('edit vocabulary'), '#href' => "admin/structure/taxonomy/$vocabulary->vid/edit");
-    $form[$vocabulary->vid]['list'] = array('#type' => 'link', '#title' => t('list terms'), '#href' => "admin/structure/taxonomy/$vocabulary->vid");
-    $form[$vocabulary->vid]['add'] = array('#type' => 'link', '#title' => t('add terms'), '#href' => "admin/structure/taxonomy/$vocabulary->vid/add");
+    $form[$vocabulary->vid]['edit'] = array('#type' => 'link', '#title' => t('edit vocabulary'), '#href' => "admin/structure/taxonomy/$vocabulary->machine_name/edit");
+    $form[$vocabulary->vid]['list'] = array('#type' => 'link', '#title' => t('list terms'), '#href' => "admin/structure/taxonomy/$vocabulary->machine_name");
+    $form[$vocabulary->vid]['add'] = array('#type' => 'link', '#title' => t('add terms'), '#href' => "admin/structure/taxonomy/$vocabulary->machine_name/add");
   }
 
   // Only make this form include a submit button and weight if more than one
   // vocabulary exists.
   if (count($vocabularies) > 1) {
-    $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
   }
   elseif (isset($vocabulary)) {
     unset($form[$vocabulary->vid]['weight']);
@@ -52,10 +53,14 @@ function taxonomy_overview_vocabularies_submit($form, &$form_state) {
 }
 
 /**
- * Theme the vocabulary overview as a sortable list of vocabularies.
+ * Returns HTML for the vocabulary overview form as a sortable list of vocabularies.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
- * @ingroup themeable
  * @see taxonomy_overview_vocabularies()
+ * @ingroup themeable
  */
 function theme_taxonomy_overview_vocabularies($variables) {
   $form = $variables['form'];
@@ -80,7 +85,7 @@ function theme_taxonomy_overview_vocabularies($variables) {
   }
 
   $header = array(t('Vocabulary name'));
-  if (isset($form['submit'])) {
+  if (isset($form['actions'])) {
     $header[] = t('Weight');
     drupal_add_tabledrag('taxonomy', 'order', 'sibling', 'vocabulary-weight');
   }
@@ -154,9 +159,10 @@ function taxonomy_form_vocabulary($form, &$form_state, $edit = array()) {
     '#value' => '0',
   );
 
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
   if (isset($edit['vid'])) {
-    $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+    $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
     $form['vid'] = array('#type' => 'value', '#value' => $edit['vid']);
     $form['module'] = array('#type' => 'value', '#value' => $edit['module']);
   }
@@ -209,12 +215,12 @@ function taxonomy_form_vocabulary_submit($form, &$form_state) {
   switch (taxonomy_vocabulary_save($vocabulary)) {
     case SAVED_NEW:
       drupal_set_message(t('Created new vocabulary %name.', array('%name' => $vocabulary->name)));
-      watchdog('taxonomy', 'Created new vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->vid . '/edit'));
+      watchdog('taxonomy', 'Created new vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit'));
       break;
 
     case SAVED_UPDATED:
       drupal_set_message(t('Updated vocabulary %name.', array('%name' => $vocabulary->name)));
-      watchdog('taxonomy', 'Updated vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->vid . '/edit'));
+      watchdog('taxonomy', 'Updated vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit'));
       break;
   }
 
@@ -378,14 +384,15 @@ function taxonomy_overview_terms($form, &$form_state, $vocabulary) {
   $form['#page_entries'] = $page_entries;
   $form['#back_step'] = $back_step;
   $form['#forward_step'] = $forward_step;
-  $form['#empty_text'] = t('No terms available. <a href="@link">Add term</a>.', array('@link' => url('admin/structure/taxonomy/' . $vocabulary->vid . '/add')));
+  $form['#empty_text'] = t('No terms available. <a href="@link">Add term</a>.', array('@link' => url('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add')));
 
   if ($vocabulary->hierarchy < 2 && count($tree) > 1) {
-    $form['submit'] = array(
+    $form['actions'] = array('#type' => 'actions', '#tree' => FALSE);
+    $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Save')
     );
-    $form['reset_alphabetical'] = array(
+    $form['actions']['reset_alphabetical'] = array(
       '#type' => 'submit',
       '#value' => t('Reset to alphabetical')
     );
@@ -513,10 +520,14 @@ function taxonomy_overview_terms_submit($form, &$form_state) {
 }
 
 /**
- * Theme the terms overview as a sortable list of terms.
+ * Returns HTML for a terms overview form as a sortable list of terms.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
- * @ingroup themeable
  * @see taxonomy_overview_terms()
+ * @ingroup themeable
  */
 function theme_taxonomy_overview_terms($variables) {
   $form = $variables['form'];
@@ -686,6 +697,9 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
 
     $tree = taxonomy_get_tree($vocabulary->vid);
     $options = array('<' . t('root') . '>');
+    if (empty($parent)) {
+      $parent = array(0);
+    }
     foreach ($tree as $term) {
       if (!in_array($term->tid, $exclude)) {
         $options[$term->tid] = str_repeat('-', $term->depth) . $term->name;
@@ -717,11 +731,7 @@ function taxonomy_form_term($form, &$form_state, $edit = array(), $vocabulary =
     '#value' => $edit['tid'],
   );
 
-  $form['actions'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-actions')),
-    '#weight' => 100,
-  );
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
@@ -791,6 +801,8 @@ function taxonomy_form_term_submit($form, &$form_state) {
     case SAVED_UPDATED:
       drupal_set_message(t('Updated term %term.', array('%term' => $term->name)));
       watchdog('taxonomy', 'Updated term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit'));
+      // Clear the page and block caches to avoid stale data.
+      cache_clear_all();
       break;
   }
 
@@ -816,7 +828,8 @@ function taxonomy_form_term_submit($form, &$form_state) {
 
   $form_state['values']['tid'] = $term->tid;
   $form_state['tid'] = $term->tid;
-  $form_state['redirect'] = 'admin/structure/taxonomy';
+  // Do not rebuild here. The term is saved by now and the form should clear.
+  $form_state['rebuild'] = FALSE;
 }
 
 /**
@@ -941,12 +954,13 @@ function taxonomy_vocabulary_confirm_reset_alphabetical($form, &$form_state, $vi
 
   $form['type'] = array('#type' => 'value', '#value' => 'vocabulary');
   $form['vid'] = array('#type' => 'value', '#value' => $vid);
+  $form['machine_name'] = array('#type' => 'value', '#value' => $vocabulary->machine_name);
   $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name);
   $form['reset_alphabetical'] = array('#type' => 'value', '#value' => TRUE);
   return confirm_form($form,
                   t('Are you sure you want to reset the vocabulary %title to alphabetical order?',
                   array('%title' => $vocabulary->name)),
-                  'admin/structure/taxonomy/' . $vid,
+                  'admin/structure/taxonomy/' . $vocabulary->machine_name,
                   t('Resetting a vocabulary will discard all custom ordering and sort items alphabetically.'),
                   t('Reset to alphabetical'),
                   t('Cancel'));
@@ -964,5 +978,5 @@ function taxonomy_vocabulary_confirm_reset_alphabetical_submit($form, &$form_sta
     ->execute();
   drupal_set_message(t('Reset vocabulary %name to alphabetical order.', array('%name' => $form_state['values']['name'])));
   watchdog('taxonomy', 'Reset vocabulary %name to alphabetical order.', array('%name' => $form_state['values']['name']), WATCHDOG_NOTICE);
-  $form_state['redirect'] = 'admin/structure/taxonomy/' . $form_state['values']['vid'];
+  $form_state['redirect'] = 'admin/structure/taxonomy/' . $form_state['values']['machine_name'];
 }
diff --git a/modules/taxonomy/taxonomy.info b/modules/taxonomy/taxonomy.info
index 5a42c39e1c7616d00f4cea3750efc93c2628dc87..6941518b5a8e50bba27b22c837cbb452e7604c29 100644
--- a/modules/taxonomy/taxonomy.info
+++ b/modules/taxonomy/taxonomy.info
@@ -12,8 +12,8 @@ files[] = taxonomy.test
 files[] = taxonomy.tokens.inc
 configure = admin/structure/taxonomy
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/taxonomy/taxonomy.install b/modules/taxonomy/taxonomy.install
index 617e05061a8ee3635cfddd9c16826de9fd56c347..62a2bc34e46ebc447248209cc25ec7d22f94b548 100644
--- a/modules/taxonomy/taxonomy.install
+++ b/modules/taxonomy/taxonomy.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: taxonomy.install,v 1.37 2010/03/06 06:39:01 dries Exp $
+// $Id: taxonomy.install,v 1.41 2010/04/20 07:47:55 webchick Exp $
 
 /**
  * @file
@@ -159,6 +159,9 @@ function taxonomy_schema() {
     'indexes' => array(
       'list' => array('weight', 'name'),
     ),
+    'unique keys' => array(
+      'machine_name' => array('machine_name'),
+    ),
   );
 
   $schema['taxonomy_index'] = array(
@@ -195,6 +198,7 @@ function taxonomy_schema() {
     ),
     'indexes' => array(
       'term_node' => array('tid', 'sticky', 'created'),
+      'nid' => array('nid'),
     ),
     'foreign keys' => array(
       'node' => 'nid',
@@ -259,11 +263,15 @@ function taxonomy_update_7002() {
   foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) {
     $machine_name = 'vocabulary_' . $vid;
     db_update('taxonomy_vocabulary')
-      ->fields(array('machine_name' => 'vocabulary_' . $vid))
+      ->fields(array('machine_name' => $machine_name))
       ->condition('vid', $vid)
       ->execute();
     field_attach_create_bundle('taxonomy_term', $machine_name);
   }
+
+  // The machine_name unique key can only be added after we ensure the
+  // machine_name column contains unique values.
+  db_add_unique_key('taxonomy_vocabulary', 'machine_name', array('machine_name'));
 }
 
 /**
@@ -314,6 +322,7 @@ function taxonomy_update_7004() {
     ),
     'indexes' => array(
       'term_node' => array('tid', 'sticky', 'created'),
+      'nid' => array('nid'),
     ),
     'foreign keys' => array(
       'node' => 'nid',
@@ -363,7 +372,7 @@ function taxonomy_update_7004() {
         'label' => $vocabulary->name,
         'field_name' => $field_name,
         'bundle' => $bundle,
-        'object_type' => 'node',
+        'entity_type' => 'node',
         'description' => $vocabulary->help,
         'widget' => array(
           'type' => $vocabulary->tags ? 'taxonomy_autocomplete' : 'select',
diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module
index 58da4b0baa4c7fb2aaaa7754ac422f3268ef3d79..d16d2aa142ce764bb981151f402a5b1d2ff38007 100644
--- a/modules/taxonomy/taxonomy.module
+++ b/modules/taxonomy/taxonomy.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: taxonomy.module,v 1.579 2010/03/08 17:50:40 dries Exp $
+// $Id: taxonomy.module,v 1.587 2010/04/23 07:54:44 webchick Exp $
 
 /**
  * @file
@@ -19,7 +19,7 @@ function taxonomy_help($path, $arg) {
       $output .= '<dl>';
       $output .= '<dt>' . t('Creating vocabularies') . '</dt>';
       $output .= '<dd>' . t('Users with sufficient <a href="@perm">permissions</a> can create <em>vocabularies</em> and <em>terms</em> through the <a href="@taxo">Taxonomy page</a>. The page listing the terms provides a drag-and-drop interface for controlling the order of the terms and sub-terms within a vocabulary, in a hierarchical fashion. A <em>controlled vocabulary</em> classifying music by genre with terms and sub-terms could look as follows:', array('@taxo' => url('admin/structure/taxonomy'), '@perm' => url('admin/people/permissions', array('fragment'=>'module-taxonomy'))));
-      $output .= '<ul><li>' . t ('<em>vocabulary</em>: Music'); '</li>';
+      $output .= '<ul><li>' . t('<em>vocabulary</em>: Music') . '</li>';
       $output .= '<ul><li>' . t('<em>term</em>: Jazz') . '</li>';
       $output .= '<ul><li>' . t('<em>sub-term</em>: Swing') . '</li>';
       $output .= '<li>' . t('<em>sub-term</em>: Fusion') . '</li></ul></ul>';
@@ -42,7 +42,7 @@ function taxonomy_help($path, $arg) {
       $output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
       return $output;
     case 'admin/structure/taxonomy/%':
-      $vocabulary = taxonomy_vocabulary_load($arg[3]);
+      $vocabulary = taxonomy_vocabulary_machine_name_load($arg[3]);
       switch ($vocabulary->hierarchy) {
         case 0:
           return '<p>' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) . '</p>';
@@ -89,7 +89,7 @@ function taxonomy_entity_info() {
       'base table' => 'taxonomy_term_data',
       'uri callback' => 'taxonomy_term_uri',
       'fieldable' => TRUE,
-      'object keys' => array(
+      'entity keys' => array(
         'id' => 'tid',
         'bundle' => 'vocabulary_machine_name',
       ),
@@ -109,8 +109,8 @@ function taxonomy_entity_info() {
     $return['taxonomy_term']['bundles'][$machine_name] = array(
       'label' => $vocabulary->name,
       'admin' => array(
-        'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary',
-        'real path' => 'admin/structure/taxonomy/' . $vocabulary->vid,
+        'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary_machine_name',
+        'real path' => 'admin/structure/taxonomy/' . $machine_name,
         'bundle argument' => 3,
         'access arguments' => array('administer taxonomy'),
       ),
@@ -120,7 +120,7 @@ function taxonomy_entity_info() {
     'label' => t('Taxonomy vocabulary'),
     'controller class' => 'TaxonomyVocabularyController',
     'base table' => 'taxonomy_vocabulary',
-    'object keys' => array(
+    'entity keys' => array(
       'id' => 'vid',
     ),
     'fieldable' => FALSE,
@@ -302,7 +302,7 @@ function taxonomy_menu() {
     'file' => 'taxonomy.pages.inc',
   );
 
-  $items['admin/structure/taxonomy/%taxonomy_vocabulary'] = array(
+  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name'] = array(
     'title callback' => 'taxonomy_admin_vocabulary_title_callback',
     'title arguments' => array(3),
     'page callback' => 'drupal_get_form',
@@ -310,12 +310,12 @@ function taxonomy_menu() {
     'access arguments' => array('administer taxonomy'),
     'file' => 'taxonomy.admin.inc',
   );
-  $items['admin/structure/taxonomy/%taxonomy_vocabulary/list'] = array(
+  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/list'] = array(
     'title' => 'List',
     'type' => MENU_DEFAULT_LOCAL_TASK,
     'weight' => -20,
   );
-  $items['admin/structure/taxonomy/%taxonomy_vocabulary/edit'] = array(
+  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/edit'] = array(
     'title' => 'Edit',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('taxonomy_form_vocabulary', 3),
@@ -325,7 +325,7 @@ function taxonomy_menu() {
     'file' => 'taxonomy.admin.inc',
   );
 
-  $items['admin/structure/taxonomy/%taxonomy_vocabulary/add'] = array(
+  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/add'] = array(
     'title' => 'Add term',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('taxonomy_form_term', array(), 3),
@@ -461,10 +461,8 @@ function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) {
  *   Status constant indicating if term was inserted or updated.
  */
 function taxonomy_term_save($term) {
-  if ($term->name) {
-    // Prevent leading and trailing spaces in term names.
-    $term->name = trim($term->name);
-  }
+  // Prevent leading and trailing spaces in term names.
+  $term->name = trim($term->name);
   if (!isset($term->vocabulary_machine_name)) {
     $vocabulary = taxonomy_vocabulary_load($term->vid);
     $term->vocabulary_machine_name = $vocabulary->machine_name;
@@ -472,32 +470,32 @@ function taxonomy_term_save($term) {
 
   field_attach_presave('taxonomy_term', $term);
 
-  if (!empty($term->tid) && $term->name) {
-    $status = drupal_write_record('taxonomy_term_data', $term, 'tid');
-    field_attach_update('taxonomy_term', $term);
-    module_invoke_all('taxonomy_term_update', $term);
-    entity_invoke('update', 'taxonomy_term', $term);
-  }
-  else {
+  if (empty($term->tid)) {
     $status = drupal_write_record('taxonomy_term_data', $term);
     field_attach_insert('taxonomy_term', $term);
     module_invoke_all('taxonomy_term_insert', $term);
     entity_invoke('insert', 'taxonomy_term', $term);
+    if (!isset($term->parent)) {
+      $term->parent = array(0);
+    }
   }
-
-  db_delete('taxonomy_term_hierarchy')
-    ->condition('tid', $term->tid)
-    ->execute();
-
-  if (!isset($term->parent) || empty($term->parent)) {
-    $term->parent = array(0);
-  }
-  if (!is_array($term->parent)) {
-    $term->parent = array($term->parent);
+  else {
+    $status = drupal_write_record('taxonomy_term_data', $term, 'tid');
+    field_attach_update('taxonomy_term', $term);
+    module_invoke_all('taxonomy_term_update', $term);
+    entity_invoke('update', 'taxonomy_term', $term);
+    if (isset($term->parent)) {
+      db_delete('taxonomy_term_hierarchy')
+        ->condition('tid', $term->tid)
+        ->execute();
+    }
   }
-  $query = db_insert('taxonomy_term_hierarchy')
-    ->fields(array('tid', 'parent'));
-  if (is_array($term->parent)) {
+  if (isset($term->parent)) {
+    if (!is_array($term->parent)) {
+      $term->parent = array($term->parent);
+    }
+    $query = db_insert('taxonomy_term_hierarchy')
+      ->fields(array('tid', 'parent'));
     foreach ($term->parent as $parent) {
       if (is_array($parent)) {
         foreach ($parent as $tid) {
@@ -514,10 +512,8 @@ function taxonomy_term_save($term) {
         ));
       }
     }
+    $query->execute();
   }
-  $query->execute();
-
-  cache_clear_all();
   taxonomy_terms_static_reset();
 
   return $status;
@@ -613,7 +609,8 @@ function template_preprocess_taxonomy_term(&$variables) {
   $variables['term'] = $variables['elements']['#term'];
   $term = $variables['term'];
 
-  $variables['term_url']  = url('taxonomy/term/' . $term->tid);
+  $uri = entity_uri('taxonomy_term', $term);
+  $variables['term_url']  = url($uri['path'], $uri['options']);
   $variables['term_name'] = check_plain($term->name);
   $variables['page']      = taxonomy_term_is_page($term);
 
@@ -974,6 +971,21 @@ function taxonomy_vocabulary_load($vid) {
   return reset($vocabularies);
 }
 
+/**
+ * Return the vocabulary object matching a vocabulary machine name.
+ *
+ * @param $name
+ *   The vocabulary's machine name.
+ *
+ * @return
+ *   The vocabulary object with all of its metadata, if exists, FALSE otherwise.
+ *   Results are statically cached.
+ */
+function taxonomy_vocabulary_machine_name_load($name) {
+  $vocabularies = taxonomy_vocabulary_load_multiple(NULL, array('machine_name' => $name));
+  return reset($vocabularies);
+}
+
 /**
  * Return the term object matching a term ID.
  *
@@ -1196,10 +1208,12 @@ function taxonomy_field_formatter_view($entity_type, $entity, $field, $instance,
     case 'taxonomy_term_reference_link':
       foreach ($items as $delta => $item) {
         $term = $item['taxonomy_term'];
+        $uri = entity_uri('taxonomy_term', $term);
         $element[$delta] = array(
           '#type' => 'link',
           '#title' => $term->name,
-          '#href' => 'taxonomy/term/' . $term->tid,
+          '#href' => $uri['path'],
+          '#options' => $uri['options'],
         );
       }
       break;
@@ -1407,12 +1421,9 @@ function taxonomy_rdf_mapping() {
         'description'   => array(
           'predicates' => array('skos:definition'),
         ),
-        // The vocabulary this term belongs to. The type 'rev' is used to denote
-        // the fact that the skos:member property domain is skos:Collection and
-        // its range is skos:Concept or skos:Collection.
         'vid'   => array(
-          'predicates' => array('skos:member'),
-          'type' => 'rev',
+          'predicates' => array('skos:inScheme'),
+          'type' => 'rel',
         ),
         'parent'   => array(
           'predicates' => array('skos:broader'),
@@ -1424,9 +1435,9 @@ function taxonomy_rdf_mapping() {
       'type' => 'taxonomy_vocabulary',
       'bundle' => RDF_DEFAULT_BUNDLE,
       'mapping' => array(
-        'rdftype' => array('skos:Collection'),
+        'rdftype' => array('skos:ConceptScheme'),
         'name'   => array(
-          'predicates' => array('rdfs:label'),
+          'predicates' => array('dc:title'),
         ),
         'description'   => array(
           'predicates' => array('rdfs:comment'),
diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test
index 47e56b407247c01d86acc357143a668a89e78593..ac8ef0d1bcc6b5bae2452180497d6cde25df19c5 100644
--- a/modules/taxonomy/taxonomy.test
+++ b/modules/taxonomy/taxonomy.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: taxonomy.test,v 1.72 2010/03/07 23:14:20 webchick Exp $
+// $Id: taxonomy.test,v 1.77 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -163,7 +163,7 @@ class TaxonomyVocabularyFunctionalTest extends TaxonomyWebTestCase {
 
     // Delete the vocabulary.
     $edit = array();
-    $this->drupalPost('admin/structure/taxonomy/' . $vid . '/edit', $edit, t('Delete'));
+    $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit', $edit, t('Delete'));
     $this->assertRaw(t('Are you sure you want to delete the vocabulary %name?', array('%name' => $vocabulary->name)), t('[confirm deletion] Asks for confirmation.'));
     $this->assertText(t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'), t('[confirm deletion] Inform that all terms will be deleted.'));
 
@@ -342,7 +342,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     $this->instance = array(
       'field_name' => 'taxonomy_' . $this->vocabulary->machine_name,
       'bundle' => 'article',
-      'object_type' => 'node',
+      'entity_type' => 'node',
       'widget' => array(
         'type' => 'options_select',
       ),
@@ -374,6 +374,12 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     $this->assertTrue(isset($children[$term2->tid]), t('Child found correctly.'));
     $this->assertTrue(isset($parents[$term1->tid]), t('Parent found correctly.'));
 
+    // Load and save a term, confirming that parents are still set.
+    $term = taxonomy_term_load($term2->tid);
+    taxonomy_term_save($term);
+    $parents = taxonomy_get_parents($term2->tid);
+    $this->assertTrue(isset($parents[$term1->tid]), t('Parent found correctly.'));
+
     // Create a third term and save this as a parent of term2.
     $term3 = $this->createTerm($this->vocabulary);
     $term2->parent = array($term1->tid, $term3->tid);
@@ -455,14 +461,14 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     $edit['parent[]'] = array(0);
 
     // Create the term to edit.
-    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->vid . '/add', $edit, t('Save'));
+    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/add', $edit, t('Save'));
 
     $terms = taxonomy_get_term_by_name($edit['name']);
     $term = reset($terms);
     $this->assertNotNull($term, t('Term found in database'));
 
     // Submitting a term takes us to the add page; we need the List page.
-    $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->vid);
+    $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name);
 
     // Test edit link as accessed from Taxonomy administration pages.
     // Because Simpletest creates its own database when running tests, we know
@@ -480,12 +486,17 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     // Edit the term.
     $this->drupalPost('taxonomy/term/' . $term->tid . '/edit', $edit, t('Save'));
 
+    // Check that the term is still present at admin UI after edit.
+    $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name);
+    $this->assertText($edit['name'], t('The randomly generated term name is present.'));
+    $this->assertLink(t('edit'));
+
     // View the term and check that it is correct.
     $this->drupalGet('taxonomy/term/' . $term->tid);
     $this->assertText($edit['name'], t('The randomly generated term name is present.'));
     $this->assertText($edit['description[value]'], t('The randomly generated term description is present.'));
 
-    // Check that term feed page is working
+    // Check that the term feed page is working.
     $this->drupalGet('taxonomy/term/' . $term->tid . '/feed');
 
     // Delete the term.
@@ -516,7 +527,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     // tabledrag.js by changing the page HTML source. Each term has three hidden
     // fields, "tid:1:0[tid]", "tid:1:0[parent]", and "tid:1:0[depth]". The
     // order of the input fields in the page is used when the form is processed.
-    $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->vid);
+    $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name);
     $reorder = array(
       'tid:' . $term1->tid . ':0' => 'tid:' . $term2->tid . ':0',
       'tid:' . $term2->tid . ':0' => 'tid:' . $term3->tid . ':0',
@@ -546,7 +557,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     $this->assertEqual($terms[1]->parents, array($term2->tid), t('Term 3 was made a child of term 2.'));
     $this->assertEqual($terms[2]->tid, $term1->tid, t('Term 1 was moved below term 2.'));
 
-    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->vid, array(), t('Reset to alphabetical'));
+    $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name, array(), t('Reset to alphabetical'));
     // Submit confirmation form.
     $this->drupalPost(NULL, array(), t('Reset to alphabetical'));
 
@@ -688,7 +699,7 @@ class TaxonomyHooksTestCase extends TaxonomyWebTestCase {
       'name' => $this->randomName(),
       'antonym' => 'Long',
     );
-    $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->vid . '/add', $edit, t('Save'));
+    $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add', $edit, t('Save'));
     $terms = taxonomy_get_term_by_name($edit['name']);
     $term = reset($terms);
     $this->assertEqual($term->antonym, $edit['antonym'], t('Antonym was loaded into the term object'));
@@ -755,7 +766,7 @@ class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase {
     field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'widget' => array(
         'type' => 'options_select',
@@ -815,7 +826,7 @@ class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase {
     field_create_field($this->field);
     $this->instance = array(
       'field_name' => $this->field_name,
-      'object_type' => 'test_entity',
+      'entity_type' => 'test_entity',
       'bundle' => 'test_bundle',
       'label' => $this->randomName() . '_label',
       'widget' => array(
@@ -888,7 +899,7 @@ class TaxonomyTokenReplaceTestCase extends TaxonomyWebTestCase {
     $this->instance = array(
       'field_name' => 'taxonomy_' . $this->vocabulary->machine_name,
       'bundle' => 'article',
-      'object_type' => 'node',
+      'entity_type' => 'node',
       'widget' => array(
         'type' => 'options_select',
       ),
@@ -905,12 +916,15 @@ class TaxonomyTokenReplaceTestCase extends TaxonomyWebTestCase {
    * Creates some terms and a node, then tests the tokens generated from them.
    */
   function testTaxonomyTokenReplacement() {
+    global $language;
+
     // Create two taxonomy terms.
     $term1 = $this->createTerm($this->vocabulary);
     $term2 = $this->createTerm($this->vocabulary);
 
     // Edit $term2, setting $term1 as parent.
     $edit = array();
+    $edit['name'] = '<blink>Blinking Text</blink>';
     $edit['parent[]'] = array($term1->tid);
     $this->drupalPost('taxonomy/term/' . $term2->tid . '/edit', $edit, t('Save'));
 
@@ -920,57 +934,59 @@ class TaxonomyTokenReplaceTestCase extends TaxonomyWebTestCase {
     $edit[$this->instance['field_name'] . '[' . $this->langcode . '][]'] = $term2->tid;
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
 
-    // Generate term token strings (before and after replacement) for term2.
-    $source  = '[term:tid]';
-    $source .= '[term:vid]';
-    $source .= '[term:name]';
-    $source .= '[term:description]';
-    $source .= '[term:url]';
-    $source .= '[term:node-count]';
-    $source .= '[term:parent:name]';
-    $source .= '[term:vocabulary:name]';
-
-    $target  = $term2->tid;
-    $target .= $term2->vid;
-    $target .= check_plain($term2->name);
-    $target .= check_markup($term2->description, $term2->format);
-    $target .= url('taxonomy/term/' . $term2->tid, array('absolute' => TRUE));
-    $target .= 1;
-    $target .= check_plain($term1->name);
-    $target .= check_plain($this->vocabulary->name);
-
-    $result = token_replace($source, array('term' => $term2));
-    $this->assertEqual(strcmp($target, $result), 0, t('Taxonomy term placeholder tokens replaced.'));
-
-    // Generate vocabulary token strings (before and after replacement).
-    $source = '[vocabulary:vid]';
-    $source .= '[vocabulary:name]';
-    $source .= '[vocabulary:description]';
-    $source .= '[vocabulary:node-count]';
-    $source .= '[vocabulary:term-count]';
-
-    $target = $this->vocabulary->vid;
-    $target .= check_plain($this->vocabulary->name);
-    $target .= filter_xss($this->vocabulary->description);
-    $target .= 1;
-    $target .= 2;
-
-    $result = token_replace($source, array('vocabulary' => $this->vocabulary));
-    $this->assertEqual(strcmp($target, $result), 0, t('Taxonomy vocabulary placeholder tokens replaced.'));
-
-    // Check that the results of token_generate are sanitized properly. This
-    // does NOT test the cleanliness of every token -- just that the $sanitize
-    // flag is being passed properly through the call stack and being handled
-    // correctly by a 'known' token, [term:name].
-    $edit = array();
-    $edit['name'] = '<blink>Blinking Text</blink>';
-    $this->drupalPost('taxonomy/term/' . $term2->tid . '/edit', $edit, t('Save'));
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[term:tid]'] = $term2->tid;
+    $tests['[term:vid]'] = $term2->vid;
+    $tests['[term:name]'] = check_plain($term2->name);
+    $tests['[term:description]'] = check_markup($term2->description, $term2->format);
+    $tests['[term:url]'] = url('taxonomy/term/' . $term2->tid, array('absolute' => TRUE));
+    $tests['[term:node-count]'] = 1;
+    $tests['[term:parent:name]'] = check_plain($term1->name);
+    $tests['[term:vocabulary:name]'] = check_plain($this->vocabulary->name);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('term' => $term2), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized taxonomy term token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[term:name]'] = $term2->name;
+    $tests['[term:description]'] = $term2->description;
+    $tests['[term:parent:name]'] = $term1->name;
+    $tests['[term:vocabulary:name]'] = $this->vocabulary->name;
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('term' => $term2), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized taxonomy term token %token replaced.', array('%token' => $input)));
+    }
 
-    $raw_tokens = array('name' => '[term:name]');
-    $generated = token_generate('term', $raw_tokens, array('term' => $term2));
-    $this->assertEqual(strcmp($generated['[term:name]'], check_plain($term2->name)), 0, t('Token sanitized.'));
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[vocabulary:vid]'] = $this->vocabulary->vid;
+    $tests['[vocabulary:name]'] = check_plain($this->vocabulary->name);
+    $tests['[vocabulary:description]'] = filter_xss($this->vocabulary->description);
+    $tests['[vocabulary:node-count]'] = 1;
+    $tests['[vocabulary:term-count]'] = 2;
 
-    $generated = token_generate('term', $raw_tokens, array('term' => $term2), array('sanitize' => FALSE));
-    $this->assertEqual(strcmp($generated['[term:name]'], $term2->name), 0, t('Unsanitized token generated properly.'));
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('vocabulary' => $this->vocabulary), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[vocabulary:name]'] = $this->vocabulary->name;
+    $tests['[vocabulary:description]'] = $this->vocabulary->description;
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('vocabulary' => $this->vocabulary), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
+    }
   }
 }
diff --git a/modules/toolbar/toolbar.css b/modules/toolbar/toolbar.css
index 0cc84df3e63a15cae5bc28007f98f43d2c8d2ad5..8ccfef2d7ad53b24c15fdbdcf9d14728627c0e9b 100644
--- a/modules/toolbar/toolbar.css
+++ b/modules/toolbar/toolbar.css
@@ -1,11 +1,11 @@
-/* $Id: toolbar.css,v 1.19 2010/02/11 08:57:30 dries Exp $ */
+/* $Id: toolbar.css,v 1.20 2010/04/04 17:11:20 dries Exp $ */
 
 body.toolbar {
   padding-top: 2.2em;
 }
 
 body.toolbar-drawer {
-  padding-top: 6.6em;
+  padding-top: 5.3em;
 }
 
 /**
diff --git a/modules/toolbar/toolbar.info b/modules/toolbar/toolbar.info
index 5f30e59a7c8fdd328aeb1a0662dc78ae9fa70025..b3560833ba61c26e3ea8769374eb6c6e4a8ea873 100644
--- a/modules/toolbar/toolbar.info
+++ b/modules/toolbar/toolbar.info
@@ -6,8 +6,8 @@ package = Core
 version = VERSION
 files[] = toolbar.module
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/toolbar/toolbar.js b/modules/toolbar/toolbar.js
index 3865f13942ca83032bba3b4de2307d5cc44f48c0..09fd86537794714968e28c9611cc5925e04861ea 100644
--- a/modules/toolbar/toolbar.js
+++ b/modules/toolbar/toolbar.js
@@ -1,4 +1,4 @@
-// $Id: toolbar.js,v 1.15 2010/02/10 10:54:12 dries Exp $
+// $Id: toolbar.js,v 1.16 2010/04/04 20:27:08 dries Exp $
 (function ($) {
 
 Drupal.toolbar = Drupal.toolbar || {};
@@ -49,7 +49,7 @@ Drupal.toolbar.collapse = function() {
     .removeClass('toggle-active')
     .attr('title',  toggle_text)
     .html(toggle_text);
-  $('body').addClass('toolbar-drawer').css('paddingTop', Drupal.toolbar.height());
+  $('body').removeClass('toolbar-drawer').css('paddingTop', Drupal.toolbar.height());
   $.cookie(
     'Drupal.toolbar.collapsed',
     1,
diff --git a/modules/toolbar/toolbar.module b/modules/toolbar/toolbar.module
index 90c06f1ef25c05d93b239700b69aef698072c5e4..588eac8fd2a7c5c278d3b4f7d52a463454f6ccc8 100644
--- a/modules/toolbar/toolbar.module
+++ b/modules/toolbar/toolbar.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: toolbar.module,v 1.35 2010/03/10 14:46:23 dries Exp $
+// $Id: toolbar.module,v 1.36 2010/04/04 17:11:20 dries Exp $
 
 /**
  * @file
@@ -109,7 +109,7 @@ function theme_toolbar_toggle($variables) {
 function _toolbar_is_collapsed() {
   // PHP converts dots into underscores in cookie names to avoid problems with
   // its parser, so we use a converted cookie name.
-  return isset($_COOKIE['Drupal_admin_toolbar_collapsed']) ? $_COOKIE['Drupal_admin_toolbar_collapsed'] : 0;
+  return isset($_COOKIE['Drupal_toolbar_collapsed']) ? $_COOKIE['Drupal_toolbar_collapsed'] : 0;
 }
 
 /**
@@ -142,7 +142,7 @@ function toolbar_pre_render($toolbar) {
  * Add some page classes, so global page theming can adjust to the toolbar.
  */
 function toolbar_preprocess_html(&$vars) {
-  if (isset($vars['page_top']['toolbar']) && user_access('access toolbar')) {
+  if (isset($vars['page']['page_top']['toolbar']) && user_access('access toolbar')) {
     $vars['classes_array'][] = 'toolbar';
     if (!_toolbar_is_collapsed()) {
       $vars['classes_array'][] = 'toolbar-drawer';
diff --git a/modules/tracker/tracker.info b/modules/tracker/tracker.info
index 73fe16e66afa50bc0136c4df7d543170edba7d0e..76bea25c34809bb753602d9b3646bb50581b1e30 100644
--- a/modules/tracker/tracker.info
+++ b/modules/tracker/tracker.info
@@ -9,8 +9,8 @@ files[] = tracker.module
 files[] = tracker.pages.inc
 files[] = tracker.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/translation/translation.info b/modules/translation/translation.info
index dd38215638c937f771411b2836817ecd961f3d12..d52de0610930ca168e7e3ff451c6562459b334ba 100644
--- a/modules/translation/translation.info
+++ b/modules/translation/translation.info
@@ -9,8 +9,8 @@ files[] = translation.module
 files[] = translation.pages.inc
 files[] = translation.test
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/translation/translation.module b/modules/translation/translation.module
index af2ddc399df7e38cf17c40ef40c91a19fa97a5ef..472999ac74b78b5c45aee33156baceb5daa33ac0 100644
--- a/modules/translation/translation.module
+++ b/modules/translation/translation.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: translation.module,v 1.77 2010/03/18 06:50:37 dries Exp $
+// $Id: translation.module,v 1.79 2010/04/16 13:52:23 dries Exp $
 
 /**
  * @file
@@ -65,6 +65,7 @@ function translation_menu() {
     'access arguments' => array(1),
     'type' => MENU_LOCAL_TASK,
     'weight' => 2,
+    'theme callback' => '_node_custom_theme',
     'file' => 'translation.pages.inc',
   );
   return $items;
@@ -229,7 +230,7 @@ function translation_node_prepare($node) {
     if (!empty($source_node->tnid)) {
       $translations = translation_node_get_translations($source_node->tnid);
       if (isset($translations[$_GET['language']])) {
-        drupal_set_message(t('A translation of %title in %language already exists, a new %type  will be created instead of a translation.', array('%title' => $source_node->title, '%language' => $language_list[$_GET['language']]->name, '%type' => $node->type)), 'error');
+        drupal_set_message(t('A translation of %title in %language already exists, a new %type will be created instead of a translation.', array('%title' => $source_node->title, '%language' => $language_list[$_GET['language']]->name, '%type' => $node->type)), 'error');
         return;
       }
     }
diff --git a/modules/translation/translation.test b/modules/translation/translation.test
index fcaebd4988619cd62fa55906838a369dcb15dc6e..c274896875152f96dba52aeea544222c2841ce08 100644
--- a/modules/translation/translation.test
+++ b/modules/translation/translation.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: translation.test,v 1.24 2010/03/12 14:38:37 dries Exp $
+// $Id: translation.test,v 1.25 2010/03/31 20:05:06 dries Exp $
 
 class TranslationTestCase extends DrupalWebTestCase {
   protected $book;
@@ -110,7 +110,7 @@ class TranslationTestCase extends DrupalWebTestCase {
         $this->assertRaw(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => $languages[$language_code]->name, '@locale-help' => url('admin/help/locale'))), t('Language has been created.'));
       }
     }
-    elseif ($this->xpath('//input[@type=\'checkbox\' and @name=\'enabled[' . $language_code . ']\' and @checked=\'checked\']')) {
+    elseif ($this->xpath('//input[@type="checkbox" and @name=:name and @checked="checked"]', array(':name' => 'enabled[' . $language_code . ']'))) {
       // It's installed and enabled. No need to do anything.
       $this->assertTrue(true, 'Language [' . $language_code . '] already installed and enabled.');
     }
diff --git a/modules/trigger/tests/trigger_test.info b/modules/trigger/tests/trigger_test.info
index 12e0b61bbc9cfaa3ed4000eea0f46f70081aeaef..5c870488e1f6508f4fe18074e30d589061b13c3a 100644
--- a/modules/trigger/tests/trigger_test.info
+++ b/modules/trigger/tests/trigger_test.info
@@ -6,8 +6,8 @@ core = 7.x
 files[] = trigger_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/trigger/tests/trigger_test.module b/modules/trigger/tests/trigger_test.module
index 04670d612179f9095848f7722315bb5b118e01a2..afa4c23bc073b6d82e30183b12942d8130d6b685 100644
--- a/modules/trigger/tests/trigger_test.module
+++ b/modules/trigger/tests/trigger_test.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: trigger_test.module,v 1.6 2010/03/07 07:22:42 webchick Exp $
+// $Id: trigger_test.module,v 1.7 2010/03/26 20:44:10 dries Exp $
 
 /**
  * @file
@@ -131,5 +131,5 @@ function trigger_test_generic_action($context) {
  */
 function trigger_test_generic_any_action($context) {
   // Indicate successful execution by setting a persistent variable.
-  variable_set('trigger_test_generic_any_action', TRUE);
+  variable_set('trigger_test_generic_any_action', variable_get('trigger_test_generic_any_action', 0) + 1);
 }
diff --git a/modules/trigger/trigger.admin.inc b/modules/trigger/trigger.admin.inc
index 1b68567ab9f1abc327545d7f10a42aff654ceff3..fa87afc12cd657863219ef8698076a2f897cb3bb 100644
--- a/modules/trigger/trigger.admin.inc
+++ b/modules/trigger/trigger.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: trigger.admin.inc,v 1.23 2010/03/07 07:22:42 webchick Exp $
+// $Id: trigger.admin.inc,v 1.25 2010/04/21 08:23:42 webchick Exp $
 
 /**
  * @file
@@ -130,7 +130,7 @@ function trigger_assign_form($form, $form_state, $module, $hook, $label) {
   $functions = array();
   // Restrict the options list to actions that declare support for this hook.
   foreach (actions_list() as $func => $metadata) {
-    if (in_array('any', $metadata['triggers']) || in_array($hook, $metadata['triggers'])) {
+    if (isset($metadata['triggers']) && array_intersect(array($hook, 'any'), $metadata['triggers'])) {
       $functions[] = $func;
     }
   }
@@ -272,15 +272,12 @@ function trigger_assign_form_submit($form, &$form_state) {
 }
 
 /**
- * Displays actions assigned to this hook in a table.
+ * Returns HTML for the form showing actions assigned to a trigger.
  *
  * @param $variables
  *   An associative array containing:
  *   - element: The fieldset including all assigned actions.
  *
- * @return
- *   The rendered form with the table prepended.
- *
  * @ingroup themeable
  */
 function theme_trigger_display($variables) {
diff --git a/modules/trigger/trigger.api.php b/modules/trigger/trigger.api.php
index 84e96ecd030f812c07e618943039996bd9e30800..f2f4c6e753abac18f42d663c35d69603fed38e16 100644
--- a/modules/trigger/trigger.api.php
+++ b/modules/trigger/trigger.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: trigger.api.php,v 1.5 2009/09/19 11:07:36 dries Exp $
+// $Id: trigger.api.php,v 1.7 2010/04/22 06:33:06 webchick Exp $
 
 /**
  * @file
@@ -12,7 +12,7 @@
  */
 
 /**
- * Declares triggers (events) for users to assign actions to.
+ * Declare triggers (events) for users to assign actions to.
  *
  * This hook is used by the trigger module to create a list of triggers (events)
  * that users can assign actions to. Your module is responsible for detecting
@@ -20,8 +20,6 @@
  * out which actions the user has associated with your trigger, and then calling
  * actions_do() to fire off the actions.
  *
- * @see hook_action_info().
- *
  * @return
  *   A nested associative array.
  *   - The outermost key is the name of the module that is defining the triggers.
@@ -38,6 +36,9 @@
  *   For example, the trigger set for the 'node' module has 'node' as the
  *   outermost key and defines triggers for 'node_insert', 'node_update',
  *   'node_delete' etc. that fire when a node is saved, updated, etc.
+ *
+ * @see hook_action_info()
+ * @see hook_trigger_info_alter()
  */
 function hook_trigger_info() {
   return array(
@@ -61,6 +62,18 @@ function hook_trigger_info() {
   );
 }
 
+/**
+ * Alter triggers declared by hook_trigger_info().
+ *
+ * @param $triggers
+ *   Array of trigger information returned by hook_trigger_info()
+ *   implementations. Modify this array in place. See hook_trigger_info()
+ *   for information on what this might contain.
+ */
+function hook_trigger_info_alter(&$triggers) {
+  $triggers['node']['node_insert']['label'] = t('When content is saved');
+}
+
 /**
  * @} End of "addtogroup hooks".
  */
diff --git a/modules/trigger/trigger.info b/modules/trigger/trigger.info
index 9b84f856d86e0275312ec9f536a3d4d9ef6a0f65..da4cada482d17b255d5101be3bc47a83521a2363 100644
--- a/modules/trigger/trigger.info
+++ b/modules/trigger/trigger.info
@@ -10,8 +10,8 @@ files[] = trigger.install
 files[] = trigger.test
 configure = admin/structure/trigger
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/trigger/trigger.module b/modules/trigger/trigger.module
index 3cb90e4a3b0cb28e064705544fbdf57aeb0de536..72c67e9a0895f2fc8a36643ee7c2bcc65945378b 100644
--- a/modules/trigger/trigger.module
+++ b/modules/trigger/trigger.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: trigger.module,v 1.60 2010/03/12 15:56:30 dries Exp $
+// $Id: trigger.module,v 1.62 2010/04/11 18:33:44 dries Exp $
 
 /**
  * @file
@@ -260,15 +260,17 @@ function _trigger_node($node, $hook, $a3 = NULL, $a4 = NULL) {
   static $objects;
   // Prevent recursion by tracking which operations have already been called.
   static $recursion;
-  if (isset($recursion[$hook])) {
-    return;
-  }
-  $recursion[$hook] = TRUE;
 
   $aids = trigger_get_assigned_actions($hook);
   if (!$aids) {
     return;
   }
+
+  if (isset($recursion[$hook])) {
+    return;
+  }
+  $recursion[$hook] = TRUE;
+
   $context = array(
     'group' => 'node',
     'hook' => $hook,
@@ -291,6 +293,8 @@ function _trigger_node($node, $hook, $a3 = NULL, $a4 = NULL) {
       actions_do($aid, $node, $context, $a3, $a4);
     }
   }
+
+  unset($recursion[$hook]);
 }
 
 /**
@@ -484,7 +488,8 @@ function trigger_user_login(&$edit, $account, $category) {
  * Implements hook_user_logout().
  */
 function trigger_user_logout($account) {
-  _trigger_user('user_logout', $edit = NULL, $account);
+  $edit = NULL;
+  _trigger_user('user_logout', $edit, $account);
 }
 
 /**
@@ -530,7 +535,8 @@ function trigger_user_delete($account) {
  * Implements hook_user_view().
  */
 function trigger_user_view($account) {
-  _trigger_user('user_view', $edit = NULL, $account, NULL);
+  $edit = NULL;
+  _trigger_user('user_view', $edit, $account, NULL);
 }
 
 /**
diff --git a/modules/trigger/trigger.test b/modules/trigger/trigger.test
index 23d3b556c321b5176524886a64273e00214d8d9c..408eb2b3e75036ed3c0d176f2d86bdbd083f652e 100644
--- a/modules/trigger/trigger.test
+++ b/modules/trigger/trigger.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: trigger.test,v 1.28 2010/03/07 18:06:06 webchick Exp $
+// $Id: trigger.test,v 1.31 2010/04/10 10:49:15 dries Exp $
 
 /**
  * Class with common helper methods.
@@ -45,7 +45,7 @@ class TriggerContentTestCase extends TriggerWebTestCase {
   }
 
   function setUp() {
-    parent::setUp('trigger');
+    parent::setUp('trigger', 'trigger_test');
   }
 
   /**
@@ -55,18 +55,18 @@ class TriggerContentTestCase extends TriggerWebTestCase {
     global $user;
     $content_actions = array('node_publish_action', 'node_unpublish_action', 'node_make_sticky_action', 'node_make_unsticky_action', 'node_promote_action', 'node_unpromote_action');
 
+    $test_user = $this->drupalCreateUser(array('administer actions'));
+    $web_user = $this->drupalCreateUser(array('create page content', 'access content', 'administer nodes'));
     foreach ($content_actions as $action) {
       $hash = md5($action);
       $info = $this->actionInfo($action);
 
       // Assign an action to a trigger, then pull the trigger, and make sure
       // the actions fire.
-      $test_user = $this->drupalCreateUser(array('administer actions'));
       $this->drupalLogin($test_user);
       $edit = array('aid' => $hash);
-      $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'));
+      $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-presave-assign-form');
       // Create an unpublished node.
-      $web_user = $this->drupalCreateUser(array('create page content', 'access content', 'administer nodes'));
       $this->drupalLogin($web_user);
       $edit = array();
       $langcode = LANGUAGE_NONE;
@@ -83,12 +83,10 @@ class TriggerContentTestCase extends TriggerWebTestCase {
 
       // There should be an error when the action is assigned to the trigger
       // twice.
-      $test_user = $this->drupalCreateUser(array('administer actions'));
       $this->drupalLogin($test_user);
+      // This action already assigned in this test.
       $edit = array('aid' => $hash);
-      $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'));
-      $edit = array('aid' => $hash);
-      $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'));
+      $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-presave-assign-form');
       $this->assertRaw(t('The action you chose is already assigned to that trigger.'), t('Check to make sure an error occurs when assigning an action to a trigger twice.'));
 
       // The action should be able to be unassigned from a trigger.
@@ -99,6 +97,35 @@ class TriggerContentTestCase extends TriggerWebTestCase {
     }
   }
 
+  /**
+   * Test that node actions are fired for each node individually if acting on
+   * multiple nodes.
+   */
+  function testActionContentMultiple() {
+    // Assign an action to the node save/update trigger.
+    $test_user = $this->drupalCreateUser(array('administer actions', 'administer nodes', 'create page content', 'access administration pages', 'access content overview'));
+    $this->drupalLogin($test_user);
+
+    for ($index = 0; $index < 3; $index++) {
+      $edit = array('title' => $this->randomName());
+      $this->drupalPost('node/add/page', $edit, t('Save'));
+    }
+
+    $action_id = 'trigger_test_generic_any_action';
+    $hash = md5($action_id);
+    $edit = array('aid' => $hash);
+    $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-update-assign-form');
+
+    $edit = array(
+      'operation' => 'unpublish',
+      'nodes[1]' => TRUE,
+      'nodes[2]' => TRUE,
+    );
+    $this->drupalPost('admin/content', $edit, t('Update'));
+    $count = variable_get('trigger_test_generic_any_action', 0);
+    $this->assertTrue($count == 2, t('Action was triggered 2 times. Actual: %count', array('%count' => $count)));
+  }
+
   /**
    * Helper function for testActionsContent(): returns some info about each of the content actions.
    *
@@ -174,7 +201,7 @@ class TriggerCronTestCase extends TriggerWebTestCase {
 
     // Assign a non-configurable action to the cron run trigger.
     $edit = array('aid' => md5('trigger_test_system_cron_action'));
-    $this->drupalPost('admin/structure/trigger/system', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/system', $edit, t('Assign'), array(), array(), 'trigger-cron-assign-form');
 
     // Assign a configurable action to the cron trigger.
     $action_label = $this->randomName();
@@ -186,7 +213,7 @@ class TriggerCronTestCase extends TriggerWebTestCase {
     // $aid is likely 3 but if we add more uses for the sequences table in
     // core it might break, so it is easier to get the value from the database.
     $edit = array('aid' => md5($aid));
-    $this->drupalPost('admin/structure/trigger/system', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/system', $edit, t('Assign'), array(), array(), 'trigger-cron-assign-form');
 
     // Add a second configurable action to the cron trigger.
     $action_label = $this->randomName();
@@ -196,7 +223,7 @@ class TriggerCronTestCase extends TriggerWebTestCase {
     );
     $aid = $this->configureAdvancedAction('trigger_test_system_cron_conf_action', $edit);
     $edit = array('aid' => md5($aid));
-    $this->drupalPost('admin/structure/trigger/system', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/system', $edit, t('Assign'), array(), array(), 'trigger-cron-assign-form');
 
     // Force a cron run.
     $this->cronRun();
@@ -240,7 +267,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase {
     $action_id = 'trigger_test_generic_action';
     $hash = md5($action_id);
     $edit = array('aid' => $hash);
-    $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-insert-assign-form');
 
     // Set action variable to FALSE.
     variable_set( $action_id, FALSE );
@@ -274,7 +301,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase {
     // Configure an advanced action that we can assign.
     $aid = $this->configureAdvancedAction('system_message_action', $action_edit);
     $edit = array('aid' => md5($aid));
-    $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger_user_login_assign_form');
+    $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), 'trigger-user-login-assign-form');
 
     // Verify that the action has been assigned to the correct hook.
     $actions = trigger_get_assigned_actions('user_login');
@@ -297,7 +324,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase {
     $action_id = 'trigger_test_generic_action';
     $hash = md5($action_id);
     $edit = array('aid' => $hash);
-    $this->drupalPost('admin/structure/trigger/comment', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/comment', $edit, t('Assign'), array(), array(), 'trigger-comment-insert-assign-form');
 
     // Set action variable to FALSE.
     variable_set($action_id, FALSE);
@@ -326,7 +353,7 @@ class TriggerOtherTestCase extends TriggerWebTestCase {
     $action_id = 'trigger_test_generic_action';
     $hash = md5($action_id);
     $edit = array('aid' => $hash);
-    $this->drupalPost('admin/structure/trigger/taxonomy', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/taxonomy', $edit, t('Assign'), array(), array(), 'trigger-taxonomy-term-insert-assign-form');
 
     // Set action variable to FALSE.
     variable_set($action_id, FALSE);
@@ -383,7 +410,7 @@ class TriggerOrphanedActionsTestCase extends DrupalWebTestCase {
     $test_user = $this->drupalCreateUser(array('administer actions'));
     $this->drupalLogin($test_user);
     $edit = array('aid' => $hash);
-    $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'));
+    $this->drupalPost('admin/structure/trigger/node', $edit, t('Assign'), array(), array(), 'trigger-node-presave-assign-form');
 
     // Create an unpublished node.
     $web_user = $this->drupalCreateUser(array('create page content', 'edit own page content', 'access content', 'administer nodes'));
diff --git a/modules/update/tests/aaa_update_test.info b/modules/update/tests/aaa_update_test.info
index 7847b37e279c52cd98a2edd60e5347ef40184558..590e88c63b1f5a277355f7f8efa12945ce4b83de 100644
--- a/modules/update/tests/aaa_update_test.info
+++ b/modules/update/tests/aaa_update_test.info
@@ -6,8 +6,8 @@ core = 7.x
 files[] = aaa_update_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/update/tests/bbb_update_test.info b/modules/update/tests/bbb_update_test.info
index f377c08b099e7b9d4bf538c93b45ef0ae52d6863..6c3f6a5e7a12028c0038ffed6ecbfe452b62bedb 100644
--- a/modules/update/tests/bbb_update_test.info
+++ b/modules/update/tests/bbb_update_test.info
@@ -6,8 +6,8 @@ core = 7.x
 files[] = bbb_update_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/update/tests/ccc_update_test.info b/modules/update/tests/ccc_update_test.info
index f35fb11dd6bcbb4ef150864485975fd9e7653f49..b6f29dd43dfb2bc95021d7d6fe557e5a46430bc2 100644
--- a/modules/update/tests/ccc_update_test.info
+++ b/modules/update/tests/ccc_update_test.info
@@ -6,8 +6,8 @@ core = 7.x
 files[] = ccc_update_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/update/tests/update_test.info b/modules/update/tests/update_test.info
index b1faacfd2560d4d026968f5035ad7b3f0d233ede..87b4cf889d360df24900d5b9fbd8e691ada2fa20 100644
--- a/modules/update/tests/update_test.info
+++ b/modules/update/tests/update_test.info
@@ -7,8 +7,8 @@ core = 7.x
 files[] = update_test.module
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/update/update.authorize.inc b/modules/update/update.authorize.inc
index cdd3431ab04cb0910a00d736f5612fb46f22f22b..567ab4976a3e6c2251c72dce25258f359a5b3fc3 100644
--- a/modules/update/update.authorize.inc
+++ b/modules/update/update.authorize.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: update.authorize.inc,v 1.3 2009/11/01 23:02:13 webchick Exp $
+// $Id: update.authorize.inc,v 1.4 2010/04/10 09:49:49 dries Exp $
 
 /**
  * @file
@@ -154,7 +154,9 @@ function update_authorize_batch_copy_project($project, $updater_name, $local_url
   }
 
   _update_batch_create_message($context['results']['log'][$project], t('Installed %project_name successfully', array('%project_name' => $project)));
-  $context['results']['tasks'] += $tasks;
+  if (!empty($tasks)) {
+    $context['results']['tasks'] += $tasks;
+  }
 
   // This particular operation is now complete, even though the batch might
   // have other operations to perform.
@@ -206,6 +208,10 @@ function update_authorize_update_batch_finished($success, $results) {
       'type' => 'error',
     );
   }
+  // Since we're doing an update of existing code, always add a task for
+  // running update.php.
+  $results['tasks'][] = t('Your modules have been downloaded and updated.');
+  $results['tasks'][] = t('<a href="@update">Run database updates</a>', array('@update' => base_path() . 'update.php'));
 
   // Set all these values into the SESSION so authorize.php can display them.
   $_SESSION['authorize_results']['success'] = $success;
diff --git a/modules/update/update.info b/modules/update/update.info
index 1c29917dbfed9b03941d88506434ff67be2e6294..0ff0012b340eb826804f9c679465dd523a91d657 100644
--- a/modules/update/update.info
+++ b/modules/update/update.info
@@ -15,8 +15,8 @@ files[] = update.settings.inc
 files[] = update.test
 configure = admin/reports/updates/settings
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/update/update.manager.inc b/modules/update/update.manager.inc
index f7b697475ed9d1d858ae8358472acef935735b1c..c9517fceb4e80cb85725989d5524a74db467ac1c 100644
--- a/modules/update/update.manager.inc
+++ b/modules/update/update.manager.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: update.manager.inc,v 1.17 2010/02/01 07:17:59 webchick Exp $
+// $Id: update.manager.inc,v 1.21 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -66,10 +66,7 @@ function update_manager_update_form($form, $form_state = array(), $context) {
     return $form;
   }
 
-  drupal_add_css('misc/ui/ui.all.css');
-  drupal_add_css('misc/ui/ui.dialog.css');
-  drupal_add_js('misc/ui/ui.core.js', array('weight' => JS_LIBRARY + 5));
-  drupal_add_js('misc/ui/ui.dialog.js', array('weight' => JS_LIBRARY + 6));
+  $form['#attached']['library'][] = array('system', 'ui.dialog');
   $form['#attached']['js'][] = drupal_get_path('module', 'update') . '/update.manager.js';
   $form['#attached']['css'][] = drupal_get_path('module', 'update') . '/update.css';
 
@@ -233,10 +230,10 @@ function update_manager_update_form($form, $form_state = array(), $context) {
   // If either table has been printed yet, we need a submit button and to
   // validate the checkboxes.
   if (!empty($projects['enabled']) || !empty($projects['disabled'])) {
-    $form['submit'] = array(
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Download these updates'),
-      '#weight' => 10,
     );
     $form['#validate'][] = 'update_manager_update_form_validate';
   }
@@ -248,7 +245,7 @@ function update_manager_update_form($form, $form_state = array(), $context) {
       '#type' => 'markup',
       '#markup' => theme('table', array('header' => $headers, 'rows' => $projects['manual'])),
       '#prefix' => $prefix,
-      '#weight' => 20,
+      '#weight' => 120,
     );
   }
 
@@ -256,10 +253,11 @@ function update_manager_update_form($form, $form_state = array(), $context) {
 }
 
 /**
- * Theme the first page in the update manager wizard to select projects.
+ * Returns HTML for the first page in the update manager wizard to select projects.
  *
  * @param $variables
- *   form: The form
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -367,10 +365,10 @@ function update_manager_update_ready_form($form, &$form_state) {
     '#default_value' => TRUE,
   );
 
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Continue'),
-    '#weight' => 100,
   );
 
   return $form;
@@ -493,7 +491,8 @@ function update_manager_install_form($form, &$form_state, $context) {
     '#description' => t('For example: %filename from your local computer', array('%filename' => 'name.tar.gz')),
   );
 
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Install'),
   );
@@ -685,7 +684,7 @@ function update_manager_archive_extract($file, $directory) {
 function update_manager_archive_verify($project, $archive_file, $directory) {
   $failures = module_invoke_all('verify_update_archive', $project, $archive_file, $directory);
   if (!empty($failures)) {
-    throw new Exception(t('Unable to extract %file', array('%file' => $file)));
+    throw new Exception(t('Unable to extract %file', array('%file' => $archive_file)));
   }
 }
 
diff --git a/modules/update/update.module b/modules/update/update.module
index e8562b51c04bed3a03ada0a79d351765a2202169..6920bc9423c87493a21edd04d40fe95aab060faa 100644
--- a/modules/update/update.module
+++ b/modules/update/update.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: update.module,v 1.63 2010/02/17 09:08:59 dries Exp $
+// $Id: update.module,v 1.65 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -427,7 +427,7 @@ function update_create_fetch_task($project) {
 /**
  * Wrapper to load the include file and then refresh the release data.
  *
- * @see _update_refresh();
+ * @see _update_refresh()
  */
 function update_refresh() {
   module_load_include('inc', 'update', 'update.fetch');
@@ -606,14 +606,15 @@ function _update_project_status_sort($a, $b) {
 }
 
 /**
- * Render the HTML to display the last time we checked for update data.
+ * Returns HTML for the last time we checked for update data.
  *
  * In addition to properly formating the given timestamp, this function also
  * provides a "Check manually" link that refreshes the available update and
  * redirects back to the same page.
  *
  * @param $variables
- *   'last': The timestamp when the site last checked for available updates.
+ *   An associative array containing:
+ *   - 'last': The timestamp when the site last checked for available updates.
  *
  * @see theme_update_report()
  * @see theme_update_available_updates_form()
diff --git a/modules/update/update.report.inc b/modules/update/update.report.inc
index d4b2a956c4a56a6681038b895998b3e22cf1a9d3..304f53997312a1e7475712556259ce00fc991b2f 100644
--- a/modules/update/update.report.inc
+++ b/modules/update/update.report.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: update.report.inc,v 1.30 2010/01/30 07:59:26 dries Exp $
+// $Id: update.report.inc,v 1.31 2010/04/13 15:23:03 dries Exp $
 
 /**
  * @file
@@ -21,7 +21,11 @@ function update_status() {
 }
 
 /**
- * Theme project status report.
+ * Returns HTML for the project status report.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - data: An array of data about each project's status.
  *
  * @ingroup themeable
  */
@@ -244,7 +248,7 @@ function theme_update_report($variables) {
 }
 
 /**
- * Generate the HTML for the label to display for a project's update status.
+ * Returns HTML for a label to display for a project's update status.
  *
  * @param $variables
  *   An associative array containing:
@@ -273,7 +277,17 @@ function theme_update_status_label($variables) {
 }
 
 /**
- * Theme the version display of a project.
+ * Returns HTML for the version display of a project.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - version: An array of data about the latest released version, containing:
+ *     - version: The version number.
+ *     - release_link: The URL for the release notes.
+ *     - date: The date of the release.
+ *     - download_link: The URL for the downloadable file.
+ *   - tag: The title of the project.
+ *   - class: A string containing extra classes for the wrapping table.
  *
  * @ingroup themeable
  */
diff --git a/modules/user/user.admin.inc b/modules/user/user.admin.inc
index 98f168dc1bca47bfb202e0502b28773dc7e75a77..4fef3274c278d5f1393a3cb73ca282e6170f6c6d 100644
--- a/modules/user/user.admin.inc
+++ b/modules/user/user.admin.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.admin.inc,v 1.101 2010/03/07 06:49:10 webchick Exp $
+// $Id: user.admin.inc,v 1.108 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -73,9 +73,9 @@ function user_filter_form() {
   }
 
   $form['filters']['actions'] = array(
-    '#type' => 'container',
+    '#type' => 'actions',
     '#id' => 'user-admin-buttons',
-    '#attributes' => array('class' => array('form-actions', 'container-inline')),
+    '#attributes' => array('class' => array('container-inline')),
   );
   $form['filters']['actions']['submit'] = array(
     '#type' => 'submit',
@@ -188,7 +188,7 @@ function user_admin_account() {
   $destination = drupal_get_destination();
 
   $status = array(t('blocked'), t('active'));
-  $roles = user_roles(TRUE);
+  $roles = array_map('check_plain', user_roles(TRUE));
   $accounts = array();
   foreach ($result as $account) {
     $users_roles = array();
@@ -663,7 +663,7 @@ function user_admin_permissions($form, $form_state, $rid = NULL) {
   // Render role/permission overview:
   $options = array();
   $module_info = system_get_info('module');
-  $hide_descriptions = !system_admin_compact_mode();
+  $hide_descriptions = system_admin_compact_mode();
 
   // Get a list of all the modules implementing a hook_permission() and sort by
   // display name.
@@ -680,11 +680,17 @@ function user_admin_permissions($form, $form_state, $rid = NULL) {
         '#id' => $module,
       );
       foreach ($permissions as $perm => $perm_item) {
+        // Fill in default values for the permission.
+        $perm_item += array(
+          'description' => '',
+          'restrict access' => FALSE,
+          'warning' => !empty($perm_item['restrict access']) ? t('Warning: Give to trusted roles only; this permission has security implications.') : '',
+        );
         $options[$perm] = '';
         $form['permission'][$perm] = array(
           '#type' => 'item',
           '#markup' => $perm_item['title'],
-          '#description' => $hide_descriptions && isset($perm_item['description']) ? $perm_item['description'] : NULL,
+          '#description' => theme('user_permission_description', array('permission_item' => $perm_item, 'hide' => $hide_descriptions)),
         );
         foreach ($role_names as $rid => $name) {
           // Builds arrays for checked boxes for each role
@@ -699,10 +705,10 @@ function user_admin_permissions($form, $form_state, $rid = NULL) {
   // Have to build checkboxes here after checkbox arrays are built
   foreach ($role_names as $rid => $name) {
     $form['checkboxes'][$rid] = array('#type' => 'checkboxes', '#options' => $options, '#default_value' => isset($status[$rid]) ? $status[$rid] : array());
-    $form['role_names'][$rid] = array('#markup' => $name, '#tree' => TRUE);
+    $form['role_names'][$rid] = array('#markup' => check_plain($name), '#tree' => TRUE);
   }
 
-  $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
+  $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save permissions'));
 
   $form['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.permissions.js';
@@ -727,7 +733,11 @@ function user_admin_permissions_submit($form, &$form_state) {
 }
 
 /**
- * Theme the administer permissions page.
+ * Returns HTML for the administer permissions page.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
@@ -764,70 +774,194 @@ function theme_user_admin_permissions($variables) {
 }
 
 /**
- * Menu callback: administer roles.
+ * Returns HTML for an individual permission description.
  *
- * @ingroup forms
- * @see user_admin_role_validate()
- * @see user_admin_role_submit()
- * @see theme_user_admin_new_role()
+ * @param $variables
+ *   An associative array containing:
+ *   - permission_item: An associative array representing the permission whose
+ *     description is being themed. Useful keys include:
+ *     - description: The text of the permission description.
+ *     - warning: A security-related warning message about the permission (if
+ *       there is one).
+ *   - hide: A boolean indicating whether or not the permission description was
+ *     requested to be hidden rather than shown.
+ *
+ * @ingroup themeable
  */
-function user_admin_role() {
-  $rid = arg(5);
-  if ($rid) {
-    if ($rid == DRUPAL_ANONYMOUS_RID || $rid == DRUPAL_AUTHENTICATED_RID) {
-      drupal_goto('admin/people/permissions/roles');
+function theme_user_permission_description($variables) {
+  if (!$variables['hide']) {
+    $description = array();
+    $permission_item = $variables['permission_item'];
+    if (!empty($permission_item['description'])) {
+      $description[] = $permission_item['description'];
+    }
+    if (!empty($permission_item['warning'])) {
+      $description[] = '<em class="permission-warning">' . $permission_item['warning'] . '</em>';
+    }
+    if (!empty($description)) {
+      return implode(' ', $description);
     }
-    // Display the edit role form.
-    $role = db_query('SELECT * FROM {role} WHERE rid = :rid', array(':rid' => $rid))->fetchObject();
-    $form['name'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Role name'),
-      '#default_value' => $role->name,
-      '#size' => 30,
-      '#required' => TRUE,
-      '#maxlength' => 64,
-      '#description' => t('The name for this role. Example: "moderator", "editorial board", "site architect".'),
-    );
-    $form['rid'] = array(
-      '#type' => 'value',
-      '#value' => $rid,
-    );
-    $form['actions'] = array('#type' => 'container', '#attributes' => array('class' => array('form-actions')));
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Save role'),
-    );
-    $form['actions']['delete'] = array(
-      '#type' => 'submit',
-      '#value' => t('Delete role'),
-    );
   }
-  else {
-    $form['name'] = array(
-      '#type' => 'textfield',
-      '#size' => 32,
-      '#maxlength' => 64,
+}
+
+/**
+ * Form to re-order roles or add a new one.
+ *
+ * @ingroup forms
+ * @see theme_user_admin_roles()
+ */
+function user_admin_roles($form, $form_state) {
+  $roles = user_roles();
+
+  $form['roles'] = array(
+    '#tree' => TRUE,
+  );
+  $order = 0;
+  foreach ($roles as $rid => $name) {
+    $form['roles'][$rid]['#role'] = (object) array(
+      'rid' => $rid,
+      'name' => $name,
+      'weight' => $order,
     );
-    $form['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Add role'),
+    $form['roles'][$rid]['#weight'] = $order;
+    $form['roles'][$rid]['weight'] = array(
+      '#type' => 'textfield',
+      '#size' => 4,
+      '#default_value' => $order,
+      '#attributes' => array('class' => array('role-weight')),
     );
-    $form['#submit'][] = 'user_admin_role_submit';
-    $form['#validate'][] = 'user_admin_role_validate';
+    $order++;
+  }
+
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#size' => 32,
+    '#maxlength' => 64,
+  );
+  $form['add'] = array(
+    '#type' => 'submit',
+    '#value' => t('Add role'),
+    '#validate' => array('user_admin_role_validate'),
+    '#submit' => array('user_admin_role_submit'),
+  );
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save order'),
+    '#submit' => array('user_admin_roles_order_submit'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submit function. Update the role weights.
+ */
+function user_admin_roles_order_submit($form, &$form_state) {
+  foreach ($form_state['values']['roles'] as $rid => $role_values) {
+    $role = $form['roles'][$rid]['#role'];
+    $role->weight = $role_values['weight'];
+    user_role_save($role);
+  }
+}
+
+/**
+ * Returns HTML for the role order and new role form.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_user_admin_roles($variables) {
+  $form = $variables['form'];
+
+  $header = array(t('Name'), t('Weight'), array('data' => t('Operations'), 'colspan' => 2));
+  foreach (element_children($form['roles']) as $rid) {
+    $name = $form['roles'][$rid]['#role']->name;
+    $row = array();
+    if (in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+      $row[] = t('@name <em>(locked)</em>', array('@name' => $name));
+      $row[] = drupal_render($form['roles'][$rid]['weight']);
+      $row[] = '';
+      $row[] = l(t('edit permissions'), 'admin/people/permissions/' . $rid);
+    }
+    else {
+      $row[] = check_plain($name);
+      $row[] = drupal_render($form['roles'][$rid]['weight']);
+      $row[] = l(t('edit role'), 'admin/people/permissions/roles/edit/' . $rid);
+      $row[] = l(t('edit permissions'), 'admin/people/permissions/' . $rid);
+    }
+    $rows[] = array('data' => $row, 'class' => array('draggable'));
   }
+  $rows[] = array(array('data' => drupal_render($form['name']) . drupal_render($form['add']), 'colspan' => 4, 'class' => 'edit-name'));
+
+  drupal_add_tabledrag('user-roles', 'order', 'sibling', 'role-weight');
+
+  $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'user-roles')));
+  $output .= drupal_render_children($form);
+
+  return $output;
+}
+
+/**
+ * Form to configure a single role.
+ *
+ * @ingroup forms
+ * @see user_admin_role_validate()
+ * @see user_admin_role_submit()
+ */
+function user_admin_role($form, $form_state, $role) {
+  if ($role->rid == DRUPAL_ANONYMOUS_RID || $role->rid == DRUPAL_AUTHENTICATED_RID) {
+    drupal_goto('admin/people/permissions/roles');
+  }
+
+  // Display the edit role form.
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Role name'),
+    '#default_value' => $role->name,
+    '#size' => 30,
+    '#required' => TRUE,
+    '#maxlength' => 64,
+    '#description' => t('The name for this role. Example: "moderator", "editorial board", "site architect".'),
+  );
+  $form['rid'] = array(
+    '#type' => 'value',
+    '#value' => $role->rid,
+  );
+  $form['weight'] = array(
+    '#type' => 'value',
+    '#value' => $role->weight,
+  );
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save role'),
+  );
+  $form['actions']['delete'] = array(
+    '#type' => 'submit',
+    '#value' => t('Delete role'),
+    '#submit' => array('user_admin_role_delete_submit'),
+  );
+
   return $form;
 }
 
+/**
+ * Form validation handler for the user_admin_role() form.
+ */
 function user_admin_role_validate($form, &$form_state) {
-  if ($form_state['values']['name']) {
+  if (!empty($form_state['values']['name'])) {
     if ($form_state['values']['op'] == t('Save role')) {
-      $role = user_role_load($form_state['values']['name']);
+      $role = user_role_load_by_name($form_state['values']['name']);
       if ($role && $role->rid != $form_state['values']['rid']) {
         form_set_error('name', t('The role name %name already exists. Choose another role name.', array('%name' => $form_state['values']['name'])));
       }
     }
     elseif ($form_state['values']['op'] == t('Add role')) {
-      if (user_role_load($form_state['values']['name'])) {
+      if (user_role_load_by_name($form_state['values']['name'])) {
         form_set_error('name', t('The role name %name already exists. Choose another role name.', array('%name' => $form_state['values']['name'])));
       }
     }
@@ -837,16 +971,15 @@ function user_admin_role_validate($form, &$form_state) {
   }
 }
 
+/**
+ * Form submit handler for the user_admin_role() form.
+ */
 function user_admin_role_submit($form, &$form_state) {
   $role = (object)$form_state['values'];
   if ($form_state['values']['op'] == t('Save role')) {
     user_role_save($role);
     drupal_set_message(t('The role has been renamed.'));
   }
-  elseif ($form_state['values']['op'] == t('Delete role')) {
-    user_role_delete($form_state['values']['rid']);
-    drupal_set_message(t('The role has been deleted.'));
-  }
   elseif ($form_state['values']['op'] == t('Add role')) {
     user_role_save($role);
     drupal_set_message(t('The role has been added.'));
@@ -856,33 +989,38 @@ function user_admin_role_submit($form, &$form_state) {
 }
 
 /**
- * Theme the new-role form.
- *
- * @ingroup themeable
+ * Form submit handler for the user_admin_role() form.
  */
-function theme_user_admin_new_role($variables) {
-  $form = $variables['form'];
-
-  $header = array(t('Name'), array('data' => t('Operations'), 'colspan' => 2));
-  foreach (user_roles() as $rid => $name) {
-    $edit_permissions = l(t('edit permissions'), 'admin/people/permissions/' . $rid);
-    if (in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
-      $rows[] = array(t('!name %locked', array('!name' => $name, '%locked' => t('(locked)'))), '', $edit_permissions);
-    }
-    else {
-      $rows[] = array($name, l(t('edit role'), 'admin/people/permissions/roles/edit/' . $rid), $edit_permissions);
-    }
-  }
-  $rows[] = array(array('data' => drupal_render($form['name']) . drupal_render($form['submit']), 'colspan' => 3, 'class' => 'edit-name'));
+function user_admin_role_delete_submit($form, &$form_state) {
+  $form_state['redirect'] = 'admin/people/permissions/roles/delete/' . $form_state['values']['rid'];
+}
 
-  $output = drupal_render_children($form);
-  $output .= theme('table', array('header' => $header, 'rows' => $rows));
+/**
+ * Form to confirm role delete operation.
+ */
+function user_admin_role_delete_confirm($form, &$form_state, $role) {
+  $form['rid'] = array(
+    '#type' => 'value',
+    '#value' => $role->rid,
+  );
+  return confirm_form($form, t('Are you sure you want to delete the role %name ?', array('%name' => $role->name)), 'admin/people/permissions/roles', t('This action cannot be undone.'), t('Delete'));
+}
 
-  return $output;
+/**
+ * Form submit handler for user_admin_role_delete_confirm().
+ */
+function user_admin_role_delete_confirm_submit($form, &$form_state) {
+  user_role_delete((int) $form_state['values']['rid']);
+  drupal_set_message(t('The role has been deleted.'));
+  $form_state['redirect'] = 'admin/people/permissions/roles';
 }
 
 /**
- * Theme user administration filter selector.
+ * Returns HTML for the user administration filter selector.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - form: A render element representing the form.
  *
  * @ingroup themeable
  */
diff --git a/modules/user/user.api.php b/modules/user/user.api.php
index 94c1f016486d02a0ca4b4a1f6f445e0791750a32..064ece011818be1a9f24ed44fe000af4c9f0d723 100644
--- a/modules/user/user.api.php
+++ b/modules/user/user.api.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.api.php,v 1.22 2010/03/20 14:55:06 dries Exp $
+// $Id: user.api.php,v 1.23 2010/03/31 13:56:59 dries Exp $
 
 /**
  * @file
@@ -177,13 +177,19 @@ function hook_user_operations() {
 }
 
 /**
- * Retrieve a list of all user setting/information categories.
+ * Retrieve a list of user setting or profile information categories.
  *
  * @return
- *   A linear array of associative arrays. These arrays have keys:
+ *   An array of associative arrays. Each inner array has elements:
  *   - "name": The internal name of the category.
  *   - "title": The human-readable, localized name of the category.
  *   - "weight": An integer specifying the category's sort ordering.
+ *   - "access callback": Name of the access callback function to use to
+ *     determine whether the user can edit the category. Defaults to using
+ *     user_edit_access(). See hook_menu() for more information on access
+ *     callbacks.
+ *   - "access arguments": Arguments for the access callback function. Defaults
+ *     to array(1).
  */
 function hook_user_categories() {
   return array(array(
diff --git a/modules/user/user.css b/modules/user/user.css
index 6c4d5032283d5f83ab9119b18958a93b35a56c81..81c76c612df5d71208399f901fca7388835df7fb 100644
--- a/modules/user/user.css
+++ b/modules/user/user.css
@@ -1,4 +1,4 @@
-/* $Id: user.css,v 1.19 2010/01/30 07:59:26 dries Exp $ */
+/* $Id: user.css,v 1.20 2010/03/24 07:34:10 dries Exp $ */
 
 #permissions td.module {
   font-weight: bold;
@@ -33,10 +33,10 @@
  * Override default textfield float to put the "Add role" button next to
  * the input textfield.
  */
-#user-admin-new-role td.edit-name {
+#user-admin-roles td.edit-name {
   clear: both;
 }
-#user-admin-new-role .form-item-name {
+#user-admin-roles .form-item-name {
   float: left;
   margin-right: 1em;
 }
diff --git a/modules/user/user.info b/modules/user/user.info
index c078d58a646742729cf345795faec8db4b071c19..8294107ad823fc0f4dae0c37db5ad2153c31a9d4 100644
--- a/modules/user/user.info
+++ b/modules/user/user.info
@@ -13,8 +13,8 @@ files[] = user.tokens.inc
 required = TRUE
 configure = admin/config/people
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/modules/user/user.install b/modules/user/user.install
index 803d19093e6647a40a12327a104ca4ce9cf5fbf1..f4866f7556b9dcb8f4147ad2bfe5180d0bf96752 100644
--- a/modules/user/user.install
+++ b/modules/user/user.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.install,v 1.40 2010/03/01 07:39:12 dries Exp $
+// $Id: user.install,v 1.45 2010/04/13 15:15:07 dries Exp $
 
 /**
  * @file
@@ -97,12 +97,22 @@ function user_schema() {
         'not null' => TRUE,
         'default' => '',
         'description' => 'Unique role name.',
+        'translatable' => TRUE,
+      ),
+      'weight' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'The weight of this role in listings and the user interface.',
       ),
     ),
     'unique keys' => array(
       'name' => array('name'),
     ),
     'primary key' => array('rid'),
+    'indexes' => array(
+      'name_weight' => array('name', 'weight'),
+    ),
   );
 
   $schema['users'] = array(
@@ -192,7 +202,7 @@ function user_schema() {
         'type' => 'int',
         'not null' => TRUE,
         'default' => 0,
-        'description' => "Foreign key: {file}.fid of user's picture.",
+        'description' => "Foreign key: {file_managed}.fid of user's picture.",
       ),
       'init' => array(
         'type' => 'varchar',
@@ -274,16 +284,16 @@ function user_install() {
       'mail' => 'placeholder-for-uid-1',
       'created' => REQUEST_TIME,
       'status' => 1,
-      'data' => serialize(array()),
+      'data' => NULL,
     ))
     ->execute();
 
   // Built-in roles.
   $rid_anonymous = db_insert('role')
-    ->fields(array('name' => 'anonymous user'))
+    ->fields(array('name' => 'anonymous user', 'weight' => 0))
     ->execute();
   $rid_authenticated = db_insert('role')
-    ->fields(array('name' => 'authenticated user'))
+    ->fields(array('name' => 'authenticated user', 'weight' => 1))
     ->execute();
 
   // Sanity check to ensure the anonymous and authenticated role IDs are the
@@ -381,8 +391,8 @@ function user_update_7002(&$sandbox) {
     $timezones = system_time_zones();
     // Update this many per page load.
     $count = 10000;
-    $contributed_date_module = db_column_exists('users', 'timezone_name');
-    $contributed_event_module = db_column_exists('users', 'timezone_id');
+    $contributed_date_module = db_field_exists('users', 'timezone_name');
+    $contributed_event_module = db_field_exists('users', 'timezone_id');
 
     $results = db_query_range("SELECT uid FROM {users} ORDER BY uid", $sandbox['user_from'], $count);
     foreach ($results as $account) {
@@ -464,7 +474,8 @@ function user_update_7003() {
 }
 
 /**
- * Add the user's pictures to the {file} table and make them managed files.
+ * Add the user's pictures to the {file_managed} table and make them managed
+ * files.
  */
 function user_update_7004(&$sandbox) {
 
@@ -472,13 +483,13 @@ function user_update_7004(&$sandbox) {
     'type' => 'int',
     'not null' => TRUE,
     'default' => 0,
-    'description' => t("Foreign key: {file}.fid of user's picture."),
+    'description' => t("Foreign key: {file_managed}.fid of user's picture."),
   );
 
   if (!isset($sandbox['progress'])) {
     // Check that the field hasn't been updated in an aborted run of this
     // update.
-    if (!db_column_exists('users', 'picture_fid')) {
+    if (!db_field_exists('users', 'picture_fid')) {
       // Add a new field for the fid.
       db_add_field('users', 'picture_fid', $picture_field);
     }
@@ -489,8 +500,8 @@ function user_update_7004(&$sandbox) {
     $sandbox['max'] = db_query("SELECT COUNT(*) FROM {users} WHERE picture <> ''")->fetchField();
   }
 
-  // As a batch operation move the photos into the {file} table and update the
-  // {users} records.
+  // As a batch operation move the photos into the {file_managed} table and
+  // update the {users} records.
   $limit = 500;
   $result = db_query_range("SELECT uid, picture FROM {users} WHERE picture <> '' AND uid > :uid ORDER BY uid", 0, $limit, array(':uid' => $sandbox['last_user_processed']));
   foreach ($result as $user) {
@@ -544,11 +555,6 @@ function user_update_7005(&$sandbox) {
   db_change_field('users', 'mail', 'mail', $schema['users']['fields']['mail']);
 }
 
-/**
- * @} End of "defgroup user-updates-6.x-to-7.x"
- * The next series of updates should start at 8000.
- */
-
 /**
  * Add module data to {role_permission}.
  */
@@ -562,7 +568,7 @@ function user_update_7006(&$sandbox) {
   );
   // Check that the field hasn't been updated in an aborted run of this
   // update.
-  if (!db_column_exists('role_permission', 'module')) {
+  if (!db_field_exists('role_permission', 'module')) {
     // Add a new field for the fid.
     db_add_field('role_permission', 'module', $module_field);
   }
@@ -574,3 +580,16 @@ function user_update_7006(&$sandbox) {
       ->execute();
   }
 }
+
+/**
+ * Add a weight column to user roles.
+ */
+function user_update_7007() {
+  db_add_field('role', 'weight', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
+  db_add_index('role', 'name_weight', array('name', 'weight'));
+}
+
+/**
+ * @} End of "defgroup user-updates-6.x-to-7.x"
+ * The next series of updates should start at 8000.
+ */
diff --git a/modules/user/user.module b/modules/user/user.module
index f5eb165f28808e9d7e6d7a5cb90a29c4b05b1c7a..4cb5a7a3c5d2effe4a09b541b767c98a537cea72 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.module,v 1.1139 2010/03/20 19:06:12 dries Exp $
+// $Id: user.module,v 1.1163 2010/04/24 14:51:49 dries Exp $
 
 /**
  * @file
@@ -40,9 +40,9 @@ function user_help($path, $arg) {
     case 'admin/people/create':
       return '<p>' . t("This web page allows administrators to register new users. Users' e-mail addresses and usernames must be unique.") . '</p>';
     case 'admin/people/permissions':
-      return '<p>' . t('Permissions let you control what users can do and see on your site. You can define a specific set of permissions for each role. (See the <a href="@role">Roles</a> page to create a role). Two important roles to consider are Authenticated Users and Administrators. Any permissions granted to the Authenticated Users role will be given to any user who can log into your site.  You can make any role the Administrator role for the site, meaning this will be granted all new permissions automatically. You can do this on the <a href="@settings">User Settings</a> page. You should be careful to ensure that only trusted users are given this access and level of control of your site.', array('@role' => url('admin/people/permissions/roles'), '@settings' => url('admin/config/people/accounts'))) . '</p>';
+      return '<p>' . t('Permissions let you control what users can do and see on your site. You can define a specific set of permissions for each role. (See the <a href="@role">Roles</a> page to create a role). Two important roles to consider are Authenticated Users and Administrators. Any permissions granted to the Authenticated Users role will be given to any user who can log into your site. You can make any role the Administrator role for the site, meaning this will be granted all new permissions automatically. You can do this on the <a href="@settings">User Settings</a> page. You should be careful to ensure that only trusted users are given this access and level of control of your site.', array('@role' => url('admin/people/permissions/roles'), '@settings' => url('admin/config/people/accounts'))) . '</p>';
     case 'admin/people/permissions/roles':
-      $output = '<p>' . t('Roles allow you to fine tune the security and administration of Drupal. A role defines a group of users that have certain privileges as defined in <a href="@permissions">user permissions</a>. Examples of roles include: anonymous user, authenticated user, moderator, administrator and so on. In this area you will define the <em>role names</em> of the various roles. To delete a role choose "edit".', array('@permissions' => url('admin/people/permissions'))) . '</p>';
+      $output = '<p>' . t('Roles allow you to fine tune the security and administration of Drupal. A role defines a group of users that have certain privileges as defined on the <a href="@permissions">permissions page</a>. Examples of roles include: anonymous user, authenticated user, moderator, administrator and so on. In this area you will define the names and order of the roles on your site. It is recommended to order your roles from least permissive (anonymous user) to most permissive (administrator). To delete a role choose "edit role".', array('@permissions' => url('admin/people/permissions'))) . '</p>';
       $output .= '<p>'. t('By default, Drupal comes with two user roles:') . '</p>';
       $output .= '<ul>';
       $output .= '<li>' . t("Anonymous user: this role is used for users that don't have a user account or that are not authenticated.") . '</li>';
@@ -102,7 +102,7 @@ function user_theme() {
       'render element' => 'form',
       'file' => 'user.admin.inc',
     ),
-    'user_admin_new_role' => array(
+    'user_admin_roles' => array(
       'render element' => 'form',
       'file' => 'user.admin.inc',
     ),
@@ -110,6 +110,10 @@ function user_theme() {
       'render element' => 'form',
       'file' => 'user.admin.inc',
     ),
+    'user_permission_description' => array(
+      'variables' => array('permission_item' => NULL, 'hide' => NULL),
+      'file' => 'user.admin.inc',
+    ),
     'user_signature' => array(
       'variables' => array('signature' => NULL),
     ),
@@ -127,7 +131,7 @@ function user_entity_info() {
       'base table' => 'users',
       'uri callback' => 'user_uri',
       'fieldable' => TRUE,
-      'object keys' => array(
+      'entity keys' => array(
         'id' => 'uid',
       ),
       'bundles' => array(
@@ -234,7 +238,7 @@ class UserController extends DrupalDefaultEntityController {
     $picture_fids = array();
     foreach ($queried_users as $key => $record) {
       $picture_fids[] = $record->picture;
-      $queried_users[$key] = drupal_unpack($record);
+      $queried_users[$key]->data = unserialize($record->data);
       $queried_users[$key]->roles = array();
       if ($record->uid) {
         $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
@@ -330,11 +334,8 @@ function user_load_by_name($name) {
  *   TRUE or omit the $account->uid field.
  * @param $edit
  *   An array of fields and values to save. For example array('name'
- *   => 'My name'). Keys that do not belong to columns in the user-related
- *   tables are added to the a serialized array in the 'data' column
- *   and will be loaded in the $user->data array by user_load().
- *   Setting a field to NULL deletes it from the data column, if you are
- *   modifying an existing user account.
+ *   => 'My name'). Key / value pairs added to the $edit['data'] will be
+ *   serialized and saved in the {users.data} column.
  * @param $category
  *   (optional) The category for storing profile information in.
  *
@@ -365,16 +366,16 @@ function user_save($account, $edit = array(), $category = 'account') {
     field_attach_presave('user', $edit);
     $edit = (array) $edit;
 
+    if (empty($account)) {
+      $account = new stdClass();
+    }
     if (!isset($account->is_new)) {
       $account->is_new = empty($account->uid);
     }
     // Prepopulate $edit['data'] with the current value of $account->data.
     // Modules can add to or remove from this array in hook_user_presave().
     if (!empty($account->data)) {
-      $data = unserialize($account->data);
-      foreach ($data as $key => $value) {
-        $edit['data'][$key] = $value;
-      }
+      $edit['data'] = !empty($edit['data']) ? array_merge($account->data, $edit['data']) : $account->data;
     }
     user_module_invoke('presave', $edit, $account, $category);
 
@@ -496,11 +497,11 @@ function user_save($account, $edit = array(), $category = 'account') {
         return FALSE;
       }
 
-      // Build the initial user object.
-      $user = user_load($edit['uid'], TRUE);
+      // Build a stub user object.
+      $user = (object) $edit;
+      $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
 
-      $entity = (object) $edit;
-      field_attach_insert('user', $entity);
+      field_attach_insert('user', $user);
 
       user_module_invoke('insert', $edit, $user, $category);
       entity_invoke('insert', 'user', $user);
@@ -518,15 +519,13 @@ function user_save($account, $edit = array(), $category = 'account') {
         }
         $query->execute();
       }
-
-      // Build the finished user object.
-      $user = user_load($edit['uid'], TRUE);
     }
 
     return $user;
   }
   catch (Exception $e) {
     $transaction->rollback('user', $e->getMessage(), array(), WATCHDOG_ERROR);
+    throw $e;
   }
 }
 
@@ -735,11 +734,11 @@ function user_permission() {
   return array(
     'administer permissions' =>  array(
       'title' => t('Administer permissions'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
     'administer users' => array(
       'title' => t('Administer users'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
     'access user profiles' => array(
       'title' => t('View user profiles'),
@@ -753,7 +752,7 @@ function user_permission() {
     ),
     'select account cancellation method' => array(
       'title' => t('Select method for cancelling own account'),
-      'description' => drupal_placeholder(array('text' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+      'restrict access' => TRUE,
     ),
   );
 }
@@ -778,7 +777,8 @@ function user_file_references($file) {
   $file_used = (bool) db_query_range('SELECT 1 FROM {users} WHERE picture = :fid', 0, 1, array(':fid' => $file->fid))->fetchField();
   if ($file_used) {
     // Return the name of the module and how many references it has to the file.
-    return array('user' => $count);
+    // If file is still used then 1 is enough to indicate this.
+    return array('user' => 1);
   }
 }
 
@@ -975,7 +975,7 @@ function user_account_form(&$form, &$form_state) {
     '#access' => $admin,
   );
 
-  $roles = user_roles(TRUE);
+  $roles = array_map('check_plain', user_roles(TRUE));
   // The disabled checkbox subelement for the 'authenticated user' role
   // must be generated separately and added to the checkboxes element,
   // because of a limitation in Form API not supporting a single disabled
@@ -1041,7 +1041,7 @@ function user_account_form(&$form, &$form_state) {
     '#type' => 'file',
     '#title' => t('Upload picture'),
     '#size' => 48,
-    '#description' => t('Your virtual face or picture. Maximum dimensions are %dimensions pixels and the maximum size is %size kB.', array('%dimensions' => variable_get('user_picture_dimensions', '85x85'), '%size' => variable_get('user_picture_file_size', '30'))) . ' ' . variable_get('user_picture_guidelines', ''),
+    '#description' => t('Your virtual face or picture. Maximum dimensions are %dimensions pixels and the maximum size is %size kB.', array('%dimensions' => variable_get('user_picture_dimensions', '85x85'), '%size' => variable_get('user_picture_file_size', '30'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')),
   );
   $form['#validate'][] = 'user_validate_picture';
 }
@@ -1165,7 +1165,8 @@ function user_login_block($form) {
     '#size' => 15,
     '#required' => TRUE,
   );
-  $form['submit'] = array('#type' => 'submit',
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit',
     '#value' => t('Log in'),
   );
   $items = array();
@@ -1339,7 +1340,7 @@ function template_preprocess_user_picture(&$variables) {
 }
 
 /**
- * Make a list of users.
+ * Returns HTML for a list of users.
  *
  * @param $variables
  *   An associative array containing:
@@ -1500,6 +1501,8 @@ function user_menu() {
     'weight' => -10,
     'file' => 'user.admin.inc',
   );
+
+  // Permissions and role forms.
   $items['admin/people/permissions'] = array(
     'title' => 'Permissions',
     'description' => 'Determine access to features by selecting permissions for roles.',
@@ -1519,19 +1522,28 @@ function user_menu() {
     'title' => 'Roles',
     'description' => 'List, edit, or add user roles.',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('user_admin_new_role'),
+    'page arguments' => array('user_admin_roles'),
     'access arguments' => array('administer permissions'),
     'file' => 'user.admin.inc',
     'type' => MENU_LOCAL_TASK,
     'weight' => -5,
   );
-  $items['admin/people/permissions/roles/edit'] = array(
+  $items['admin/people/permissions/roles/edit/%user_role'] = array(
     'title' => 'Edit role',
-    'page arguments' => array('user_admin_role'),
-    'access arguments' => array('administer permissions'),
+    'page arguments' => array('user_admin_role', 5),
+    'access callback' => 'user_role_edit_access',
+    'access arguments' => array(5),
     'type' => MENU_CALLBACK,
   );
-
+  $items['admin/people/permissions/roles/delete/%user_role'] = array(
+    'title' => 'Delete role',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('user_admin_role_delete_confirm', 5),
+    'access callback' => 'user_role_edit_access',
+    'access arguments' => array(5),
+    'type' => MENU_CALLBACK,
+    'file' => 'user.admin.inc',
+  );
 
   $items['admin/people/create'] = array(
     'title' => 'Add user',
@@ -1544,7 +1556,8 @@ function user_menu() {
   $items['admin/config/people'] = array(
    'title' => 'People',
    'description' => 'Configure user accounts.',
-   'position' => 'right',
+   'position' => 'left',
+   'weight' => -20,
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array('access administration pages'),
    'file' => 'system.admin.inc',
@@ -1817,7 +1830,8 @@ function user_login($form, &$form_state) {
     '#required' => TRUE,
   );
   $form['#validate'] = user_login_default_validators();
-  $form['submit'] = array('#type' => 'submit', '#value' => t('Log in'), '#weight' => 2);
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
 
   return $form;
 }
@@ -2494,19 +2508,18 @@ function user_mail_tokens(&$replacements, $data, $options) {
  *   value.
  */
 function user_roles($membersonly = FALSE, $permission = NULL) {
-  // System roles take the first two positions.
-  $roles = array(
-    DRUPAL_ANONYMOUS_RID => NULL,
-    DRUPAL_AUTHENTICATED_RID => NULL,
-  );
-
+  $query = db_select('role', 'r');
+  $query->addTag('translatable');
+  $query->fields('r', array('rid', 'name'));
+  $query->orderBy('weight');
+  $query->orderBy('name');
   if (!empty($permission)) {
-    $result = db_query("SELECT r.* FROM {role} r INNER JOIN {role_permission} p ON r.rid = p.rid WHERE p.permission = :permission ORDER BY r.name", array(':permission' => $permission));
-  }
-  else {
-    $result = db_query('SELECT * FROM {role} ORDER BY name');
+    $query->innerJoin('role_permission', 'p', 'r.rid = p.rid');
+    $query->condition('p.permission', $permission);
   }
+  $result = $query->execute();
 
+  $roles = array();
   foreach ($result as $role) {
     switch ($role->rid) {
       // We only translate the built in role names
@@ -2523,27 +2536,49 @@ function user_roles($membersonly = FALSE, $permission = NULL) {
     }
   }
 
-  // Filter to remove unmatched system roles.
-  return array_filter($roles);
+  return $roles;
 }
 
 /**
- * Fetch a user role from database.
+ * Fetches a user role by role ID.
+ *
+ * @param $rid
+ *   An integer representing the role ID.
  *
- * @param $role
- *   A string with the role name, or an integer with the role ID.
  * @return
- *   A fully-loaded role object if a role with the given name or ID
- *   exists, FALSE otherwise.
+ *   A fully-loaded role object if a role with the given ID exists, or FALSE
+ *   otherwise.
+ *
+ * @see user_role_load_by_name()
  */
-function user_role_load($role) {
-  $field = is_int($role) ? 'rid' : 'name';
+function user_role_load($rid) {
   return db_select('role', 'r')
     ->fields('r')
-    ->condition($field, $role)
+    ->condition('rid', $rid)
     ->execute()
     ->fetchObject();
 }
+
+/**
+ * Fetches a user role by role name.
+ *
+ * @param $role_name
+ *   A string representing the role name.
+ *
+ * @return
+ *   A fully-loaded role object if a role with the given name exists, or FALSE
+ *   otherwise.
+ *
+ * @see user_role_load()
+ */
+function user_role_load_by_name($role_name) {
+  return db_select('role', 'r')
+    ->fields('r')
+    ->condition('name', $role_name)
+    ->execute()
+    ->fetchObject();
+}
+
 /**
  * Save a user role to the database.
  *
@@ -2561,6 +2596,12 @@ function user_role_save($role) {
     // Prevent leading and trailing spaces in role names.
     $role->name = trim($role->name);
   }
+  if (!isset($role->weight)) {
+    // Set a role weight to make this new role last.
+    $query = db_select('role');
+    $query->addExpression('MAX(weight)');
+    $role->weight = $query->execute()->fetchField() + 1;
+  }
   if (!empty($role->rid) && $role->name) {
     $status = drupal_write_record('role', $role, 'rid');
     module_invoke_all('user_role_update', $role);
@@ -2584,7 +2625,12 @@ function user_role_save($role) {
  *   A string with the role name, or an integer with the role ID.
  */
 function user_role_delete($role) {
-  $role = user_role_load($role);
+  if (is_int($role)) {
+    $role = user_role_load($role);
+  }
+  else {
+    $role = user_role_load_by_name($role);
+  }
 
   db_delete('role')
     ->condition('rid', $role->rid)
@@ -2604,6 +2650,18 @@ function user_role_delete($role) {
   drupal_static_reset('user_role_permissions');
 }
 
+/**
+ * Menu access callback for user role editing.
+ */
+function user_role_edit_access($role) {
+  // Prevent the system-defined roles from being altered or removed.
+  if ($role->rid == DRUPAL_ANONYMOUS_RID || $role->rid == DRUPAL_AUTHENTICATED_RID) {
+    return FALSE;
+  }
+
+  return user_access('administer permissions');
+}
+
 /**
  * Determine the modules that permissions belong to.
  *
@@ -3045,7 +3103,6 @@ function user_build_filter_query(SelectQuery $query) {
 function user_forms() {
   $forms['user_admin_access_add_form']['callback'] = 'user_admin_access_form';
   $forms['user_admin_access_edit_form']['callback'] = 'user_admin_access_form';
-  $forms['user_admin_new_role']['callback'] = 'user_admin_role';
   return $forms;
 }
 
@@ -3062,7 +3119,11 @@ function user_comment_view($comment) {
 }
 
 /**
- * Theme output of user signature.
+ * Returns HTML for a user signature.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - signature: The user's signature.
  *
  * @ingroup themeable
  */
@@ -3298,10 +3359,10 @@ function user_register_form($form, &$form_state) {
     $form_state['redirect'] = $_GET['q'];
   }
 
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Create new account'),
-    '#weight' => 30,
   );
 
   // Add the final user registration form submit handler.
@@ -3343,10 +3404,7 @@ function user_register_submit($form, &$form_state) {
   }
   $notify = !empty($form_state['values']['notify']);
 
-  // The unset below is needed to prevent these form values from being saved as
-  // user data.
   form_state_values_clean($form_state);
-  unset($form_state['values']['notify']);
 
   $form_state['values']['pass'] = $pass;
   $form_state['values']['init'] = $form_state['values']['mail'];
@@ -3368,8 +3426,9 @@ function user_register_submit($form, &$form_state) {
   $account->password = $pass;
 
   // New administrative account without notification.
+  $uri = entity_uri('user', $account);
   if ($admin && !$notify) {
-    drupal_set_message(t('Created a new user account for <a href="@url">%name</a>. No e-mail has been sent.', array('@url' => url("user/$account->uid"), '%name' => $account->name)));
+    drupal_set_message(t('Created a new user account for <a href="@url">%name</a>. No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
   }
   // No e-mail verification required; log in user immediately.
   elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) {
@@ -3384,7 +3443,7 @@ function user_register_submit($form, &$form_state) {
     $op = $notify ? 'register_admin_created' : 'register_no_approval_required';
     _user_mail_notify($op, $account);
     if ($notify) {
-      drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user <a href="@url">%name</a>.', array('@url' => url("user/$account->uid"), '%name' => $account->name)));
+      drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user <a href="@url">%name</a>.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
     }
     else {
       drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));
@@ -3445,20 +3504,25 @@ function user_login_destination() {
  * Saves visitor information as a cookie so it can be reused.
  *
  * @param $values
- *   An array of submitted form values with identifying information about the
- *   current user, typically $form_state['values'] from a submit handler.
- * @param $fields
- *   An array of key values from $values to be saved into a cookie.
- */
-function user_cookie_save(array $values, array $fields = array('name', 'mail', 'homepage')) {
-  foreach ($fields as $field) {
-    if (isset($values[$field])) {
-      // Set cookie for 365 days.
-      setrawcookie('Drupal.visitor.' . $field, rawurlencode($values[$field]), REQUEST_TIME + 31536000, '/');
-    }
+ *   An array of key/value pairs to be saved into a cookie.
+ */
+function user_cookie_save(array $values) {
+  foreach ($values as $field => $value) {
+    // Set cookie for 365 days.
+    setrawcookie('Drupal.visitor.' . $field, rawurlencode($value), REQUEST_TIME + 31536000, '/');
   }
 }
 
+/**
+ * Delete a visitor information cookie.
+ *
+ * @param $cookie_name
+ *   A cookie name such as 'homepage'.
+ */
+function user_cookie_delete($cookie_name) {
+  setrawcookie('Drupal.visitor.' . $cookie_name, '', REQUEST_TIME - 3600, '/');
+}
+
 /**
  * Implements hook_rdf_mapping().
  */
@@ -3468,7 +3532,7 @@ function user_rdf_mapping() {
       'type' => 'user',
       'bundle' => RDF_DEFAULT_BUNDLE,
       'mapping' => array(
-        'rdftype' => array('sioc:User'),
+        'rdftype' => array('sioc:UserAccount'),
         'name' => array(
           'predicates' => array('foaf:name'),
         ),
diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc
index db70017c20343c74f81d121cab4b3b8b39f0b061..643d5b852780bf9d8a48956c244100a9ebd886fa 100644
--- a/modules/user/user.pages.inc
+++ b/modules/user/user.pages.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.pages.inc,v 1.68 2010/02/28 20:10:34 dries Exp $
+// $Id: user.pages.inc,v 1.70 2010/04/24 14:49:14 dries Exp $
 
 /**
  * @file
@@ -48,7 +48,8 @@ function user_pass() {
       '#suffix' => '</p>',
     );
   }
-  $form['submit'] = array('#type' => 'submit', '#value' => t('E-mail new password'));
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('E-mail new password'));
 
   return $form;
 }
@@ -140,7 +141,8 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a
         else {
           $form['message'] = array('#markup' => t('<p>This is a one-time login for %user_name and will expire on %expiration_date.</p><p>Click on this button to log in to the site and change your password.</p>', array('%user_name' => $account->name, '%expiration_date' => format_date($timestamp + $timeout))));
           $form['help'] = array('#markup' => '<p>' . t('This login can be used only once.') . '</p>');
-          $form['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
+          $form['actions'] = array('#type' => 'actions');
+          $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
           $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login");
           return $form;
         }
@@ -251,16 +253,15 @@ function user_profile_form($form, &$form_state, $account, $category = 'account')
   // Attach field widgets.
   field_attach_form('user', $account, $form, $form_state);
 
-  $form['submit'] = array(
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
-    '#weight' => 30,
   );
   if ($category == 'account') {
-    $form['cancel'] = array(
+    $form['actions']['cancel'] = array(
       '#type' => 'submit',
       '#value' => t('Cancel account'),
-      '#weight' => 31,
       '#submit' => array('user_edit_cancel_submit'),
       '#access' => $account->uid > 1 && (($account->uid == $user->uid && user_access('cancel account')) || user_access('administer users')),
     );
@@ -495,13 +496,13 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
   $current = REQUEST_TIME;
 
   // Basic validation of arguments.
-  if (isset($account->user_cancel_method) && !empty($timestamp) && !empty($hashed_pass)) {
+  if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
     // Validate expiration and hashed password/login.
     if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
       $edit = array(
-        'user_cancel_notify' => isset($account->user_cancel_notify) ? $account->user_cancel_notify : variable_get('user_mail_status_canceled_notify', FALSE),
+        'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE),
       );
-      user_cancel($edit, $account->uid, $account->user_cancel_method);
+      user_cancel($edit, $account->uid, $account->data['user_cancel_method']);
       // Since user_cancel() is not invoked via Form API, batch processing needs
       // to be invoked manually and should redirect to the front page after
       // completion.
diff --git a/modules/user/user.permissions.js b/modules/user/user.permissions.js
index 0bcfe33ffca948d9aba93dcfdf621de596ff094b..ca4e5ff7724e3ed6017627add5d96930af9c3a6e 100644
--- a/modules/user/user.permissions.js
+++ b/modules/user/user.permissions.js
@@ -1,4 +1,4 @@
-// $Id: user.permissions.js,v 1.1 2009/05/12 08:33:19 dries Exp $
+// $Id: user.permissions.js,v 1.2 2010/04/19 21:17:16 webchick Exp $
 (function ($) {
 
 /**
@@ -16,8 +16,8 @@ Drupal.behaviors.permissions = {
         $(this).addClass('real-checkbox');
         $('<input type="checkbox" class="dummy-checkbox" disabled="disabled" checked="checked" />')
           .attr('title', Drupal.t("This permission is inherited from the authenticated user role."))
-          .hide()
-          .insertAfter(this);
+          .insertAfter(this)
+          .hide();
       });
 
       // Helper function toggles all dummy checkboxes based on the checkboxes'
diff --git a/modules/user/user.test b/modules/user/user.test
index 511d0f69373428a36155b5388f153db18fd53196..30572cba95f802c3fa5e419f56ae47dde47febed 100644
--- a/modules/user/user.test
+++ b/modules/user/user.test
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.test,v 1.86 2010/03/07 18:46:55 webchick Exp $
+// $Id: user.test,v 1.89 2010/04/20 09:48:06 webchick Exp $
 
 class UserRegistrationTestCase extends DrupalWebTestCase {
   public static function getInfo() {
@@ -1456,4 +1456,132 @@ class UserEditedOwnAccountTestCase extends DrupalWebTestCase {
     $account->name = $edit['name'];
     $this->drupalLogin($account);
   }
-}
\ No newline at end of file
+}
+
+/**
+ * Test case to test adding, editing and deleting roles.
+ */
+class UserRoleAdminTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'User role administration',
+      'description' => 'Test adding, editing and deleting user roles.',
+      'group' => 'User',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    $this->admin_user = $this->drupalCreateUser(array('administer permissions', 'administer users'));
+  }
+
+  /**
+   * Test adding, renaming and deleting roles.
+   */
+  function testRoleAdministration() {
+    $this->drupalLogin($this->admin_user);
+
+    // Test adding a role. (In doing so, we use a role name that happens to
+    // correspond to an integer, to test that the role administration pages
+    // correctly distinguish between role names and IDs.)
+    $role_name = '123';
+    $edit = array('name' => $role_name);
+    $this->drupalPost('admin/people/permissions/roles', $edit, t('Add role'));
+    $this->assertText(t('The role has been added.'), t('The role has been added.'));
+    $role = user_role_load_by_name($role_name);
+    $this->assertTrue(is_object($role), t('The role was successfully retrieved from the database.'));
+
+    // Try adding a duplicate role.
+    $this->drupalPost(NULL, $edit, t('Add role'));
+    $this->assertRaw(t('The role name %name already exists. Choose another role name.', array('%name' => $role_name)), t('Duplicate role warning displayed.'));
+
+    // Test renaming a role.
+    $old_name = $role_name;
+    $role_name = '456';
+    $edit = array('name' => $role_name);
+    $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", $edit, t('Save role'));
+    $this->assertText(t('The role has been renamed.'), t('The role has been renamed.'));
+    $this->assertFalse(user_role_load_by_name($old_name), t('The role can no longer be retrieved from the database using its old name.'));
+    $this->assertTrue(is_object(user_role_load_by_name($role_name)), t('The role can be retrieved from the database using its new name.'));
+
+    // Test deleting a role.
+    $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", NULL, t('Delete role'));
+    $this->drupalPost(NULL, NULL, t('Delete'));
+    $this->assertText(t('The role has been deleted.'), t('The role has been deleted'));
+    $this->assertNoLinkByHref("admin/people/permissions/roles/edit/{$role->rid}", t('Role edit link removed.'));
+    $this->assertFalse(user_role_load_by_name($role_name), t('A deleted role can no longer be loaded.'));
+
+    // Make sure that the system-defined roles cannot be edited via the user
+    // interface.
+    $this->drupalGet('admin/people/permissions/roles/edit/' . DRUPAL_ANONYMOUS_RID);
+    $this->assertResponse(403, t('Access denied when trying to edit the built-in anonymous role.'));
+    $this->drupalGet('admin/people/permissions/roles/edit/' . DRUPAL_AUTHENTICATED_RID);
+    $this->assertResponse(403, t('Access denied when trying to edit the built-in authenticated role.'));
+  }
+}
+
+/**
+ * Test user token replacement in strings.
+ */
+class UserTokenReplaceTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'User token replacement',
+      'description' => 'Generates text using placeholders for dummy content to check user token replacement.',
+      'group' => 'User',
+    );
+  }
+
+  /**
+   * Creates a user, then tests the tokens generated from it.
+   */
+  function testUserTokenReplacement() {
+    global $language;
+    $url_options = array(
+      'absolute' => TRUE,
+      'language' => $language,
+    );
+
+    // Create two users and log them in one after another.
+    $user1 = $this->drupalCreateUser(array());
+    $user2 = $this->drupalCreateUser(array());
+    $this->drupalLogin($user1);
+    $this->drupalLogout();
+    $this->drupalLogin($user2);
+
+    $account = user_load($user1->uid);
+    global $user;
+
+    // Generate and test sanitized tokens.
+    $tests = array();
+    $tests['[user:uid]'] = $account->uid;
+    $tests['[user:name]'] = filter_xss($account->name);
+    $tests['[user:mail]'] = check_plain($account->mail);
+    $tests['[user:url]'] = url("user/$account->uid", $url_options);
+    $tests['[user:edit-url]'] = url("user/$account->uid/edit", $url_options);
+    $tests['[user:last-login]'] = format_date($account->login, 'medium', '', NULL, $language->language);
+    $tests['[user:last-login:short]'] = format_date($account->login, 'short', '', NULL, $language->language);
+    $tests['[user:created]'] = format_date($account->created, 'medium', '', NULL, $language->language);
+    $tests['[user:created:short]'] = format_date($account->created, 'short', '', NULL, $language->language);
+    $tests['[current-user:name]'] = check_plain($user->name);
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.'));
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('user' => $account), array('language' => $language));
+      $this->assertFalse(strcmp($output, $expected), t('Sanitized user token %token replaced.', array('%token' => $input)));
+    }
+
+    // Generate and test unsanitized tokens.
+    $tests['[user:name]'] = $account->name;
+    $tests['[user:mail]'] = $account->mail;
+    $tests['[current-user:name]'] = $user->name;
+
+    foreach ($tests as $input => $expected) {
+      $output = token_replace($input, array('user' => $account), array('language' => $language, 'sanitize' => FALSE));
+      $this->assertFalse(strcmp($output, $expected), t('Unsanitized user token %token replaced.', array('%token' => $input)));
+    }
+  }
+}
diff --git a/modules/user/user.tokens.inc b/modules/user/user.tokens.inc
index a664b3f9231f68842f0a75d2015730632aa6fdce..f1db1fede5b27c7dfea19bdb2f33dc2cf4a2c2e5 100644
--- a/modules/user/user.tokens.inc
+++ b/modules/user/user.tokens.inc
@@ -1,5 +1,5 @@
 <?php
-// $Id: user.tokens.inc,v 1.5 2010/03/07 23:18:06 webchick Exp $
+// $Id: user.tokens.inc,v 1.6 2010/04/20 09:48:06 webchick Exp $
 
 /**
  * @file
@@ -63,7 +63,6 @@ function user_token_info() {
  * Implements hook_tokens().
  */
 function user_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  global $user;
   $url_options = array('absolute' => TRUE);
   if (isset($options['language'])) {
     $url_options['language'] = $options['language'];
diff --git a/profiles/minimal/minimal.info b/profiles/minimal/minimal.info
index 884c66d6be504eea85bb180c4f3c31294fb66dbf..8d5880db910fc68df079537c2f22023a627f3457 100644
--- a/profiles/minimal/minimal.info
+++ b/profiles/minimal/minimal.info
@@ -7,8 +7,8 @@ dependencies[] = block
 dependencies[] = dblog
 files[] = minimal.profile
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/profiles/standard/standard.info b/profiles/standard/standard.info
index 3c7e7aa0052c76e8340774af722764356dfa7385..334742c2c0694b759740ca6b0deb60d82507a2f9 100644
--- a/profiles/standard/standard.info
+++ b/profiles/standard/standard.info
@@ -23,8 +23,8 @@ dependencies[] = file
 dependencies[] = rdf
 files[] = standard.profile
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/profiles/standard/standard.install b/profiles/standard/standard.install
index 7b36b98950177fdfba5c2ac8e21fdca142586479..a9de9dc5de8c33ea30e5e86bf78c1426637c3b6b 100644
--- a/profiles/standard/standard.install
+++ b/profiles/standard/standard.install
@@ -1,5 +1,5 @@
 <?php
-// $Id: standard.install,v 1.8 2010/03/11 22:48:34 dries Exp $
+// $Id: standard.install,v 1.13 2010/04/22 09:55:32 webchick Exp $
 
 /**
  * Implements hook_install().
@@ -217,7 +217,7 @@ function standard_install() {
       'type' => 'article',
       'name' => st('Article'),
       'base' => 'node_content',
-      'description' => st('Use <em>articles</em> for time-specific content like news, press releases or blog posts.'),
+      'description' => st('Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.'),
       'custom' => 1,
       'modified' => 1,
       'locked' => 0,
@@ -247,7 +247,7 @@ function standard_install() {
           'predicates' => array('rdfs:seeAlso'),
           'type' => 'rel',
         ),
-        'taxonomy_tags' => array(
+        'field_tags' => array(
           'predicates' => array('dc:subject'),
           'type' => 'rel',
         ),
@@ -284,7 +284,7 @@ function standard_install() {
   taxonomy_vocabulary_save($vocabulary);
 
   $field = array(
-    'field_name' => 'taxonomy_' . $vocabulary->machine_name,
+    'field_name' => 'field_' . $vocabulary->machine_name,
     'type' => 'taxonomy_term_reference',
     // Set cardinality to unlimited for tagging.
     'cardinality' => FIELD_CARDINALITY_UNLIMITED,
@@ -300,8 +300,8 @@ function standard_install() {
   field_create_field($field);
 
   $instance = array(
-    'field_name' => 'taxonomy_' . $vocabulary->machine_name,
-    'object_type' => 'node',
+    'field_name' => 'field_' . $vocabulary->machine_name,
+    'entity_type' => 'node',
     'label' => $vocabulary->name,
     'bundle' => 'article',
     'description' => $vocabulary->help,
@@ -315,7 +315,7 @@ function standard_install() {
 
   // Create an image field named "Image", enabled for the 'article' content type.
   // Many of the following values will be defaulted, they're included here as an illustrative examples.
-  // @see: http://api.drupal.org/api/function/field_create_field/7
+  // See http://api.drupal.org/api/function/field_create_field/7
 
   $field = array(
     'field_name' => 'field_image',
@@ -337,10 +337,10 @@ function standard_install() {
 
 
   // Many of the following values will be defaulted, they're included here as an illustrative examples.
-  // @see: http://api.drupal.org/api/function/field_create_instance/7
+  // See http://api.drupal.org/api/function/field_create_instance/7
   $instance = array(
     'field_name' => 'field_image',
-    'object_type' => 'node',
+    'entity_type' => 'node',
     'label' => 'Image',
     'bundle' => 'article',
     'description' => 'Upload an image to go with this article.',
@@ -408,6 +408,7 @@ function standard_install() {
   // Create a default role for site administrators, with all available permissions assigned.
   $admin_role = new stdClass();
   $admin_role->name = 'administrator';
+  $admin_role->weight = 2;
   user_role_save($admin_role);
   user_role_grant_permissions($admin_role->rid, array_keys(module_invoke_all('permission')));
   // Set this as the administrator role.
diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php
index 471122c6af7e8285905d55d64551f8a23749ea6d..2cee98107217b2817ce672c8db2332d61ce9ba95 100644
--- a/sites/default/default.settings.php
+++ b/sites/default/default.settings.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: default.settings.php,v 1.42 2010/03/17 13:58:45 dries Exp $
+// $Id: default.settings.php,v 1.44 2010/04/07 15:07:59 dries Exp $
 
 /**
  * @file
@@ -131,6 +131,22 @@
  *     'authmap'   => 'shared_',
  *   );
  *
+ * You can also use db_prefix as a reference to a schema/database. This maybe
+ * useful if your Drupal installation exists in a schema that is not the default
+ * or you want to access several databases from the same code base at the same 
+ * time.
+ * Example:
+ *
+ *  $db_prefix = array(
+ *    'default' => 'main.',
+ *     'users'      => 'shared.',
+ *     'sessions'  => 'shared.',
+ *     'role'      => 'shared.',
+ *     'authmap'   => 'shared.',
+ *  );
+ *
+ * NOTE: MySQL and SQLite's definition of a schema is a database.
+ *
  * Database configuration format:
  *   $databases['default']['default'] = array(
  *     'driver' => 'mysql',
@@ -155,7 +171,7 @@ $databases = array();
 $db_prefix = '';
 
 /**
- * Access control for update.php script
+ * Access control for update.php script.
  *
  * If you are updating your Drupal installation using the update.php script but
  * are not logged in using either an account with the "Administer software
diff --git a/sites/example.sites.php b/sites/example.sites.php
index 9c2bb27115c26a894f8a60c6f770d0f446ad16be..1775824006e45e196c334f2f0e0951109fdea0b7 100644
--- a/sites/example.sites.php
+++ b/sites/example.sites.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: example.sites.php,v 1.2 2009/12/08 06:28:50 webchick Exp $
+// $Id: example.sites.php,v 1.3 2010/04/15 12:01:28 dries Exp $
 
 /**
  * @file
@@ -18,7 +18,7 @@
  *
  * $sites = array(
  *   'devexample.com' => 'example.com',
- *   'localhost/example' => 'example.com',
+ *   'localhost.example' => 'example.com',
  * );
  *
  * The above array will cause Drupal to look for a directory named
@@ -41,4 +41,4 @@
  * signs to enable.
  */
 # $sites['devexample.com'] = 'example.com';
-# $sites['localhost/example'] = 'example.com';
+# $sites['localhost.example'] = 'example.com';
diff --git a/themes/garland/block.tpl.php b/themes/garland/block.tpl.php
index 7687a1056a609782e6696dca7a42268601961a14..5997594a109e4a04a46f7249b93646c6227c6480 100644
--- a/themes/garland/block.tpl.php
+++ b/themes/garland/block.tpl.php
@@ -1,7 +1,7 @@
 <?php
-// $Id: block.tpl.php,v 1.13 2010/01/04 03:57:19 webchick Exp $
+// $Id: block.tpl.php,v 1.14 2010/04/26 14:10:40 dries Exp $
 ?>
-<div id="block-<?php print $block->module . '-' . $block->delta; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>
+<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>
 
   <?php print render($title_prefix); ?>
 <?php if (!empty($block->subject)): ?>
diff --git a/themes/garland/color/color.inc b/themes/garland/color/color.inc
index 88da8199e2ed1b26d4957cbc23ea12cd3296fd0f..416983b7bc2113c4bdf3530c12de42076a21f47e 100644
--- a/themes/garland/color/color.inc
+++ b/themes/garland/color/color.inc
@@ -1,25 +1,162 @@
 <?php
-// $Id: color.inc,v 1.4 2007/12/14 17:00:14 goba Exp $
+// $Id: color.inc,v 1.5 2010/04/03 08:05:08 webchick Exp $
 
 $info = array(
 
+  // Available colors and color labels used in theme.
+  'fields' => array(
+    'base' => t('Base color'),
+    'link' => t('Link color'),
+    'top' => t('Header top'),
+    'bottom' => t('Header bottom'),
+    'text' => t('Text color'),
+  ),
   // Pre-defined color schemes.
   'schemes' => array(
-    '#0072b9,#027ac6,#2385c2,#5ab5ee,#494949' => t('Blue Lagoon (Default)'),
-    '#464849,#2f416f,#2a2b2d,#5d6779,#494949' => t('Ash'),
-    '#55c0e2,#000000,#085360,#007e94,#696969' => t('Aquamarine'),
-    '#d5b048,#6c420e,#331900,#971702,#494949' => t('Belgian Chocolate'),
-    '#3f3f3f,#336699,#6598cb,#6598cb,#000000' => t('Bluemarine'),
-    '#d0cb9a,#917803,#efde01,#e6fb2d,#494949' => t('Citrus Blast'),
-    '#0f005c,#434f8c,#4d91ff,#1a1575,#000000' => t('Cold Day'),
-    '#c9c497,#0c7a00,#03961e,#7be000,#494949' => t('Greenbeam'),
-    '#ffe23d,#a9290a,#fc6d1d,#a30f42,#494949' => t('Mediterrano'),
-    '#788597,#3f728d,#a9adbc,#d4d4d4,#707070' => t('Mercury'),
-    '#5b5fa9,#5b5faa,#0a2352,#9fa8d5,#494949' => t('Nocturnal'),
-    '#7db323,#6a9915,#b5d52a,#7db323,#191a19' => t('Olivia'),
-    '#12020b,#1b1a13,#f391c6,#f41063,#898080' => t('Pink Plastic'),
-    '#b7a0ba,#c70000,#a1443a,#f21107,#515d52' => t('Shiny Tomato'),
-    '#18583d,#1b5f42,#34775a,#52bf90,#2d2d2d' => t('Teal Top'),
+    'default' => array(
+      'title' => t('Blue Lagoon (Default)'),
+      'colors' => array(
+        'base' => '#0072b9',
+        'link' => '#027ac6',
+        'top' => '#2385c2',
+        'bottom' => '#5ab5ee',
+        'text' => '#494949',
+      ),
+    ),
+    'ash' => array(
+      'title' => t('Ash'),
+      'colors' => array(
+        'base' => '#464849',
+        'link' => '#2f416f',
+        'top' => '#2a2b2d',
+        'bottom' => '#5d6779',
+      ),
+    ),
+    'aquamarine' => array(
+      'title' => t('Aquamarine'),
+      'colors' => array(
+        'base' => '#55c0e2',
+        'link' => '#000000',
+        'text' => '#696969',
+        'top' => '#085360',
+        'bottom' => '#007e94',
+      ),
+    ),
+    'chocolate' => array(
+      'title' => t('Belgian Chocolate'),
+      'colors' => array(
+        'base' => '#d5b048',
+        'link' => '#6c420e',
+        'top' => '#331900',
+        'bottom' => '#971702',
+      ),
+    ),
+    'bluemarine' => array(
+      'title' => t('Bluemarine'),
+      'colors' => array(
+        'base' => '#3f3f3f',
+        'link' => '#336699',
+        'text' => '#000000',
+        'top' => '#6598cb',
+        'bottom' => '#6598cb',
+      ),
+    ),
+    'citrus' => array(
+      'title' => t('Citrus Blast'),
+      'colors' => array(
+        'base' => '#d0cb9a',
+        'link' => '#917803',
+        'top' => '#efde01',
+        'bottom' => '#e6fb2d',
+      ),
+    ),
+    'cold' => array(
+      'title' => t('Cold Day'),
+      'colors' => array(
+        'base' => '#0f005c',
+        'link' => '#434f8c',
+        'text' => '#000000',
+        'top' => '#4d91ff',
+        'bottom' => '#1a1575',
+      ),
+    ),
+    'greenbeam' => array(
+      'title' => t('Greenbeam'),
+      'colors' => array(
+        'base' => '#c9c497',
+        'link' => '#0c7a00',
+        'top' => '#03961e',
+        'bottom' => '#7be000',
+      ),
+    ),
+    'mediterrano' => array(
+      'title' => t('Mediterrano'),
+      'colors' => array(
+        'base' => '#ffe23d',
+        'link' => '#a9290a',
+        'top' => '#fc6d1d',
+        'bottom' => '#a30f42',
+      ),
+    ),
+    'mercury' => array(
+      'title' => t('Mercury'),
+      'colors' => array(
+        'base' => '#788597',
+        'link' => '#3f728d',
+        'top' => '#a9adbc',
+        'bottom' => '#d4d4d4',
+        'text' => '#707070',
+      ),
+    ),
+    'nocturnal' => array(
+      'title' => t('Nocturnal'),
+      'colors' => array(
+        'base' => '#5b5fa9',
+        'link' => '#5b5faa',
+        'top' => '#0a2352',
+        'bottom' => '#9fa8d5',
+      ),
+    ),
+    'olivia' => array(
+      'title' => t('Olivia'),
+      'colors' => array(
+        'base' => '#7db323',
+        'link' => '#6a9915',
+        'top' => '#b5d52a',
+        'bottom' => '#7db323',
+        'text' => '#191a19',
+      ),
+    ),
+    'pink_plastic' => array(
+      'title' => t('Pink Plastic'),
+      'colors' => array(
+        'base' => '#12020b',
+        'link' => '#1b1a13',
+        'top' => '#f391c6',
+        'bottom' => '#f41063',
+        'text' => '#898080',
+      ),
+    ),
+    'shiny_tomato' => array(
+      'title' => t('Shiny Tomato'),
+      'colors' => array(
+        'base' => '#b7a0ba',
+        'link' => '#c70000',
+        'top' => '#a1443a',
+        'bottom' => '#f21107',
+        'text' => '#515d52',
+      ),
+    ),
+    'teal_top' => array(
+      'title' => t('Teal Top'),
+      'colors' => array(
+        'base' => '#18583d',
+        'link' => '#1b5f42',
+        'top' => '#34775a',
+        'bottom' => '#52bf90',
+        'text' => '#2d2d2d',
+      ),
+    ),
   ),
 
   // Images to copy over.
@@ -35,8 +172,17 @@ $info = array(
     'style.css',
   ),
 
-  // Coordinates of gradient (x, y, width, height).
-  'gradient' => array(0, 37, 760, 121),
+  // Gradient definitions.
+  'gradients' => array(
+    array(
+      // (x, y, width, height).
+      'dimension' => array(0, 38, 760, 121),
+      // Direction of gradient ('vertical' or 'horizontal').
+      'direction' => 'vertical',
+      // Keys of colors to use for the gradient.
+      'colors' => array('top', 'bottom'),
+    ),
+  ),
 
   // Color areas to fill (x, y, width, height).
   'fill' => array(
diff --git a/themes/garland/color/preview.css b/themes/garland/color/preview.css
index 37a4fe6cd9b42d37ca0a1c3185e4e27bb3e382ba..314391e9f0cc2ec5a410ec9bfd3bbcf591364a6d 100644
--- a/themes/garland/color/preview.css
+++ b/themes/garland/color/preview.css
@@ -1,4 +1,4 @@
-/* $Id: preview.css,v 1.2 2010/01/30 07:59:26 dries Exp $ */
+/* $Id: preview.css,v 1.4 2010/04/22 05:18:21 webchick Exp $ */
 
 /* Positioning */
 #preview {
@@ -6,10 +6,10 @@
   max-width: 100%;
 }
 #preview, #preview #img {
-  width: 596px;
+  width: 600px;
   height: 371px;
 }
-#preview #gradient {
+#preview #gradient-0 {
   position: absolute;
   left: 0;
   right: 0;
@@ -28,8 +28,9 @@
 #preview #img {
   position: relative;
   z-index: 3;
+  background-image: url(preview.png);
 }
-#preview #gradient .gradient-line {
+#preview #gradient-0 .gradient-line {
   height: 10px;
   overflow: hidden;
 }
diff --git a/themes/garland/fix-ie-rtl.css b/themes/garland/fix-ie-rtl.css
index 9d784e6e4440876748ad39ae6b92b750d4cec737..665af8e99c90512e23ffab81cd4e59cd6b79f679 100644
--- a/themes/garland/fix-ie-rtl.css
+++ b/themes/garland/fix-ie-rtl.css
@@ -1,4 +1,4 @@
-/* $Id: fix-ie-rtl.css,v 1.4 2008/03/13 20:02:18 dries Exp $ */
+/* $Id: fix-ie-rtl.css,v 1.5 2010/03/31 20:24:13 dries Exp $ */
 
 body {
   /* Center layout */
@@ -36,7 +36,6 @@ fieldset {
 
 /* Prevent fieldsets from shifting when changing collapsed state. */
 html.js fieldset.collapsible {
-  position: relative;
   top: -1em;
 }
 
diff --git a/themes/garland/fix-ie.css b/themes/garland/fix-ie.css
index b0e14d66e595070d95790098a02a1c4a1ade7b17..865efed6faae6f92dad99b7dcef160e95f32d2ab 100644
--- a/themes/garland/fix-ie.css
+++ b/themes/garland/fix-ie.css
@@ -1,4 +1,4 @@
-/* $Id: fix-ie.css,v 1.11 2009/04/11 22:19:46 webchick Exp $ */
+/* $Id: fix-ie.css,v 1.12 2010/03/31 20:24:13 dries Exp $ */
 
 body {
   /* Center layout */
@@ -37,7 +37,6 @@ ul.primary {
 
 /* Prevent fieldsets from shifting when changing collapsed state. */
 html.js fieldset.collapsible {
-  position: relative;
   top: -1em;
 }
 html.js fieldset.collapsed {
diff --git a/themes/garland/garland.info b/themes/garland/garland.info
index 568c5b9bce8fe260a599a7bf0fdd7770e2646cce..85de5721ab13a1b4565b6491d3cfc79ab4f1c316 100644
--- a/themes/garland/garland.info
+++ b/themes/garland/garland.info
@@ -9,8 +9,8 @@ stylesheets[all][] = style.css
 stylesheets[print][] = print.css
 settings[garland_width] = fluid
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/themes/garland/node.tpl.php b/themes/garland/node.tpl.php
index 6f4a1478e238a40b28b3443ffad5f74d5aa90588..8c54eee6e31163d4c3694ebdf0063b61f9eaae55 100644
--- a/themes/garland/node.tpl.php
+++ b/themes/garland/node.tpl.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: node.tpl.php,v 1.21 2010/01/04 03:57:19 webchick Exp $
+// $Id: node.tpl.php,v 1.23 2010/04/08 18:26:42 dries Exp $
 ?>
 <div id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
 
@@ -7,7 +7,7 @@
 
   <?php print render($title_prefix); ?>
   <?php if (!$page): ?>
-    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $node_title; ?></a></h2>
+    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>
   <?php endif; ?>
   <?php print render($title_suffix); ?>
 
@@ -25,12 +25,6 @@
   </div>
 
   <div class="clearfix">
-    <?php if (!empty($content['links']['terms'])): ?>
-      <div class="meta">
-        <div class="terms"><?php print render($content['links']['terms']); ?></div>
-      </div>
-    <?php endif; ?>
-
     <?php if (!empty($content['links'])): ?>
       <div class="links"><?php print render($content['links']); ?></div>
     <?php endif; ?>
diff --git a/themes/garland/style-rtl.css b/themes/garland/style-rtl.css
index 53dcab1a619e6a1c0c92b06cbca150e895c6f06b..9d1e8dff679d581fe9781c48800f9d5791170944 100644
--- a/themes/garland/style-rtl.css
+++ b/themes/garland/style-rtl.css
@@ -1,4 +1,4 @@
-/* $Id: style-rtl.css,v 1.18 2010/03/03 19:46:26 dries Exp $ */
+/* $Id: style-rtl.css,v 1.19 2010/04/08 18:26:42 dries Exp $ */
 
 html {
   direction: rtl;
@@ -200,12 +200,6 @@ ul.links li, ul.inline li {
   text-align: right;
 }
 
-.node .links ul.links li, .comment .links ul.links li {}
-.terms ul.links li {
-  padding-right: 1em;
-  padding-left: 0;
-}
-
 .user-picture,
 .comment .submitted {
   padding-left: 0;
@@ -218,10 +212,6 @@ ul.links li, ul.inline li {
   float: left;
 }
 
-.terms {
-  float: left;
-}
-
 .indented {
   margin-left: 0;
   margin-right: 25px;
diff --git a/themes/garland/style.css b/themes/garland/style.css
index c4e94fac9dc394425aa3739296b5c9b0734b9f9a..4b7c79badb5456510f55311deda1d38da5ca2c93 100644
--- a/themes/garland/style.css
+++ b/themes/garland/style.css
@@ -1,4 +1,4 @@
-/* $Id: style.css,v 1.75 2010/03/10 20:32:56 webchick Exp $ */
+/* $Id: style.css,v 1.78 2010/04/08 18:26:42 dries Exp $ */
 
 /**
  * Generic elements
@@ -234,7 +234,7 @@ span.form-required {
   color: #ffae00;
 }
 
-span.submitted, .description, .vertical-tab-button .summary {
+.submitted, .description, .vertical-tab-button .summary {
   font-size: 0.92em;
   color: #898989;
 }
@@ -254,7 +254,7 @@ span.submitted, .description, .vertical-tab-button .summary {
   padding: .5em 1em;
 }
 
-.messages {
+div.messages {
   margin: .75em 0 .75em;
   padding: .1em .5em .15em;
 }
@@ -723,14 +723,6 @@ ul.links li, ul.inline li {
   text-align: left; /* LTR */
 }
 
-.node .links ul.links li, .comment .links ul.links li {}
-.terms ul.links li {
-  margin-left: 0;
-  margin-right: 0;
-  padding-right: 0;
-  padding-left: 1em;
-}
-
 .user-picture,
 .comment .submitted {
   float: right; /* LTR */
@@ -745,10 +737,6 @@ ul.links li, ul.inline li {
   float: right; /* LTR */
 }
 
-.terms {
-  float: right; /* LTR */
-}
-
 .preview .node, .preview .comment, .node-sticky {
   margin: 0;
   padding: 0.5em 0;
@@ -910,7 +898,7 @@ html.js fieldset.collapsed .fieldset-legend {
   background: url(images/menu-collapsed.gif) no-repeat 0% 50%; /* LTR */
 }
 
-html.js fieldset.collapsible legend span.summary {
+.fieldset-legend span.summary {
   color: #898989;
 }
 
@@ -1096,7 +1084,7 @@ tr.taxonomy-term-divider-bottom {
 /**
  * Generic elements.
  */
-.messages {
+div.messages {
   background-color: #fff;
   border: 1px solid #b8d3e5;
 }
@@ -1107,8 +1095,13 @@ tr.taxonomy-term-divider-bottom {
 }
 
 div.status {
-  color: #33a333;
+  background-color: #fff;
   border-color: #c7f2c8;
+  color: #33a333;
+}
+
+div.error {
+  border: 1px solid #d77;
 }
 
 div.error, tr.error {
@@ -1116,6 +1109,12 @@ div.error, tr.error {
   background-color: #FFCCCC;
 }
 
+div.warning {
+  background-color: #ffd;
+  border: 1px solid #f0c020;
+  color: #220;
+}
+
 .form-item input.error, .form-item textarea.error {
   border: 1px solid #c52020;
   color: #363636;
diff --git a/themes/seven/ie.css b/themes/seven/ie.css
index 9479c7d922c4afcaf4fb375d888c8811e4b6d3d0..39a17e088aea1e85498c70b90c3fa4d61275c226 100644
--- a/themes/seven/ie.css
+++ b/themes/seven/ie.css
@@ -1,4 +1,4 @@
-/* $Id: ie.css,v 1.2 2010/03/07 06:31:48 webchick Exp $ */
+/* $Id: ie.css,v 1.3 2010/04/12 17:33:35 webchick Exp $ */
 
 /* IE7 renders legends in nested fieldsets without a width. */
 fieldset legend {
@@ -10,11 +10,3 @@ fieldset .fieldset-legend {
   left: 0;
   top: 0;
 }
-
-/**
- * Fixes an issue in IE6/IE7 where links in table headers and table row
- * background colors do not appear.
- */
-tr {
-  position: relative;
-}
diff --git a/themes/seven/reset.css b/themes/seven/reset.css
index a37532e87000063c81675113536f117b119d760b..c2d04a47d7989ca244aa3bbac9e14983bc050940 100644
--- a/themes/seven/reset.css
+++ b/themes/seven/reset.css
@@ -1,4 +1,4 @@
-/* $Id: reset.css,v 1.7 2010/03/10 19:31:17 webchick Exp $ */
+/* $Id: reset.css,v 1.8 2010/04/12 17:33:35 webchick Exp $ */
 
 /**
  * Reset CSS styles.
@@ -131,7 +131,6 @@ ul.secondary a.active,
   border: 0;
   font-size: 100%;
   vertical-align: baseline;
-  background: transparent;
   line-height: inherit;
 }
 /* Drupal: system-menus.css */
diff --git a/themes/seven/seven.info b/themes/seven/seven.info
index 8e57e45128de111556602f0e2e8d34d7627a0481..81afb59956cd09375ac42556b116b880874ecd46 100644
--- a/themes/seven/seven.info
+++ b/themes/seven/seven.info
@@ -15,8 +15,8 @@ regions[page_bottom] = Page bottom
 regions[sidebar_first] = First sidebar
 regions_hidden[] = sidebar_first
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/themes/seven/style.css b/themes/seven/style.css
index ae70337256c6d17d1ff797143dd045ada4bbb3df..709c8ed454235cc995903db5a6e35cecadddcd8b 100644
--- a/themes/seven/style.css
+++ b/themes/seven/style.css
@@ -1,4 +1,4 @@
-/* $Id: style.css,v 1.49 2010/03/20 14:45:05 dries Exp $ */
+/* $Id: style.css,v 1.52 2010/04/26 13:33:43 dries Exp $ */
 
 /**
  * Generic elements.
@@ -293,7 +293,8 @@ ul.primary li a,
 ul.primary li a.active,
 ul.primary li a:active,
 ul.primary li a:visited,
-ul.primary li a:hover {
+ul.primary li a:hover,
+ul.primary li.active a {
   background-color: #a6a7a2;
   color: #000;
   font-weight: bold;
@@ -480,10 +481,15 @@ tr.even, tr.odd {
   border-color: #bebfb9;
   background: #f3f4ee;
 }
-
 tr.odd {
   background: #fff;
 }
+tr.drag {
+  background: #fe7;
+}
+tr.drag-previous {
+  background: #ffb;
+}
 table th {
   font-size: 12px;
   text-transform: uppercase;
@@ -558,7 +564,6 @@ fieldset.collapsed {
 html.js fieldset.collapsed {
   border-width: 1px;
   height: auto;
-  margin-bottom: 10px;
 }
 fieldset fieldset {
   background-color: #fff;
@@ -566,9 +571,6 @@ fieldset fieldset {
 fieldset fieldset fieldset {
   background-color: #f8f8f8;
 }
-html.js fieldset.collapsible .fieldset-wrapper {
-  overflow: visible;
-}
 
 /**
  * Form elements.
diff --git a/themes/seven/template.php b/themes/seven/template.php
index 03d16b53735f0086c9d9e46a9473fca4a1710c7d..1b86efdf418dbd86c1f047e551748ac919a6ea9e 100644
--- a/themes/seven/template.php
+++ b/themes/seven/template.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: template.php,v 1.14 2010/03/03 19:46:26 dries Exp $
+// $Id: template.php,v 1.16 2010/04/21 06:55:23 webchick Exp $
 
 /**
  * Override or insert variables into the html template.
@@ -52,7 +52,7 @@ function seven_admin_block_content($variables) {
       $output .= '<li class="leaf">';
       $output .= l($item['title'], $item['href'], $item['localized_options']);
       if (!system_admin_compact_mode()) {
-        $output .= '<div class="description">' . $item['description'] . '</div>';
+        $output .= '<div class="description">' . filter_xss_admin($item['description']) . '</div>';
       }
       $output .= '</li>';
     }
@@ -69,7 +69,7 @@ function seven_admin_block_content($variables) {
 function seven_tablesort_indicator($variables) {
   $style = $variables['style'];
   $theme_path = drupal_get_path('theme', 'seven');
-  if ($style == "asc") {
+  if ($style == 'asc') {
     return theme('image', array('path' => $theme_path . '/images/arrow-asc.png', 'alt' => t('sort ascending'), 'title' => t('sort ascending')));
   }
   else {
diff --git a/themes/stark/stark.info b/themes/stark/stark.info
index 05bf7fb02482ddd84fb20360e40fc3a80f2f2ee0..fe6b2a0433247472f802a5c6f881f46de4a9a66e 100644
--- a/themes/stark/stark.info
+++ b/themes/stark/stark.info
@@ -7,8 +7,8 @@ core = 7.x
 engine = phptemplate
 stylesheets[all][] = layout.css
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/themes/tests/test_theme/test_theme.info b/themes/tests/test_theme/test_theme.info
index 4747533b6f3e2b340389f797ab83ccdb2781e133..78a9ddcb8a4dd25cb96d382aa4bf1967fedc0747 100644
--- a/themes/tests/test_theme/test_theme.info
+++ b/themes/tests/test_theme/test_theme.info
@@ -4,8 +4,8 @@ description = Theme for testing the theme system
 core = 7.x
 engine = phptemplate
 hidden = TRUE
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/themes/tests/update_test_basetheme/update_test_basetheme.info b/themes/tests/update_test_basetheme/update_test_basetheme.info
index d2611167b75af5dfe0e3159d1903500e1bafe841..6ce7e183b651d6b06774b5b75c20d7550e31ac92 100644
--- a/themes/tests/update_test_basetheme/update_test_basetheme.info
+++ b/themes/tests/update_test_basetheme/update_test_basetheme.info
@@ -5,8 +5,8 @@ core = 7.x
 engine = phptemplate
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/themes/tests/update_test_subtheme/update_test_subtheme.info b/themes/tests/update_test_subtheme/update_test_subtheme.info
index e8ebe7811e26deef8af73d76d8d9fee194a4ce04..636987892579d7f5b0a122cc5702f9a11a1e4198 100644
--- a/themes/tests/update_test_subtheme/update_test_subtheme.info
+++ b/themes/tests/update_test_subtheme/update_test_subtheme.info
@@ -6,8 +6,8 @@ engine = phptemplate
 base theme = update_test_basetheme
 hidden = TRUE
 
-; Information added by drupal.org packaging script on 2010-03-21
-version = "7.0-alpha3"
+; Information added by drupal.org packaging script on 2010-04-26
+version = "7.0-alpha4"
 project = "drupal"
-datestamp = "1269192313"
+datestamp = "1272318008"
 
diff --git a/update.php b/update.php
index a6c522ee972def91f9d2e93af55f4e50eb7987d6..5277061427e4e9232095b3bb4f11e97d7e5d51a2 100644
--- a/update.php
+++ b/update.php
@@ -1,5 +1,5 @@
 <?php
-// $Id: update.php,v 1.317 2010/03/06 06:31:23 dries Exp $
+// $Id: update.php,v 1.320 2010/04/24 14:49:13 dries Exp $
 
 /**
  * Root directory of Drupal installation.
@@ -21,16 +21,17 @@ define('DRUPAL_ROOT', getcwd());
  */
 
 /**
- * Global flag to identify update.php run, and so avoid various unwanted
- * operations, such as hook_init() and hook_exit() invokes, css/js preprocessing
- * and translation, and solve some theming issues. This flag is checked on several
- * places in Drupal code (not just update.php).
+ * Global flag indicating that update.php is being run.
+ *
+ * When this flag is set, various operations do not take place, such as invoking
+ * hook_init() and hook_exit(), css/js preprocessing, and translation.
  */
 define('MAINTENANCE_MODE', 'update');
 
 function update_selection_page() {
   drupal_set_title('Drupal database update');
-  $output = drupal_render(drupal_get_form('update_script_selection_form'));
+  $elements = drupal_get_form('update_script_selection_form');
+  $output = drupal_render($elements);
 
   update_task_list('select');
 
@@ -47,7 +48,7 @@ function update_script_selection_form($form, &$form_state) {
     '#collapsible' => TRUE,
   );
 
-  // Ensure system.module's updates appear first
+  // Ensure system.module's updates appear first.
   $form['start']['system'] = array();
 
   $updates = update_get_update_list();
@@ -128,7 +129,8 @@ function update_script_selection_form($form, &$form_state) {
       '#type' => 'hidden',
       '#default_value' => FALSE,
     );
-    $form['submit'] = array(
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
       '#type' => 'submit',
       '#value' => 'Apply pending updates',
     );
@@ -137,7 +139,8 @@ function update_script_selection_form($form, &$form_state) {
 }
 
 function update_helpful_links() {
-  // NOTE: we can't use l() here because the URL would point to 'update.php?q=admin'.
+  // NOTE: we can't use l() here because the URL would point to
+  // 'update.php?q=admin'.
   $links[] = '<a href="' . base_path() . '">Front page</a>';
   $links[] = '<a href="' . base_path() . '?q=admin">Administration pages</a>';
   return $links;
@@ -148,7 +151,7 @@ function update_results_page() {
   $links = update_helpful_links();
 
   update_task_list();
-  // Report end result
+  // Report end result.
   if (module_exists('dblog')) {
     $log_message = ' All errors have been <a href="' . base_path() . '?q=admin/reports/dblog">logged</a>.';
   }
@@ -174,7 +177,7 @@ function update_results_page() {
 
   $output .= theme('item_list', array('items' => $links));
 
-  // Output a list of queries executed
+  // Output a list of queries executed.
   if (!empty($_SESSION['update_results'])) {
     $output .= '<div id="update-results">';
     $output .= '<h2>The following updates returned messages</h2>';
@@ -397,7 +400,7 @@ if (update_access_allowed()) {
 
   $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
   switch ($op) {
-    // update.php ops
+    // update.php ops.
 
     case 'selection':
       if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) {
@@ -419,7 +422,7 @@ if (update_access_allowed()) {
       $output = update_results_page();
       break;
 
-    // Regular batch ops : defer to batch processing API
+    // Regular batch ops : defer to batch processing API.
     default:
       update_task_list('run');
       $output = _batch_page();
diff --git a/web.config b/web.config
new file mode 100644
index 0000000000000000000000000000000000000000..d97e8953106eeab048c85696d172a91ddfe91664
--- /dev/null
+++ b/web.config
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+  <system.webServer>
+  <!-- Don't show directory listings for URLs which map to a directory. -->
+    <directoryBrowse enabled="false" />
+    <rewrite>
+      <rules>
+        <rule name="Protect files and directories from prying eyes" stopProcessing="true">
+          <match url="\.(engine|inc|info|install|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl|svn-base)$|^(\..*|Entries.*|Repository|Root|Tag|Template)$" />
+          <action type="CustomResponse" statusCode="403" subStatusCode="0" statusReason="Forbidden" statusDescription="Access is forbidden." />
+        </rule>
+        <rule name="Force simple error message for requests for non-existent favicon.ico" stopProcessing="true">
+          <match url="favicon\.ico" />
+          <action type="CustomResponse" statusCode="404" subStatusCode="1" statusReason="File Not Found" statusDescription="The requested file favicon.ico was not found" />
+        </rule>
+        <!-- Rewrite URLs of the form 'x' to the form 'index.php?q=x'. -->
+        <rule name="Short URLS" stopProcessing="true">
+          <match url="^(.*)$" ignoreCase="false" />
+          <conditions>
+            <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
+            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+            <add input="{URL}" pattern="^/favicon.ico$" ignoreCase="false" negate="true" />
+          </conditions>
+          <action type="Rewrite" url="index.php?q={R:1}" appendQueryString="true" />
+        </rule>
+      </rules>
+    </rewrite>
+
+    <httpErrors>
+      <remove statusCode="404" subStatusCode="-1" />
+      <error statusCode="404" prefixLanguageFilePath="" path="/index.php" responseMode="ExecuteURL" />
+    </httpErrors>
+
+    <defaultDocument>
+     <!-- Set the default document -->
+      <files>
+        <remove value="index.php" />
+        <add value="index.php" />
+      </files>
+    </defaultDocument>
+  </system.webServer>
+</configuration>
+