From c75fc6d4b8f09dfc4fb2286814cbccc737ccc40f Mon Sep 17 00:00:00 2001 From: Miguel Torres Date: Tue, 22 Nov 2022 17:19:07 +0100 Subject: [PATCH] Update `gutenberg_get_global_stylesheet` to use `WP_Object_Cache` (#45679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André <583546+oandregal@users.noreply.github.com> --- .../get-global-styles-and-settings.php | 85 -------------- .../class-wp-theme-json-resolver-6-2.php | 25 ++++ lib/compat/wordpress-6.2/default-filters.php | 10 ++ .../get-global-styles-and-settings.php | 110 ++++++++++++++++++ phpunit/wp-get-global-stylesheet-test.php | 98 ++++++++++++++++ 5 files changed, 243 insertions(+), 85 deletions(-) create mode 100644 phpunit/wp-get-global-stylesheet-test.php diff --git a/lib/compat/wordpress-6.1/get-global-styles-and-settings.php b/lib/compat/wordpress-6.1/get-global-styles-and-settings.php index 35c540ce1c57a..fd6113c7405c4 100644 --- a/lib/compat/wordpress-6.1/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.1/get-global-styles-and-settings.php @@ -54,88 +54,3 @@ function ( $item ) { } } } - -/** - * Returns the stylesheet resulting of merging core, theme, and user data. - * - * @param array $types Types of styles to load. Optional. - * It accepts 'variables', 'styles', 'presets' as values. - * If empty, it'll load all for themes with theme.json support - * and only [ 'variables', 'presets' ] for themes without theme.json support. - * - * @return string Stylesheet. - */ -function gutenberg_get_global_stylesheet( $types = array() ) { - // Return cached value if it can be used and exists. - // It's cached by theme to make sure that theme switching clears the cache. - $can_use_cached = ( - ( empty( $types ) ) && - ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) && - ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) && - ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) && - ! is_admin() - ); - $transient_name = 'gutenberg_global_styles_' . get_stylesheet(); - if ( $can_use_cached ) { - $cached = get_transient( $transient_name ); - if ( $cached ) { - return $cached; - } - } - $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data(); - $supports_theme_json = wp_theme_has_theme_json(); - if ( empty( $types ) && ! $supports_theme_json ) { - $types = array( 'variables', 'presets', 'base-layout-styles' ); - } elseif ( empty( $types ) ) { - $types = array( 'variables', 'styles', 'presets' ); - } - - /* - * If variables are part of the stylesheet, - * we add them. - * - * This is so themes without a theme.json still work as before 5.9: - * they can override the default presets. - * See https://core.trac.wordpress.org/ticket/54782 - */ - $styles_variables = ''; - if ( in_array( 'variables', $types, true ) ) { - /* - * We only use the default, theme, and custom origins. - * This is because styles for blocks origin are added - * at a later phase (render cycle) so we only render the ones in use. - * @see wp_add_global_styles_for_blocks - */ - $origins = array( 'default', 'theme', 'custom' ); - $styles_variables = $tree->get_stylesheet( array( 'variables' ), $origins ); - $types = array_diff( $types, array( 'variables' ) ); - } - - /* - * For the remaining types (presets, styles), we do consider origins: - * - * - themes without theme.json: only the classes for the presets defined by core - * - themes with theme.json: the presets and styles classes, both from core and the theme - */ - $styles_rest = ''; - if ( ! empty( $types ) ) { - /* - * We only use the default, theme, and custom origins. - * This is because styles for blocks origin are added - * at a later phase (render cycle) so we only render the ones in use. - * @see wp_add_global_styles_for_blocks - */ - $origins = array( 'default', 'theme', 'custom' ); - if ( ! $supports_theme_json ) { - $origins = array( 'default' ); - } - $styles_rest = $tree->get_stylesheet( $types, $origins ); - } - $stylesheet = $styles_variables . $styles_rest; - if ( $can_use_cached ) { - // Cache for a minute. - // This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites. - set_transient( $transient_name, $stylesheet, MINUTE_IN_SECONDS ); - } - return $stylesheet; -} diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php index e10710e0f4709..7adb37ffbd9e0 100644 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php +++ b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php @@ -32,4 +32,29 @@ public static function theme_has_support() { return wp_theme_has_theme_json(); } + /** + * Private method to clean the cached data after an upgrade. + * + * It is hooked into the `upgrader_process_complete` action. + * + * @see default-filters.php + * + * @param WP_Upgrader $upgrader WP_Upgrader instance. + * @param array $options Array of bulk item update data. + */ + public static function _clean_cached_data_upon_upgrading( $upgrader, $options ) { + if ( 'update' !== $options['action'] ) { + return; + } + + if ( + 'core' === $options['type'] || + 'plugin' === $options['type'] || + // Clean cache only if the active theme was updated. + ( 'theme' === $options['type'] && ( isset( $options['themes'][ get_stylesheet() ] ) || isset( $options['themes'][ get_template() ] ) ) ) + ) { + static::clean_cached_data(); + } + } + } diff --git a/lib/compat/wordpress-6.2/default-filters.php b/lib/compat/wordpress-6.2/default-filters.php index 861b8a01421c6..927ff2bd12aa3 100644 --- a/lib/compat/wordpress-6.2/default-filters.php +++ b/lib/compat/wordpress-6.2/default-filters.php @@ -20,3 +20,13 @@ add_action( 'switch_theme', 'wp_theme_has_theme_json_clean_cache' ); add_action( 'start_previewing_theme', 'wp_theme_has_theme_json_clean_cache' ); add_action( 'upgrader_process_complete', '_wp_theme_has_theme_json_clean_cache_upon_upgrading_active_theme', 10, 2 ); +add_action( 'save_post_wp_global_styles', array( 'WP_Theme_JSON_Resolver_Gutenberg', 'clean_cached_data' ) ); +add_action( 'activated_plugin', array( 'WP_Theme_JSON_Resolver_Gutenberg', 'clean_cached_data' ) ); +add_action( 'deactivated_plugin', array( 'WP_Theme_JSON_Resolver_Gutenberg', 'clean_cached_data' ) ); +add_action( 'upgrader_process_complete', array( 'WP_Theme_JSON_Resolver_Gutenberg', '_clean_cached_data_upon_upgrading', 10, 2 ) ); +add_action( 'save_post_wp_global_styles', 'gutenberg_get_global_stylesheet_clean_cache' ); +add_action( 'switch_theme', 'gutenberg_get_global_stylesheet_clean_cache' ); +add_action( 'start_previewing_theme', 'gutenberg_get_global_stylesheet_clean_cache' ); +add_action( 'activated_plugin', 'gutenberg_get_global_stylesheet_clean_cache' ); +add_action( 'deactivated_plugin', 'gutenberg_get_global_stylesheet_clean_cache' ); +add_action( 'upgrader_process_complete', '_gutenberg_get_global_stylesheet_clean_cache_upon_upgrading', 10, 2 ); diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index be9781556868c..5d37d3182ccc8 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -79,3 +79,113 @@ function _wp_theme_has_theme_json_clean_cache_upon_upgrading_active_theme( $upgr } } } + +/** + * Returns the stylesheet resulting of merging core, theme, and user data. + * + * @param array $types Types of styles to load. Optional. + * It accepts 'variables', 'styles', 'presets' as values. + * If empty, it'll load all for themes with theme.json support + * and only [ 'variables', 'presets' ] for themes without theme.json support. + * + * @return string Stylesheet. + */ +function gutenberg_get_global_stylesheet( $types = array() ) { + // Ignore cache when `WP_DEBUG` is enabled, so it doesn't interfere with the theme developers workflow. + $can_use_cached = empty( $types ) && ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ); + $cache_key = 'gutenberg_get_global_stylesheet'; + $cache_group = 'theme_json'; + if ( $can_use_cached ) { + $cached = wp_cache_get( $cache_key, $cache_group ); + if ( $cached ) { + return $cached; + } + } + $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data(); + $supports_theme_json = wp_theme_has_theme_json(); + if ( empty( $types ) && ! $supports_theme_json ) { + $types = array( 'variables', 'presets', 'base-layout-styles' ); + } elseif ( empty( $types ) ) { + $types = array( 'variables', 'styles', 'presets' ); + } + + /* + * If variables are part of the stylesheet, + * we add them. + * + * This is so themes without a theme.json still work as before 5.9: + * they can override the default presets. + * See https://core.trac.wordpress.org/ticket/54782 + */ + $styles_variables = ''; + if ( in_array( 'variables', $types, true ) ) { + /* + * We only use the default, theme, and custom origins. + * This is because styles for blocks origin are added + * at a later phase (render cycle) so we only render the ones in use. + * @see wp_add_global_styles_for_blocks + */ + $origins = array( 'default', 'theme', 'custom' ); + $styles_variables = $tree->get_stylesheet( array( 'variables' ), $origins ); + $types = array_diff( $types, array( 'variables' ) ); + } + + /* + * For the remaining types (presets, styles), we do consider origins: + * + * - themes without theme.json: only the classes for the presets defined by core + * - themes with theme.json: the presets and styles classes, both from core and the theme + */ + $styles_rest = ''; + if ( ! empty( $types ) ) { + /* + * We only use the default, theme, and custom origins. + * This is because styles for blocks origin are added + * at a later phase (render cycle) so we only render the ones in use. + * @see wp_add_global_styles_for_blocks + */ + $origins = array( 'default', 'theme', 'custom' ); + if ( ! $supports_theme_json ) { + $origins = array( 'default' ); + } + $styles_rest = $tree->get_stylesheet( $types, $origins ); + } + $stylesheet = $styles_variables . $styles_rest; + if ( $can_use_cached ) { + wp_cache_set( $cache_key, $stylesheet, $cache_group ); + } + return $stylesheet; +} + +/** + * Clean the cache used by the `gutenberg_get_global_stylesheet` function. + */ +function gutenberg_get_global_stylesheet_clean_cache() { + wp_cache_delete( 'gutenberg_get_global_stylesheet', 'theme_json' ); +} + +/** + * Private function to clean the cache used by the `gutenberg_get_global_stylesheet` function after an upgrade. + * + * It is hooked into the `upgrader_process_complete` action. + * + * @see default-filters.php + * + * @param WP_Upgrader $upgrader WP_Upgrader instance. + * @param array $options Array of bulk item update data. + */ +function _gutenberg_get_global_stylesheet_clean_cache_upon_upgrading( $upgrader, $options ) { + if ( 'update' !== $options['action'] ) { + return; + } + + if ( + 'core' === $options['type'] || + 'plugin' === $options['type'] || + // Clean cache only if the active theme was updated. + ( 'theme' === $options['type'] && ( isset( $options['themes'][ get_stylesheet() ] ) || isset( $options['themes'][ get_template() ] ) ) ) + ) { + gutenberg_get_global_stylesheet_clean_cache(); + } +} + diff --git a/phpunit/wp-get-global-stylesheet-test.php b/phpunit/wp-get-global-stylesheet-test.php new file mode 100644 index 0000000000000..cb4d0242ce3e3 --- /dev/null +++ b/phpunit/wp-get-global-stylesheet-test.php @@ -0,0 +1,98 @@ +user->create( + array( + 'role' => 'administrator', + 'user_email' => 'administrator@example.com', + ) + ); + } + + public function set_up() { + parent::set_up(); + + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + // Set up the new root. + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + public function tear_down() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + + // Clear up the filters to modify the theme root. + remove_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + + parent::tear_down(); + } + + public function filter_set_theme_root() { + return $this->theme_root; + } + + public function test_global_styles_user_cpt_change_invalidates_cached_stylesheet() { + add_filter( 'wp_get_global_stylesheet_can_use_cache', '__return_true' ); + switch_theme( 'block-theme' ); + wp_set_current_user( self::$administrator_id ); + + $styles = gutenberg_get_global_stylesheet(); + $this->assertStringNotContainsString( 'background-color: hotpink;', $styles ); + + $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( wp_get_theme(), true ); + $config = json_decode( $user_cpt['post_content'], true ); + $config['styles']['color']['background'] = 'hotpink'; + $user_cpt['post_content'] = wp_json_encode( $config ); + + wp_update_post( $user_cpt, true, false ); + + $styles = gutenberg_get_global_stylesheet(); + $this->assertStringContainsString( 'background-color: hotpink;', $styles ); + remove_filter( 'wp_get_global_stylesheet_can_use_cache', '__return_true' ); + } +}