Index: includes/path.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/path.inc,v
retrieving revision 1.71
diff -u -p -r1.71 path.inc
--- includes/path.inc	9 Aug 2010 00:13:06 -0000	1.71
+++ includes/path.inc	28 Nov 2010 22:09:09 -0000
@@ -76,6 +76,9 @@ function drupal_lookup_path($action, $pa
   // requested URL, we might end up being unable to check if there is a path
   // alias matching the URL path.
   $path_language = $path_language ? $path_language : $language_url->language;
+  if (!isset($cache['map'][$path_language])) {
+    $cache['map'][$path_language] = array();
+  }
 
   if ($action == 'wipe') {
     $cache = array();
@@ -88,19 +91,12 @@ function drupal_lookup_path($action, $pa
       if (!empty($cache['first_call'])) {
         $cache['first_call'] = FALSE;
 
-        $cache['map'][$path_language] = array();
         // Load system paths from cache.
         $cid = current_path();
         if ($cached = cache_get($cid, 'cache_path')) {
           $cache['system_paths'] = $cached->data;
           // Now fetch the aliases corresponding to these system paths.
-          // We order by ASC and overwrite array keys to ensure the correct
-          // alias is used when there are multiple aliases per path.
-          $cache['map'][$path_language] = db_query("SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language ASC, pid ASC", array(
-            ':system' => $cache['system_paths'],
-            ':language' => $path_language,
-            ':language_none' => LANGUAGE_NONE,
-          ))->fetchAllKeyed();
+          $cache['map'][$path_language] = _drupal_get_sorted_aliases($action, $cache['system_paths'], $path_language);
           // Keep a record of paths with no alias to avoid querying twice.
           $cache['no_aliases'][$path_language] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$path_language])));
         }
@@ -117,14 +113,8 @@ function drupal_lookup_path($action, $pa
       }
       // For system paths which were not cached, query aliases individually.
       elseif (!isset($cache['no_aliases'][$path_language][$path])) {
-        // Get the most fitting result falling back with alias without language
-        $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", array(
-          ':source' => $path,
-          ':language' => $path_language,
-          ':language_none' => LANGUAGE_NONE,
-        ))->fetchField();
-        $cache['map'][$path_language][$path] = $alias;
-        return $alias;
+        $cache['map'][$path_language] += _drupal_get_sorted_aliases($action, $path, $path_language);
+        return isset($cache['map'][$path_language][$path]) ? $cache['map'][$path_language][$path] : FALSE;
       }
     }
     // Check $no_source for this $path in case we've already determined that there
@@ -133,12 +123,11 @@ function drupal_lookup_path($action, $pa
       // Look for the value $path within the cached $map
       $source = FALSE;
       if (!isset($cache['map'][$path_language]) || !($source = array_search($path, $cache['map'][$path_language]))) {
-        // Get the most fitting result falling back with alias without language
-        if ($source = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", array(
-                     ':alias' => $path,
-                     ':language' => $path_language,
-                     ':language_none' => LANGUAGE_NONE))
-            ->fetchField()) {
+        if ($aliases = _drupal_get_sorted_aliases($action, $path, $path_language)) {
+          // Use the first key in $aliases, which will contain the source in
+          // $path_language (if available) reflecting the most recently created
+          // alias, i.e. MAX(pid).
+          list($source) = each($aliases);
           $cache['map'][$path_language][$source] = $path;
         }
         else {
@@ -156,6 +145,54 @@ function drupal_lookup_path($action, $pa
 }
 
 /**
+ * Fetch alias records, with preference for recent language-specific aliases.
+ *
+ * If multiple aliases exist for a given path, this function will return
+ * the most recently created alias in $path_language (if available).
+ *
+ * Note: For a simple 1:1 lookup backed by caching, use drupal_lookup_path().
+ *
+ * @param $action
+ *   One of the following values:
+ *   - alias: return alias records for a given Drupal system path.
+ *   - source: return alias records for a given path alias.
+ * @param $path
+ *   A system path if $action is 'alias', or an alias if $action is 'source'.
+ * @param $path_language
+ *   Language code to prefer in the alias search.
+ *
+ * @return
+ *   Associative array keyed by system path, with aliased paths as values.
+ *
+ * @see drupal_lookup_path()
+ */
+function _drupal_get_sorted_aliases($action, $path = '', $path_language = NULL) {
+  global $language_url;
+
+  $path_language = $path_language ? $path_language : $language_url->language;
+  $field = $action == 'alias' ? 'source' : 'alias';
+  $aliases = array(
+    $path_language => array(),
+    LANGUAGE_NONE => array(),
+  );
+  $result = db_select('url_alias', 'u')
+    ->fields('u', array('source', 'alias', 'language'))
+    ->condition($field, $path)
+    ->condition('u.language', array($path_language, LANGUAGE_NONE), 'IN')
+    ->orderBy('u.pid')
+    ->execute();
+
+  // Since the query was ordered by pid ASC, $aliases will contain the highest
+  // pid record for each language/source.
+  foreach ($result as $alias) {
+    $aliases[$alias->language][$alias->source] = $alias->alias;
+  }
+
+  // Return aliases in $path_language, falling back to LANGUAGE_NONE.
+  return $aliases[$path_language] + $aliases[LANGUAGE_NONE];
+}
+
+/**
  * Cache system paths for a page.
  *
  * Cache an array of the system paths available on each page. We assume
Index: modules/locale/locale.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v
retrieving revision 1.87
diff -u -p -r1.87 locale.test
--- modules/locale/locale.test	21 Nov 2010 09:53:05 -0000	1.87
+++ modules/locale/locale.test	28 Nov 2010 22:09:09 -0000
@@ -1577,7 +1577,58 @@ class LocalePathFunctionalTest extends D
     $this->drupalGet($prefix . '/' . $custom_language_path);
     $this->assertText($node->title, t('Custom language alias works.'));
 
-    $this->drupalLogout();
+    // Create a custom path.
+    $custom_path = $this->randomName(8);
+
+    // Check priority of language for alias by source path.
+    $edit = array(
+      'source'   => 'node/' . $node->nid,
+      'alias'    => $custom_path,
+      'language' => LANGUAGE_NONE,
+    );
+    path_save($edit);
+    $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en');
+    $this->assertEqual($english_path, $lookup_path, t('English language alias has priority.'));
+    // Same check for language 'xx'.
+    $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix);
+    $this->assertEqual($custom_language_path, $lookup_path, t('Custom language alias has priority.'));
+    path_delete($edit);
+
+    // Create language nodes to check priority of aliases.
+    $first_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
+    $second_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
+
+    // Assign a custom path alias to the first node with the English language.
+    $edit = array(
+      'source'   => 'node/' . $first_node->nid,
+      'alias'    => $custom_path,
+      'language' => 'en',
+    );
+    path_save($edit);
+
+    // Assign a custom path alias to second node with LANGUAGE_NONE.
+    $edit = array(
+      'source'   => 'node/' . $second_node->nid,
+      'alias'    => $custom_path,
+      'language' => LANGUAGE_NONE,
+    );
+    path_save($edit);
+
+    // Test that both node titles link to our path alias.
+    $this->drupalGet('<front>');
+    $custom_path_url = base_path() . (variable_get('clean_url', 0) ? $custom_path : '?q=' . $custom_path);
+    $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $first_node->title));
+    $this->assertTrue(!empty($elements), t('First node links to the path alias.'));
+    $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $second_node->title));
+    $this->assertTrue(!empty($elements), t('Second node links to the path alias.'));
+
+    // Confirm that the custom path leads to the first node.
+    $this->drupalGet($custom_path);
+    $this->assertText($first_node->title, t('Custom alias returns first node.'));
+
+    // Confirm that the custom path with prefix leads to the second node.
+    $this->drupalGet($prefix . '/' . $custom_path);
+    $this->assertText($second_node->title, t('Custom alias with prefix returns second node.'));
   }
 }
 
Index: modules/simpletest/tests/path.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/path.test,v
retrieving revision 1.7
diff -u -p -r1.7 path.test
--- modules/simpletest/tests/path.test	5 Aug 2010 23:53:38 -0000	1.7
+++ modules/simpletest/tests/path.test	28 Nov 2010 22:09:09 -0000
@@ -236,3 +236,83 @@ class UrlAlterFunctionalTest extends Dru
     $this->assertIdentical($result, $final, t('Altered inbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result)));
   }
 }
+
+/**
+ * Unit test for drupal_lookup_path().
+ */
+class PathLookupTest extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => t('Path lookup'),
+      'description' => t('Tests that drupal_lookup_path() returns correct paths.'),
+      'group' => t('Path API'),
+    );
+  }
+
+  /**
+   * Test that drupal_lookup_path() returns the correct path.
+   */
+  function testDrupalLookupPath() {
+    $account = $this->drupalCreateUser();
+    $uid = $account->uid;
+    $name = $account->name;
+
+    // Test the situation where the source is the same for multiple aliases.
+    // Start with a language-neutral alias, which we will override.
+    $path = array(
+      'source' => "user/$uid",
+      'alias' => 'foo',
+    );
+    path_save($path);
+    $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], t('Basic alias lookup works.'));
+    $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('Basic source lookup works.'));
+
+    // Create a language specific alias for the default language (English).
+    $path = array(
+      'source' => "user/$uid",
+      'alias' => "users/$name",
+      'language' => 'en',
+    );
+    path_save($path);
+    $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], t('English alias overrides language-neutral alias.'));
+    $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('English source overrides language-neutral source.'));
+
+    // Create a language-neutral alias for the same path, again.
+    $path = array(
+      'source' => "user/$uid",
+      'alias' => 'bar',
+    );
+    path_save($path);
+    $this->assertEqual(drupal_lookup_path('alias', $path['source']), "users/$name", t('English alias still returned after entering a language-neutral alias.'));
+
+    // Create a language-specific (xx-lolspeak) alias for the same path.
+    $path = array(
+      'source' => "user/$uid",
+      'alias' => 'LOL',
+      'language' => 'xx-lolspeak',
+    );
+    path_save($path);
+    $this->assertEqual(drupal_lookup_path('alias', $path['source']), "users/$name", t('English alias still returned after entering a LOLspeak alias.'));
+    // The LOLspeak alias should be returned if we really want LOLspeak.
+    $this->assertEqual(drupal_lookup_path('alias', $path['source'], 'xx-lolspeak'), 'LOL', t('LOLspeak alias returned if we specify xx-lolspeak to drupal_lookup_path().'));
+
+    // Create a new alias for this path in English, which should override the
+    // previous alias for "user/$uid".
+    $path = array(
+      'source' => "user/$uid",
+      'alias' => 'users/my-new-path',
+      'language' => 'en',
+    );
+    path_save($path);
+    $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], t('Recently created English alias returned.'));
+    $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('Recently created English source returned.'));
+
+    // Remove the English aliases, which should cause a fallback to the most
+    // recently created language-neutral alias, 'bar'.
+    db_delete('url_alias')
+      ->condition('language', 'en')
+      ->execute();
+    drupal_clear_path_cache();
+    $this->assertEqual(drupal_lookup_path('alias', $path['source']), 'bar', t('Path lookup falls back to recently created language-neutral alias.'));
+  }
+}
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.526
diff -u -p -r1.526 system.install
--- modules/system/system.install	27 Nov 2010 20:25:44 -0000	1.526
+++ modules/system/system.install	28 Nov 2010 22:09:10 -0000
@@ -1617,7 +1617,7 @@ function system_schema() {
         'default' => '',
       ),
       'language' => array(
-        'description' => 'The language this alias is for; if blank, the alias will be used for unknown languages. Each Drupal path can have an alias for each supported language.',
+        'description' => "The language this alias is for; if 'und', the alias will be used for unknown languages. Each Drupal path can have an alias for each supported language.",
         'type' => 'varchar',
         'length' => 12,
         'not null' => TRUE,
@@ -2938,6 +2938,20 @@ function system_update_7067() {
 }
 
 /**
+ * Update {url_alias}.language description.
+ */
+function system_update_7068() {
+  $spec = array(
+    'description' => "The language this alias is for; if 'und', the alias will be used for unknown languages. Each Drupal path can have an alias for each supported language.",
+    'type' => 'varchar',
+    'length' => 12,
+    'not null' => TRUE,
+    'default' => '',
+  );
+  db_change_field('url_alias', 'language', 'language', $spec);
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
