Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1223
diff -u -p -r1.1223 common.inc
--- includes/common.inc	22 Sep 2010 03:24:09 -0000	1.1223
+++ includes/common.inc	23 Sep 2010 00:27:06 -0000
@@ -2041,15 +2041,22 @@ function format_username($account) {
  *     Drupal on a web server that cannot be configured to automatically find
  *     index.php, then hook_url_outbound_alter() can be implemented to force
  *     this value to 'index.php'.
- *   - 'entity_type': The entity type of the object that called url(). Only set if
- *     url() is invoked by entity_uri().
+ *   - 'entity_type': The entity type (such as 'node') of the object for which
+ *     the URL is being generated.
  *   - 'entity': The entity object (such as a node) for which the URL is being
- *     generated. Only set if url() is invoked by entity_uri().
+ *     generated.
  *
  * @return
  *   A string containing a URL to the given path.
  */
 function url($path = NULL, array $options = array()) {
+  // Use the advanced drupal_static() pattern, since this is called very often.
+  static $drupal_static_fast;
+  if (!isset($drupal_static_fast)) {
+    $drupal_static_fast['entity_base_paths'] = &drupal_static(__FUNCTION__);
+  }
+  $entity_base_paths = &$drupal_static_fast['entity_base_paths'];
+
   // Merge in defaults.
   $options += array(
     'fragment' => '',
@@ -2069,6 +2076,84 @@ function url($path = NULL, array $option
     $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path);
   }
 
+  // A lot of Drupal code (core as well as contributed modules and themes) call
+  // url('node/' . $node->nid) to generate a URL to a node, and similarly for
+  // other entity types such as comments and taxonomy terms. When url() is
+  // invoked in this way, $options['entity_type'] and $options['entity'] are not
+  // set, and the entity type's or bundle's 'uri callback' does not have a
+  // chance to participate in determining $path and $options prior to this
+  // point. However, there are many use-cases for modules to alter an entity's
+  // URL. For example, the Forum module alters the path from 'taxonomy/term/ID'
+  // to 'forum/ID' for taxonomy terms that represent forums. The most performant
+  // way for a module to achieve this is to use hook_entity_info_alter() to
+  // alter an entity type's or bundle's 'uri callback' (see
+  // forum_entity_info_alter() for an example). Modules may alternatively or
+  // additionally implement hook_url_outbound_alter() and have conditional logic
+  // based on $options['entity_type'] and $options['entity'], a technique which
+  // is usually slower, unless the module requires the overhead of a
+  // hook_url_outbound_alter() implementation anyway (for example, if the module
+  // also alters non-entity URLs). However, both techniques rely on entity_uri()
+  // being called to generate the $path and $options used by url(). Therefore,
+  // unless the caller ensures that entity information is passed to url(), we
+  // inspect the path to determine if it's a path to an entity, and if so, call
+  // entity_uri() ourselves to ensure we're using the correct $path and $options
+  // for the entity. Since this code has some overhead, for optimal performance,
+  // modules that generate many entity links on a page are encouraged to call
+  // entity_uri() themselves.
+  // @todo Remove this in Drupal 8, and instead require modules to standardize
+  //   on an API that supports flexible entity URLs. See
+  //   http://drupal.org/node/823428.
+  if (!$options['external'] && !isset($options['entity_type'])) {
+    // Once per page request, collect all the base paths for entity pages.
+    if (!isset($entity_base_paths)) {
+      $entity_base_paths = array();
+      foreach (entity_get_info() as $type => $info) {
+        foreach ($info['base paths'] as $entity_base_path) {
+          $entity_base_paths[$entity_base_path] = $type;
+        }
+      }
+    }
+    // For fastest execution, we assume a path is to an entity only if it is a
+    // registered entity base path followed by '/' followed by a number, and we
+    // try to exit this code block as quickly as possible otherwise. For
+    // example, 'node/2', 'taxonomy/term/2', and 'forum/2' are all treated as
+    // entity paths, but 'admin', 'node/add', 'node/2/edit', and 'node/2/view'
+    // are not.
+    // @todo For Drupal 8, the entity system should support 'node/2/edit' being
+    //   treated as an entity related path.
+    $slash_position = strrpos($path, '/');
+    if ($slash_position !== FALSE) {
+      $possible_entity_base_path = substr($path, 0, $slash_position);
+      if (isset($entity_base_paths[$possible_entity_base_path])) {
+        $possible_entity_id = substr($path, $slash_position + 1);
+        if (is_numeric($possible_entity_id)) {
+          $entity_type = $entity_base_paths[$possible_entity_base_path];
+          $entity_id = $possible_entity_id;
+          // Avoid infinite recursion; do not try to load an entity if it is
+          // currently in the process of being loaded. Modules that call url()
+          // for an entity during its loading process should call entity_uri()
+          // explicitly for generating the path and options arguments, in order
+          // to ensure compatibility with modules that alter entity URLs.
+          if (!entity_is_loading($entity_type, $entity_id)) {
+            $entities = entity_load($entity_type, array($entity_id));
+            $entity = $entities[$entity_id];
+            // If the entity has a URI (which it should if url() is being called
+            // for it), then override the path and options passed in with the
+            // actual values required for the entity. However, retain options
+            // that aren't in conflict with what the entity requires.
+            if ($actual_path_and_options = entity_uri($entity_type, $entity)) {
+              $path = $actual_path_and_options['path'];
+              if (isset($actual_path_and_options['options']['query'])) {
+                $actual_path_and_options['options']['query'] += $options['query'];
+              }
+              $options = $actual_path_and_options['options'] + $options;
+            }
+          }
+        }
+      }
+    }
+  }
+
   // Preserve the original path before altering or aliasing.
   $original_path = $path;
 
@@ -6701,6 +6786,7 @@ function entity_get_info($entity_type = 
           'view modes' => array(),
           'entity keys' => array(),
           'translation' => array(),
+          'base paths' => array(),
         );
         $entity_info[$name]['entity keys'] += array(
           'revision' => '',
@@ -6746,6 +6832,8 @@ function entity_info_cache_clear() {
   drupal_static_reset('entity_get_info');
   // Clear all languages.
   cache_clear_all('entity_info:', 'cache', TRUE);
+  // Clear url()'s cache of entity base paths.
+  drupal_static_reset('url');
 }
 
 /**
@@ -6841,6 +6929,14 @@ function entity_load($entity_type, $ids 
 }
 
 /**
+ * Determines if an entity is loading. Use this to avoid race conditions or
+ * infinite recursions.
+ */
+function entity_is_loading($entity_type, $entity_id) {
+  return entity_get_controller($entity_type)->isLoading($entity_type, $entity_id);
+}
+
+/**
  * Get the entity controller class for an entity type.
  */
 function entity_get_controller($entity_type) {
@@ -6894,56 +6990,66 @@ function entity_prepare_view($entity_typ
 }
 
 /**
- * Returns the uri elements of an entity.
+ * Returns the 'path' and 'options' of an entity for passing to url().
  *
  * @param $entity_type
  *   The entity type; e.g. 'node' or 'user'.
  * @param $entity
- *   The entity for which to generate a path.
+ *   The entity for which to generate a URL.
  * @return
- *   An array containing the 'path' and 'options' keys used to build the uri of
+ *   An array containing the 'path' and 'options' keys used to build the URL of
  *   the entity, and matching the signature of url(). NULL if the entity has no
- *   uri of its own.
+ *   URL of its own.
+ *
+ * @todo The function name entity_uri() does not properly convey what the
+ *   function returns. This function does not return a URI, but rather, the path
+ *   and options arguments used by the url() function to generate a URL. For
+ *   Drupal 8, change either the function's name or its signature so that the
+ *   two are consistent.
  */
 function entity_uri($entity_type, $entity) {
-  // 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)) {
+  // This check enables the path and options 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. The check is entity type
+  // specific, because in some cases, code uses an entity object of one type as
+  // a mock entity of another type (for example, node and comment entities are
+  // passed to theme('username') which treats the passed entity as a mock user
+  // entity).
+  if (!isset($entity->_url_arguments[$entity_type])) {
     $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'];
+      $callback = $info['bundles'][$bundle]['uri callback'];
     }
     elseif (isset($info['uri callback'])) {
-      $uri_callback = $info['uri callback'];
+      $callback = $info['uri callback'];
     }
     else {
-      $uri_callback = NULL;
+      $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();
+    // Invoke the callback to get the path and options. If there is no callback,
+    // set the entity's '_url_arguments' property to FALSE to indicate that it
+    // is known to not have a URL.
+    if (isset($callback) && function_exists($callback)) {
+      $entity->_url_arguments[$entity_type] = $callback($entity);
+      if (!isset($entity->_url_arguments[$entity_type]['options'])) {
+        $entity->_url_arguments[$entity_type]['options'] = array();
       }
       // Pass the entity data to url() so that alter functions do not need to
       // lookup this entity again.
-      $entity->uri['options']['entity_type'] = $entity_type;
-      $entity->uri['options']['entity'] = $entity;
+      $entity->_url_arguments[$entity_type]['options']['entity_type'] = $entity_type;
+      $entity->_url_arguments[$entity_type]['options']['entity'] = $entity;
     }
     else {
-      $entity->uri = FALSE;
+      $entity->_url_arguments[$entity_type] = FALSE;
     }
   }
-  return $entity->uri ? $entity->uri : NULL;
+  return $entity->_url_arguments[$entity_type] ? $entity->_url_arguments[$entity_type] : NULL;
 }
 
 /**
Index: includes/entity.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/entity.inc,v
retrieving revision 1.14
diff -u -p -r1.14 entity.inc
--- includes/entity.inc	15 Sep 2010 04:34:26 -0000	1.14
+++ includes/entity.inc	23 Sep 2010 00:27:06 -0000
@@ -39,6 +39,12 @@ interface DrupalEntityControllerInterfac
    *   An array of entity objects indexed by their ids.
    */
   public function load($ids = array(), $conditions = array());
+
+  /**
+   * Determines if an entity is loading. Use this to avoid race conditions or
+   * infinite recursions.
+   */
+  public function isLoading($entity_type, $entity_id);
 }
 
 /**
@@ -114,6 +120,13 @@ class DrupalDefaultEntityController impl
   protected $cache;
 
   /**
+   * Stores a record of which entities are currently being loaded.
+   *
+   * @var array
+   */
+  protected $isLoadingCache;
+
+  /**
    * Constructor: sets basic variables.
    */
   public function __construct($entityType) {
@@ -121,6 +134,7 @@ class DrupalDefaultEntityController impl
     $this->entityInfo = entity_get_info($entityType);
     $this->entityCache = array();
     $this->hookLoadArguments = array();
+    $this->isLoadingCache = array();
     $this->idKey = $this->entityInfo['entity keys']['id'];
 
     // Check if the entity type supports revisions.
@@ -190,8 +204,14 @@ class DrupalDefaultEntityController impl
     // which attaches fields (if supported by the entity type) and calls the
     // entity type specific load callback, for example hook_node_load().
     if (!empty($queried_entities)) {
+      foreach (array_keys($queried_entities) as $entity_id) {
+        $this->isLoadingCache[$this->entityType][$entity_id] = TRUE;
+      }
       $this->attachLoad($queried_entities, $revision_id);
       $entities += $queried_entities;
+      foreach (array_keys($queried_entities) as $entity_id) {
+        unset($this->isLoadingCache[$this->entityType][$entity_id]);
+      }
     }
 
     if ($this->cache) {
@@ -216,6 +236,13 @@ class DrupalDefaultEntityController impl
   }
 
   /**
+   * Implements DrupalEntityControllerInterface::isLoading().
+   */
+  public function isLoading($entity_type, $entity_id) {
+    return isset($this->isLoadingCache[$entity_type][$entity_id]);
+  }
+
+  /**
    * Builds the query to load the entity.
    *
    * This has full revision support. For entities requiring special queries,
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.898
diff -u -p -r1.898 comment.module
--- modules/comment/comment.module	13 Sep 2010 05:52:18 -0000	1.898
+++ modules/comment/comment.module	23 Sep 2010 00:27:07 -0000
@@ -97,6 +97,7 @@ function comment_entity_info() {
     'comment' => array(
       'label' => t('Comment'),
       'base table' => 'comment',
+      'base paths' => array('comment'),
       'uri callback' => 'comment_uri',
       'fieldable' => TRUE,
       'controller class' => 'CommentController',
Index: modules/forum/forum.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/forum/forum.module,v
retrieving revision 1.575
diff -u -p -r1.575 forum.module
--- modules/forum/forum.module	7 Sep 2010 23:39:08 -0000	1.575
+++ modules/forum/forum.module	23 Sep 2010 00:27:07 -0000
@@ -221,6 +221,10 @@ function forum_menu_local_tasks_alter(&$
  * Implements hook_entity_info_alter().
  */
 function forum_entity_info_alter(&$info) {
+  // Register 'forum' as a base path for taxonomy terms, so that url() can
+  // ensure that all URLs for forums are treated as entity URLs.
+  $info['taxonomy_term']['base paths'][] = 'forum';
+
   // 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
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1301
diff -u -p -r1.1301 node.module
--- modules/node/node.module	18 Sep 2010 01:39:33 -0000	1.1301
+++ modules/node/node.module	23 Sep 2010 00:27:07 -0000
@@ -170,6 +170,7 @@ function node_entity_info() {
       'label' => t('Node'),
       'controller class' => 'NodeController',
       'base table' => 'node',
+      'base paths' => array('node'),
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
       'fieldable' => TRUE,
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.193
diff -u -p -r1.193 system.api.php
--- modules/system/system.api.php	11 Sep 2010 14:35:13 -0000	1.193
+++ modules/system/system.api.php	23 Sep 2010 00:27:07 -0000
@@ -76,6 +76,10 @@ function hook_hook_info_alter(&$hooks) {
  *     Leave blank to use the DrupalDefaultEntityController implementation.
  *   - base table: (used by DrupalDefaultEntityController) The name of the
  *     entity type's base table.
+ *   - base paths: (used by url()) An array of Drupal paths that can be used by
+ *     url() to determine if a given path is to an entity of this type. For
+ *     example, node paths are usually of the form 'node/ID', so
+ *     node_entity_info() sets 'base paths' to array('node').
  *   - 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
@@ -176,6 +180,7 @@ function hook_entity_info() {
       'label' => t('Node'),
       'controller class' => 'NodeController',
       'base table' => 'node',
+      'base paths' => array('node'),
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
       'fieldable' => TRUE,
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.605
diff -u -p -r1.605 taxonomy.module
--- modules/taxonomy/taxonomy.module	11 Sep 2010 06:03:12 -0000	1.605
+++ modules/taxonomy/taxonomy.module	23 Sep 2010 00:27:08 -0000
@@ -95,6 +95,7 @@ function taxonomy_entity_info() {
       'label' => t('Taxonomy term'),
       'controller class' => 'TaxonomyTermController',
       'base table' => 'taxonomy_term_data',
+      'base paths' => array('taxonomy/term'),
       'uri callback' => 'taxonomy_term_uri',
       'fieldable' => TRUE,
       'entity keys' => array(
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.1203
diff -u -p -r1.1203 user.module
--- modules/user/user.module	22 Sep 2010 03:24:09 -0000	1.1203
+++ modules/user/user.module	23 Sep 2010 00:27:08 -0000
@@ -141,6 +141,7 @@ function user_entity_info() {
       'label' => t('User'),
       'controller class' => 'UserController',
       'base table' => 'users',
+      'base paths' => array('user'),
       'uri callback' => 'user_uri',
       'label callback' => 'format_username',
       'fieldable' => TRUE,
