diff --git a/projects/plugins/boost/.phpcs.dir.xml b/projects/plugins/boost/.phpcs.dir.xml index de9dcf15a11a6..0d50ca55e7c99 100644 --- a/projects/plugins/boost/.phpcs.dir.xml +++ b/projects/plugins/boost/.phpcs.dir.xml @@ -21,4 +21,38 @@ + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + diff --git a/projects/plugins/boost/app/admin/class-admin.php b/projects/plugins/boost/app/admin/class-admin.php index 014fc768c3b77..4cfe6eb0e9b69 100644 --- a/projects/plugins/boost/app/admin/class-admin.php +++ b/projects/plugins/boost/app/admin/class-admin.php @@ -10,14 +10,14 @@ use Automattic\Jetpack\Admin_UI\Admin_Menu; use Automattic\Jetpack\Status; +use Automattic\Jetpack_Boost\Features\Optimizations\Critical_CSS\Critical_CSS; +use Automattic\Jetpack_Boost\Features\Optimizations\Optimizations; +use Automattic\Jetpack_Boost\Features\Speed_Score\Speed_Score; use Automattic\Jetpack_Boost\Jetpack_Boost; use Automattic\Jetpack_Boost\Lib\Analytics; use Automattic\Jetpack_Boost\Lib\Environment_Change_Detector; -use Automattic\Jetpack_Boost\Lib\Speed_Score; +use Automattic\Jetpack_Boost\REST_API\Permissions\Nonce; -/** - * Class Admin - */ class Admin { /** @@ -45,7 +45,7 @@ class Admin { * * @var Jetpack_Boost Plugin. */ - private $jetpack_boost; + private $modules; /** * Speed_Score class instance. @@ -54,21 +54,13 @@ class Admin { */ private $speed_score; - /** - * Initialize the class and set its properties. - * - * @param Jetpack_Boost $jetpack_boost Main plugin instance. - * - * @since 1.0.0 - */ - public function __construct( Jetpack_Boost $jetpack_boost ) { - $this->jetpack_boost = $jetpack_boost; - $this->speed_score = new Speed_Score( $jetpack_boost ); + public function __construct( Optimizations $modules ) { + $this->modules = $modules; + $this->speed_score = new Speed_Score( $modules ); Environment_Change_Detector::init(); add_action( 'init', array( new Analytics(), 'init' ) ); add_filter( 'plugin_action_links_' . JETPACK_BOOST_PLUGIN_BASE, array( $this, 'plugin_page_settings_link' ) ); - add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); add_action( 'admin_notices', array( $this, 'show_notices' ) ); add_action( 'wp_ajax_set_show_rating_prompt', array( $this, 'handle_set_show_rating_prompt' ) ); add_filter( 'jetpack_boost_js_constants', array( $this, 'add_js_constants' ) ); @@ -102,7 +94,7 @@ public function enqueue_styles() { $internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' ); wp_enqueue_style( - $this->jetpack_boost->get_plugin_name() . '-css', + 'jetpack-boost-css', plugins_url( $internal_path . 'jetpack-boost.css', JETPACK_BOOST_PATH ), array( 'wp-components' ), JETPACK_BOOST_VERSION @@ -117,7 +109,7 @@ public function enqueue_styles() { public function enqueue_scripts() { $internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' ); - $admin_js_handle = $this->jetpack_boost->get_plugin_name() . '-admin'; + $admin_js_handle = 'jetpack-boost-admin'; wp_register_script( $admin_js_handle, @@ -127,6 +119,7 @@ public function enqueue_scripts() { true ); + $optimizations = ( new Optimizations() )->get_status(); // Prepare configuration constants for JavaScript. $constants = array( 'version' => JETPACK_BOOST_VERSION, @@ -134,8 +127,7 @@ public function enqueue_scripts() { 'namespace' => JETPACK_BOOST_REST_NAMESPACE, 'prefix' => JETPACK_BOOST_REST_PREFIX, ), - 'modules' => $this->jetpack_boost->get_available_modules(), - 'config' => $this->jetpack_boost->config()->get_data(), + 'optimizations' => $optimizations, 'locale' => get_locale(), 'site' => array( 'url' => get_home_url(), @@ -146,6 +138,14 @@ public function enqueue_scripts() { 'preferences' => array( 'showRatingPrompt' => $this->get_show_rating_prompt(), ), + + /** + * A bit of necessary magic, + * Explained more in the Nonce class. + * + * Nonces are automatically generated when registering routes. + */ + 'nonces' => Nonce::get_generated_nonces(), ); // Give each module an opportunity to define extra constants. @@ -179,7 +179,7 @@ public function plugin_page_settings_link( $links ) { */ public function render_settings() { wp_localize_script( - $this->jetpack_boost->get_plugin_name() . '-admin', + 'jetpack-boost-admin', 'wpApiSettings', array( 'root' => esc_url_raw( rest_url() ), @@ -200,49 +200,6 @@ public function check_for_permissions() { return current_user_can( 'manage_options' ); } - /** - * Register REST routes for settings. - * - * @return void - */ - public function register_rest_routes() { - // Activate and deactivate a module. - register_rest_route( - JETPACK_BOOST_REST_NAMESPACE, - JETPACK_BOOST_REST_PREFIX . '/module/(?P[a-z\-]+)/status', - array( - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'set_module_status' ), - 'permission_callback' => array( $this, 'check_for_permissions' ), - ) - ); - } - - /** - * Handler for the /module/(?P[a-z\-]+)/status endpoint. - * - * @param \WP_REST_Request $request The request object. - * - * @return \WP_REST_Response|\WP_Error The response. - */ - public function set_module_status( $request ) { - $params = $request->get_json_params(); - - if ( ! isset( $params['status'] ) ) { - return new \WP_Error( - 'jetpack_boost_error_missing_module_status_param', - __( 'Missing status param', 'jetpack-boost' ) - ); - } - - $module_slug = $request['slug']; - $this->jetpack_boost->set_module_status( (bool) $params['status'], $module_slug ); - - return rest_ensure_response( - $this->jetpack_boost->get_module_status( $module_slug ) - ); - } - /** * Show any admin notices from enabled modules. */ @@ -250,7 +207,7 @@ public function show_notices() { // Determine if we're already on the settings page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $on_settings_page = isset( $_GET['page'] ) && self::MENU_SLUG === $_GET['page']; - $notices = $this->jetpack_boost->get_admin_notices(); + $notices = $this->get_admin_notices(); // Filter out any that have been dismissed, unless newer than the dismissal. $dismissed_notices = \get_option( self::DISMISSED_NOTICE_OPTION, array() ); @@ -281,7 +238,7 @@ function ( $notice ) use ( $dismissed_notices ) { * @return array List of notice ids. */ private function get_shown_admin_notice_ids() { - $notices = $this->jetpack_boost->get_admin_notices(); + $notices = $this->get_admin_notices(); $ids = array(); foreach ( $notices as $notice ) { $ids[] = $notice->get_id(); @@ -290,6 +247,18 @@ private function get_shown_admin_notice_ids() { return $ids; } + /** + * Returns a list of admin notices to show. Asks each module to provide admin notices the user needs to see. + * + * @TODO: This is still a code smell. We're carrying the whole modules instance just to get a list of admin notices. + * + * @return \Automattic\Jetpack_Boost\Admin\Admin_Notice[] + */ + public function get_admin_notices() { + $critical_css = new Critical_CSS(); + return $critical_css->get_admin_notices(); + } + /** * Check for a GET parameter used to dismiss an admin notice. * diff --git a/projects/plugins/boost/app/assets/src/js/global.d.ts b/projects/plugins/boost/app/assets/src/js/global.d.ts index 16d75ef98e167..b341177e5a5a8 100644 --- a/projects/plugins/boost/app/assets/src/js/global.d.ts +++ b/projects/plugins/boost/app/assets/src/js/global.d.ts @@ -12,7 +12,7 @@ import type { BrowserInterfaceIframe, generateCriticalCSS } from 'jetpack-boost- */ import type { ConnectionStatus } from './stores/connection'; import type { CriticalCssStatus } from './stores/critical-css-status'; -import type { ModulesState } from './stores/modules'; +import type { Optimizations } from './stores/modules'; declare global { const wpApiSettings: { @@ -35,15 +35,17 @@ declare global { connection: ConnectionStatus; criticalCssStatus?: CriticalCssStatus; showRatingPromptNonce?: string; - criticalCssDismissRecommendationsNonce?: string; criticalCssDismissedRecommendations: string[]; site: { url: string; online: boolean; assetPath: string; }; - config: ModulesState; + optimizations: Optimizations; shownAdminNoticeIds: string[]; + nonces: { + [ key: string ]: string; + }; }; // Critical CSS Generator library. diff --git a/projects/plugins/boost/app/assets/src/js/stores/critical-css-recommendations.ts b/projects/plugins/boost/app/assets/src/js/stores/critical-css-recommendations.ts index 4f80206670bc4..b6c98fe8d5aac 100644 --- a/projects/plugins/boost/app/assets/src/js/stores/critical-css-recommendations.ts +++ b/projects/plugins/boost/app/assets/src/js/stores/critical-css-recommendations.ts @@ -6,11 +6,11 @@ import { writable, derived } from 'svelte/store'; /** * Internal dependencies */ +import api from '../api/api'; import { CriticalCssErrorDetails, criticalCssStatus } from './critical-css-status'; import type { JSONObject } from '../utils/json-types'; import { objectFilter } from '../utils/object-filter'; import { sortByFrequency } from '../utils/sort-by-frequency'; -import { makeAdminAjaxRequest } from '../utils/make-admin-ajax-request'; import { castToString } from '../utils/cast-to-string'; const importantProviders = [ @@ -118,10 +118,9 @@ export function setDismissalError( title: string, error: JSONObject ): void { * @param {string} key Key of recommendation to dismiss. */ export async function dismissRecommendation( key: string ): Promise< void > { - await makeAdminAjaxRequest( { - action: 'dismiss_recommendations', + await api.post( '/recommendations/dismiss', { providerKey: key, - nonce: Jetpack_Boost.criticalCssDismissRecommendationsNonce, + nonce: Jetpack_Boost.nonces[ 'recommendations/dismiss' ], } ); dismissed.update( keys => [ ...keys, key ] ); } @@ -130,9 +129,8 @@ export async function dismissRecommendation( key: string ): Promise< void > { * Clear all the dismissed recommendations. */ export async function clearDismissedRecommendations(): Promise< void > { - await makeAdminAjaxRequest( { - action: 'reset_dismissed_recommendations', - nonce: Jetpack_Boost.criticalCssDismissRecommendationsNonce, + await api.post( '/recommendations/reset', { + nonce: Jetpack_Boost.nonces[ 'recommendations/reset' ], } ); dismissed.set( [] ); } diff --git a/projects/plugins/boost/app/assets/src/js/stores/modules.ts b/projects/plugins/boost/app/assets/src/js/stores/modules.ts index f9accfb9b2544..668f0771c61e6 100644 --- a/projects/plugins/boost/app/assets/src/js/stores/modules.ts +++ b/projects/plugins/boost/app/assets/src/js/stores/modules.ts @@ -9,6 +9,10 @@ import { writable } from 'svelte/store'; import config from './config'; import { setModuleState } from '../api/modules'; +export type Optimizations = { + [ slug: string ]: boolean; +}; + export type ModulesState = { [ slug: string ]: { enabled: boolean; @@ -16,7 +20,13 @@ export type ModulesState = { }; }; -const initialState = config.config; +const initialState = {}; +for ( const [ name, value ] of Object.entries( config.optimizations ) ) { + initialState[ name ] = { + enabled: value, + }; +} + const { subscribe, update } = writable< ModulesState >( initialState ); // Keep a subscribed copy for quick reading. diff --git a/projects/plugins/boost/app/assets/src/js/utils/generate-critical-css.ts b/projects/plugins/boost/app/assets/src/js/utils/generate-critical-css.ts index 9e931710023bb..a76d34c2321d6 100644 --- a/projects/plugins/boost/app/assets/src/js/utils/generate-critical-css.ts +++ b/projects/plugins/boost/app/assets/src/js/utils/generate-critical-css.ts @@ -76,12 +76,12 @@ export default async function generateCriticalCss( hasGenerateRun = true; let cancelling = false; - if ( reset ) { - await clearDismissedRecommendations(); - updateGenerateStatus( true, 0 ); - } - try { + if ( reset ) { + await clearDismissedRecommendations(); + updateGenerateStatus( true, 0 ); + } + // Fetch a list of provider keys and URLs while loading the Critical CSS lib. const cssStatus = await requestGeneration( reset, isShowstopperRetry ); diff --git a/projects/plugins/boost/app/class-jetpack-boost.php b/projects/plugins/boost/app/class-jetpack-boost.php index cbef9b8fcd3f3..02ac717a4041c 100644 --- a/projects/plugins/boost/app/class-jetpack-boost.php +++ b/projects/plugins/boost/app/class-jetpack-boost.php @@ -12,19 +12,17 @@ namespace Automattic\Jetpack_Boost; -use Automattic\Jetpack\Config as Jetpack_Config; use Automattic\Jetpack_Boost\Admin\Admin; +use Automattic\Jetpack_Boost\Features\Optimizations\Critical_CSS\Critical_CSS; +use Automattic\Jetpack_Boost\Features\Optimizations\Critical_CSS\Critical_CSS_Storage; +use Automattic\Jetpack_Boost\Features\Optimizations\Critical_CSS\Regenerate_Admin_Notice; +use Automattic\Jetpack_Boost\Features\Optimizations\Optimizations; use Automattic\Jetpack_Boost\Lib\Analytics; use Automattic\Jetpack_Boost\Lib\CLI; -use Automattic\Jetpack_Boost\Lib\Config; use Automattic\Jetpack_Boost\Lib\Connection; -use Automattic\Jetpack_Boost\Lib\Speed_Score_History; -use Automattic\Jetpack_Boost\Lib\Viewport; -use Automattic\Jetpack_Boost\Modules\Critical_CSS\Critical_CSS; -use Automattic\Jetpack_Boost\Modules\Critical_CSS\Regenerate_Admin_Notice; -use Automattic\Jetpack_Boost\Modules\Lazy_Images\Lazy_Images; -use Automattic\Jetpack_Boost\Modules\Module; -use Automattic\Jetpack_Boost\Modules\Render_Blocking_JS\Render_Blocking_JS; +use Automattic\Jetpack_Boost\Lib\Setup; +use Automattic\Jetpack_Boost\REST_API\Endpoints\Optimization_Status; +use Automattic\Jetpack_Boost\REST_API\REST_API; /** * The core plugin class. @@ -40,28 +38,6 @@ */ class Jetpack_Boost { - const MODULES = array( - Critical_CSS::MODULE_SLUG => Critical_CSS::class, - Lazy_Images::MODULE_SLUG => Lazy_Images::class, - Render_Blocking_JS::MODULE_SLUG => Render_Blocking_JS::class, - ); - - /** - * Default enabled modules. - */ - const ENABLED_MODULES_DEFAULT = array(); - - /** - * Default available modules. - */ - const AVAILABLE_MODULES_DEFAULT = array( - Critical_CSS::MODULE_SLUG, - Render_Blocking_JS::MODULE_SLUG, - Lazy_Images::MODULE_SLUG, - ); - - const CURRENT_CONFIG_ID = 'default'; - /** * The unique identifier of this plugin. * @@ -78,21 +54,6 @@ class Jetpack_Boost { */ private $version; - /** - * The config - * - * @since 1.0.0 - * @var Config|null $config The configuration object - */ - private $config; - - /** - * Store all plugin module instances here - * - * @var array - */ - private $modules = array(); - /** * The Jetpack Boost Connection manager instance. * @@ -127,20 +88,12 @@ public function __construct() { \WP_CLI::add_command( 'jetpack-boost', $cli_instance ); } - // Initialize the config module separately. - $this->init_config(); - - $this->prepare_modules(); + $optimizations = new Optimizations(); + Setup::add( $optimizations ); // Initialize the Admin experience. - $this->init_admin(); - - // Module readiness filter. - add_action( 'wp_head', array( $this, 'display_meta_field_module_ready' ) ); - - add_action( 'init', array( $this, 'initialize_modules' ) ); + $this->init_admin( $optimizations ); add_action( 'init', array( $this, 'init_textdomain' ) ); - add_action( 'init', array( $this, 'register_cache_clear_actions' ) ); add_action( 'handle_theme_change', array( $this, 'handle_theme_change' ) ); @@ -156,307 +109,23 @@ private function register_deactivation_hook() { register_deactivation_hook( $plugin_file, array( $this, 'deactivate' ) ); } - /** - * Wipe all cached values. - */ - public function clear_cache() { - do_action( 'jetpack_boost_clear_cache' ); - } - /** * Plugin deactivation handler. Clear cache, and reset admin notices. */ public function deactivate() { do_action( 'jetpack_boost_deactivate' ); - - $this->clear_cache(); - Admin::clear_dismissed_notices(); - } - - /** - * Plugin uninstallation handler. Delete all settings and cache. - */ - public function uninstall() { - do_action( 'jetpack_boost_uninstall' ); - - Speed_Score_History::clear_all(); - $this->clear_cache(); - delete_option( apply_filters( 'jetpack_boost_options_store_key_name', 'jetpack_boost_config' ) ); - } - - /** - * Handlers for clearing module caches go here, so that caches get cleared even if the module is not enabled. - */ - public function register_cache_clear_actions() { - add_action( 'jetpack_boost_clear_cache', array( $this, 'record_clear_cache_event' ) ); - } - - /** - * Record the clear cache event. - */ - public function record_clear_cache_event() { + do_action( 'jetpack_boost_clear_cache' ); Analytics::record_user_event( 'clear_cache' ); - } - - /** - * Initialize modules. - * - * Note: this method ignores the nonce verification linter rule, as jb-disable-modules is intended to work - * without a nonce. - * - * phpcs:disable WordPress.Security.NonceVerification.Recommended - */ - public function prepare_modules() { - $available_modules = $this->get_available_modules(); - - $forced_disabled_modules = array(); - - // Get the lists of modules explicitly disabled from the 'jb-disable-modules' query string. - // The parameter is a comma separated value list of module slug. - if ( ! empty( $_GET['jb-disable-modules'] ) ) { - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $forced_disabled_modules = array_map( 'sanitize_key', explode( ',', $_GET['jb-disable-modules'] ) ); - } - - foreach ( self::MODULES as $module_slug => $module_class ) { - // Don't register modules that have been forcibly disabled from the url 'jb-disable-modules' query string parameter. - if ( in_array( $module_slug, $forced_disabled_modules, true ) || in_array( 'all', $forced_disabled_modules, true ) ) { - continue; - } - - // All Jetpack Boost modules should extend Module class. - if ( ! is_subclass_of( $module_class, Module::class ) ) { - continue; - } - - // Don't register modules that aren't available. - if ( ! in_array( $module_slug, $available_modules, true ) ) { - continue; - } - - $module = new $module_class(); - $this->modules[ $module_slug ] = $module; - } - - do_action( 'jetpack_boost_modules_loaded' ); - } - - /** - * Initialize modules when WordPress is ready - */ - public function initialize_modules() { - foreach ( $this->modules as $module_slug => $module ) { - if ( true === $this->get_module_status( $module_slug ) ) { - $module->initialize(); - } - } - } - - /** - * Returns the list of available modules. - * - * @return array The available modules. - */ - public function get_available_modules() { - $available_modules = self::AVAILABLE_MODULES_DEFAULT; - - // Add the Lazy Images module if Jetpack Lazy Images module is enabled. - if ( Lazy_Images::is_jetpack_lazy_images_module_enabled() ) { - $available_modules = array_unique( array_merge( self::AVAILABLE_MODULES_DEFAULT, array( Lazy_Images::MODULE_SLUG ) ) ); - } - - return apply_filters( - 'jetpack_boost_modules', - $available_modules - ); - } - - /** - * Returns an array of active modules. - */ - public function get_active_modules() { - // Cache active modules. - static $active_modules = null; - if ( null !== $active_modules ) { - return $active_modules; - } - - return array_filter( - $this->modules, - function ( $module, $module_slug ) { - return true === $this->get_module_status( $module_slug ); - }, - ARRAY_FILTER_USE_BOTH - ); - } - - /** - * Returns the status of a given module. - * - * @param string $module_slug The module's slug. - * - * @return bool The enablement status of the module. - */ - public function get_module_status( $module_slug ) { - $default_module_status = in_array( $module_slug, self::ENABLED_MODULES_DEFAULT, true ); - - return apply_filters( 'jetpack_boost_module_enabled', $default_module_status, $module_slug ); - } - - /** - * Check if a module is enabled. - * - * @param boolean $is_enabled Default value. - * @param string $module_slug The module we are checking. - * - * @return mixed|null - */ - public function is_module_enabled( $is_enabled, $module_slug ) { - do_action( 'jetpack_boost_pre_is_module_enabled', $is_enabled, $module_slug ); - - return $this->config()->get_value( "$module_slug/enabled", $is_enabled ); - } - - /** - * Set status of a module. - * - * @param boolean $is_enabled Default value. - * @param string $module_slug The module we are checking. - */ - public function set_module_status( $is_enabled, $module_slug ) { - do_action( 'jetpack_boost_pre_set_module_status', $is_enabled, $module_slug ); - Analytics::record_user_event( - 'set_module_status', - array( - 'module' => $module_slug, - 'status' => $is_enabled, - ) - ); - $this->config()->set_value( "$module_slug/enabled", $is_enabled, true ); - } - - /** - * Get critical CSS viewport sizes. - * - * @param mixed $default The default value. - * - * @return mixed|null - */ - public function get_critical_css_viewport_sizes( $default ) { - return $this->config()->get_value( 'critical-css/settings/viewport_sizes', $default ); - } - - /** - * Get critical CSS default viewports. - * - * @param mixed $default The default value. - * - * @return mixed|null - */ - public function get_critical_css_default_viewports( $default ) { - return $this->config()->get_value( 'critical-css/settings/default_viewports', $default ); - } - - /** - * Get critical CSS ignore rules. - * - * @param mixed $default The default value. - * - * @return mixed|null - */ - public function get_critical_css_ignore_rules( $default ) { - return $this->config()->get_value( 'critical-css/settings/css-ignore-rules', $default ); - } - - /** - * Returns configuration array. - * - * @return Config Configuration array. - */ - public function config() { - if ( ! $this->config ) { - do_action( 'jetpack_boost_pre_get_config' ); - $this->config = Config::get( self::CURRENT_CONFIG_ID ); // under the hood, this actually fetches from an option, not the config cache. - } - - return apply_filters( 'jetpack_boost_config', $this->config ); - } - - /** - * Initialize config system. - * - * @todo This should be replaced by a proper configuration implementation eventually. - */ - public function init_config() { - add_action( 'switch_blog', array( $this, 'clear_memoized_config' ) ); - add_filter( 'jetpack_boost_module_enabled', array( $this, 'is_module_enabled' ), 0, 2 ); - add_filter( 'jetpack_boost_critical_css_viewport_sizes', array( $this, 'get_critical_css_viewport_sizes' ) ); - add_filter( 'jetpack_boost_critical_css_default_viewports', array( $this, 'get_critical_css_default_viewports' ) ); - add_filter( 'jetpack_boost_critical_css_ignore_rules', array( $this, 'get_critical_css_ignore_rules' ) ); - } - - /** - * Clear the memoized config, executed on `switch_blog` - */ - public function clear_memoized_config() { - $this->config = null; - } - - /** - * Returns a default config array. - * - * @return array Default config. - */ - public static function get_default_config_array() { - return apply_filters( - 'jetpack_boost_config_array', - array( - Render_Blocking_JS::MODULE_SLUG => array( - 'enabled' => false, - ), - Critical_CSS::MODULE_SLUG => array( - 'enabled' => false, - 'settings' => array( - 'viewport_sizes' => Viewport::DEFAULT_VIEWPORT_SIZES, - 'default_viewports' => Viewport::DEFAULT_VIEWPORTS, - 'css-ignore-rules' => array( - // TODO: Define if we need any default CSS ignore rules - // Example regex, exclude all css where there is a url inside. - 'url\(', - ), - ), - ), - Lazy_Images::MODULE_SLUG => array( - 'enabled' => false, - ), - 'show_rating_prompt' => true, - ) - ); + Admin::clear_dismissed_notices(); } /** * Initialize the admin experience. */ - public function init_admin() { - if ( ! apply_filters( 'jetpack_boost_connection_bypass', false ) ) { - $jetpack_config = new Jetpack_Config(); - $jetpack_config->ensure( - 'connection', - array( - 'slug' => 'jetpack-boost', - 'name' => 'Jetpack Boost', - 'url_info' => '', // Optional, URL of the plugin. - ) - ); - } - - /** - * The class responsible for defining all actions that occur in the admin area. - */ - require_once plugin_dir_path( __FILE__ ) . 'admin/class-admin.php'; - - new Admin( $this ); + public function init_admin( $modules ) { + REST_API::register( Optimization_Status::class ); + $this->connection->ensure_connection(); + new Admin( $modules ); } /** @@ -470,15 +139,6 @@ public function init_textdomain() { ); } - /** - * Registers the `jetpack_boost_url_ready` filter which allows modules to provide their readiness status. - */ - public function display_meta_field_module_ready() { - ?> - - version; } - /** - * Returns a list of admin notices to show. Asks each module to provide admin notices the user needs to see. - * - * @return \Automattic\Jetpack_Boost\Admin\Admin_Notice[] - */ - public function get_admin_notices() { - $all_notices = array(); - - foreach ( $this->get_active_modules() as $module ) { - $module_notices = $module->get_admin_notices(); - - if ( ! empty( $module_notices ) ) { - $all_notices = array_merge( $all_notices, $module_notices ); - } - } - - return $all_notices; - } - /** * Handle an environment change to set the correct status to the Critical CSS request. * This is done here so even if the Critical CSS module is switched off we can @@ -528,4 +169,28 @@ public function handle_theme_change() { Admin::clear_dismissed_notice( Regenerate_Admin_Notice::SLUG ); \update_option( Critical_CSS::RESET_REASON_STORAGE_KEY, Regenerate_Admin_Notice::REASON_THEME_CHANGE, false ); } + + /** + * Plugin uninstallation handler. Delete all settings and cache. + */ + public function uninstall() { + + global $wpdb; + + // When uninstalling, make sure all deactivation cleanups have run as well. + $this->deactivate(); + + // Delete all Jetpack Boost options. + $wpdb->query( + " + DELETE + FROM `$wpdb->options` + WHERE `option_name` LIKE jetpack_boost_% + " + ); + + // Delete stored Critical CSS. + ( new Critical_CSS_Storage() )->clear(); + + } } diff --git a/projects/plugins/boost/app/contracts/Feature.php b/projects/plugins/boost/app/contracts/Feature.php new file mode 100644 index 0000000000000..33edf2c86b622 --- /dev/null +++ b/projects/plugins/boost/app/contracts/Feature.php @@ -0,0 +1,11 @@ +feature = $feature; + $this->status = new Status( $feature->get_slug() ); + } +} diff --git a/projects/plugins/boost/app/features/optimizations/Optimizations.php b/projects/plugins/boost/app/features/optimizations/Optimizations.php new file mode 100644 index 0000000000000..a933dbeb54edd --- /dev/null +++ b/projects/plugins/boost/app/features/optimizations/Optimizations.php @@ -0,0 +1,127 @@ +get_slug(); + $this->features[ $slug ] = new Optimization( $feature ); + } + } + + public function available_modules() { + $forced_disabled_modules = $this->get_disabled_modules(); + + if ( empty( $forced_disabled_modules ) ) { + return $this->features; + } + + if ( array( 'all' ) === $forced_disabled_modules ) { + return array(); + } + + $available_modules = array(); + foreach ( $this->features as $slug => $feature ) { + if ( ! in_array( $slug, $forced_disabled_modules, true ) ) { + $available_modules[ $slug ] = $feature; + } + } + + return $available_modules; + } + + public function have_enabled_modules() { + return count( $this->get_status() ) > 0; + } + + public function get_status() { + $status = array(); + foreach ( $this->features as $slug => $optimization ) { + $status[ $slug ] = $optimization->status->is_enabled(); + } + return $status; + } + + public function register_endpoints( $feature ) { + if ( ! $feature instanceof Has_Endpoints ) { + return false; + } + + if ( empty( $feature->get_endpoints() ) ) { + return false; + } + + } + + /** + * @inheritDoc + */ + public function setup() { + + foreach ( $this->available_modules() as $slug => $optimization ) { + + if ( ! $optimization->status->is_enabled() ) { + continue; + } + + $optimization->feature->setup(); + $this->register_endpoints( $optimization->feature ); + + do_action( "jetpack_boost_{$slug}_initialized", $this ); + + } + } + + /** + * Get the lists of modules explicitly disabled from the 'jb-disable-modules' query string. + * The parameter is a comma separated value list of module slug. + * + * @return array + */ + + public function get_disabled_modules() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! empty( $_GET['jb-disable-modules'] ) ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + return array_map( 'sanitize_key', explode( ',', $_GET['jb-disable-modules'] ) ); + } + + return array(); + } + + /** + * @inheritDoc + */ + public function setup_trigger() { + return 'plugins_loaded'; + } + +} diff --git a/projects/plugins/boost/app/modules/critical-css/class-admin-bar-css-compat.php b/projects/plugins/boost/app/features/optimizations/critical-css/Admin_Bar_Compatibilty.php similarity index 75% rename from projects/plugins/boost/app/modules/critical-css/class-admin-bar-css-compat.php rename to projects/plugins/boost/app/features/optimizations/critical-css/Admin_Bar_Compatibilty.php index 5d7924ac6bf13..a2f18f83e04c0 100644 --- a/projects/plugins/boost/app/modules/critical-css/class-admin-bar-css-compat.php +++ b/projects/plugins/boost/app/features/optimizations/critical-css/Admin_Bar_Compatibilty.php @@ -1,24 +1,11 @@ storage = new Critical_CSS_Storage(); + $this->paths = new Source_Providers(); + + } + + /** + * This is only run if Critical CSS module has been activated. + */ + public function setup() { + // Touch to setup the post type. This is a temporary hack. + // This should instantiate a new Post_Type_Storage class, + // so that Critical_CSS class is responsible + // for setting up the storage. + $recommendations = new Recommendations(); + $recommendations->attach_hooks(); + + add_action( 'wp', array( $this, 'display_critical_css' ) ); + + if ( Generator::is_generating_critical_css() ) { + add_action( 'wp_head', array( $this, 'display_generate_meta' ), 0 ); + $this->force_logged_out_render(); + } + + add_action( 'handle_theme_change', array( $this, 'clear_critical_css' ) ); + add_action( 'jetpack_boost_clear_cache', array( $this, 'clear_critical_css' ) ); + add_filter( 'jetpack_boost_js_constants', array( $this, 'add_critical_css_constants' ) ); + + REST_API::register( $this->get_endpoints() ); + return true; + } + + public function get_slug() { + return 'critical-css'; + } + + /** + * Renders a tag used to verify this is a valid page to generate Critical CSS with. + */ + public function display_generate_meta() { + ?> + + paths->get_current_request_css(); + if ( ! $critical_css ) { + return; + } + + $display = new Display_Critical_CSS( $critical_css ); + add_action( 'wp_head', array( $display, 'display_critical_css' ), 0 ); + add_filter( 'style_loader_tag', array( $display, 'asynchronize_stylesheets' ), 10, 4 ); + add_action( 'wp_footer', array( $display, 'onload_flip_stylesheets' ) ); + } + + /** + * Clear Critical CSS. + */ + public function clear_critical_css() { + // Mass invalidate all cached values. + // ^^ Not true anymore. Mass invalidate __some__ cached values. + $this->storage->clear(); + Critical_CSS_State::reset(); + } + + /** + * Force the current page to render as viewed by a logged out user. Useful when generating + * Critical CSS. + */ + private function force_logged_out_render() { + $current_user_id = get_current_user_id(); + + if ( 0 !== $current_user_id ) { + // Force current user to 0 to ensure page is rendered as a non-logged-in user. + wp_set_current_user( 0 ); + + // Turn off display of admin bar. + add_filter( 'show_admin_bar', '__return_false', PHP_INT_MAX ); + } + } + + /** + * Override; returns an admin notice to show if there was a reset reason. + * + * @TODO: + * There should be an Admin_Notice class + * To create a notice, (new Admin_Notice())->create("notice text"); + * To view notices: (new Admin_Notice())->get_all(); + * @return null|\Automattic\Jetpack_Boost\Admin\Admin_Notice[] + */ + public function get_admin_notices() { + $reason = \get_option( self::RESET_REASON_STORAGE_KEY ); + + if ( ! $reason ) { + return array(); + } + + return array( new Regenerate_Admin_Notice( $reason ) ); + } + + /** + * Clear Critical CSS reset reason option. + * + * @TODO: Admin notices need to be moved elsewhere. + * Note: Looks like we need a way to and options throughout the app. + * This is why it's currently awkwardly using a static method with a constant + * If we could trust classes to use constructors properly - without performing actions + * Then we could easily (and cheaply) instantiate all Boost objects + * and kindly ask them to delete themselves + */ + public static function clear_reset_reason() { + \delete_option( self::RESET_REASON_STORAGE_KEY ); + } + + /** + * Add Critical CSS related constants to be passed to JavaScript only if the module is enabled. + * + * @param array $constants Constants to be passed to JavaScript. + * + * @return array + */ + public function add_critical_css_constants( $constants ) { + // Information about the current status of Critical CSS / generation. + $generator = new Generator(); + $constants['criticalCssStatus'] = $generator->get_local_critical_css_generation_info(); + + return $constants; + } + + /** + * @TODO: Facepalm. PHP Typehinting is broken. + * @return Endpoint[] + * + */ + public function get_endpoints() { + return array( + Generator_Status::class, + Generator_Request::class, + Generator_Success::class, + Recommendations_Dismiss::class, + Recommendations_Reset::class, + Generator_Error::class, + ); + } + + /** + * @inheritDoc + */ + public function setup_trigger() { + return 'init'; + } +} diff --git a/projects/plugins/boost/app/modules/critical-css/class-critical-css-state.php b/projects/plugins/boost/app/features/optimizations/critical-css/Critical_CSS_State.php similarity index 97% rename from projects/plugins/boost/app/modules/critical-css/class-critical-css-state.php rename to projects/plugins/boost/app/features/optimizations/critical-css/Critical_CSS_State.php index 1768bd0227abc..e5867d9887da1 100644 --- a/projects/plugins/boost/app/modules/critical-css/class-critical-css-state.php +++ b/projects/plugins/boost/app/features/optimizations/critical-css/Critical_CSS_State.php @@ -7,10 +7,9 @@ * @package automattic/jetpack-boost */ -namespace Automattic\Jetpack_Boost\Modules\Critical_CSS; +namespace Automattic\Jetpack_Boost\Features\Optimizations\Critical_CSS; use Automattic\Jetpack_Boost\Lib\Transient; -use Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers\Provider; /** * Critical CSS State @@ -203,11 +202,6 @@ public function get_core_providers_status( $keys ) { protected function get_provider_sources( $providers ) { $sources = array(); - /** - * Provider. - * - * @var $provider Provider - */ foreach ( $providers as $provider ) { $provider_name = $provider::get_provider_name(); @@ -382,7 +376,7 @@ public function get_percent_complete() { /** * Reset the Critical CSS state. */ - public function reset() { + public static function reset() { Transient::delete( self::KEY ); } diff --git a/projects/plugins/boost/app/modules/critical-css/class-critical-css-storage.php b/projects/plugins/boost/app/features/optimizations/critical-css/Critical_CSS_Storage.php similarity index 94% rename from projects/plugins/boost/app/modules/critical-css/class-critical-css-storage.php rename to projects/plugins/boost/app/features/optimizations/critical-css/Critical_CSS_Storage.php index 29f233f445fe5..820b560ac3de3 100644 --- a/projects/plugins/boost/app/modules/critical-css/class-critical-css-storage.php +++ b/projects/plugins/boost/app/features/optimizations/critical-css/Critical_CSS_Storage.php @@ -7,7 +7,7 @@ * @package automattic/jetpack-boost */ -namespace Automattic\Jetpack_Boost\Modules\Critical_CSS; +namespace Automattic\Jetpack_Boost\Features\Optimizations\Critical_CSS; use Automattic\Jetpack_Boost\Lib\Storage_Post_Type; diff --git a/projects/plugins/boost/app/features/optimizations/critical-css/Display_Critical_CSS.php b/projects/plugins/boost/app/features/optimizations/critical-css/Display_Critical_CSS.php new file mode 100644 index 0000000000000..35cf33817306c --- /dev/null +++ b/projects/plugins/boost/app/features/optimizations/critical-css/Display_Critical_CSS.php @@ -0,0 +1,149 @@ +css = $css; + } + + /** + * Converts existing screen CSS to be asynchronously loaded. + * + * @param string $html The link tag for the enqueued style. + * @param string $handle The style's registered handle. + * @param string $href The stylesheet's source URL. + * @param string $media The stylesheet's media attribute. + * + * @return string + * @see style_loader_tag + */ + public function asynchronize_stylesheets( + $html, + $handle, + $href, + $media + ) { + // If there is no critical CSS, do not alter the stylesheet loading. + if ( false === $this->css ) { + return $html; + } + + $available_methods = array( + 'async' => 'media="not all" data-media="' . $media . '" onload="this.media=this.dataset.media; delete this.dataset.media; this.removeAttribute( \'onload\' );"', + 'deferred' => 'media="not all" data-media="' . $media . '"', + ); + + /** + * Loading method for stylesheets. + * + * Filter the loading method for each stylesheet for the screen with following values: + * async - Stylesheets are loaded asynchronously. + * Styles are applied once the stylesheet is loaded completely without render blocking. + * deferred - Loading of stylesheets are deferred until the window load event. + * Styles from all the stylesheets are applied at once after the page load. + * + * Stylesheet loading behaviour is not altered for any other value such as false or 'default'. + * Stylesheet loading is instant and the process blocks the page rendering. + * Eg: add_filter( 'jetpack_boost_async_style', '__return_false' ); + * + * @param string $handle The style's registered handle. + * @param string $media The stylesheet's media attribute. + * + * @see onload_flip_stylesheets for how stylesheets loading is deferred. + * + * @todo Retrieve settings from database, either via auto-configuration or UI option. + */ + $method = apply_filters( 'jetpack_boost_async_style', 'async', $handle, $media ); + + // If the loading method is not allowed, do not alter the stylesheet loading. + if ( ! isset( $available_methods[ $method ] ) ) { + return $html; + } + + $html_no_script = ''; + + // Update the stylesheet markup for allowed methods. + $html = preg_replace( '~media=(\'[^\']+\')|("[^"]+")~', $available_methods[ $method ], $html ); + + // Append to the HTML stylesheet tag the same untouched HTML stylesheet tag within the noscript tag + // to support the rendering of the stylesheet when JavaScript is disabled. + return $html_no_script . $html; + } + + /** + * Prints the critical CSS to the page. + */ + public function display_critical_css() { + $critical_css = $this->css; + + if ( false === $critical_css ) { + return false; + } + + echo ' tag (or any HTML tags) in output. + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo wp_strip_all_tags( $critical_css ); + + echo ''; + } + + /** + * Add a small piece of JavaScript to the footer, which on load flips all + * linked stylesheets from media="not all" to "all", and switches the + * Critical CSS tag (or any HTML tags) in output. - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo wp_strip_all_tags( $critical_css ); - - echo ''; - } - - /** - * Check if the current URL is warmed up. For this module, "warmed up" means that - * either Critical CSS has been generated for this page, or this page is not - * eligible to have Critical CSS generated for it. - * - * @param bool $ready Injected filter value. - * - * @return bool - */ - public function is_ready_filter( $ready ) { - if ( ! $ready ) { - return $ready; - } - - // If this page has no provider keys, it is ineligible for Critical CSS. - $keys = $this->get_current_request_css_keys(); - if ( count( $keys ) === 0 ) { - return true; - } - - // Return "ready" if Critical CSS has been generated. - return ! empty( $this->get_critical_css() ); - } - - /** - * Force the current page to render as viewed by a logged out user. Useful when generating - * Critical CSS. - */ - private function force_logged_out_render() { - $current_user_id = get_current_user_id(); - - if ( 0 !== $current_user_id ) { - // Force current user to 0 to ensure page is rendered as a non-logged-in user. - wp_set_current_user( 0 ); - - // Turn off display of admin bar. - add_filter( 'show_admin_bar', '__return_false', PHP_INT_MAX ); - } - } - - /** - * AJAX handler to handle proxying of external CSS resources. - */ - public function handle_css_proxy() { - // Verify valid nonce. - if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), self::GENERATE_PROXY_NONCE ) ) { - wp_die( '', 400 ); - } - - // Make sure currently logged in as admin. - if ( ! $this->current_user_can_modify_critical_css() ) { - wp_die( '', 400 ); - } - - // Reject any request made when not generating. - if ( ! $this->state->is_pending() ) { - wp_die( '', 400 ); - } - - // Validate URL and fetch. - $proxy_url = filter_var( wp_unslash( $_POST['proxy_url'] ), FILTER_VALIDATE_URL ); - if ( ! wp_http_validate_url( $proxy_url ) ) { - die( 'Invalid URL' ); - } - - $response = wp_remote_get( $proxy_url ); - if ( is_wp_error( $response ) ) { - // TODO: Nicer error handling. - die( 'error' ); - } - - header( 'Content-type: text/css' ); - - // Outputting proxied CSS contents unescaped. - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo wp_strip_all_tags( $response['body'] ); - - die(); - } - - /** - * API helper for ensuring this module is enabled before allowing an API - * endpoint to continue. Will die if this module is not initialized, with - * a status message indicating so. - */ - public function ensure_module_initialized() { - if ( ! $this->is_initialized() ) { - wp_send_json( array( 'status' => 'module-unavailable' ) ); - } - } - - /** - * Add a small piece of JavaScript to the footer, which on load flips all - * linked stylesheets from media="not all" to "all", and switches the - * Critical CSS