Index: .htaccess
===================================================================
RCS file: /cvs/drupal/drupal/.htaccess,v
retrieving revision 1.104
diff -u -9 -p -r1.104 .htaccess
--- .htaccess	16 Aug 2009 12:10:36 -0000	1.104
+++ .htaccess	2 Jan 2010 23:47:30 -0000
@@ -82,17 +82,18 @@ DirectoryIndex index.php index.html inde
   # VirtualDocumentRoot and the rewrite rules are not working properly.
   # For example if your site is at http://example.com/drupal uncomment and
   # modify the following line:
   # RewriteBase /drupal
   #
   # If your site is running in a VirtualDocumentRoot at http://example.com/,
   # uncomment the following line:
   # RewriteBase /
 
-  # Rewrite URLs of the form 'x' to the form 'index.php?q=x'.
+  # Pass all requests not referring directly to files in the filesystem to
+  # index.php. Clean URLs are handled in drupal_environment_initialize().
   RewriteCond %{REQUEST_FILENAME} !-f
   RewriteCond %{REQUEST_FILENAME} !-d
   RewriteCond %{REQUEST_URI} !=/favicon.ico
-  RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
+  RewriteRule ^ index.php [L]
 </IfModule>
 
 # $Id: .htaccess,v 1.104 2009/08/16 12:10:36 dries Exp $
Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.338
diff -u -9 -p -r1.338 bootstrap.inc
--- includes/bootstrap.inc	30 Dec 2009 08:16:55 -0000	1.338
+++ includes/bootstrap.inc	2 Jan 2010 23:47:31 -0000
@@ -486,18 +486,25 @@ function drupal_environment_initialize()
       exit;
     }
   }
   else {
     // Some pre-HTTP/1.1 clients will not send a Host header. Ensure the key is
     // defined for E_ALL compliance.
     $_SERVER['HTTP_HOST'] = '';
   }
 
+  // When clean URLs are enabled, emulate ?q=foo/bar using REQUEST_URI. It is
+  // not possible to append the query string using mod_rewrite without the B
+  // flag (this was added in Apache 2.2.8), because mod_rewrite unescapes the
+  // path before passing it on to PHP. This is a problem when the path contains
+  // e.g. "&" or "%" that have special meanings in URLs and must be encoded.
+  $_GET['q'] = request_path();
+
   // Enforce E_ALL, but allow users to set levels not part of E_ALL.
   error_reporting(E_ALL | error_reporting());
 
   // Override PHP settings required for Drupal to work properly.
   // sites/default/default.settings.php contains more runtime settings.
   // The .htaccess file contains settings that cannot be changed at runtime.
 
   // Prevent PHP from generating HTML error messages.
   ini_set('html_errors', 0);
@@ -553,20 +560,20 @@ function drupal_settings_initialize() {
   else {
     // Create base URL
     $http_protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
     $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST'];
 
     $base_url = $base_root;
 
     // $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not
     // be modified by a visitor.
-    if ($dir = trim(dirname($_SERVER['SCRIPT_NAME']), '\,/')) {
-      $base_path = "/$dir";
+    if ($dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')) {
+      $base_path = $dir;
       $base_url .= $base_path;
       $base_path .= '/';
     }
     else {
       $base_path = '/';
     }
   }
   $is_https = $http_protocol == 'https';
   $base_secure_url = str_replace('http://', 'https://', $base_url);
@@ -1850,18 +1857,62 @@ function language_list($field = 'languag
  * @param $property
  *   Optional property of the language object to return
  */
 function language_default($property = NULL) {
   $language = variable_get('language_default', (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''));
   return $property ? $language->$property : $language;
 }
 
 /**
+ * Returns the requested URL path of the page being viewed.
+ *
+ * Examples:
+ * - http://example.com/node/306 returns "node/306".
+ * - http://example.com/drupalfolder/node/306 returns "node/306" while
+ *   base_path() returns "/drupalfolder/".
+ * - http://example.com/path/alias (which is a path alias for node/306) returns
+ *   "path/alias" as opposed to the internal path.
+ *
+ * @return
+ *   The requested Drupal URL path.
+ *
+ * @see current_path()
+ */
+function request_path() {
+  static $path;
+
+  if (isset($path)) {
+    return $path;
+  }
+
+  if (isset($_GET['q'])) {
+    // This is a request with a ?q=foo/bar query string. $_GET['q'] is
+    // overwritten in drupal_path_initialize(), but request_path() is called
+    // very early in the bootstrap process, so the original value is saved in
+    // $path and returned in later calls.
+    $path = $_GET['q'];
+  }
+  elseif (isset($_SERVER['REQUEST_URI'])) {
+    // This is a request using a clean URL. Extract the path from REQUEST_URI.
+    $request_path = strtok($_SERVER['REQUEST_URI'], '?');
+    $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/'));
+    // Unescape and strip $base_path prefix, leaving q without a leading slash.
+    $path = substr(urldecode($request_path), $base_path_len + 1);
+  }
+  else {
+    // This is the front page.
+    $path = '';
+  }
+
+  return $path;
+}
+
+/**
  * If Drupal is behind a reverse proxy, we use the X-Forwarded-For header
  * instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of
  * the proxy server, and not the client's. If Drupal is run in a cluster
  * we use the X-Cluster-Client-Ip header instead.
  *
  * @return
  *   IP address of client machine, adjusted for reverse proxy and/or cluster
  *   environments.
  */
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1072
diff -u -9 -p -r1.1072 common.inc
--- includes/common.inc	2 Jan 2010 17:39:19 -0000	1.1072
+++ includes/common.inc	2 Jan 2010 23:47:31 -0000
@@ -495,19 +495,18 @@ function drupal_http_build_query(array $
     if (is_array($value)) {
       $params[] = drupal_http_build_query($value, $key);
     }
     // If a query parameter value is NULL, only append its key.
     elseif (!isset($value)) {
       $params[] = $key;
     }
     else {
       // For better readability of paths in query strings, we decode slashes.
-      // @see drupal_encode_path()
       $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
     }
   }
 
   return implode('&', $params);
 }
 
 /**
  * Prepare a 'destination' URL query parameter for use in combination with drupal_goto().
@@ -617,50 +616,27 @@ function drupal_parse_url($url) {
   if (isset($options['query']['q'])) {
     $options['path'] = $options['query']['q'];
     unset($options['query']['q']);
   }
 
   return $options;
 }
 
 /**
- * Encode a path for usage in a URL.
+ * Encodes a Drupal path for use in a URL.
  *
- * Wrapper around rawurlencode() which avoids Apache quirks. Should be used when
- * placing arbitrary data into the path component of an URL.
+ * For aesthetic reasons slashes are not escaped.
  *
- * Do not use this function to pass a path to url(). url() properly handles
- * and encodes paths internally.
- * This function should only be used on paths, not on query string arguments.
- * Otherwise, unwanted double encoding will occur.
- *
- * Notes:
- * - For esthetic reasons, we do not escape slashes. This also avoids a 'feature'
- *   in Apache where it 404s on any path containing '%2F'.
- * - mod_rewrite unescapes %-encoded ampersands, hashes, and slashes when clean
- *   URLs are used, which are interpreted as delimiters by PHP. These
- *   characters are double escaped so PHP will still see the encoded version.
- * - With clean URLs, Apache changes '//' to '/', so every second slash is
- *   double escaped.
- *
- * @param $path
- *   The URL path component to encode.
+ * Note that url() takes care of calling this function, so a path passed to that
+ * function should not be encoded in advance.
  */
-function drupal_encode_path($path) {
-  if (!empty($GLOBALS['conf']['clean_url'])) {
-    return str_replace(array('%2F', '%26', '%23', '//'),
-                       array('/', '%2526', '%2523', '/%252F'),
-                       rawurlencode($path)
-    );
-  }
-  else {
-    return str_replace('%2F', '/', rawurlencode($path));
-  }
+function drupal_encode_path($text) {
+  return str_replace('%2F', '/', rawurlencode($text));
 }
 
 /**
  * Send the user to a different Drupal page.
  *
  * This issues an on-site HTTP redirect. The function makes sure the redirected
  * URL is formatted correctly.
  *
  * Usually the redirected URL is constructed from this function's input
Index: includes/file.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/file.inc,v
retrieving revision 1.200
diff -u -9 -p -r1.200 file.inc
--- includes/file.inc	30 Dec 2009 08:16:55 -0000	1.200
+++ includes/file.inc	2 Jan 2010 23:47:32 -0000
@@ -871,18 +871,22 @@ function file_unmunge_filename($filename
  * @param $basename
  *   String filename
  * @param $directory
  *   String containing the directory or parent URI.
  * @return
  *   File path consisting of $directory and a unique filename based off
  *   of $basename.
  */
 function file_create_filename($basename, $directory) {
+  // Strip control characters (ASCII value < 32). Though these are allowed in
+  // some filesystems, not many applications handle them well.
+  $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
+
   // A URI or path may already have a trailing slash or look like "public://".
   if (substr($directory, -1) == '/') {
     $separator = '';
   }
   else {
     $separator = '/';
   }
 
   $destination = $directory . $separator . $basename;
Index: includes/path.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/path.inc,v
retrieving revision 1.54
diff -u -9 -p -r1.54 path.inc
--- includes/path.inc	17 Dec 2009 13:10:18 -0000	1.54
+++ includes/path.inc	2 Jan 2010 23:47:32 -0000
@@ -372,18 +372,20 @@ function drupal_match_path($path, $patte
  *   "node/306" as opposed to the path alias.
  *
  * This function is not available in hook_boot() so use $_GET['q'] instead.
  * However, be careful when doing that because in the case of Example #3
  * $_GET['q'] will contain "path/alias". If "node/306" is needed, calling
  * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
  *
  * @return
  *   The current Drupal URL path.
+ *
+ * @see request_path()
  */
 function current_path() {
   return $_GET['q'];
 }
 
 /**
  * Rebuild the path alias white list.
  *
  * @return
Index: includes/stream_wrappers.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/stream_wrappers.inc,v
retrieving revision 1.8
diff -u -9 -p -r1.8 stream_wrappers.inc
--- includes/stream_wrappers.inc	15 Dec 2009 05:22:05 -0000	1.8
+++ includes/stream_wrappers.inc	2 Jan 2010 23:47:32 -0000
@@ -574,19 +574,19 @@ class DrupalPublicStreamWrapper extends 
   }
 
   /**
    * Overrides getExternalUrl().
    *
    * Return the HTML URI of a public file.
    */
   function getExternalUrl() {
     $path = str_replace('\\', '/', file_uri_target($this->uri));
-    return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . $path;
+    return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . drupal_encode_path($path);
   }
 }
 
 
 /**
  * Drupal private (private://) stream wrapper class.
  *
  * Provides support for storing privately accessible files with the Drupal file
  * interface.
Index: misc/autocomplete.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/autocomplete.js,v
retrieving revision 1.34
diff -u -9 -p -r1.34 autocomplete.js
--- misc/autocomplete.js	5 Sep 2009 12:03:31 -0000	1.34
+++ misc/autocomplete.js	2 Jan 2010 23:47:32 -0000
@@ -270,19 +270,19 @@ Drupal.ACDB.prototype.search = function 
   if (this.timer) {
     clearTimeout(this.timer);
   }
   this.timer = setTimeout(function () {
     db.owner.setStatus('begin');
 
     // Ajax GET request for autocompletion.
     $.ajax({
       type: 'GET',
-      url: db.uri + '/' + Drupal.encodePath(searchString),
+      url: db.uri + '/' + encodeURIComponent(searchString),
       dataType: 'json',
       success: function (matches) {
         if (typeof matches.status == 'undefined' || matches.status != 0) {
           db.cache[searchString] = matches;
           // Verify if these are still the matches the user wants to see.
           if (db.searchString == searchString) {
             db.owner.found(matches);
           }
           db.owner.setStatus('found');
Index: misc/drupal.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/drupal.js,v
retrieving revision 1.62
diff -u -9 -p -r1.62 drupal.js
--- misc/drupal.js	14 Dec 2009 23:57:39 -0000	1.62
+++ misc/drupal.js	2 Jan 2010 23:47:32 -0000
@@ -276,26 +276,25 @@ Drupal.freezeHeight = function () {
 
 /**
  * Unfreeze the body height.
  */
 Drupal.unfreezeHeight = function () {
   $('#freeze-height').remove();
 };
 
 /**
- * Wrapper around encodeURIComponent() which avoids Apache quirks (equivalent of
- * drupal_encode_path() in PHP). This function should only be used on paths, not
- * on query string arguments.
+ * Encodes a Drupal path for use in a URL.
+ *
+ * For aesthetic reasons slashes are not escaped.
  */
 Drupal.encodePath = function (item, uri) {
   uri = uri || location.href;
-  item = encodeURIComponent(item).replace(/%2F/g, '/');
-  return (uri.indexOf('?q=') != -1) ? item : item.replace(/%26/g, '%2526').replace(/%23/g, '%2523').replace(/\/\//g, '/%252F');
+  return encodeURIComponent(item).replace(/%2F/g, '/');
 };
 
 /**
  * Get the text selection in a textarea.
  */
 Drupal.getSelection = function (element) {
   if (typeof element.selectionStart != 'number' && document.selection) {
     // The current selection.
     var range1 = document.selection.createRange();
Index: modules/path/path.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/path/path.test,v
retrieving revision 1.29
diff -u -9 -p -r1.29 path.test
--- modules/path/path.test	9 Dec 2009 19:22:04 -0000	1.29
+++ modules/path/path.test	2 Jan 2010 23:47:32 -0000
@@ -60,23 +60,25 @@ class PathTestCase extends DrupalWebTest
     $edit['source'] = 'node/' . $node1->nid;
     $edit['alias'] = $this->randomName(8);
     $this->drupalPost('admin/config/search/path/add', $edit, t('Create new alias'));
 
     // Confirm that the alias works.
     $this->drupalGet($edit['alias']);
     $this->assertText($node1->title[LANGUAGE_NONE][0]['value'], 'Alias works.');
     $this->assertResponse(200);
 
-    // Change alias.
+    // Change alias to one containing "exotic" characters.
     $pid = $this->getPID($edit['alias']);
 
     $previous = $edit['alias'];
-    $edit['alias'] = $this->randomName(8);
+    $edit['alias'] = "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
+      "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
+      "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
     $this->drupalPost('admin/config/search/path/edit/' . $pid, $edit, t('Update alias'));
 
     // Confirm that the alias works.
     $this->drupalGet($edit['alias']);
     $this->assertText($node1->title[LANGUAGE_NONE][0]['value'], 'Changed alias works.');
     $this->assertResponse(200);
 
     drupal_static_reset('drupal_lookup_path');
     // Confirm that previous alias no longer works.
@@ -115,21 +117,23 @@ class PathTestCase extends DrupalWebTest
     $edit = array();
     $edit['path[alias]'] = $this->randomName(8);
     $this->drupalPost('node/' . $node1->nid . '/edit', $edit, t('Save'));
 
     // Confirm that the alias works.
     $this->drupalGet($edit['path[alias]']);
     $this->assertText($node1->title[LANGUAGE_NONE][0]['value'], 'Alias works.');
     $this->assertResponse(200);
 
-    // Change alias.
+    // Change alias to one containing "exotic" characters.
     $previous = $edit['path[alias]'];
-    $edit['path[alias]'] = $this->randomName(8);
+    $edit['path[alias]'] = "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
+      "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
+      "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
     $this->drupalPost('node/' . $node1->nid . '/edit', $edit, t('Save'));
 
     // Confirm that the alias works.
     $this->drupalGet($edit['path[alias]']);
     $this->assertText($node1->title[LANGUAGE_NONE][0]['value'], 'Changed alias works.');
     $this->assertResponse(200);
 
     // Make sure that previous alias no longer works.
     $this->drupalGet($previous);
Index: modules/simpletest/tests/file.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v
retrieving revision 1.50
diff -u -9 -p -r1.50 file.test
--- modules/simpletest/tests/file.test	28 Dec 2009 14:00:01 -0000	1.50
+++ modules/simpletest/tests/file.test	2 Jan 2010 23:47:32 -0000
@@ -1882,18 +1882,20 @@ class FileDownloadTest extends FileTestC
     return array(
       'name' => 'File download',
       'description' => 'Tests for file download/transfer functions.',
       'group' => 'File API',
     );
   }
 
   function setUp() {
     parent::setUp('file_test');
+    // Clear out any hook calls.
+    file_test_reset();
   }
 
   /**
    * Test the public file transfer system.
    */
   function testPublicFileTransfer() {
     // Test generating an URL to a created file.
     $file = $this->createFile();
     $url = file_create_url($file->uri);
@@ -1931,18 +1933,82 @@ class FileDownloadTest extends FileTestC
     file_test_set_return('download', -1);
     $this->drupalHead($url);
     $this->assertResponse(403, t('Correctly denied access to a file when file_test sets the header to -1.'));
 
     // Try non-existent file.
     $url = file_create_url('private://' . $this->randomName());
     $this->drupalHead($url);
     $this->assertResponse(404, t('Correctly returned 404 response for a non-existent file.'));
   }
+
+  /**
+   * Test file_create_url().
+   */
+  function testFileCreateUrl() {
+    global $base_url;
+
+    $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__' .
+      '%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';
+
+    $this->checkUrl('public', '', $basename, $base_url . '/' . file_directory_path() . '/' . $basename_encoded);
+    $this->checkUrl('private', '', $basename, $base_url . '/system/files/' . $basename_encoded);
+    $this->checkUrl('private', '', $basename, $base_url . '/?q=system/files/' . $basename_encoded, '0');
+  }
+
+  /**
+   * Download a file from the URL generated by file_create_url().
+   *
+   * Create a file with the specified scheme, directory and filename; check that
+   * the URL generated by file_create_url() for the specified file equals the
+   * specified URL; fetch the URL and then compare the contents to the file.
+   *
+   * @param $scheme
+   *   A scheme, e.g. "public"
+   * @param $directory
+   *   A directory, possibly ""
+   * @param $filename
+   *   A filename
+   * @param $expected_url
+   *   The expected URL
+   * @param $clean_url
+   *   The value of the clean_url setting
+   */
+  private function checkUrl($scheme, $directory, $filename, $expected_url, $clean_url = '1') {
+    variable_set('clean_url', $clean_url);
+
+    // Convert $filename to a valid filename, i.e. strip characters not
+    // supported by the filesystem, and create the file in the specified
+    // directory.
+    $filepath = file_create_filename($filename, $directory);
+    $directory_uri = $scheme . '://' . dirname($filepath);
+    file_prepare_directory($directory_uri, FILE_CREATE_DIRECTORY);
+    $file = $this->createFile($filepath, NULL, $scheme);
+
+    $url = file_create_url($file->uri);
+    $this->assertEqual($url, $expected_url, t('Generated URL matches expected URL.'));
+
+    if ($scheme == 'private') {
+      // Tell the implementation of hook_file_download() in file_test.module
+      // that this file may be downloaded.
+      file_test_set_return('download', array('x-foo' => 'Bar'));
+    }
+
+    $this->drupalGet($url);
+    if ($this->assertResponse(200) == 'pass') {
+      $this->assertRaw(file_get_contents($file->uri), t('Contents of the file are correct.'));
+    }
+
+    file_delete($file);
+  }
 }
 
 /**
  * Tests for file URL rewriting.
  */
 class FileURLRewritingTest extends FileTestCase {
   public static function getInfo() {
     return array(
       'name' => 'File URL rewriting',
Index: modules/simpletest/tests/menu.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu.test,v
retrieving revision 1.26
diff -u -9 -p -r1.26 menu.test
--- modules/simpletest/tests/menu.test	14 Dec 2009 20:23:01 -0000	1.26
+++ modules/simpletest/tests/menu.test	2 Jan 2010 23:47:32 -0000
@@ -46,18 +46,29 @@ class MenuRouterTestCase extends DrupalW
    * Test that the theme callback is properly inherited.
    */
   function testThemeCallbackInheritance() {
     $this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance');
     $this->assertText('Requested theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', t('Theme callback inheritance correctly uses the administrative theme.'));
     $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
   }
 
   /**
+   * Test path containing "exotic" characters.
+   */
+  function testExoticPath() {
+    $path = "menu-test/ -._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
+      "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
+      "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
+    $this->drupalGet($path);
+    $this->assertRaw('This is menu_test_callback().');
+  }
+
+  /**
    * Test the theme callback when the site is in maintenance mode.
    */
   function testThemeCallbackMaintenanceMode() {
     variable_set('maintenance_mode', TRUE);
 
     // For a regular user, the fact that the site is in maintenance mode means
     // we expect the theme callback system to be bypassed entirely.
     $this->drupalGet('menu-test/theme-callback/use-admin-theme');
     $this->assertRaw('garland/style.css', t("The maintenance theme's CSS appears on the page."));
Index: modules/simpletest/tests/menu_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu_test.module,v
retrieving revision 1.11
diff -u -9 -p -r1.11 menu_test.module
--- modules/simpletest/tests/menu_test.module	14 Dec 2009 20:23:01 -0000	1.11
+++ modules/simpletest/tests/menu_test.module	2 Jan 2010 23:47:32 -0000
@@ -52,18 +52,27 @@ function menu_test_menu() {
     'theme callback' => 'menu_test_theme_callback',
     'theme arguments' => array(2),
   );
   $items['menu-test/theme-callback/%/inheritance'] = array(
     'title' => 'Page that tests theme callback inheritance.',
     'page callback' => 'menu_test_theme_page_callback',
     'page arguments' => array(TRUE),
     'access arguments' => array('access content'),
   );
+  // Path containing "exotic" characters.
+  $path = "menu-test/ -._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
+    "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
+    "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
+  $items[$path] = array(
+    'title' => '"Exotic" path',
+    'page callback' => 'menu_test_callback',
+    'access arguments' => array('access content'),
+  );
 
   // Hidden tests; base parents.
   // Same structure as in Menu and Block modules. Since those structures can
   // change, we need to simulate our own in here.
   $items['menu-test'] = array(
     'title' => 'Menu test root',
     'page callback' => 'node_page_default',
     'access arguments' => array('access content'),
   );
@@ -168,19 +177,19 @@ function menu_test_menu() {
 }
 
 /**
  * Dummy callback for hook_menu() to point to.
  *
  * @return
  *  A random string.
  */
 function menu_test_callback() {
-  return $this->randomName();
+  return 'This is menu_test_callback().';
 }
 
 /**
  * Page callback to use when testing the theme callback functionality.
  *
  * @param $inherited
  *   An optional boolean to set to TRUE when the requested page is intended to
  *   inherit the theme of its parent.
  * @return
Index: modules/simpletest/tests/path.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/path.test,v
retrieving revision 1.2
diff -u -9 -p -r1.2 path.test
--- modules/simpletest/tests/path.test	24 Oct 2009 05:13:44 -0000	1.2
+++ modules/simpletest/tests/path.test	2 Jan 2010 23:47:32 -0000
@@ -195,18 +195,27 @@ class UrlAlterFunctionalTest extends Dru
     $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");
   }
 
   /**
+   * Test current_path() and request_path().
+   */
+  function testCurrentUrlRequestedPath() {
+    $this->drupalGet('url-alter-test/bar');
+    $this->assertRaw('request_path=url-alter-test/bar', t('request_path() returns the requested path.'));
+    $this->assertRaw('current_path=url-alter-test/foo', t('current_path() returns the internal path.'));
+  }
+
+  /**
    * Assert that an outbound path is altered to an expected value.
    *
    * @param $original
    *   A string with the original path that is run through url().
    * @param $final
    *   A string with the expected result after url().
    * @return
    *   TRUE if $original was correctly altered to $final, FALSE otherwise.
    */
Index: modules/simpletest/tests/url_alter_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/url_alter_test.module,v
retrieving revision 1.2
diff -u -9 -p -r1.2 url_alter_test.module
--- modules/simpletest/tests/url_alter_test.module	4 Dec 2009 16:49:47 -0000	1.2
+++ modules/simpletest/tests/url_alter_test.module	2 Jan 2010 23:47:32 -0000
@@ -1,33 +1,58 @@
 <?php
 // $Id: url_alter_test.module,v 1.2 2009/12/04 16:49:47 dries Exp $
 
 /**
  * @file
  * Module to help test hook_url_inbound_alter() and hook_url_outbound_alter().
  */
 
 /**
+ * Implements hook_menu().
+ */
+function url_alter_test_menu() {
+  $items['url-alter-test/foo'] = array(
+    'title' => 'Foo',
+    'page callback' => 'url_alter_test_foo',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
+ * Menu callback.
+ */
+function url_alter_test_foo() {
+  print 'current_path=' . current_path() . ' request_path=' . request_path();
+  exit;
+}
+
+/**
  * Implements hook_url_inbound_alter().
  */
 function url_alter_test_url_inbound_alter(&$path, $original_path, $path_language) {
   // Rewrite user/username to user/uid.
   if (preg_match('!^user/([^/]+)(/.*)?!', $path, $matches)) {
     if ($account = user_load_by_name($matches[1])) {
       $matches += array(2 => '');
       $path = 'user/' . $account->uid . $matches[2];
     }
   }
 
   // Rewrite community/ to forum/.
   if ($path == 'community' || strpos($path, 'community/') === 0) {
     $path = 'forum' . substr($path, 9);
   }
+
+  if ($path == 'url-alter-test/bar') {
+    $path = 'url-alter-test/foo';
+  }
 }
 
 /**
  * Implements hook_url_outbound_alter().
  */
 function url_alter_test_url_outbound_alter(&$path, &$options, $original_path) {
   // Rewrite user/uid to user/username.
   if (preg_match('!^user/([0-9]+)(/.*)?!', $path, $matches)) {
     if ($account = user_load($matches[1])) {
