diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index c5953e1d4ce2c..6c15135f6a361 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -507,6 +507,73 @@ function _remove_theme_attribute_from_template_part_block( &$block ) { } } +/** + * Gets the contents for the given template file path. + * + * @since 6.4.0 + * @access private + * + * @param string $template_file_path Absolute path to a theme template file. + * @return string The template file contents. + */ +function _get_block_template_file_content( $template_file_path ) { + $theme = wp_get_theme(); + + $template_file_path = wp_normalize_path( $template_file_path ); + $theme_dir = wp_normalize_path( $theme->get_stylesheet_directory() ) . '/'; + + if ( str_starts_with( $template_file_path, $theme_dir ) ) { + $relative_path = substr( $template_file_path, strlen( $theme_dir ) ); + } elseif ( $theme->parent() ) { + $theme = $theme->parent(); + $theme_dir = wp_normalize_path( $theme->get_stylesheet_directory() ) . '/'; + if ( str_starts_with( $template_file_path, $theme_dir ) ) { + $relative_path = substr( $template_file_path, strlen( $theme_dir ) ); + } + } + + // Bypass cache if the file is not within the theme directory. + if ( ! isset( $relative_path ) ) { + return file_get_contents( $template_file_path ); + } + + /* + * Bypass cache while developing a theme. + * If there is an existing cache, it should be deleted. + * This ensures that no stale cache values can be served when temporarily + * enabling "theme" development mode and then disabling it again. + */ + if ( wp_is_development_mode( 'theme' ) ) { + $template_data = get_transient( 'wp_theme_template_contents_' . $theme->get_stylesheet() ); + if ( false !== $template_data ) { + delete_transient( 'wp_theme_template_contents_' . $theme->get_stylesheet() ); + } + + return file_get_contents( $template_file_path ); + } + + // Check theme template cache first (if cached version matches the current theme version). + $template_data = get_transient( 'wp_theme_template_contents_' . $theme->get_stylesheet() ); + if ( is_array( $template_data ) && $template_data['version'] === $theme->get( 'Version' ) ) { + if ( isset( $template_data['template_content'][ $relative_path ] ) ) { + return $template_data['template_content'][ $relative_path ]; + } + } else { + $template_data = array( + 'version' => $theme->get( 'Version' ), + 'template_content' => array(), + ); + } + + // Retrieve fresh file contents if not found in cache. + $template_data['template_content'][ $relative_path ] = file_get_contents( $template_file_path ); + + // Update the cache. + set_transient( 'wp_theme_template_contents_' . $theme->get_stylesheet(), $template_data, WEEK_IN_SECONDS ); + + return $template_data['template_content'][ $relative_path ]; +} + /** * Builds a unified template object based on a theme file. * @@ -520,7 +587,7 @@ function _remove_theme_attribute_from_template_part_block( &$block ) { */ function _build_block_template_result_from_file( $template_file, $template_type ) { $default_template_types = get_default_block_template_types(); - $template_content = file_get_contents( $template_file['path'] ); + $template_content = _get_block_template_file_content( $template_file['path'] ); $theme = get_stylesheet(); $template = new WP_Block_Template(); diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index b5fba76159dad..7fca8fe178c6a 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -877,6 +877,12 @@ function switch_theme( $stylesheet ) { $new_theme->delete_pattern_cache(); $old_theme->delete_pattern_cache(); + // Clear template content caches. + delete_transient( 'wp_theme_template_contents_' . $new_theme->get_stylesheet() ); + delete_transient( 'wp_theme_template_contents_' . $new_theme->get_template() ); + delete_transient( 'wp_theme_template_contents_' . $old_theme->get_stylesheet() ); + delete_transient( 'wp_theme_template_contents_' . $old_theme->get_template() ); + /** * Fires after the theme is switched. * diff --git a/tests/phpunit/tests/block-template-utils.php b/tests/phpunit/tests/block-template-utils.php index b06e931529f1e..ccc22f724bd8e 100644 --- a/tests/phpunit/tests/block-template-utils.php +++ b/tests/phpunit/tests/block-template-utils.php @@ -86,6 +86,12 @@ public function set_up() { switch_theme( self::TEST_THEME ); } + public function tear_down() { + parent::tear_down(); + + unset( $GLOBALS['_wp_tests_development_mode'] ); + } + public function test_build_block_template_result_from_post() { $template = _build_block_template_result_from_post( self::$template_post, @@ -523,4 +529,290 @@ public function test_wp_generate_block_templates_export_file() { } $this->assertTrue( $has_html_files, 'contains at least one html file' ); } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the current theme. + * + * This should store the file content in cache. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_current_theme_file() { + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content, 'Unexpected file content' ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme' ); + $this->assertArrayHasKey( 'template_content', $cache_result, 'Invalid cache value' ); + $this->assertArrayHasKey( 'parts/small-header.html', $cache_result['template_content'], 'File not set in cache' ); + $this->assertSame( $content, $cache_result['template_content']['parts/small-header.html'], 'File has incorrect content in cache' ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the current parent theme. + * + * This should store the file content in cache. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_current_parent_theme_file() { + switch_theme( 'block-theme-child' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/templates/index.html'; + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content, 'Unexpected file content' ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme' ); + $this->assertArrayHasKey( 'template_content', $cache_result, 'Invalid cache value' ); + $this->assertArrayHasKey( 'templates/index.html', $cache_result['template_content'], 'File not set in cache' ); + $this->assertSame( $content, $cache_result['template_content']['templates/index.html'], 'File has incorrect content in cache' ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is not part of the current theme. + * + * This should not set any cache. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_another_theme_file() { + switch_theme( 'block-theme-patterns' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme-child/templates/page-1.html'; + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content, 'Unexpected file content' ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme-patterns' ); + $this->assertFalse( $cache_result, 'Cache unexpectedly set for current theme' ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme' ); + $this->assertFalse( $cache_result, 'Cache unexpectedly set for current parent theme' ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme-child' ); + $this->assertFalse( $cache_result, 'Cache unexpectedly set for non-current theme' ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the current theme while using 'theme' development mode. + * + * This should not set any cache. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_current_theme_file_and_theme_development_mode() { + global $_wp_tests_development_mode; + + $_wp_tests_development_mode = 'theme'; + + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content, 'Unexpected file content' ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme' ); + $this->assertFalse( $cache_result, 'Cache unexpectedly set despite theme development mode' ); + } + + /** + * Tests `_get_block_template_file_content()` with files that are part of the current theme expands the existing cache. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_expands_existing_cache() { + switch_theme( 'block-theme' ); + + $template_file1 = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + $template_file2 = DIR_TESTDATA . '/themedir1/block-theme/templates/index.html'; + $template_file3 = DIR_TESTDATA . '/themedir1/block-theme/templates/page-home.html'; + + $content1 = _get_block_template_file_content( $template_file1 ); + $content2 = _get_block_template_file_content( $template_file2 ); + $content3 = _get_block_template_file_content( $template_file3 ); + + $cache_result = get_transient( 'wp_theme_template_contents_block-theme' ); + $this->assertSame( + array( + 'version' => '1.0.0', + 'template_content' => array( + 'parts/small-header.html' => $content1, + 'templates/index.html' => $content2, + 'templates/page-home.html' => $content3, + ), + ), + $cache_result + ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the current theme relies on cached values. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_current_theme_file_relies_on_cache() { + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + $forced_content = '
Some cache content that is not actually the file content.
'; + set_transient( + 'wp_theme_template_contents_block-theme', + array( + 'version' => '1.0.0', + 'template_content' => array( + 'parts/small-header.html' => $forced_content, + ), + ), + WEEK_IN_SECONDS + ); + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( $forced_content, $content ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the another theme ignores cached values. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_another_theme_file_ignores_cache() { + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme-child/templates/page-1.html'; + $forced_content = '
Some cache content that is not actually the file content.
'; + set_transient( + 'wp_theme_template_contents_block-theme-child', + array( + 'version' => '1.0.0', + 'template_content' => array( + 'templates/page-1.html' => $forced_content, + ), + ), + WEEK_IN_SECONDS + ); + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the current theme refreshes existing cache when version is outdated. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_current_theme_file_refreshes_cache_when_version_outdated() { + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + $forced_content = '
Some cache content that is not actually the file content.
'; + set_transient( + 'wp_theme_template_contents_block-theme', + array( + 'version' => '0.9.0', + 'template_content' => array( + 'parts/small-header.html' => $forced_content, + ), + ), + WEEK_IN_SECONDS + ); + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content, 'Cached file content unexpectedly returned' ); + + $this->assertSame( + array( + 'version' => '1.0.0', + 'template_content' => array( + 'parts/small-header.html' => $content, + ), + ), + get_transient( 'wp_theme_template_contents_block-theme' ), + 'Cached transient was not updated' + ); + } + + /** + * Tests `_get_block_template_file_content()` with a file that is part of the current theme ignores cached values while using 'theme' development mode. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_current_theme_file_and_theme_development_mode_ignores_cache() { + global $_wp_tests_development_mode; + + $_wp_tests_development_mode = 'theme'; + + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + $forced_content = '
Some cache content that is not actually the file content.
'; + set_transient( + 'wp_theme_template_contents_block-theme', + array( + 'version' => '1.0.0', + 'template_content' => array( + 'parts/small-header.html' => $forced_content, + ), + ), + WEEK_IN_SECONDS + ); + + $content = _get_block_template_file_content( $template_file ); + $this->assertSame( file_get_contents( $template_file ), $content ); + } + + /** + * Tests `_get_block_template_file_content()` while using 'theme' development mode clears the existing cache for the current theme. + * + * @ticket 59600 + * + * @covers ::_get_block_template_file_content + */ + public function test_get_block_template_file_content_with_theme_development_mode_clears_existing_cache() { + global $_wp_tests_development_mode; + + $_wp_tests_development_mode = 'theme'; + + switch_theme( 'block-theme' ); + + $template_file = DIR_TESTDATA . '/themedir1/block-theme/parts/small-header.html'; + set_transient( + 'wp_theme_template_contents_block-theme', + array( + 'version' => '1.0.0', + 'template_content' => array( + 'parts/small-header.html' => '
Some content.
', + ), + ), + WEEK_IN_SECONDS + ); + + // We don't care about the value here as it is already covered by the test above. + _get_block_template_file_content( $template_file ); + + // Ensure the relevant transient was deleted. + $this->assertFalse( get_transient( 'wp_theme_template_contents_block-theme' ) ); + } }