From c7936b8785f2af064d30c2a079d8442c8bc4ddf4 Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Wed, 17 Feb 2016 22:51:01 +0000 Subject: [PATCH] Improve the performance of `wp_upload_dir()`: - Cache the output in non-persistent cache. - Cache the result from `wp_mkdir_p()` in persistent cache (when present). - Introduce `wp_get_upload_dir()` for use when not uploading files. It is equivalent to `wp_upload_dir()` but does not check for the existence or create the upload directory. - Change tests to use the non-cached `_wp_upload_dir()`. They change options on the fly (should never be used in production) to simulate different environments. - Introduce `_upload_dir_no_subdir()` and `_upload_dir_https()` to facilitate testing. These use the proper `upload_dir` filter to simulate different environments. Props kovshenin, azaozz. See #34359. git-svn-id: https://develop.svn.wordpress.org/trunk@36565 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 115 ++++++++++++++++------- tests/phpunit/includes/functions.php | 23 +++++ tests/phpunit/tests/media.php | 9 +- tests/phpunit/tests/post/attachments.php | 6 +- tests/phpunit/tests/upload.php | 76 +++++++++------ 5 files changed, 159 insertions(+), 70 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 9ea524d55b..17d60c4865 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -1799,6 +1799,20 @@ function win_is_writable( $path ) { return true; } +/** + * Get uploads directory information. + * + * Same as wp_upload_dir() but "light weight" as it doesn't attempt to create the uploads directory. + * Intended for use in themes, when only 'basedir' and 'baseurl' are needed, generally in all cases when not uploading files. + * + * @since 4.5.0 + * + * @return array See wp_upload_dir() for description. + */ +function wp_get_upload_dir() { + return wp_upload_dir( null, false ); +} + /** * Get an array containing the current upload directory's path and url. * @@ -1824,14 +1838,73 @@ function win_is_writable( $path ) { * 'subdir' - sub directory if uploads use year/month folders option is on. * 'basedir' - path without subdir. * 'baseurl' - URL path without subdir. - * 'error' - set to false. + * 'error' - false or error message. * * @since 2.0.0 + * @uses _wp_upload_dir() * * @param string $time Optional. Time formatted in 'yyyy/mm'. Default null. + * @param bool $create_dir Optional. Whether to check and create the uploads directory. Default true (backwards compatible). + * @param bool $refresh_cache Optional. Whether to refresh the cache. Default false. * @return array See above for description. */ -function wp_upload_dir( $time = null ) { +function wp_upload_dir( $time = null, $create_dir = true, $refresh_cache = false ) { + static $cache = array(); + + $key = sprintf( '%d-%s', get_current_blog_id(), (string) $time ); + + if ( $refresh_cache || empty( $cache[ $key ] ) ) { + $cache[ $key ] = _wp_upload_dir( $time ); + } + + /** + * Filter the uploads directory data. + * + * @since 2.0.0 + * + * @param array $uploads Array of upload directory data with keys of 'path', + * 'url', 'subdir, 'basedir', and 'error'. + */ + $uploads = apply_filters( 'upload_dir', $cache[ $key ] ); + + if ( $create_dir ) { + $path = $uploads['path']; + $tested_paths = wp_cache_get( 'upload_dir_tested_paths' ); + + if ( ! is_array( $tested_paths ) ) { + $tested_paths = array(); + } + + if ( array_key_exists( $path, $tested_paths ) ) { + $uploads['error'] = $tested_paths[ $path ]; + } else { + if ( ! wp_mkdir_p( $path ) ) { + if ( 0 === strpos( $uploads['basedir'], ABSPATH ) ) { + $error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir']; + } else { + $error_path = basename( $uploads['basedir'] ) . $uploads['subdir']; + } + + $uploads['error'] = sprintf( __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), $error_path ); + } + + $tested_paths[ $path ] = $uploads['error']; + wp_cache_set( 'upload_dir_tested_paths', $tested_paths ); + } + } + + return $uploads; +} + +/** + * A non-filtered, non-cached version of wp_upload_dir() that doesn't check the path. + * + * @access private + * + * @param string $time Optional. Time formatted in 'yyyy/mm'. Default null. + * @return array See wp_upload_dir() + */ +function _wp_upload_dir( $time = null ) { $siteurl = get_option( 'siteurl' ); $upload_path = trim( get_option( 'upload_path' ) ); @@ -1920,36 +1993,14 @@ function wp_upload_dir( $time = null ) { $dir .= $subdir; $url .= $subdir; - /** - * Filter the uploads directory data. - * - * @since 2.0.0 - * - * @param array $uploads Array of upload directory data with keys of 'path', - * 'url', 'subdir, 'basedir', and 'error'. - */ - $uploads = apply_filters( 'upload_dir', - array( - 'path' => $dir, - 'url' => $url, - 'subdir' => $subdir, - 'basedir' => $basedir, - 'baseurl' => $baseurl, - 'error' => false, - ) ); - - // Make sure we have an uploads directory. - if ( ! wp_mkdir_p( $uploads['path'] ) ) { - if ( 0 === strpos( $uploads['basedir'], ABSPATH ) ) - $error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir']; - else - $error_path = basename( $uploads['basedir'] ) . $uploads['subdir']; - - $message = sprintf( __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), $error_path ); - $uploads['error'] = $message; - } - - return $uploads; + return array( + 'path' => $dir, + 'url' => $url, + 'subdir' => $subdir, + 'basedir' => $basedir, + 'baseurl' => $baseurl, + 'error' => false, + ); } /** diff --git a/tests/phpunit/includes/functions.php b/tests/phpunit/includes/functions.php index b7b9f87397..1030edee82 100644 --- a/tests/phpunit/includes/functions.php +++ b/tests/phpunit/includes/functions.php @@ -115,3 +115,26 @@ function _wp_die_handler_txt( $message, $title, $args ) { function _set_default_permalink_structure_for_tests() { update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); } + +/** + * Helper used with the `upload_dir` filter to remove the /year/month sub directories from the uploads path and URL. + */ +function _upload_dir_no_subdir( $uploads ) { + $subdir = $uploads['subdir']; + + $uploads['subdir'] = ''; + $uploads['path'] = str_replace( $subdir, '', $uploads['path'] ); + $uploads['url'] = str_replace( $subdir, '', $uploads['url'] ); + + return $uploads; +} + +/** + * Helper used with the `upload_dir` filter to set https upload URL. + */ +function _upload_dir_https( $uploads ) { + $uploads['url'] = str_replace( 'http://', 'https://', $uploads['url'] ); + $uploads['baseurl'] = str_replace( 'http://', 'https://', $uploads['baseurl'] ); + + return $uploads; +} diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 7ea1f4f908..d5ae231c2c 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -895,11 +895,8 @@ EOF; function test_wp_calculate_image_srcset_no_date_uploads() { global $_wp_additional_image_sizes; - // Save the current setting for uploads folders - $uploads_use_yearmonth_folders = get_option( 'uploads_use_yearmonth_folders' ); - // Disable date organized uploads - update_option( 'uploads_use_yearmonth_folders', 0 ); + add_filter( 'upload_dir', '_upload_dir_no_subdir' ); // Make an image. $filename = DIR_TESTDATA . '/images/test-image-large.png'; @@ -937,9 +934,7 @@ EOF; // Remove the attachment wp_delete_attachment( $id ); - - // Leave the uploads option the way you found it. - update_option( 'uploads_use_yearmonth_folders', $uploads_use_yearmonth_folders ); + remove_filter( 'upload_dir', '_upload_dir_no_subdir' ); } /** diff --git a/tests/phpunit/tests/post/attachments.php b/tests/phpunit/tests/post/attachments.php index ccfea3e5bc..3b8fd57f83 100644 --- a/tests/phpunit/tests/post/attachments.php +++ b/tests/phpunit/tests/post/attachments.php @@ -441,9 +441,8 @@ class Tests_Post_Attachments extends WP_UnitTestCase { * @ticket 15928 */ public function test_wp_get_attachment_url_should_force_https_when_administering_over_https_and_siteurl_is_https() { - // Must set the upload_url_path to fake out `wp_upload_dir()`. - $siteurl = get_option( 'siteurl' ); - update_option( 'upload_url_path', set_url_scheme( $siteurl, 'https' ) . '/uploads' ); + // Set https upload URL + add_filter( 'upload_dir', '_upload_dir_https' ); $filename = ( DIR_TESTDATA . '/images/test-image.jpg' ); $contents = file_get_contents( $filename ); @@ -463,6 +462,7 @@ class Tests_Post_Attachments extends WP_UnitTestCase { // Cleanup. $_SERVER['HTTPS'] = $is_ssl ? 'on' : 'off'; set_current_screen( 'front' ); + remove_filter( 'upload_dir', '_upload_dir_https' ); $this->assertSame( set_url_scheme( $url, 'https' ), $url ); } diff --git a/tests/phpunit/tests/upload.php b/tests/phpunit/tests/upload.php index 399d059e2e..8c3189bdcd 100644 --- a/tests/phpunit/tests/upload.php +++ b/tests/phpunit/tests/upload.php @@ -26,22 +26,24 @@ class Tests_Upload extends WP_UnitTestCase { function test_upload_dir_default() { // wp_upload_dir() with default parameters $info = wp_upload_dir(); - $this->assertEquals( get_option( 'siteurl' ) . '/wp-content/uploads/' . gmstrftime('%Y/%m'), $info['url'] ); - $this->assertEquals( ABSPATH . 'wp-content/uploads/' . gmstrftime('%Y/%m'), $info['path'] ); - $this->assertEquals( gmstrftime('/%Y/%m'), $info['subdir'] ); - $this->assertEquals( '', $info['error'] ); + $subdir = gmstrftime('/%Y/%m'); + + $this->assertEquals( get_option( 'siteurl' ) . '/wp-content/uploads' . $subdir, $info['url'] ); + $this->assertEquals( ABSPATH . 'wp-content/uploads' . $subdir, $info['path'] ); + $this->assertEquals( $subdir, $info['subdir'] ); + $this->assertEquals( false, $info['error'] ); } function test_upload_dir_relative() { // wp_upload_dir() with a relative upload path that is not 'wp-content/uploads' update_option( 'upload_path', 'foo/bar' ); - $info = wp_upload_dir(); - $this->delete_folders( ABSPATH . 'foo' ); + $info = _wp_upload_dir(); + $subdir = gmstrftime('/%Y/%m'); - $this->assertEquals( get_option( 'siteurl' ) . '/foo/bar/' . gmstrftime('%Y/%m'), $info['url'] ); - $this->assertEquals( ABSPATH . 'foo/bar/' . gmstrftime('%Y/%m'), $info['path'] ); - $this->assertEquals( gmstrftime('/%Y/%m'), $info['subdir'] ); - $this->assertEquals( '', $info['error'] ); + $this->assertEquals( get_option( 'siteurl' ) . '/foo/bar' . $subdir, $info['url'] ); + $this->assertEquals( ABSPATH . 'foo/bar' . $subdir, $info['path'] ); + $this->assertEquals( $subdir, $info['subdir'] ); + $this->assertEquals( false, $info['error'] ); } /** @@ -49,45 +51,63 @@ class Tests_Upload extends WP_UnitTestCase { */ function test_upload_dir_absolute() { $path = '/tmp/wp-unit-test'; + // wp_upload_dir() with an absolute upload path update_option( 'upload_path', $path ); + // doesn't make sense to use an absolute file path without setting the url path update_option( 'upload_url_path', '/baz' ); - $info = wp_upload_dir(); - $this->delete_folders( $path ); - $this->assertEquals( '/baz/' . gmstrftime('%Y/%m'), $info['url'] ); - $this->assertEquals( "$path/" . gmstrftime('%Y/%m'), $info['path'] ); - $this->assertEquals( gmstrftime('/%Y/%m'), $info['subdir'] ); - $this->assertEquals( '', $info['error'] ); + // Use `_wp_upload_dir()` directly to bypass caching and work with the changed options. + // It doesn't create the /year/month directories. + $info = _wp_upload_dir(); + $subdir = gmstrftime('/%Y/%m'); + + $this->assertEquals( '/baz' . $subdir, $info['url'] ); + $this->assertEquals( $path . $subdir, $info['path'] ); + $this->assertEquals( $subdir, $info['subdir'] ); + $this->assertEquals( false, $info['error'] ); } function test_upload_dir_no_yearnum() { update_option( 'uploads_use_yearmonth_folders', 0 ); - $info = wp_upload_dir(); + + // Use `_wp_upload_dir()` directly to bypass caching and work with the changed options. + $info = _wp_upload_dir(); + $this->assertEquals( get_option( 'siteurl' ) . '/wp-content/uploads', $info['url'] ); $this->assertEquals( ABSPATH . 'wp-content/uploads', $info['path'] ); $this->assertEquals( '', $info['subdir'] ); - $this->assertEquals( '', $info['error'] ); + $this->assertEquals( false, $info['error'] ); } function test_upload_path_absolute() { update_option( 'upload_url_path', 'http://example.org/asdf' ); - $info = wp_upload_dir(); - $this->assertEquals( 'http://example.org/asdf/' . gmstrftime('%Y/%m'), $info['url'] ); - $this->assertEquals( ABSPATH . 'wp-content/uploads/' . gmstrftime('%Y/%m'), $info['path'] ); - $this->assertEquals( gmstrftime('/%Y/%m'), $info['subdir'] ); - $this->assertEquals( '', $info['error'] ); + + // Use `_wp_upload_dir()` directly to bypass caching and work with the changed options. + // It doesn't create the /year/month directories. + $info = _wp_upload_dir(); + $subdir = gmstrftime('/%Y/%m'); + + $this->assertEquals( 'http://example.org/asdf' . $subdir, $info['url'] ); + $this->assertEquals( ABSPATH . 'wp-content/uploads' . $subdir, $info['path'] ); + $this->assertEquals( $subdir, $info['subdir'] ); + $this->assertEquals( false, $info['error'] ); } function test_upload_dir_empty() { // upload path setting is empty - it should default to 'wp-content/uploads' update_option('upload_path', ''); - $info = wp_upload_dir(); - $this->assertEquals( get_option( 'siteurl' ) . '/wp-content/uploads/' . gmstrftime('%Y/%m'), $info['url'] ); - $this->assertEquals( ABSPATH . 'wp-content/uploads/' . gmstrftime('%Y/%m'), $info['path'] ); - $this->assertEquals( gmstrftime('/%Y/%m'), $info['subdir'] ); - $this->assertEquals( '', $info['error'] ); + + // Use `_wp_upload_dir()` directly to bypass caching and work with the changed options. + // It doesn't create the /year/month directories. + $info = _wp_upload_dir(); + $subdir = gmstrftime('/%Y/%m'); + + $this->assertEquals( get_option( 'siteurl' ) . '/wp-content/uploads' . $subdir, $info['url'] ); + $this->assertEquals( ABSPATH . 'wp-content/uploads' . $subdir, $info['path'] ); + $this->assertEquals( $subdir, $info['subdir'] ); + $this->assertEquals( false, $info['error'] ); } }