diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php index 6bb450ee1b..ea73d8650d 100644 --- a/src/wp-admin/includes/theme.php +++ b/src/wp-admin/includes/theme.php @@ -1166,11 +1166,14 @@ function resume_theme( $theme, $redirect = '' ) { * creating a fatal error. */ if ( ! empty( $redirect ) ) { + $stylesheet_path = get_stylesheet_directory(); + $template_path = get_template_directory(); + $functions_path = ''; - if ( str_contains( STYLESHEETPATH, $extension ) ) { - $functions_path = STYLESHEETPATH . '/functions.php'; - } elseif ( str_contains( TEMPLATEPATH, $extension ) ) { - $functions_path = TEMPLATEPATH . '/functions.php'; + if ( str_contains( $stylesheet_path, $extension ) ) { + $functions_path = $stylesheet_path . '/functions.php'; + } elseif ( str_contains( $template_path, $extension ) ) { + $functions_path = $template_path . '/functions.php'; } if ( ! empty( $functions_path ) ) { diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index 03f450dc9b..a80dc60e41 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -1381,7 +1381,7 @@ function wp_comment_form_unfiltered_html_nonce() { * and the post ID respectively. * * The `$file` path is passed through a filter hook called {@see 'comments_template'}, - * which includes the TEMPLATEPATH and $file combined. Tries the $filtered path + * which includes the template directory and $file combined. Tries the $filtered path * first and if it fails it will require the default comment template from the * default theme. If either does not exist, then the WordPress process will be * halted. It is advised for that reason, that the default theme is not deleted. @@ -1600,7 +1600,10 @@ function comments_template( $file = '/comments.php', $separate_comments = false define( 'COMMENTS_TEMPLATE', true ); } - $theme_template = STYLESHEETPATH . $file; + $stylesheet_path = get_stylesheet_directory(); + $template_path = get_template_directory(); + + $theme_template = $stylesheet_path . $file; /** * Filters the path to the theme template file used for the comments template. @@ -1613,8 +1616,8 @@ function comments_template( $file = '/comments.php', $separate_comments = false if ( file_exists( $include ) ) { require $include; - } elseif ( file_exists( TEMPLATEPATH . $file ) ) { - require TEMPLATEPATH . $file; + } elseif ( file_exists( $template_path . $file ) ) { + require $template_path . $file; } else { // Backward compat code will be removed in a future release. require ABSPATH . WPINC . '/theme-compat/comments.php'; } diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index 6e64e789a0..0c0ee771eb 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -407,6 +407,8 @@ function wp_templating_constants() { * Filesystem path to the current active template directory. * * @since 1.5.0 + * @deprecated 6.4.0 Use get_template_directory() instead. + * @see get_template_directory() */ define( 'TEMPLATEPATH', get_template_directory() ); @@ -414,6 +416,8 @@ function wp_templating_constants() { * Filesystem path to the current active template stylesheet directory. * * @since 2.1.0 + * @deprecated 6.4.0 Use get_stylesheet_directory() instead. + * @see get_stylesheet_directory() */ define( 'STYLESHEETPATH', get_stylesheet_directory() ); diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 8f18a3a6e9..77e17b3f8b 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -1049,11 +1049,14 @@ function wp_get_active_and_valid_themes() { return $themes; } - if ( TEMPLATEPATH !== STYLESHEETPATH ) { - $themes[] = STYLESHEETPATH; + $stylesheet_path = get_stylesheet_directory(); + $template_path = get_template_directory(); + + if ( $template_path !== $stylesheet_path ) { + $themes[] = $stylesheet_path; } - $themes[] = TEMPLATEPATH; + $themes[] = $template_path; /* * Remove themes from the list of active themes when we're on an endpoint diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index afc2ca6edf..989d630f7f 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -684,8 +684,9 @@ function get_attachment_template() { /** * Retrieves the name of the highest priority template file that exists. * - * Searches in the STYLESHEETPATH before TEMPLATEPATH and wp-includes/theme-compat - * so that themes which inherit from a parent theme can just overload one file. + * Searches in the stylesheet directory before the template directory and + * wp-includes/theme-compat so that themes which inherit from a parent theme + * can just overload one file. * * @since 2.7.0 * @since 5.5.0 The `$args` parameter was added. @@ -699,16 +700,20 @@ function get_attachment_template() { * @return string The template filename if one is located. */ function locate_template( $template_names, $load = false, $load_once = true, $args = array() ) { + $stylesheet_path = get_stylesheet_directory(); + $template_path = get_template_directory(); + $is_child_theme = $stylesheet_path !== $template_path; + $located = ''; foreach ( (array) $template_names as $template_name ) { if ( ! $template_name ) { continue; } - if ( file_exists( STYLESHEETPATH . '/' . $template_name ) ) { - $located = STYLESHEETPATH . '/' . $template_name; + if ( file_exists( $stylesheet_path . '/' . $template_name ) ) { + $located = $stylesheet_path . '/' . $template_name; break; - } elseif ( is_child_theme() && file_exists( TEMPLATEPATH . '/' . $template_name ) ) { - $located = TEMPLATEPATH . '/' . $template_name; + } elseif ( $is_child_theme && file_exists( $template_path . '/' . $template_name ) ) { + $located = $template_path . '/' . $template_name; break; } elseif ( file_exists( ABSPATH . WPINC . '/theme-compat/' . $template_name ) ) { $located = ABSPATH . WPINC . '/theme-compat/' . $template_name; diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 5ff31953b1..6be2ad700a 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -157,7 +157,7 @@ function wp_clean_themes_cache( $clear_update_cache = true ) { * @return bool True if a child theme is in use, false otherwise. */ function is_child_theme() { - return ( TEMPLATEPATH !== STYLESHEETPATH ); + return get_template_directory() !== get_stylesheet_directory(); } /** @@ -187,24 +187,40 @@ function get_stylesheet() { * Retrieves stylesheet directory path for the active theme. * * @since 1.5.0 + * @since 6.4.0 Memoizes filter execution so that it only runs once for the current theme. + * + * @global string $wp_stylesheet_path Current theme stylesheet directory path. * * @return string Path to active theme's stylesheet directory. */ function get_stylesheet_directory() { - $stylesheet = get_stylesheet(); - $theme_root = get_theme_root( $stylesheet ); - $stylesheet_dir = "$theme_root/$stylesheet"; + global $wp_stylesheet_path; - /** - * Filters the stylesheet directory path for the active theme. - * - * @since 1.5.0 - * - * @param string $stylesheet_dir Absolute path to the active theme. - * @param string $stylesheet Directory name of the active theme. - * @param string $theme_root Absolute path to themes directory. - */ - return apply_filters( 'stylesheet_directory', $stylesheet_dir, $stylesheet, $theme_root ); + if ( null === $wp_stylesheet_path ) { + $stylesheet = get_stylesheet(); + $theme_root = get_theme_root( $stylesheet ); + $stylesheet_dir = "$theme_root/$stylesheet"; + + /** + * Filters the stylesheet directory path for the active theme. + * + * @since 1.5.0 + * + * @param string $stylesheet_dir Absolute path to the active theme. + * @param string $stylesheet Directory name of the active theme. + * @param string $theme_root Absolute path to themes directory. + */ + $stylesheet_dir = apply_filters( 'stylesheet_directory', $stylesheet_dir, $stylesheet, $theme_root ); + + // If there are filter callbacks, force the logic to execute on every call. + if ( has_filter( 'stylesheet' ) || has_filter( 'theme_root' ) || has_filter( 'stylesheet_directory' ) ) { + return $stylesheet_dir; + } + + $wp_stylesheet_path = $stylesheet_dir; + } + + return $wp_stylesheet_path; } /** @@ -321,24 +337,40 @@ function get_template() { * Retrieves template directory path for the active theme. * * @since 1.5.0 + * @since 6.4.0 Memoizes filter execution so that it only runs once for the current theme. + * + * @global string $wp_template_path Current theme template directory path. * * @return string Path to active theme's template directory. */ function get_template_directory() { - $template = get_template(); - $theme_root = get_theme_root( $template ); - $template_dir = "$theme_root/$template"; + global $wp_template_path; - /** - * Filters the active theme directory path. - * - * @since 1.5.0 - * - * @param string $template_dir The path of the active theme directory. - * @param string $template Directory name of the active theme. - * @param string $theme_root Absolute path to the themes directory. - */ - return apply_filters( 'template_directory', $template_dir, $template, $theme_root ); + if ( null === $wp_template_path ) { + $template = get_template(); + $theme_root = get_theme_root( $template ); + $template_dir = "$theme_root/$template"; + + /** + * Filters the active theme directory path. + * + * @since 1.5.0 + * + * @param string $template_dir The path of the active theme directory. + * @param string $template Directory name of the active theme. + * @param string $theme_root Absolute path to the themes directory. + */ + $template_dir = apply_filters( 'template_directory', $template_dir, $template, $theme_root ); + + // If there are filter callbacks, force the logic to execute on every call. + if ( has_filter( 'template' ) || has_filter( 'theme_root' ) || has_filter( 'template_directory' ) ) { + return $template_dir; + } + + $wp_template_path = $template_dir; + } + + return $wp_template_path; } /** @@ -744,11 +776,13 @@ function locale_stylesheet() { * @global WP_Customize_Manager $wp_customize * @global array $sidebars_widgets * @global array $wp_registered_sidebars + * @global string $wp_stylesheet_path + * @global string $wp_template_path * * @param string $stylesheet Stylesheet name. */ function switch_theme( $stylesheet ) { - global $wp_theme_directories, $wp_customize, $sidebars_widgets, $wp_registered_sidebars; + global $wp_theme_directories, $wp_customize, $sidebars_widgets, $wp_registered_sidebars, $wp_stylesheet_path, $wp_template_path; $requirements = validate_theme_requirements( $stylesheet ); if ( is_wp_error( $requirements ) ) { @@ -832,6 +866,13 @@ function switch_theme( $stylesheet ) { update_option( 'theme_switched', $old_theme->get_stylesheet() ); + /* + * Reset globals to force refresh the next time these directories are + * accessed via `get_stylesheet_directory()` / `get_template_directory()`. + */ + $wp_stylesheet_path = null; + $wp_template_path = null; + /** * Fires after the theme is switched. * diff --git a/tests/phpunit/tests/comment/commentsTemplate.php b/tests/phpunit/tests/comment/commentsTemplate.php index 7c27bf1d43..cb033a55e3 100644 --- a/tests/phpunit/tests/comment/commentsTemplate.php +++ b/tests/phpunit/tests/comment/commentsTemplate.php @@ -9,6 +9,14 @@ */ class Tests_Comment_CommentsTemplate extends WP_UnitTestCase { + /** + * Performs setup tasks for every test. + */ + public function set_up() { + parent::set_up(); + switch_theme( 'default' ); + } + /** * @ticket 8071 */ diff --git a/tests/phpunit/tests/comment/wpListComments.php b/tests/phpunit/tests/comment/wpListComments.php index f49ef84561..8997b681f7 100644 --- a/tests/phpunit/tests/comment/wpListComments.php +++ b/tests/phpunit/tests/comment/wpListComments.php @@ -6,6 +6,15 @@ * @covers ::wp_list_comments */ class Tests_Comment_WpListComments extends WP_UnitTestCase { + + /** + * Performs setup tasks for every test. + */ + public function set_up() { + parent::set_up(); + switch_theme( 'default' ); + } + /** * @ticket 35175 */ diff --git a/tests/phpunit/tests/general/template.php b/tests/phpunit/tests/general/template.php index 8a29853526..e5b88d7d4d 100644 --- a/tests/phpunit/tests/general/template.php +++ b/tests/phpunit/tests/general/template.php @@ -10,6 +10,7 @@ require_once ABSPATH . 'wp-admin/includes/class-wp-site-icon.php'; class Tests_General_Template extends WP_UnitTestCase { + protected $wp_site_icon; public $site_icon_id; public $site_icon_url; @@ -41,6 +42,7 @@ class Tests_General_Template extends WP_UnitTestCase { public function set_up() { parent::set_up(); + switch_theme( 'default' ); $this->wp_site_icon = new WP_Site_Icon(); } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 3ae0bf31b8..82505bba8e 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -629,6 +629,44 @@ class Tests_Template extends WP_UnitTestCase { ); } + /** + * Tests that `locate_template()` uses the current theme even after switching the theme. + * + * @ticket 18298 + * + * @covers ::locate_template + */ + public function test_locate_template_uses_current_theme() { + $themes = wp_get_themes(); + + // Look for parent themes with an index.php template. + $relevant_themes = array(); + foreach ( $themes as $theme ) { + if ( $theme->get_stylesheet() !== $theme->get_template() ) { + continue; + } + $php_templates = $theme['Template Files']; + if ( ! isset( $php_templates['index.php'] ) ) { + continue; + } + $relevant_themes[] = $theme; + } + if ( count( $relevant_themes ) < 2 ) { + $this->markTestSkipped( 'Test requires at least two parent themes with an index.php template.' ); + } + + $template_names = array( 'index.php' ); + + $old_theme = $relevant_themes[0]; + $new_theme = $relevant_themes[1]; + + switch_theme( $old_theme->get_stylesheet() ); + $this->assertSame( $old_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in initial theme.' ); + + switch_theme( $new_theme->get_stylesheet() ); + $this->assertSame( $new_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in theme after switch.' ); + } + public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { $this->go_to( $url ); $hierarchy = $this->get_template_hierarchy(); diff --git a/tests/phpunit/tests/theme.php b/tests/phpunit/tests/theme.php index 378ed16093..b93edfaf63 100644 --- a/tests/phpunit/tests/theme.php +++ b/tests/phpunit/tests/theme.php @@ -36,8 +36,9 @@ class Tests_Theme extends WP_UnitTestCase { parent::set_up(); + // Sets up the `wp-content/themes/` directory to ensure consistency when running tests. $this->orig_theme_dir = $wp_theme_directories; - $wp_theme_directories = array( WP_CONTENT_DIR . '/themes' ); + $wp_theme_directories = array( WP_CONTENT_DIR . '/themes', realpath( DIR_TESTDATA . '/themedir1' ) ); add_filter( 'extra_theme_headers', array( $this, 'theme_data_extra_headers' ) ); wp_clean_themes_cache(); @@ -282,6 +283,11 @@ class Tests_Theme extends WP_UnitTestCase { for ( $i = 0; $i < 3; $i++ ) { foreach ( $themes as $name => $theme ) { + // Skip invalid theme directory names (such as `block_theme-[0.4.0]`). + if ( ! preg_match( '/^[a-z0-9-]+$/', $theme['Stylesheet'] ) ) { + continue; + } + // Switch to this theme. if ( 2 === $i ) { switch_theme( $theme['Template'], $theme['Stylesheet'] ); @@ -289,16 +295,16 @@ class Tests_Theme extends WP_UnitTestCase { switch_theme( $theme['Stylesheet'] ); } - $this->assertSame( $name, get_current_theme() ); + $this->assertSame( $theme['Name'], get_current_theme() ); // Make sure the various get_* functions return the correct values. $this->assertSame( $theme['Template'], get_template() ); $this->assertSame( $theme['Stylesheet'], get_stylesheet() ); - $root_fs = get_theme_root(); + $root_fs = $theme->get_theme_root(); $this->assertTrue( is_dir( $root_fs ) ); - $root_uri = get_theme_root_uri(); + $root_uri = $theme->get_theme_root_uri(); $this->assertNotEmpty( $root_uri ); $this->assertSame( $root_fs . '/' . get_stylesheet(), get_stylesheet_directory() ); @@ -309,19 +315,38 @@ class Tests_Theme extends WP_UnitTestCase { $this->assertSame( $root_fs . '/' . get_template(), get_template_directory() ); $this->assertSame( $root_uri . '/' . get_template(), get_template_directory_uri() ); - // get_query_template() + // Skip block themes for get_query_template() tests since this test is focused on classic templates. + if ( wp_is_block_theme() && current_theme_supports( 'block-templates' ) ) { + continue; + } // Template file that doesn't exist. $this->assertSame( '', get_query_template( 'nonexistant' ) ); // Template files that do exist. - /* foreach ( $theme['Template Files'] as $path ) { - $file = basename($path, '.php'); - FIXME: untestable because get_query_template() uses TEMPLATEPATH. - $this->assertSame('', get_query_template($file)); + $file = basename( $path, '.php' ); + + // The functions.php file is not a template. + if ( 'functions' === $file ) { + continue; + } + + // Underscores are not supported by `locate_template()`. + if ( 'taxonomy-post_format' === $file ) { + $file = 'taxonomy'; + } + + $child_theme_file = get_stylesheet_directory() . '/' . $file . '.php'; + $parent_theme_file = get_template_directory() . '/' . $file . '.php'; + if ( file_exists( $child_theme_file ) ) { + $this->assertSame( $child_theme_file, get_query_template( $file ) ); + } elseif ( file_exists( $parent_theme_file ) ) { + $this->assertSame( $parent_theme_file, get_query_template( $file ) ); + } else { + $this->assertSame( '', get_query_template( $file ) ); + } } - */ // These are kind of tautologies but at least exercise the code. $this->assertSame( get_404_template(), get_query_template( '404' ) ); @@ -854,6 +879,184 @@ class Tests_Theme extends WP_UnitTestCase { ); } + /** + * Tests that a theme in the custom test data theme directory is recognized. + * + * @ticket 18298 + */ + public function test_theme_in_custom_theme_dir_is_valid() { + switch_theme( 'block-theme' ); + $this->assertTrue( wp_get_theme()->exists() ); + } + + /** + * Tests that `is_child_theme()` returns true for child theme. + * + * @ticket 18298 + * + * @covers ::is_child_theme + */ + public function test_is_child_theme_true() { + switch_theme( 'block-theme-child' ); + $this->assertTrue( is_child_theme() ); + } + + /** + * Tests that `is_child_theme()` returns false for parent theme. + * + * @ticket 18298 + * + * @covers ::is_child_theme + */ + public function test_is_child_theme_false() { + switch_theme( 'block-theme' ); + $this->assertFalse( is_child_theme() ); + } + + /** + * Tests that the child theme directory is correctly detected. + * + * @ticket 18298 + * + * @covers ::get_stylesheet_directory + */ + public function test_get_stylesheet_directory() { + switch_theme( 'block-theme-child' ); + $this->assertSame( realpath( DIR_TESTDATA ) . '/themedir1/block-theme-child', get_stylesheet_directory() ); + } + + /** + * Tests that the parent theme directory is correctly detected. + * + * @ticket 18298 + * + * @covers ::get_template_directory + */ + public function test_get_template_directory() { + switch_theme( 'block-theme-child' ); + $this->assertSame( realpath( DIR_TESTDATA ) . '/themedir1/block-theme', get_template_directory() ); + } + + /** + * Tests that get_stylesheet_directory() behaves correctly with filters. + * + * @ticket 18298 + * @dataProvider data_get_stylesheet_directory_with_filter + * + * @covers ::get_stylesheet_directory + * + * @param string $theme Theme slug / directory name. + * @param string $hook_name Filter hook name. + * @param callable $callback Filter callback. + * @param string $expected Expected stylesheet directory with the filter active. + */ + public function test_get_stylesheet_directory_with_filter( $theme, $hook_name, $callback, $expected ) { + switch_theme( $theme ); + + // Add filter, then call get_stylesheet_directory() to compute value. + add_filter( $hook_name, $callback ); + $this->assertSame( $expected, get_stylesheet_directory(), 'Stylesheet directory returned incorrect result not considering filters' ); + + // Remove filter again, then ensure result is recalculated and not the same as before. + remove_filter( $hook_name, $callback ); + $this->assertNotSame( $expected, get_stylesheet_directory(), 'Stylesheet directory returned previous value even though filters were removed' ); + } + + /** + * Data provider for `test_get_stylesheet_directory_with_filter()`. + * + * @return array[] + */ + public function data_get_stylesheet_directory_with_filter() { + return array( + 'with stylesheet_directory filter' => array( + 'block-theme', + 'stylesheet_directory', + static function ( $dir ) { + return str_replace( realpath( DIR_TESTDATA ) . '/themedir1', '/fantasy-dir', $dir ); + }, + '/fantasy-dir/block-theme', + ), + 'with theme_root filter' => array( + 'block-theme', + 'theme_root', + static function () { + return '/fantasy-dir'; + }, + '/fantasy-dir/block-theme', + ), + 'with stylesheet filter' => array( + 'block-theme', + 'stylesheet', + static function () { + return 'another-theme'; + }, + // Because the theme does not exist, `get_theme_root()` returns the default themes directory. + WP_CONTENT_DIR . '/themes/another-theme', + ), + ); + } + + /** + * Tests that get_template_directory() behaves correctly with filters. + * + * @ticket 18298 + * @dataProvider data_get_template_directory_with_filter + * + * @covers ::get_template_directory + * + * @param string $theme Theme slug / directory name. + * @param string $hook_name Filter hook name. + * @param callable $callback Filter callback. + * @param string $expected Expected template directory with the filter active. + */ + public function test_get_template_directory_with_filter( $theme, $hook_name, $callback, $expected ) { + switch_theme( $theme ); + + // Add filter, then call get_template_directory() to compute value. + add_filter( $hook_name, $callback ); + $this->assertSame( $expected, get_template_directory(), 'Template directory returned incorrect result not considering filters' ); + + // Remove filter again, then ensure result is recalculated and not the same as before. + remove_filter( $hook_name, $callback ); + $this->assertNotSame( $expected, get_template_directory(), 'Template directory returned previous value even though filters were removed' ); + } + + /** + * Data provider for `test_get_template_directory_with_filter()`. + * + * @return array[] + */ + public function data_get_template_directory_with_filter() { + return array( + 'with template_directory filter' => array( + 'block-theme', + 'template_directory', + static function ( $dir ) { + return str_replace( realpath( DIR_TESTDATA ) . '/themedir1', '/fantasy-dir', $dir ); + }, + '/fantasy-dir/block-theme', + ), + 'with theme_root filter' => array( + 'block-theme', + 'theme_root', + static function () { + return '/fantasy-dir'; + }, + '/fantasy-dir/block-theme', + ), + 'with template filter' => array( + 'block-theme', + 'template', + static function () { + return 'another-theme'; + }, + // Because the theme does not exist, `get_theme_root()` returns the default themes directory. + WP_CONTENT_DIR . '/themes/another-theme', + ), + ); + } + /** * Helper function to ensure that a block theme is available and active. */