Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.187 diff -u -F^f -r1.187 file.inc --- includes/file.inc 25 Aug 2009 21:53:47 -0000 1.187 +++ includes/file.inc 27 Aug 2009 20:40:06 -0000 @@ -285,13 +285,28 @@ function file_stream_wrapper_get_instanc } /** - * Creates the web accessible URL to a stream. + * Creates the web accessible URL to a stream, which may be an external file + * or a local file (i.e. a file in Drupal). A local file can be either a + * shipped or a created file. * * Compatibility: normal paths and stream wrappers. * @see http://drupal.org/node/515192 * + * There are two kinds of local files: + * - "created files", i.e. those in the files directory (which is stored in + * the file_directory_path variable and can be retrieved using + * file_directory_path()). These are files that have either been uploaded by + * users or were generated automatically (for example through CSS + * aggregation). + * - "shipped files", i.e. those outside of the files directory, which ship as + * part of Drupal core or contributed modules or themes. + * When a hook_file_url_alter() function is defined and overrides the path, + * then that rewritten path is used instead of creating a URL for the file at + * the given path. + * * @param $uri - * The URI to for which we need an external URL. + * The URI to a file for which we need an external URL, or the path to a + * shipped file. * @return * A string containing a URL that may be used to access the file. * If the provided string already contains a preceding 'http', nothing is done @@ -299,11 +314,29 @@ function file_stream_wrapper_get_instanc * found to generate an external URL, then FALSE will be returned. */ function file_create_url($uri) { + $old_uri = $uri; + + drupal_alter('file_url', $uri); + + // If any module has altered the path, then return the alteration, which + // must be a valid URL. + if ($uri != $old_uri) { + return $uri; + } + + // Otherwise serve the file from Drupal's web server. This point will only + // be reached when either no hook_file_url_alter() implementations are + // defined, or when that function returns FALSE, thereby indicating that + // it cannot (or doesn't wish to) rewrite the URL. This is typically + // because the file doesn't match some conditions to be served from a CDN + // or static file server, or because the file has not yet been synced to + // the CDN or static file server. + $scheme = file_uri_scheme($uri); if (!$scheme) { - // If this is not a properly formatted stream return the URI with the base - // url prepended. + // If this is not a properly formatted stream, then it is a shipped file. + // Therefor, return the URI with the base URL prepended. return $GLOBALS['base_url'] . '/' . $uri; } elseif ($scheme == 'http' || $scheme == 'https') { @@ -320,9 +353,6 @@ function file_create_url($uri) { return FALSE; } } - - // @todo Implement CDN integration hook stuff in this function. - // @see http://drupal.org/node/499156 } /** Index: modules/simpletest/tests/file.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/file.test,v retrieving revision 1.40 diff -u -F^f -r1.40 file.test --- modules/simpletest/tests/file.test 19 Aug 2009 08:38:09 -0000 1.40 +++ modules/simpletest/tests/file.test 27 Aug 2009 20:40:10 -0000 @@ -1879,6 +1879,26 @@ function file_test_file_scan_callback_re } /** + * 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); + $this->assertEqual($GLOBALS['base_url'] . '/' . file_directory_path() . '/' . $file->filename, $url, t('Correctly generated a URL for a created file.')); + $this->drupalHead($url); + $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the created file.')); + + // Test generating an URL to a shipped file (i.e. a file that is part of + // Drupal core, a module or a theme, for example a JavaScript file). + $filepath = 'misc/jquery.js'; + $url = file_create_url($filepath); + $this->assertEqual($GLOBALS['base_url'] . '/' . $filepath, $url, t('Correctly generated a URL for a shipped file.')); + $this->drupalHead($url); + $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.')); + } + + /** * Test the private file transfer system. */ function testPrivateFileTransfer() { @@ -1908,6 +1928,47 @@ function file_test_file_scan_callback_re } /** + * Tests for file URL rewriting. + */ +class FileURLRewritingTest extends FileTestCase { + public static function getInfo() { + return array( + 'name' => t('File URL rewriting'), + 'description' => t('Tests for file URL rewriting.'), + 'group' => t('File'), + ); + } + + function setUp() { + parent::setUp('file_test', 'file_url_test'); + } + + /** + * Test the generating of rewritten shipped file URLs. + */ + function testShippedFileURL() { + // Test generating an URL to a shipped file (i.e. a file that is part of + // Drupal core, a module or a theme, for example a JavaScript file). + $filepath = 'misc/jquery.js'; + $url = file_create_url($filepath); + $this->assertEqual(FILE_URL_TEST_CDN_1 . '/' . $filepath, $url, t('Correctly generated a URL for a shipped file.')); + $filepath = 'misc/favicon.ico'; + $url = file_create_url($filepath); + $this->assertEqual(FILE_URL_TEST_CDN_2 . '/' . $filepath, $url, t('Correctly generated a URL for a shipped file.')); + } + + /** + * Test the generating of rewritten public created file URLs. + */ + function testPublicCreatedFileURL() { + // Test generating an URL to a created file. + $file = $this->createFile(); + $url = file_create_url($file->uri); + $this->assertEqual(FILE_URL_TEST_CDN_2 . '/' . file_directory_path() . '/' . $file->filename, $url, t('Correctly generated a URL for a created file.')); + } +} + +/** * Tests for file_munge_filename() and file_unmunge_filename(). */ class FileNameMungingTest extends FileTestCase { Index: modules/simpletest/tests/file_url_test.info =================================================================== RCS file: modules/simpletest/tests/file_url_test.info diff -N modules/simpletest/tests/file_url_test.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/file_url_test.info 27 Aug 2009 20:40:10 -0000 @@ -0,0 +1,8 @@ +; $Id$ +name = "File URL test" +description = "Support module for file URL rewrite tests." +package = Testing +version = VERSION +core = 7.x +files[] = file_url_test.module +hidden = TRUE Index: modules/simpletest/tests/file_url_test.module =================================================================== RCS file: modules/simpletest/tests/file_url_test.module diff -N modules/simpletest/tests/file_url_test.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/file_url_test.module 27 Aug 2009 20:40:10 -0000 @@ -0,0 +1,53 @@ +getDirectoryPath() . '/' . file_uri_target($uri); + } + + // Clean up Windows paths. + $path = str_replace('\\', '/', $path); + + // Serve files with one of the CDN extensions from CDN 1, all others from + // CDN 2. + $pathinfo = pathinfo($path); + if (array_key_exists('extension', $pathinfo) && in_array($pathinfo['extension'], $cdn_extensions)) { + $uri = FILE_URL_TEST_CDN_1 . '/' . $path; + } + else { + $uri = FILE_URL_TEST_CDN_2 . '/' . $path; + } + } +} Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.65 diff -u -F^f -r1.65 system.api.php --- modules/system/system.api.php 26 Aug 2009 03:09:12 -0000 1.65 +++ modules/system/system.api.php 27 Aug 2009 20:40:12 -0000 @@ -1434,6 +1434,67 @@ function hook_file_download($filepath) { } /** + * Alter the URL to a file. + * + * This hook is called from file_create_url(), and is called fairly + * frequently (10+ times per page), depending on how many files there are in a + * given page. + * If CSS and JS aggregation are disabled, this can become very frequently + * (50+ times per page) so performance is critical. + * + * This function should alter the URI, if it wants to rewrite the file URL. + * If it does so, no other hook_file_url_alter() implementation will be + * allowed to further alter the path. + * + * @param $uri + * The URI to a file for which we need an external URL, or the path to a + * shipped file. + */ +function hook_file_url_alter(&$uri) { + global $user; + + // User 1 will always see the local file in this example. + if ($user->uid == 1) { + return; + } + + $cdn1 = 'http://cdn1.example.com'; + $cdn2 = 'http://cdn2.example.com'; + $cdn_extensions = array('css', 'js', 'gif', 'jpg', 'jpeg', 'png'); + + // Most CDNs don't support private file transfers without a lot of hassle, + // so don't support this in the common case. + $schemes = array('public'); + + $scheme = file_uri_scheme($uri); + + // Only serve shipped files and public created files from the CDN. + if (!$scheme || in_array($scheme, $schemes)) { + // Shipped files. + if (!$scheme) { + $path = $uri; + } + // Public created files. + else { + $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); + $path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri); + } + + // Clean up Windows paths. + $path = str_replace('\\', '/', $path); + + // Serve files with one of the CDN extensions from CDN 1, all others from + // CDN 2. + $pathinfo = pathinfo($path); + if (array_key_exists('extension', $pathinfo) && in_array($pathinfo['extension'], $cdn_extensions)) { + $uri = $cdn1 . '/' . $path; + } + else { + $uri = $cdn2 . '/' . $path; + } + } +} + /** * Check installation requirements and do status reporting. * * This hook has two closely related uses, determined by the $phase argument: