Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.925
diff -u -p -r1.925 common.inc
--- includes/common.inc	18 Jun 2009 21:19:01 -0000	1.925
+++ includes/common.inc	24 Jun 2009 17:38:32 -0000
@@ -2003,10 +2003,6 @@ function url($path = NULL, array $option
     $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path));
   }
 
-  // May need language dependent rewriting if language.inc is present.
-  if (function_exists('language_url_rewrite')) {
-    language_url_rewrite($path, $options);
-  }
   if ($options['fragment']) {
     $options['fragment'] = '#' . $options['fragment'];
   }
@@ -2056,9 +2052,10 @@ function url($path = NULL, array $option
     $path = drupal_get_path_alias($path, isset($options['language']) ? $options['language']->language : '');
   }
 
-  if (function_exists('custom_url_rewrite_outbound')) {
-    // Modules may alter outbound links by reference.
-    custom_url_rewrite_outbound($path, $options, $original_path);
+  // Allow other modules to alter the outbound URL and options.
+  foreach (module_implements('url_alter_outbound') as $module) {
+    $function = $module . '_url_alter_outbound';
+    $function($path, $options);
   }
 
   $base = $options['absolute'] ? $options['base_url'] . '/' : base_path();
Index: includes/language.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/language.inc,v
retrieving revision 1.19
diff -u -p -r1.19 language.inc
--- includes/language.inc	1 Feb 2009 16:45:53 -0000	1.19
+++ includes/language.inc	24 Jun 2009 13:50:53 -0000
@@ -97,48 +97,3 @@ function language_from_browser() {
     }
   }
 }
-
-/**
- * Rewrite URLs with language based prefix. Parameters are the same
- * as those of the url() function.
- */
-function language_url_rewrite(&$path, &$options) {
-  global $language;
-
-  // Only modify relative (insite) URLs.
-  if (!$options['external']) {
-
-    // Language can be passed as an option, or we go for current language.
-    if (!isset($options['language'])) {
-      $options['language'] = $language;
-    }
-
-    switch (variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE)) {
-      case LANGUAGE_NEGOTIATION_NONE:
-        // No language dependent path allowed in this mode.
-        unset($options['language']);
-        break;
-
-      case LANGUAGE_NEGOTIATION_DOMAIN:
-        if ($options['language']->domain) {
-          // Ask for an absolute URL with our modified base_url.
-          $options['absolute'] = TRUE;
-          $options['base_url'] = $options['language']->domain;
-        }
-        break;
-
-      case LANGUAGE_NEGOTIATION_PATH_DEFAULT:
-        $default = language_default();
-        if ($options['language']->language == $default->language) {
-          break;
-        }
-        // Intentionally no break here.
-
-      case LANGUAGE_NEGOTIATION_PATH:
-        if (!empty($options['language']->prefix)) {
-          $options['prefix'] = $options['language']->prefix . '/';
-        }
-        break;
-    }
-  }
-}
Index: includes/path.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/path.inc,v
retrieving revision 1.40
diff -u -p -r1.40 path.inc
--- includes/path.inc	8 Jun 2009 04:44:58 -0000	1.40
+++ includes/path.inc	24 Jun 2009 17:42:18 -0000
@@ -210,13 +210,19 @@ function drupal_get_path_alias($path = N
  */
 function drupal_get_normal_path($path, $path_language = '') {
   $result = $path;
-  if ($src = drupal_lookup_path('source', $path, $path_language)) {
-    $result = $src;
+
+  // Allow other modules to alter the inbound URL request. Call them in reverse
+  // order so that the module that altered the outbound URL last will alter the
+  // inbound URL first. This is necessary for consistent behavior.
+  foreach (array_reverse(module_implements('url_alter_inbound')) as $module) {
+    $function = $module . '_url_alter_inbound';
+    $function($result, $path, $path_language);
   }
-  if (function_exists('custom_url_rewrite_inbound')) {
-    // Modules may alter the inbound request path by reference.
-    custom_url_rewrite_inbound($result, $path, $path_language);
+
+  if ($src = drupal_lookup_path('source', $result, $path_language)) {
+    $result = $src;
   }
+
   return $result;
 }
 
Index: modules/locale/locale.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v
retrieving revision 1.242
diff -u -p -r1.242 locale.module
--- modules/locale/locale.module	8 Jun 2009 05:00:11 -0000	1.242
+++ modules/locale/locale.module	24 Jun 2009 13:50:53 -0000
@@ -639,3 +639,49 @@ function theme_locale_translation_filter
   $output .= '<div id="locale-translation-buttons">' . drupal_render($form['buttons']) . '</div>';
   return $output;
 }
+
+/**
+ * Implementation of hook_url_alter().
+ *
+ * Rewrite outbound URLs with language based prefix.
+ */
+function locale_url_alter_outbound(&$path, &$options) {
+  global $language;
+
+  // Only modify relative (insite) URLs.
+  if (!$options['external']) {
+
+    // Language can be passed as an option, or we go for current language.
+    if (!isset($options['language'])) {
+      $options['language'] = $language;
+    }
+
+    switch (variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE)) {
+      case LANGUAGE_NEGOTIATION_NONE:
+        // No language dependent path allowed in this mode.
+        unset($options['language']);
+        break;
+
+      case LANGUAGE_NEGOTIATION_DOMAIN:
+        if ($options['language']->domain) {
+          // Ask for an absolute URL with our modified base_url.
+          $options['absolute'] = TRUE;
+          $options['base_url'] = $options['language']->domain;
+        }
+        break;
+
+      case LANGUAGE_NEGOTIATION_PATH_DEFAULT:
+        $default = language_default();
+        if ($options['language']->language == $default->language) {
+          break;
+        }
+        // Intentionally no break here.
+
+      case LANGUAGE_NEGOTIATION_PATH:
+        if (!empty($options['language']->prefix)) {
+          $options['prefix'] = $options['language']->prefix . '/';
+        }
+        break;
+    }
+  }
+}
Index: modules/simpletest/simpletest.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v
retrieving revision 1.6
diff -u -p -r1.6 simpletest.info
--- modules/simpletest/simpletest.info	8 Jun 2009 09:23:53 -0000	1.6
+++ modules/simpletest/simpletest.info	24 Jun 2009 17:10:00 -0000
@@ -24,6 +24,7 @@ files[] = tests/graph.test
 files[] = tests/image.test
 files[] = tests/menu.test
 files[] = tests/module.test
+files[] = tests/path.test
 files[] = tests/registry.test
 files[] = tests/schema.test
 files[] = tests/session.test
Index: modules/simpletest/tests/hook_url_alter_1.info
===================================================================
RCS file: modules/simpletest/tests/hook_url_alter_1.info
diff -N modules/simpletest/tests/hook_url_alter_1.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/hook_url_alter_1.info	24 Jun 2009 15:11:59 -0000
@@ -0,0 +1,8 @@
+; $Id$
+name = Hook url_alter tests (1)
+description = The first of two support modules for url_alter hook testing.
+core = 7.x
+package = Testing
+version = VERSION
+files[] = hook_url_alter_1.module
+hidden = TRUE
Index: modules/simpletest/tests/hook_url_alter_1.module
===================================================================
RCS file: modules/simpletest/tests/hook_url_alter_1.module
diff -N modules/simpletest/tests/hook_url_alter_1.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/hook_url_alter_1.module	24 Jun 2009 17:32:02 -0000
@@ -0,0 +1,36 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Module to help test hook_url_alter_inbound() and hook_url_alter_outbound().
+ */
+
+/**
+ * Implementation of hook_url_alter_inbound().
+ */
+function hook_url_alter_1_url_alter_inbound(&$path, $original_path, $path_language) {
+  // Rewrite user/username to user/uid.
+  if (preg_match('|^user/([^/]*)(/.*)?|', $path, $matches)) {
+    $matches += array(2 => '');
+    $users = user_load_multiple(array(), array('name' => $matches[1]));
+    $user = reset($users);
+    if (!empty($user->uid)) {
+      $path = 'user/' . $user->uid . $matches[2];
+    }
+  }
+}
+
+/**
+ * Implementation of hook_url_alter_outbound().
+ */
+function hook_url_alter_1_url_alter_outbound(&$path, &$options) {
+  // Rewrite user/uid to user/username.
+  if (preg_match('|^user/([0-9]*)(/.*)?|', $path, $matches)) {
+    $matches += array(2 => '');
+    $user = user_load($matches[1]);
+    if (!empty($user->uid)) {
+      $path = 'user/' . $user->name . $matches[2];
+    }
+  }
+}
Index: modules/simpletest/tests/hook_url_alter_2.info
===================================================================
RCS file: modules/simpletest/tests/hook_url_alter_2.info
diff -N modules/simpletest/tests/hook_url_alter_2.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/hook_url_alter_2.info	24 Jun 2009 15:37:21 -0000
@@ -0,0 +1,8 @@
+; $Id$
+name = Hook url_alter tests (2)
+description = The second of two support modules for url_alter hook testing.
+core = 7.x
+package = Testing
+version = VERSION
+files[] = hook_url_alter_2.module
+hidden = TRUE
Index: modules/simpletest/tests/hook_url_alter_2.module
===================================================================
RCS file: modules/simpletest/tests/hook_url_alter_2.module
diff -N modules/simpletest/tests/hook_url_alter_2.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/hook_url_alter_2.module	24 Jun 2009 17:20:31 -0000
@@ -0,0 +1,29 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Module to help test hook_url_alter_inbound() and hook_url_alter_outbound().
+ */
+
+/**
+ * Implementation of hook_url_alter_inbound().
+ */
+function hook_url_alter_2_url_alter_inbound(&$path, $original_path, $path_language) {
+  // Rewrite member/* to user/*.
+  if (preg_match('|^member(/.*)?|', $path, $matches)) {
+    $matches += array(1 => '');
+    $path = 'user' . $matches[1];
+  }
+}
+
+/**
+ * Implementation of hook_url_alter_outbound().
+ */
+function hook_url_alter_2_url_alter_outbound(&$path, &$options) {
+  // Rewrite user/* to member/*
+  if (preg_match('|^user(/.*)?|', $path, $matches)) {
+    $matches += array(1 => '');
+    $path = 'member' . $matches[1];
+  }
+}
Index: modules/simpletest/tests/path.test
===================================================================
RCS file: modules/simpletest/tests/path.test
diff -N modules/simpletest/tests/path.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/path.test	24 Jun 2009 17:44:12 -0000
@@ -0,0 +1,76 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Tests for path.inc
+ */
+
+class HookUrlAlterTestCase extends DrupalWebTestCase {
+  protected $user;
+  protected $testCases = array();
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => t('Path rewriting'),
+      'description' => t('Tests hook_url_alter_inbound() and hook_url_alter_outbound() to make sure they work properly.'),
+      'group' => t('Path'),
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  function setUp() {
+    parent::setUp('hook_url_alter_1', 'hook_url_alter_2');
+
+    $this->user = $this->drupalCreateUser();
+    $uid = $this->user->uid;
+    $name = $this->user->name;
+    // Set a series of test aliases, not all of which should work.
+    path_set_alias("user/$uid/test1", 'alias/test1');
+    path_set_alias("user/$uid/test2", 'user/test2');
+    path_set_alias("user/$uid/test3", "user/$uid/alias/test3");
+    path_set_alias("member/$name/test4", 'alias/test4');
+
+    $this->testCases = array(
+      // "User" should be rewritten to "member" by the second implementation.
+      'user'              => 'member',
+      // Similarly, "user/register" should be rewritten to "member/register".
+      'user/register'     => 'member/register',
+      // "User/$uid" should be rewritten to "user/$name" first and then further
+      // rewritten to "member/$name".
+      "user/$uid"         => "member/$name",
+      // The same logic applies to "user/$uid/edit".
+      "user/$uid/edit"    => "member/$name/edit",
+      // A path alias was set, so that should be returned.
+      "user/$uid/test1"   => 'alias/test1',
+      // The path alias was set, but it will then be altered by the second hook
+      // implementation.
+      "user/$uid/test2"   => "member/test2",
+      // The path alias was set, and will be altered by both test modules.
+      "user/$uid/test3"        => "member/$name/alias/test3",
+      // This path alias should not work, as it was set for the final path.
+      "user/$uid/test4"  => "member/$name/test4",
+    );
+  }
+
+  /**
+   * Test that URL altering works and that it occurs in the correct order.
+   */
+  function testUrlAlter() {
+    foreach ($this->testCases as $original => $final) {
+      // Test outbound altering.
+      $final_result = url($original);
+      $final_result = str_replace(url() . (variable_get('clean_url', '0') ? '' : '?q='), '', $final_result);
+      $this->assertIdentical($final, $final_result, t('In altering outbound %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $final_result)));
+
+      // Test inbound altering.
+      $original_result = drupal_get_normal_path($final);
+      $this->assertIdentical($original, $original_result, t('In altering inbound %final, expected %original, and got %result.', array('%final' => $final, '%original' => $original, '%result' => $original_result)));
+    }
+  }
+}
\ No newline at end of file
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.43
diff -u -p -r1.43 system.api.php
--- modules/system/system.api.php	22 Jun 2009 09:10:06 -0000	1.43
+++ modules/system/system.api.php	24 Jun 2009 15:00:42 -0000
@@ -1758,5 +1758,77 @@ function hook_registry_files_alter(&$fil
 }
 
 /**
+ * Perform alterations to inbound URL requests.
+ *
+ * @param $path
+ *   The path being constructed, which, if a path alias, has been resolved to a
+ *   Drupal path by the database, and which also may have been altered by other
+ *   modules before this one.
+ * @param $original_path
+ *   The original path, before being checked for path aliases or altered by the
+ *   modules.
+ * @param $path_language
+ *   The language of the path.
+ *
+ * @see drupal_get_normal_path()
+ */
+function hook_url_alter_inbound(&$path, $original_path, $path_language) {
+  // Change all node/[title]/edit paths to node/[nid]/edit.
+  if (preg_match('|^node(/.*)/edit|', $path, $matches)) {
+    $nodes = node_load_multiple(array(), array('title' => $matches[1]));
+    if (count($nodes) === 1) {
+      $node = array_shift($nodes);
+      $path = "node/$node->nid/edit";
+    }
+  }
+
+  // Create the path user/me/edit, which allows a user to edit their account.
+  if (preg_match('|^user/me/edit(/.*)?|', $path, $matches)) {
+    global $user;
+    $path = 'user/' . $user->uid . '/edit' . $matches[1];
+  }
+}
+
+/**
+ * Perform alterations to outbound URLs.
+ *
+ * @param $path
+ *   The outbound path to alter, not adjusted for path aliases yet. It won't be
+ *   adjusted for path aliases until all modules are finished altering it, thus
+ *   being consistent with hook_url_alter_inbound(), which adjusts for all path
+ *   aliases before allowing modules to alter it. This may have been altered by
+ *   other modules before this one.
+ * @param $options
+ *   A set of URL options for the URL so elements such as a fragment or a query
+ *   string can be added to the URL.
+ *
+ * @see url()
+ */
+function hook_url_alter_outbound(&$path, &$options) {
+  // Use an external RSS feed rather than the Drupal one.
+  if ($path == 'rss.xml') {
+    $path = 'http://example.com/rss.xml';
+    $options['external'] = TRUE;
+  }
+
+  // Instead of pointing to user/[uid]/edit, point to user/me/edit.
+  if (preg_match('|^user/([0-9]*)/edit(/.*)?|', $path, $matches)) {
+    global $user;
+    if ($user->uid == $matches[1]) {
+      $path = 'user/me/edit' . $matches[2];
+    }
+  }
+
+  // Change node/[nid]/edit paths to node/[title]/edit.
+  if (preg_match('|^node([0-9]*)/edit|', $path, $matches)) {
+    $node = node_load($matches[1]);
+    $nodes = node_load_multiple(array(), array('title' => $node->title));
+    if (count($nodes) === 1) {
+      $path = "node/$node->title/edit";
+    }
+  }
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
