=== modified file 'includes/common.inc'
--- includes/common.inc	2010-08-20 01:17:51 +0000
+++ includes/common.inc	2010-08-22 10:45:59 +0000
@@ -2032,13 +2032,29 @@
  *     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().
+ *     @todo Document that this is usually handled internally.
  *   - 'entity': The entity object (such as a node) for which the URL is being
  *     generated. Only set if url() is invoked by entity_uri().
+ *     @todo Document that this is usually handled internally.
  *
  * @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_altered_root_path_list'] = &drupal_static(__FUNCTION__);
+  }
+  $entity_altered_root_path_list = &$drupal_static_fast['entity_altered_root_path_list'];
+
+  // The first time this function is called, populate a list of entity-related
+  // root paths we will need to search for, in order to properly link to
+  // entities.
+  if (!isset($entity_altered_root_path_list)) {
+    $entity_altered_root_path_list = variable_get('entity_altered_root_path_list', array());
+  }
+
   // Merge in defaults.
   $options += array(
     'fragment' => '',
@@ -2058,6 +2074,58 @@
     $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path);
   }
 
+  // If there are modules enabled which alter entity URIs, we need to check
+  // this path against that list and possibly rewrite it. If no modules did
+  // this, or if the path was passed through entity_uri() already, there is no
+  // need to go any further.
+  if (!empty($entity_altered_root_path_list) && !isset($options['entity_type'])) {
+    // First do a quick check to see if the provided path is on our list; we
+    // only need to invoke the entity code if it is. We do this by stripping
+    // off the last part of the path and checking the rest. For example, if the
+    // path is 'taxonomy/term/2' we want to check if 'taxonomy/term' is on the
+    // list of altered root paths.
+    $slash_position = strrpos($path, '/');
+    if ($slash_position !== FALSE) {
+      $root_path = substr($path, 0, $slash_position);
+      if (isset($entity_altered_root_path_list[$root_path])) {
+        // Load the entity by ID. In most cases, it will already have been
+        // loaded (an exception is if it was stored as a menu link in the
+        // database), so as long as the entity allows static caching this
+        // should be fairly quick. We also make sure the last part of the path
+        // is actually a numeric ID before proceeding, since we don't want to
+        // trigger this on a hypothetical path like 'taxonomy/term/admin'.
+        $entity_id = substr($path, $slash_position + 1);
+        if (is_numeric($entity_id)) {
+          $entity_type = $entity_altered_root_path_list[$root_path];
+          // Avoid infinite recursion; do not try to load an entity if it is
+          // currently in the process of being loaded. In that case, we
+          // wouldn't be able to generate a correct altered URL anyway, since
+          // entity_uri() requires a fully-loaded entity as input. So the only
+          // thing we can do here is stick with the original, passed-in URL.
+          if (!entity_is_loading($entity_type, $entity_id)) {
+            $entities = entity_load($entity_type, array($entity_id));
+            $entity = reset($entities);
+            // Generate the entity URI and use the results of that to construct
+            // the URL in the rest of this function.
+            $entity_uri = entity_uri($entity_type, $entity);
+            if (isset($entity_uri)) {
+              $path = $entity_uri['path'];
+              // Merge in the entity URI options. These must take precedence
+              // over the passed-in options, since, for example, if the entity
+              // URI is associated with a particular fragment or query string,
+              // that must appear in the final URL in order to link to the
+              // entity correctly.
+              if (isset($entity_uri['options']['query'])) {
+                $entity_uri['options']['query'] = array_merge($options['query'], $entity_uri['options']['query']);
+              }
+              $options = array_merge($options, $entity_uri['options']);
+            }
+          }
+        }
+      }
+    }
+  }
+
   // Preserve the original path before altering or aliasing.
   $original_path = $path;
 
@@ -6490,8 +6558,43 @@
           $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label']));
         }
       }
-      // Let other modules alter the entity info.
+      // Let other modules alter the entity info. Make a copy of the original
+      // entity info beforehand so we can compare what changed below.
+      $original_entity_info = $entity_info;
       drupal_alter('entity_info', $entity_info);
+      // Search for any URI callbacks changed or added by the drupal_alter()
+      // call, and record the root path (the non-entity-specific part of the
+      // URI). For example, when the Forum module is enabled, we will use this
+      // information in url() to rewrite links like 'taxonomy/term/2' to links
+      // like 'forum/2'; in that case, 'taxonomy/term' will be recorded by the
+      // code below.
+      $entity_altered_root_path_list = array();
+      foreach ($entity_info as $name => $info) {
+        // Strip the original and altered entity info down to arrays that only
+        // contain 'uri callback' keys, and save the altered root paths if any
+        // of them changed. If there were no uri callbacks originally, then
+        // there is no need to record anything, because there is no way anyone
+        // could link to the original entity.
+        // @todo This code does not support the edge case of multiple levels
+        // of alteration; e.g., if a module implements hook_entity_info_alter()
+        // to further rewrite the Forum module's URLs, then calls such as
+        // url('taxonomy/term/2') will properly be translated to the final
+        // value, but direct calls to url('forum/2') won't. To support that, we
+        // would need to replace the drupal_alter() call above with an explicit
+        // loop, and run the code here each time through the loop.
+        _entity_info_extract_uri_callbacks($original_entity_info[$name]);
+        if (!empty($original_entity_info[$name])) {
+          _entity_info_extract_uri_callbacks($info);
+          if ($info != $original_entity_info[$name]) {
+            foreach (_entity_uri_collect_root_paths($name, $original_entity_info[$name]) as $root_path) {
+              $entity_altered_root_path_list[$root_path] = $name;
+            }
+          }
+        }
+      }
+      if ($entity_altered_root_path_list != variable_get('entity_altered_root_path_list', array())) {
+        variable_set('entity_altered_root_path_list', $entity_altered_root_path_list);
+      }
       cache_set("entity_info:$langcode", $entity_info);
     }
   }
@@ -6505,6 +6608,63 @@
 }
 
 /**
+ * @todo Document
+ */
+function _entity_info_extract_uri_callbacks(&$info) {
+  foreach ($info as $key => &$data) {
+    if ($key != 'uri callback' && $key != 'bundles') {
+      unset($info[$key]);
+    }
+    elseif ($key == 'bundles') {
+      foreach ($data as $bundle_name => $bundle_data) {
+        if (isset($bundle_data['uri callback'])) {
+          $data[$bundle_name] = array('uri callback' => $bundle_data['uri callback']);
+        }
+        else {
+          unset($data[$bundle_name]);
+        }
+      }
+    }
+  }
+  if (empty($info['bundles'])) {
+    unset($info['bundles']);
+  }
+}
+
+/**
+ * @todo Document
+ */
+function _entity_uri_collect_root_paths($entity_type, $info) {
+  $root_paths = array();
+  foreach ($info as $value) {
+    if (is_array($value)) {
+      $root_paths = array_merge($root_paths, _entity_uri_collect_root_paths($entity_type, $value));
+    }
+    else {
+      $root_path = entity_uri_root_path($entity_type, $value);
+      if (isset($root_path)) {
+        $root_paths[] = $root_path;
+      }
+    }
+  }
+  return $root_paths;
+}
+
+/**
+ * @todo Document
+ */
+function entity_uri_root_path($entity_type, $uri_callback) {
+  if (function_exists($uri_callback)) {
+    $entity = entity_create_stub_entity($entity_type, array(1, NULL, NULL));
+    $uri = $uri_callback($entity);
+    $slash_position = strrpos($uri['path'], '/');
+    if ($slash_position !== FALSE) {
+      return substr($uri['path'], 0, $slash_position);
+    }
+  }
+}
+
+/**
  * Resets the cached information about entity types.
  */
 function entity_info_cache_clear() {
@@ -6606,6 +6766,13 @@
 }
 
 /**
+ * @todo Document
+ */
+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) {
@@ -6669,6 +6836,9 @@
  *   An array containing the 'path' and 'options' keys used to build the uri of
  *   the entity, and matching the signature of url(). NULL if the entity has no
  *   uri of its own.
+ *
+ * @todo Document that this does not need to be called unless you are dealing
+ *   with generic entities and therefore do not know the specific URL pattern.
  */
 function entity_uri($entity_type, $entity) {
   // This check enables the URI of an entity to be easily overridden from what

=== modified file 'includes/entity.inc'
--- includes/entity.inc	2010-07-26 02:51:46 +0000
+++ includes/entity.inc	2010-08-22 10:45:45 +0000
@@ -39,6 +39,11 @@
    *   An array of entity objects indexed by their ids.
    */
   public function load($ids = array(), $conditions = array());
+
+  /**
+   * @todo Document
+   */
+  public function isLoading($entity_type, $entity_id);
 }
 
 /**
@@ -114,6 +119,13 @@
   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 +133,7 @@
     $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 +203,14 @@
     // 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 +235,13 @@
   }
 
   /**
+   * 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,

=== modified file 'modules/comment/comment.module'
--- modules/comment/comment.module	2010-08-20 01:21:14 +0000
+++ modules/comment/comment.module	2010-08-22 10:45:59 +0000
@@ -2214,9 +2214,8 @@
   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
   $variables['signature'] = $comment->signature;
 
-  $uri = entity_uri('comment', $comment);
-  $variables['title']     = l($comment->subject, $uri['path'], $uri['options']);
-  $variables['permalink'] = l('#', $uri['path'], $uri['options']);
+  $variables['title']     = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid));
+  $variables['permalink'] = l('#', 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid));
 
   // Preprocess fields.
   field_attach_preprocess('comment', $comment, $variables['elements'], $variables);

=== modified file 'modules/node/node.module'
--- modules/node/node.module	2010-08-17 16:20:07 +0000
+++ modules/node/node.module	2010-08-22 10:45:45 +0000
@@ -1352,8 +1352,7 @@
   $variables['date']      = format_date($node->created);
   $variables['name']      = theme('username', array('account' => $node));
 
-  $uri = entity_uri('node', $node);
-  $variables['node_url']  = url($uri['path'], $uri['options']);
+  $variables['node_url']  = url('node/' . $node->nid);
   $variables['title']     = check_plain($node->title);
   $variables['page']      = $variables['view_mode'] == 'full' && node_is_page($node);
 
@@ -1579,9 +1578,8 @@
 
     $extra = module_invoke_all('node_search_result', $node);
 
-    $uri = entity_uri('node', $node);
     $results[] = array(
-      'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE))),
+      'link' => url('node/' . $node->nid, array('absolute' => TRUE)),
       'type' => check_plain(node_type_get_name($node)),
       'title' => $node->title,
       'user' => theme('username', array('account' => $node)),
@@ -2514,11 +2512,10 @@
   // of the active trail, and the link name becomes the page title.
   // Thus, we must explicitly set the page title to be the node title.
   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($uri['path'], $uri['options'])), TRUE);
+  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)), TRUE);
   // Set the non-aliased path as a default shortlink.
-  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
+  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url('node/' . $node->nid, array('alias' => TRUE))), TRUE);
   return node_show($node);
 }
 

=== modified file 'modules/rdf/rdf.module'
--- modules/rdf/rdf.module	2010-07-23 04:39:32 +0000
+++ modules/rdf/rdf.module	2010-08-22 10:45:45 +0000
@@ -565,30 +565,30 @@
  */
 function rdf_preprocess_user_profile(&$variables) {
   $account = $variables['elements']['#account'];
-  $uri = entity_uri('user', $account);
+  $account_url = url('user/' . $account->uid);
 
   // 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($uri['path'], $uri['options']);
+    $variables['attributes_array']['about'] = $account_url;
   }
   // 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($uri['path'], array_merge($uri['options'], array('fragment' => 'me'))),
+      'about' => url('user/' . $account->uid, array('fragment' => 'me')),
       'typeof' => array('foaf:Person'),
       'rel' => array('foaf:account'),
-      'resource' => url($uri['path'], $uri['options']),
+      'resource' => $account_url,
     ),
   );
   // Adds the markup for username.
   $username_meta = array(
     '#tag' => 'meta',
     '#attributes' => array(
-      'about' => url($uri['path'], $uri['options']),
+      'about' => $account_url,
       'property' => $account->rdf_mapping['name']['predicates'],
       'content' => $account->name,
     )
@@ -661,8 +661,7 @@
     // 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.).
-    $uri = entity_uri('comment', $comment);
-    $variables['attributes_array']['about'] = url($uri['path'], $uri['options']);
+    $variables['attributes_array']['about'] = url('comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid));
     $variables['attributes_array']['typeof'] = $comment->rdf_mapping['rdftype'];
   }
 

=== modified file 'modules/system/system.api.php'
--- modules/system/system.api.php	2010-08-17 13:52:31 +0000
+++ modules/system/system.api.php	2010-08-22 10:45:45 +0000
@@ -87,6 +87,22 @@
  *   - 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().
+ *     @todo Document that any URI returned from this callback which wants to
+ *       allow other modules to properly alter it (and have those links
+ *       respected everywhere in Drupal) must obey the following rules:
+ *       - The callback must only use the entity ID (and no other entity
+ *         property) to construct the URI.
+ *       - The entity ID must be the last part of the path.
+ *       - Query strings and fragments are optionally allowed, but should also
+ *         only use the entity ID (is this true?).
+ *       As an example, for nodes the URI can be some/random/path/{$node->nid}.
+ *       This restriction is pretty reasonable and is already met by all core
+ *       entity URI callbacks. (One-time alterations, which alter an entity URI
+ *       callback but do not expect themselves to ever be further overridden,
+ *       e.g. path alias-type applications, do not need to follow the above
+ *       guidelines.)
+ *     @todo Document the various performance implications of altering URI
+ *       callbacks.
  *   - fieldable: Set to TRUE if you want your entity type to be fieldable.
  *   - entity keys: An array describing how the Field API can extract the
  *     information it needs from the objects of the type. Elements:

=== modified file 'modules/taxonomy/taxonomy.module'
--- modules/taxonomy/taxonomy.module	2010-08-17 16:20:07 +0000
+++ modules/taxonomy/taxonomy.module	2010-08-22 10:45:45 +0000
@@ -634,8 +634,7 @@
   $variables['term'] = $variables['elements']['#term'];
   $term = $variables['term'];
 
-  $uri = entity_uri('taxonomy_term', $term);
-  $variables['term_url']  = url($uri['path'], $uri['options']);
+  $variables['term_url']  = url('taxonomy/term/' . $term->tid);
   $variables['term_name'] = check_plain($term->name);
   $variables['page']      = $variables['view_mode'] == 'full' && taxonomy_term_is_page($term);
 
@@ -1240,12 +1239,10 @@
         }
         else {
           $term = $item['taxonomy_term'];
-          $uri = entity_uri('taxonomy_term', $term);
           $element[$delta] = array(
             '#type' => 'link',
             '#title' => $term->name,
-            '#href' => $uri['path'],
-            '#options' => $uri['options'],
+            '#href' => 'taxonomy/term/' . $term->tid,
           );
         }
       }

=== modified file 'modules/taxonomy/taxonomy.tokens.inc'
--- modules/taxonomy/taxonomy.tokens.inc	2010-08-08 02:06:56 +0000
+++ modules/taxonomy/taxonomy.tokens.inc	2010-08-22 10:45:45 +0000
@@ -111,8 +111,7 @@
           break;
 
         case 'url':
-          $uri = entity_uri('taxonomy_term', $term);
-          $replacements[$original] = url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE)));
+          $replacements[$original] = url('taxonomy/term/' . $term->tid, array('absolute' => TRUE));
           break;
 
         case 'node-count':

=== modified file 'modules/user/user.module'
--- modules/user/user.module	2010-08-15 01:49:45 +0000
+++ modules/user/user.module	2010-08-22 10:45:45 +0000
@@ -3550,9 +3550,9 @@
   $account->password = $pass;
 
   // New administrative account without notification.
-  $uri = entity_uri('user', $account);
+  $account_url = url('user/' . $account->uid);
   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($uri['path'], $uri['options']), '%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' => $account_url, '%name' => $account->name)));
   }
   // No e-mail verification required; log in user immediately.
   elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) {
@@ -3567,7 +3567,7 @@
     $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($uri['path'], $uri['options']), '%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' => $account_url, '%name' => $account->name)));
     }
     else {
       drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));

