diff --git a/.htaccess b/.htaccess index c0cef3949c9feff58a6f699afe02241cf3114449..77ef61b8c400a42613785a3966e27c1c68eab5cb 100644 --- a/.htaccess +++ b/.htaccess @@ -88,11 +88,12 @@ DirectoryIndex index.php index.html index.htm # 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] # $Id$ diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 4184c4036af3a8899d4fb4e276fb63f0e11089d4..8fff1c40e0493af2e7db798f1a7a324b06b72957 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -492,6 +492,13 @@ function drupal_environment_initialize() { $_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()); @@ -559,8 +566,8 @@ function drupal_settings_initialize() { // $_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 .= '/'; } @@ -1858,6 +1865,50 @@ function language_default($property = NULL) { 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 diff --git a/includes/common.inc b/includes/common.inc index c6472be18d95eed165ae5720ec7c62d73345b46d..20b0648c77f7b2600d0caaeff61ebdb35cbddcdc 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -501,7 +501,6 @@ function drupal_http_build_query(array $query, $parent = '') { } else { // For better readability of paths in query strings, we decode slashes. - // @see drupal_encode_path() $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value)); } } @@ -623,38 +622,18 @@ function drupal_parse_url($url) { } /** - * 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 aesthetic 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. + * Note that url() takes care of calling this function, so a path passed to that + * function should not be encoded in advance. * * @param $path - * The URL path component to encode. + * The Drupal path to encode. */ 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)); - } + return str_replace('%2F', '/', rawurlencode($path)); } /** diff --git a/includes/file.inc b/includes/file.inc index 6395c632337bcf8a8ea7cb556b13390b6812ee62..ccac9cabcbeb6116b07020e09295f07a4cae3346 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -901,6 +901,10 @@ function file_unmunge_filename($filename) { * 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 = ''; diff --git a/includes/path.inc b/includes/path.inc index 0f61d6c277ddda4b2bc5cbf6904af52783fdc221..8585a3b788c5c0f156427f3a2b331019ed6ada8e 100644 --- a/includes/path.inc +++ b/includes/path.inc @@ -384,6 +384,8 @@ function drupal_match_path($path, $patterns) { * * @return * The current Drupal URL path. + * + * @see request_path() */ function current_path() { return $_GET['q']; diff --git a/includes/stream_wrappers.inc b/includes/stream_wrappers.inc index b4edb0933735b266498d78a6177771bff4073435..2940b1e665e354b3f81add3831a993d8e5508209 100644 --- a/includes/stream_wrappers.inc +++ b/includes/stream_wrappers.inc @@ -639,7 +639,7 @@ public function getDirectoryPath() { */ 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); } } diff --git a/misc/autocomplete.js b/misc/autocomplete.js index 8c839c7c31f14d49f0ed836071ade7ef7dacd7bc..73aafbf68757af141f9f52de1bb5ec5ccd28061c 100644 --- a/misc/autocomplete.js +++ b/misc/autocomplete.js @@ -276,7 +276,7 @@ Drupal.ACDB.prototype.search = function (searchString) { // 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) { diff --git a/misc/drupal.js b/misc/drupal.js index 17997f400b4d80be0e2c6ece9fe2dc62840294a9..c9d219898f38e2abd71047a57c30a01083259d4c 100644 --- a/misc/drupal.js +++ b/misc/drupal.js @@ -282,14 +282,13 @@ Drupal.unfreezeHeight = function () { }; /** - * 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, '/'); }; /** diff --git a/modules/path/path.test b/modules/path/path.test index baba8e86459679762aaa14d104aebc208642dbbd..66b5c13447041329b9dc438c76af724b708a917a 100644 --- a/modules/path/path.test +++ b/modules/path/path.test @@ -66,11 +66,13 @@ class PathTestCase extends DrupalWebTestCase { $this->assertText($node1->title, '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('Save')); // Confirm that the alias works. @@ -122,9 +124,11 @@ class PathTestCase extends DrupalWebTestCase { $this->assertText($node1->title, '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. diff --git a/modules/simpletest/tests/file.test b/modules/simpletest/tests/file.test index 46931e69b721647bb2475190cdb1a767d3ab60cc..cf7c8f1cac45f00eeff51e6ba3d8a455bac257f0 100644 --- a/modules/simpletest/tests/file.test +++ b/modules/simpletest/tests/file.test @@ -1888,6 +1888,8 @@ class FileDownloadTest extends FileTestCase { function setUp() { parent::setUp('file_test'); + // Clear out any hook calls. + file_test_reset(); } /** @@ -1937,6 +1939,70 @@ class FileDownloadTest extends FileTestCase { $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); + } } /** diff --git a/modules/simpletest/tests/menu.test b/modules/simpletest/tests/menu.test index 4a34369feb43993271d13dc4b08586e4f38a40d3..c98014b0638ce9bbc7364609c1681afe43d6dd9d 100644 --- a/modules/simpletest/tests/menu.test +++ b/modules/simpletest/tests/menu.test @@ -51,6 +51,17 @@ class MenuRouterTestCase extends DrupalWebTestCase { $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. */ diff --git a/modules/simpletest/tests/menu_test.module b/modules/simpletest/tests/menu_test.module index 2648612408a717cbd1a3fca5436fde2544def648..194fe8006a48de669d550b0536de5eb2f22f9722 100644 --- a/modules/simpletest/tests/menu_test.module +++ b/modules/simpletest/tests/menu_test.module @@ -58,6 +58,15 @@ function menu_test_menu() { '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 @@ -174,7 +183,7 @@ function menu_test_menu() { * A random string. */ function menu_test_callback() { - return $this->randomName(); + return 'This is menu_test_callback().'; } /** diff --git a/modules/simpletest/tests/path.test b/modules/simpletest/tests/path.test index 567731421bb4fa4c091fcf0c8b0741b515e737a7..676c709cbce05687b0c290268dc1f261c92e7255 100644 --- a/modules/simpletest/tests/path.test +++ b/modules/simpletest/tests/path.test @@ -200,6 +200,15 @@ class UrlAlterFunctionalTest extends DrupalWebTestCase { $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. * diff --git a/modules/simpletest/tests/url_alter_test.module b/modules/simpletest/tests/url_alter_test.module index 2734d8caa640050e6fd206cd7ae29f946cbd04f5..da6cd635bc41596516d017c74a3d1ea838475ccb 100644 --- a/modules/simpletest/tests/url_alter_test.module +++ b/modules/simpletest/tests/url_alter_test.module @@ -6,6 +6,27 @@ * 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(). */ @@ -22,6 +43,10 @@ function url_alter_test_url_inbound_alter(&$path, $original_path, $path_language if ($path == 'community' || strpos($path, 'community/') === 0) { $path = 'forum' . substr($path, 9); } + + if ($path == 'url-alter-test/bar') { + $path = 'url-alter-test/foo'; + } } /**