diff --git .htaccess .htaccess index e78c440..6c507f2 100644 --- .htaccess +++ .htaccess @@ -88,11 +88,10 @@ 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'. 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: .htaccess,v 1.104 2009-08-16 12:10:36 dries Exp $ diff --git includes/bootstrap.inc includes/bootstrap.inc index c265610..b33e189 100644 --- includes/bootstrap.inc +++ includes/bootstrap.inc @@ -469,6 +469,20 @@ 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. + if (!isset($_GET['q']) && isset($_SERVER['REQUEST_URI'])) { + $request_path = $_SERVER['REQUEST_URI']; + $query_pos = strpos($_SERVER['REQUEST_URI'], '?'); + if ($query_pos !== FALSE) { + $request_path = substr($request_path, 0, $query_pos); + } + $base_path_len = strlen(trim(dirname($_SERVER['SCRIPT_NAME']), '\,/')); + $_GET['q'] = substr(urldecode($request_path), $base_path_len + 2); + } + // Enforce E_ALL, but allow users to set levels not part of E_ALL. error_reporting(E_ALL | error_reporting()); diff --git includes/common.inc includes/common.inc index 9f45fec..ed85377 100644 --- includes/common.inc +++ includes/common.inc @@ -2236,7 +2236,7 @@ function url($path = NULL, array $options = array()) { $base = $options['absolute'] ? $options['base_url'] . '/' : base_path(); $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; - $path = drupal_encode_path($prefix . $path); + $path = drupal_urlencode($prefix . $path); if (variable_get('clean_url', '0')) { // With Clean URLs. @@ -3529,35 +3529,19 @@ function drupal_json($var = NULL) { } /** - * Wrapper around urlencode() which avoids Apache quirks. + * Wrapper around rawurlencode(). + * + * For aesthetic reasons slashes are not escaped. * * Should be used when placing arbitrary data in an URL. Note that Drupal paths * are urlencoded() when passed through url() and do not require urlencoding() * of individual components. * - * 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. - * - This function should only be used on paths, not on query string arguments, - * otherwise unwanted double encoding will occur. - * * @param $text * String to encode */ -function drupal_encode_path($text) { - if (variable_get('clean_url', '0')) { - return str_replace(array('%2F', '%26', '%23', '//'), - array('/', '%2526', '%2523', '/%252F'), - rawurlencode($text)); - } - else { - return str_replace('%2F', '/', rawurlencode($text)); - } +function drupal_urlencode($text) { + return str_replace('%2F', '/', rawurlencode($text)); } /** diff --git includes/file.inc includes/file.inc index ecd6536..8cc1d6c 100644 --- includes/file.inc +++ includes/file.inc @@ -868,6 +868,13 @@ function file_unmunge_filename($filename) { * of $basename. */ function file_create_filename($basename, $directory) { + // Strip control characters. + $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename); + if (substr(PHP_OS, 0, 3) == 'WIN') { + // These characters are not allowed in filenames on Windows. + $basename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $basename); + } + // A URI or path may already have a trailing slash or look like "public://". if (substr($directory, -1) == '/') { $separator = ''; diff --git includes/stream_wrappers.inc includes/stream_wrappers.inc index dde5389..912874c 100644 --- includes/stream_wrappers.inc +++ includes/stream_wrappers.inc @@ -580,7 +580,21 @@ class DrupalPublicStreamWrapper extends DrupalLocalStreamWrapper { */ function getExternalUrl() { $path = str_replace('\\', '/', file_uri_target($this->uri)); - return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . $path; + if (substr(PHP_OS, 0, 3) == 'WIN') { + // The filesystem functions in PHP 5 on Windows do not support + // UTF-8-encoded filenames but always assume that filenames are encoded + // in Windows-1252. In order to support filenames containing characters + // not in Windows-1252, Drupal passes UTF-8-encoded filenames to the + // filesystem functions, making PHP treat each octet in the + // UTF-8-encoded string as a Windows-1252 character. This will make the + // filenames look mangled (like viewing an UTF-8-encoded file in a text + // editor without support for UTF-8) when viewed with external programs, + // e.g. Windows Explorer. The web server does not know about this + // convention, so we need to make the URL reflecting the actual filenames + // when using public files. + $path = drupal_convert_to_utf8($path, 'Windows-1252'); + } + return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . drupal_urlencode($path); } } diff --git misc/autocomplete.js misc/autocomplete.js index 4e24a0e..eb007f8 100644 --- misc/autocomplete.js +++ misc/autocomplete.js @@ -275,7 +275,7 @@ Drupal.ACDB.prototype.search = function (searchString) { // Ajax GET request for autocompletion. $.ajax({ type: 'GET', - url: db.uri + '/' + Drupal.encodePath(searchString), + url: db.uri + '/' + Drupal.encodeURIComponent(searchString), dataType: 'json', success: function (matches) { if (typeof matches.status == 'undefined' || matches.status != 0) { diff --git misc/drupal.js misc/drupal.js index f3a7d2b..6a9dc1e 100644 --- misc/drupal.js +++ misc/drupal.js @@ -267,14 +267,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. + * Wrapper around encodeURIComponent(). + * + * For aesthetic reasons slashes are not escaped. */ -Drupal.encodePath = function (item, uri) { +Drupal.encodeURIComponent = 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 modules/simpletest/tests/file.test modules/simpletest/tests/file.test index 5568061..a120f93 100644 --- modules/simpletest/tests/file.test +++ modules/simpletest/tests/file.test @@ -1880,6 +1880,8 @@ class FileDownloadTest extends FileTestCase { function setUp() { parent::setUp('file_test'); + // Clear out any hook calls. + file_test_reset(); } /** @@ -1929,6 +1931,101 @@ 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. + + // Fewer characters are supported on Windows. The unsupported characters + // are replaced with "_" in the filesystem and in the URLs. + if (substr(PHP_OS, 0, 3) == 'WIN') { + $expected_url_public = $base_url . '/' . file_directory_path() . '/' . + '%20-._%7E%21%24%27_%28%29_%40%5B%5D_%26%2B%25%23%2C%3B%3D___' . + '%2523%2525%2526%252B%252F%253F' . + '%C3%83%C2%A9%C3%83%C2%B8%C3%83%C2%AF%C3%90%C2%B2%C3%8E%C2%B2%C3%A4%C2%B8%C2%AD%C3%A5%C5%93%E2%80%B9%C3%A6%E2%80%BA%C2%B8%C3%9B%C5%BE'; + $expected_url_private_clean = $base_url . '/system/files/' . + '%20-._%7E%21%24%27_%28%29_%40%5B%5D_%26%2B%25%23%2C%3B%3D___' . + '%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'; + $expected_url_private_unclean = $base_url . '/?q=system/files/' . + '%20-._%7E%21%24%27_%28%29_%40%5B%5D_%26%2B%25%23%2C%3B%3D___' . + '%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'; + } + else { + $expected_url_public = $base_url . '/' . file_directory_path() . '/' . + '%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'; + $expected_url_private_clean = $base_url . '/system/files/' . + '%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'; + $expected_url_private_unclean = $base_url . '/?q=system/files/' . + '%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, $expected_url_public); + $this->checkUrl('private', '', $basename, $expected_url_private_clean); + $this->checkUrl('private', '', $basename, $expected_url_private_unclean, '0'); + + // "\" works as path separator. +// $this->checkurl('public', 'foo\bar', 'baz', $base_url . '/' . file_directory_path() . '/foo/bar/baz'); +// $this->checkurl('private', 'foo\bar', 'baz', $base_url . '/system/files/foo/bar/baz'); +// $this->checkurl('private', 'foo\bar', 'baz', $base_url . '/?q=system/files/foo/bar/baz', '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 $path to a valid filename, i.e. strip characters not supported + // by the filesystem, and create the file. + $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.') . $directory . ' * ' . $filepath);#. $url . ' ' . $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); + } } /**