initial
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple wrapper for Tracks library
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
use Jetpack_Options;
|
||||
use Jetpack_Tracks_Client;
|
||||
|
||||
/**
|
||||
* Class Analytics
|
||||
*/
|
||||
class Analytics {
|
||||
/**
|
||||
* Initialize tracking.
|
||||
*/
|
||||
public function init() {
|
||||
$tracks = self::get_tracking();
|
||||
|
||||
// For tracking events via js/ajax.
|
||||
add_action( 'admin_enqueue_scripts', array( $tracks, 'enqueue_tracks_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracking and manager objects for Boost.
|
||||
*/
|
||||
public static function get_tracking() {
|
||||
return new Tracking( 'jetpack_boost', new Manager( 'jetpack-boost' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a user event.
|
||||
*
|
||||
* @param string $slug The event slug.
|
||||
* @param array $data Optional event data.
|
||||
*/
|
||||
public static function record_user_event( $slug, $data = array() ) {
|
||||
if ( ! isset( $data['boost_version'] ) && defined( 'JETPACK_BOOST_VERSION' ) ) {
|
||||
$data['boost_version'] = JETPACK_BOOST_VERSION;
|
||||
}
|
||||
|
||||
return self::get_tracking()->record_user_event( $slug, $data );
|
||||
}
|
||||
|
||||
public static function init_tracks_scripts() {
|
||||
$tracks = self::get_tracking();
|
||||
|
||||
$tracks::register_tracks_functions_scripts();
|
||||
|
||||
wp_enqueue_script( 'jp-tracks' );
|
||||
|
||||
wp_add_inline_script(
|
||||
'jp-tracks',
|
||||
sprintf(
|
||||
'window.jpTracksContext = window.jpTracksContext || {}; window.jpTracksContext.blog_id = %s;',
|
||||
absint( \Jetpack_Options::get_option( 'id', 0 ) )
|
||||
),
|
||||
'before'
|
||||
);
|
||||
}
|
||||
|
||||
public static function get_tracking_data() {
|
||||
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
$user = wp_get_current_user();
|
||||
$user_data = array(
|
||||
'userid' => $user->ID,
|
||||
'username' => $user->user_login,
|
||||
);
|
||||
$blog_id = get_current_blog_id();
|
||||
} else {
|
||||
$user_data = Jetpack_Tracks_Client::get_connected_user_tracks_identity();
|
||||
$blog_id = Jetpack_Options::get_option( 'id', 0 );
|
||||
}
|
||||
|
||||
return array(
|
||||
'userData' => $user_data,
|
||||
'blogId' => $blog_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Asset Manager for Jetpack Boost.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Class Assets
|
||||
*/
|
||||
class Assets {
|
||||
|
||||
/**
|
||||
* Given a minified path, and a non-minified path, will return
|
||||
* a minified or non-minified file URL based on whether SCRIPT_DEBUG is set and truthy.
|
||||
*
|
||||
* Both `$min_base` and `$non_min_base` are expected to be relative to the
|
||||
* root Jetpack directory.
|
||||
*
|
||||
* @param string $min_path minified path.
|
||||
* @param string $non_min_path non-minified path.
|
||||
*
|
||||
* @return string The URL to the file
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_file_url_for_environment( $min_path, $non_min_path ) {
|
||||
/**
|
||||
* Filters the internal path to the distributed assets used by the plugin
|
||||
*
|
||||
* @param string $path the path to the assets
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$internal_path = apply_filters( 'jetpack_boost_asset_internal_path', 'app/assets/dist/' );
|
||||
$url = plugins_url( $internal_path . trim( $non_min_path, '/' ), JETPACK_BOOST_PATH );
|
||||
|
||||
/**
|
||||
* Filters the URL for a file passed through the get_file_url_for_environment function.
|
||||
*
|
||||
* @param string $url The URL to the file.
|
||||
* @param string $min_path The minified path.
|
||||
* @param string $non_min_path The non-minified path.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_asset_url', $url, $min_path, $non_min_path );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS\Critical_CSS;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
|
||||
|
||||
/**
|
||||
* Class Boost_Health
|
||||
*
|
||||
* Represents the health of the Jetpack Boost plugin.
|
||||
*/
|
||||
class Boost_Health {
|
||||
|
||||
/**
|
||||
* @var array List of issues affecting the health of the plugin.
|
||||
*/
|
||||
private $issues = array();
|
||||
|
||||
/**
|
||||
* Boost_Health constructor.
|
||||
*
|
||||
* Initializes the Boost_Health object and checks for any health issues.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( self::critical_css_needs_regeneration() ) {
|
||||
$this->issues[] = __( 'Outdated Critical CSS', 'jetpack-boost' );
|
||||
}
|
||||
|
||||
if ( self::critical_css_has_errors() ) {
|
||||
$this->issues[] = __( 'Failed to generate Critical CSS', 'jetpack-boost' );
|
||||
}
|
||||
|
||||
if ( self::cache_engine_not_loading() ) {
|
||||
$this->issues[] = __( 'Cache engine is not loading', 'jetpack-boost' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of issues affecting the health of the plugin.
|
||||
*
|
||||
* @return int Total number of issues.
|
||||
*/
|
||||
public function get_total_issues() {
|
||||
return count( $this->issues );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the issues affecting the health of the plugin.
|
||||
*
|
||||
* @return array List of issues.
|
||||
*/
|
||||
public function get_all_issues() {
|
||||
return $this->issues;
|
||||
}
|
||||
|
||||
private static function is_critical_css_enabled() {
|
||||
return ( new Status( Critical_CSS::get_slug() ) )->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Critical CSS needs regeneration.
|
||||
*
|
||||
* @return bool True if regeneration is needed, false otherwise.
|
||||
*/
|
||||
public static function critical_css_needs_regeneration() {
|
||||
if ( Cloud_CSS::is_available() || ! self::is_critical_css_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$suggest_regenerate = jetpack_boost_ds_get( 'critical_css_suggest_regenerate' );
|
||||
|
||||
return in_array( $suggest_regenerate, Environment_Change_Detector::get_available_env_change_statuses(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Critical CSS generation has errors.
|
||||
*
|
||||
* @return bool True if errors are present, false otherwise.
|
||||
*/
|
||||
public static function critical_css_has_errors() {
|
||||
if ( ! self::is_critical_css_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
return ( new Critical_CSS_State() )->has_errors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache engine is not loading.
|
||||
*
|
||||
* @return bool True if the cache engine is not loading, false otherwise.
|
||||
*/
|
||||
public static function cache_engine_not_loading() {
|
||||
if ( ! ( new Status( Page_Cache::get_slug() ) )->get() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Boost_Cache::is_loaded() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Compatibility class for caching plugins.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Compatibility class for caching plugins.
|
||||
*/
|
||||
class Cache_Compatibility {
|
||||
/**
|
||||
* Checks if the site has a caching plugin.
|
||||
*
|
||||
* Supports:
|
||||
* - Endurance Page Cache
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return bool True if the site has a caching plugin, false otherwise.
|
||||
*/
|
||||
public static function has_cache() {
|
||||
/**
|
||||
* Filters whether the site has a caching plugin.
|
||||
* Useful for testing.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param bool $has_cache True if the site has a caching plugin, false otherwise.
|
||||
*/
|
||||
$has_cache = apply_filters( 'jetpack_boost_compatibility_has_cache', false );
|
||||
if ( $has_cache ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Disable Page Cache on sites that run Newfold's caching service.
|
||||
* Their cache is considered enabled when the endurance_cache_level option is set to 2 or 3.
|
||||
*
|
||||
* @link https://github.com/bluehost/endurance-page-cache/blob/59fe9993d2cb8a03d1df6da8325f73ad0851ba0a/endurance-page-cache.php#L343
|
||||
*/
|
||||
if (
|
||||
class_exists( '\\Endurance_Page_Cache' )
|
||||
&& 2 <= (int) get_option( 'endurance_cache_level' )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
/**
|
||||
* CLI commands for Jetpack Boost.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack_Boost\Data_Sync\Getting_Started_Entry;
|
||||
use Automattic\Jetpack_Boost\Modules\Module;
|
||||
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Garbage_Collection;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
|
||||
|
||||
/**
|
||||
* Control your local Jetpack Boost installation.
|
||||
*/
|
||||
class CLI {
|
||||
|
||||
/**
|
||||
* Jetpack Boost plugin.
|
||||
*
|
||||
* @var \Automattic\Jetpack_Boost\Jetpack_Boost $jetpack_boost
|
||||
*/
|
||||
private $jetpack_boost;
|
||||
|
||||
const MAKE_E2E_TESTS_WORK_MODULES = array( 'critical_css', 'speculation_rules', 'render_blocking_js', 'page_cache', 'lcp', 'minify_js', 'minify_css', 'image_cdn', 'image_guide' );
|
||||
|
||||
/**
|
||||
* CLI constructor.
|
||||
*
|
||||
* @param \Automattic\Jetpack_Boost\Jetpack_Boost $jetpack_boost Jetpack Boost plugin.
|
||||
*/
|
||||
public function __construct( $jetpack_boost ) {
|
||||
$this->jetpack_boost = $jetpack_boost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage Jetpack Boost Modules
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <activate|deactivate>
|
||||
* : The action to take.
|
||||
* ---
|
||||
* options:
|
||||
* - activate
|
||||
* - deactivate
|
||||
* ---
|
||||
*
|
||||
* [<module_slug>]
|
||||
* : The slug of the module to perform an action on.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp jetpack-boost module activate critical_css
|
||||
* wp jetpack-boost module deactivate critical_css
|
||||
*
|
||||
* @param array $args Command arguments.
|
||||
*/
|
||||
public function module( $args ) {
|
||||
$action = $args[0] ?? null;
|
||||
$module_slug = $args[1] ?? null;
|
||||
|
||||
if ( $module_slug === null ) {
|
||||
/* translators: Placeholder is list of available modules. */
|
||||
\WP_CLI::error( sprintf( __( 'Please specify a valid module. It should be one of %s', 'jetpack-boost' ), wp_json_encode( self::MAKE_E2E_TESTS_WORK_MODULES, JSON_UNESCAPED_SLASHES ) ) );
|
||||
}
|
||||
|
||||
if ( ! in_array( $module_slug, self::MAKE_E2E_TESTS_WORK_MODULES, true ) ) {
|
||||
\WP_CLI::error(
|
||||
/* translators: %1$s refers to the module slug like 'critical-css', %2$s is the list of available modules. */
|
||||
sprintf( __( "The '%1\$s' module slug is invalid. It should be one of %2\$s", 'jetpack-boost' ), $module_slug, wp_json_encode( self::MAKE_E2E_TESTS_WORK_MODULES, JSON_UNESCAPED_SLASHES ) )
|
||||
);
|
||||
}
|
||||
|
||||
switch ( $action ) {
|
||||
case 'activate':
|
||||
$this->set_module_status( $module_slug, true );
|
||||
break;
|
||||
case 'deactivate':
|
||||
$this->set_module_status( $module_slug, false );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getting_started( $args ) {
|
||||
$status = $args[0] ?? null;
|
||||
|
||||
if ( ! in_array( $status, array( 'true', 'false' ), true ) ) {
|
||||
\WP_CLI::error(
|
||||
/* translators: %s refers to the module slug like 'critical-css' */
|
||||
sprintf( __( "The '%s' status is invalid", 'jetpack-boost' ), $status )
|
||||
);
|
||||
}
|
||||
|
||||
( new Getting_Started_Entry() )->set( 'true' === $status );
|
||||
|
||||
\WP_CLI::success(
|
||||
/* translators: %s refers to 'true' or 'false' */
|
||||
sprintf( __( 'Getting started is set to %s', 'jetpack-boost' ), $status )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a module status.
|
||||
*
|
||||
* @param string $module_slug Module slug.
|
||||
* @param string $status Module status.
|
||||
*/
|
||||
private function set_module_status( $module_slug, $status ) {
|
||||
$modules = ( new Modules_Setup() )->get_available_modules_and_submodules();
|
||||
$module = $modules[ $module_slug ] ?? false;
|
||||
if ( ! ( $module instanceof Module ) ) {
|
||||
\WP_CLI::error(
|
||||
/* translators: %s refers to the module slug like 'critical-css' */
|
||||
sprintf( __( "The '%s' module slug is invalid", 'jetpack-boost' ), $module_slug )
|
||||
);
|
||||
}
|
||||
|
||||
$module->update( $status );
|
||||
|
||||
if ( $module_slug === 'page_cache' && $status ) {
|
||||
$setup_result = Page_Cache_Setup::run_setup();
|
||||
if ( is_wp_error( $setup_result ) ) {
|
||||
\WP_CLI::error(
|
||||
sprintf(
|
||||
/* translators: %s refers to the error code */
|
||||
__( 'Setup: %s', 'jetpack-boost' ),
|
||||
$setup_result->get_error_code()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Garbage_Collection::activate();
|
||||
Boost_Cache_Settings::get_instance()->set( array( 'enabled' => true ) );
|
||||
}
|
||||
|
||||
$status_label = $status ? __( 'activated', 'jetpack-boost' ) : __( 'deactivated', 'jetpack-boost' );
|
||||
|
||||
/* translators: The %1$s refers to the module slug, %2$s refers to the module state (either activated or deactivated)*/
|
||||
\WP_CLI::success(
|
||||
sprintf( __( "'%1\$s' has been %2\$s.", 'jetpack-boost' ), $module_slug, $status_label )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage Jetpack Boost connection
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <activate|deactivate|status>
|
||||
* : The action to take.
|
||||
* ---
|
||||
* options:
|
||||
* - activate
|
||||
* - deactivate
|
||||
* - status
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp jetpack-boost connection activate
|
||||
* wp jetpack-boost connection deactivate
|
||||
* wp jetpack-boost connection status
|
||||
*
|
||||
* @param array $args Command arguments.
|
||||
*/
|
||||
public function connection( $args ) {
|
||||
$action = $args[0] ?? null;
|
||||
|
||||
switch ( $action ) {
|
||||
case 'activate':
|
||||
$result = $this->jetpack_boost->connection->register();
|
||||
if ( true === $result ) {
|
||||
\WP_CLI::success( __( 'Boost is connected to WP.com', 'jetpack-boost' ) );
|
||||
} else {
|
||||
\WP_CLI::error( __( 'Boost could not be connected to WP.com', 'jetpack-boost' ) );
|
||||
}
|
||||
break;
|
||||
case 'deactivate':
|
||||
require_once ABSPATH . '/wp-admin/includes/plugin.php';
|
||||
|
||||
if ( is_plugin_active_for_network( JETPACK_BOOST_PATH ) ) {
|
||||
$this->jetpack_boost->connection->deactivate_disconnect_network();
|
||||
} else {
|
||||
$this->jetpack_boost->connection->disconnect();
|
||||
}
|
||||
|
||||
\WP_CLI::success( __( 'Boost is disconnected from WP.com', 'jetpack-boost' ) );
|
||||
break;
|
||||
case 'status':
|
||||
$is_connected = $this->jetpack_boost->connection->is_connected();
|
||||
if ( $is_connected ) {
|
||||
\WP_CLI::line( 'connected' );
|
||||
} else {
|
||||
\WP_CLI::line( 'disconnected' );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Jetpack Boost
|
||||
*
|
||||
* ## EXAMPLE
|
||||
*
|
||||
* wp jetpack-boost reset
|
||||
*/
|
||||
public function reset() {
|
||||
$this->jetpack_boost->deactivate();
|
||||
$this->jetpack_boost->uninstall();
|
||||
\WP_CLI::success( 'Reset successfully' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* A collection of WordPress options that's stored as a single ar
|
||||
*/
|
||||
class Collection {
|
||||
|
||||
private $key;
|
||||
|
||||
/**
|
||||
* Collections imply that they may carry more data than regular options,
|
||||
* This might unnecessarily slow down sites.
|
||||
* Disable autoloading by default.
|
||||
*
|
||||
* @see autoload() to enable autoloading.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $autoload = false;
|
||||
|
||||
/**
|
||||
* @param string $key Collection key.
|
||||
*/
|
||||
public function __construct( $key ) {
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow autoloading collections
|
||||
*/
|
||||
public function autoload() {
|
||||
$this->autoload = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the whole collection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get() {
|
||||
$result = get_option( $this->key, array() );
|
||||
if ( is_array( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a single item to the collection
|
||||
*
|
||||
* @param mixed $item
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function append( $item ) {
|
||||
$items = $this->get();
|
||||
|
||||
if ( ! in_array( $item, $items, true ) ) {
|
||||
$items[] = $item;
|
||||
return update_option( $this->key, $items, $this->autoload );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the whole collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete() {
|
||||
return delete_option( $this->key );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack connection client.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Terms_Of_Service;
|
||||
|
||||
/**
|
||||
* Class Connection
|
||||
*
|
||||
* Manages the Jetpack connection on behalf of Jetpack Boost.
|
||||
*/
|
||||
class Connection {
|
||||
|
||||
/**
|
||||
* Jetpack Connection Manager.
|
||||
*
|
||||
* @var Manager $manager The connection manager.
|
||||
*/
|
||||
private $manager;
|
||||
|
||||
public function __construct() {
|
||||
$this->manager = new Manager( 'jetpack-boost' );
|
||||
}
|
||||
|
||||
public function init() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
||||
|
||||
$this->initialize_deactivate_disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin deactivation hook.
|
||||
*/
|
||||
public function initialize_deactivate_disconnect() {
|
||||
require_once ABSPATH . '/wp-admin/includes/plugin.php';
|
||||
|
||||
if ( is_plugin_active_for_network( JETPACK_BOOST_PATH ) ) {
|
||||
register_deactivation_hook( JETPACK_BOOST_PATH, array( $this, 'deactivate_disconnect_network' ) );
|
||||
} else {
|
||||
register_deactivation_hook( JETPACK_BOOST_PATH, array( $this, 'deactivate_disconnect' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the connection on plugin disconnect.
|
||||
*/
|
||||
public function deactivate_disconnect() {
|
||||
$this->manager->remove_connection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the connection on plugin disconnect for network-activated plugins.
|
||||
*/
|
||||
public function deactivate_disconnect_network() {
|
||||
if ( ! is_network_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( get_sites() as $s ) {
|
||||
switch_to_blog( (int) $s->blog_id );
|
||||
|
||||
$active_plugins = get_option( 'active_plugins' );
|
||||
|
||||
/*
|
||||
* If this plugin was activated in the subsite individually
|
||||
* we do not want to call disconnect. Plugins activated
|
||||
* individually (before network activation) stay activated
|
||||
* when the network deactivation occurs
|
||||
*/
|
||||
if ( ! in_array( JETPACK_BOOST_PATH, $active_plugins, true ) ) {
|
||||
$this->deactivate_disconnect();
|
||||
}
|
||||
|
||||
restore_current_blog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection Lifecycle methods.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the WordPress.com blog ID of this site, if it's connected
|
||||
*/
|
||||
public static function wpcom_blog_id() {
|
||||
return defined( 'IS_WPCOM' ) && IS_WPCOM ? get_current_blog_id() : (int) \Jetpack_Options::get_option( 'id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the site is connected to WP.com.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_connected() {
|
||||
/**
|
||||
* Filter that fakes the connection to WordPress.com. Useful for testing.
|
||||
*
|
||||
* @param bool $connection Return true to fake the connection.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
if ( true === apply_filters( 'jetpack_boost_connection_bypass', false ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->manager->is_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register site using connection manager.
|
||||
*
|
||||
* @return true|\WP_Error The error object.
|
||||
*/
|
||||
public function register() {
|
||||
if ( $this->is_connected() ) {
|
||||
Analytics::record_user_event( 'using_existing_connection' );
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = $this->manager->register();
|
||||
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
Analytics::record_user_event( 'established_connection' );
|
||||
Premium_Features::clear_cache();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Jetpack account.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function disconnect() {
|
||||
// @todo implement check for Jetpack::validate_sync_error_idc_option() so we don't disconnect production site from staging etc.
|
||||
Analytics::record_user_event( 'disconnect_site' );
|
||||
|
||||
$this->manager->remove_connection();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST endpoint methods.
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route(
|
||||
JETPACK_BOOST_REST_NAMESPACE,
|
||||
JETPACK_BOOST_REST_PREFIX . '/connection',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_connection_endpoint' ),
|
||||
'permission_callback' => array( $this, 'can_manage_connection' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
JETPACK_BOOST_REST_NAMESPACE,
|
||||
JETPACK_BOOST_REST_PREFIX . '/connection',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'create_connection_endpoint' ),
|
||||
'permission_callback' => array( $this, 'can_manage_connection' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register site using connection manager.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function create_connection_endpoint( \WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
( new Terms_Of_Service() )->agree();
|
||||
|
||||
$response = $this->register();
|
||||
|
||||
// Clear premium features cache to force a refresh.
|
||||
Premium_Features::clear_cache();
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
do_action( 'jetpack_boost_connection_established' );
|
||||
|
||||
return rest_ensure_response( $this->get_connection_api_response() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch connection info.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function get_connection_endpoint( \WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return rest_ensure_response( $this->get_connection_api_response() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection state sent to client on initialization and after updates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_connection_api_response() {
|
||||
/**
|
||||
* Filter that fakes the connection to WordPress.com. Useful for testing.
|
||||
*
|
||||
* @param bool $connection Return true to fake the connection.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$force_connected = apply_filters( 'jetpack_boost_connection_bypass', false );
|
||||
|
||||
$response = array(
|
||||
'connected' => $force_connected || $this->is_connected(),
|
||||
'wpcomBlogId' => ( $force_connected || $this->is_connected() ) ? self::wpcom_blog_id() : null,
|
||||
'userConnected' => $this->manager->is_user_connected(),
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can user manage the connection?
|
||||
*
|
||||
* @return boolean | \WP_Error
|
||||
*/
|
||||
public function can_manage_connection() {
|
||||
if ( current_user_can( 'manage_options' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$user_permissions_error_msg = __(
|
||||
'You do not have the correct user permissions to perform this action.
|
||||
Please contact your site admin if you think this is a mistake.',
|
||||
'jetpack-boost'
|
||||
);
|
||||
|
||||
return new \WP_Error(
|
||||
'invalid_user_permission_jetpack_connect',
|
||||
$user_permissions_error_msg,
|
||||
array( 'status' => self::rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual HTTP error code for authorization failure.
|
||||
*
|
||||
* Taken from rest_authorization_required_code() in WP-API plugin until is added to core.
|
||||
*
|
||||
* @see https://github.com/WP-API/WP-API/commit/7ba0ae6fe4f605d5ffe4ee85b1cd5f9fb46900a6
|
||||
*
|
||||
* @since 4.3.0
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function rest_authorization_required_code() {
|
||||
return is_user_logged_in() ? 403 : 401;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Implement debug helper methods.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Class Debug
|
||||
*/
|
||||
class Debug {
|
||||
/**
|
||||
* Returns whether the debug mode has been triggered.
|
||||
*/
|
||||
public static function is_debug_mode() {
|
||||
$script_debug = defined( 'SCRIPT_DEBUG' ) && \SCRIPT_DEBUG;
|
||||
$manual_debug_mode = filter_input( INPUT_GET, 'jetpack-boost-debug' );
|
||||
|
||||
$debug = $script_debug || $manual_debug_mode;
|
||||
|
||||
/**
|
||||
* Filter debug status on/off
|
||||
*
|
||||
* @param bool $debug_status enable or disable debug mode.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_debug', $debug );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Environment Change Detector class.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
|
||||
|
||||
/**
|
||||
* Class Environment_Change_Detector
|
||||
*/
|
||||
class Environment_Change_Detector {
|
||||
|
||||
const ENV_CHANGE_LEGACY = '1';
|
||||
const ENV_CHANGE_PAGE_SAVED = 'page_saved';
|
||||
const ENV_CHANGE_POST_SAVED = 'post_saved';
|
||||
const ENV_CHANGE_SWITCHED_THEME = 'switched_theme';
|
||||
const ENV_CHANGE_PLUGIN_CHANGE = 'plugin_change';
|
||||
const ENV_CHANGE_CORNERSTONE_PAGE_SAVED = 'cornerstone_page_saved';
|
||||
const ENV_CHANGE_CORNERSTONE_PAGES_LIST_UPDATED = 'cornerstone_pages_list_updated';
|
||||
|
||||
/**
|
||||
* Initialize the change detection hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
$object = new self();
|
||||
$object->register_hooks();
|
||||
}
|
||||
|
||||
public function register_hooks() {
|
||||
add_action( 'after_switch_theme', array( $this, 'handle_theme_change' ) );
|
||||
add_action( 'save_post', array( $this, 'handle_post_change' ), 10, 2 );
|
||||
add_action( 'activated_plugin', array( $this, 'handle_plugin_change' ) );
|
||||
add_action( 'deactivated_plugin', array( $this, 'handle_plugin_change' ) );
|
||||
}
|
||||
|
||||
public function handle_post_change( $post_id, $post ) {
|
||||
// Ignore changes to any post which is not published.
|
||||
if ( 'publish' !== $post->post_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore changes to post types which do not affect the front-end UI
|
||||
if ( ! $this->is_post_type_invalidating( $post->post_type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->do_action( false, $this->get_post_change_type( $post ) );
|
||||
}
|
||||
|
||||
public function handle_theme_change() {
|
||||
$this->do_action( true, $this::ENV_CHANGE_SWITCHED_THEME );
|
||||
}
|
||||
|
||||
public function handle_plugin_change() {
|
||||
$this->do_action( false, $this::ENV_CHANGE_PLUGIN_CHANGE );
|
||||
}
|
||||
|
||||
public function handle_cornerstone_pages_list_update() {
|
||||
$this->do_action( false, $this::ENV_CHANGE_CORNERSTONE_PAGES_LIST_UPDATED );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the environment change action.
|
||||
*
|
||||
* @param bool $is_major_change Whether the change is such that we should stop serving existing critical CSS immediately unless refreshed.
|
||||
* @param string $change_type The change type.
|
||||
*/
|
||||
public function do_action( $is_major_change, $change_type ) {
|
||||
do_action_deprecated(
|
||||
'jetpack_boost_critical_css_environment_changed',
|
||||
array( $is_major_change, $change_type ),
|
||||
'3.20.0',
|
||||
'jetpack_boost_environment_changed'
|
||||
);
|
||||
|
||||
do_action( 'jetpack_boost_environment_changed', $is_major_change, $change_type );
|
||||
}
|
||||
|
||||
public static function get_available_env_change_statuses() {
|
||||
return array(
|
||||
self::ENV_CHANGE_PAGE_SAVED,
|
||||
self::ENV_CHANGE_POST_SAVED,
|
||||
self::ENV_CHANGE_SWITCHED_THEME,
|
||||
self::ENV_CHANGE_PLUGIN_CHANGE,
|
||||
self::ENV_CHANGE_CORNERSTONE_PAGE_SAVED,
|
||||
self::ENV_CHANGE_CORNERSTONE_PAGES_LIST_UPDATED,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a post_type, return true if this post type affects the front end of
|
||||
* the site - i.e.: should cause cached optimizations to be invalidated.
|
||||
*
|
||||
* @param string $post_type The post type to check
|
||||
* @return bool True if this post type affects the front end of the site.
|
||||
*/
|
||||
private function is_post_type_invalidating( $post_type ) {
|
||||
// Special cases: items which are not viewable, but affect the UI.
|
||||
if ( in_array( $post_type, array( 'wp_template', 'wp_template_part' ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( is_post_type_viewable( $post_type ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of change for a specific post.
|
||||
*
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return string The change type.
|
||||
*/
|
||||
private function get_post_change_type( $post ) {
|
||||
if ( Cornerstone_Utils::is_cornerstone_page( $post->ID ) ) {
|
||||
return $this::ENV_CHANGE_CORNERSTONE_PAGE_SAVED;
|
||||
}
|
||||
|
||||
if ( 'page' === $post->post_type ) {
|
||||
$change_type = $this::ENV_CHANGE_PAGE_SAVED;
|
||||
} else {
|
||||
$change_type = $this::ENV_CHANGE_POST_SAVED;
|
||||
}
|
||||
|
||||
return $change_type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
<?php
|
||||
/**
|
||||
* Heuristic structural validator for minified JavaScript.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Answers one narrow question: does a chunk of minified JS look structurally
|
||||
* broken/truncated? It is NOT a full JS validator (PHP has none built in) -- it
|
||||
* is a single left-to-right lexer pass that tracks string/template/regex/comment
|
||||
* state so brackets inside those are ignored, counts bracket nesting in code, and
|
||||
* reports "broken" when, at EOF, a string/template/regex/block-comment is still
|
||||
* open or brackets are unbalanced. That is exactly the signature of the truncation
|
||||
* corruption the bundled MatthiasMullie minifier produces on modern JS.
|
||||
*
|
||||
* It deliberately fails safe: a "broken" verdict only causes the caller to skip
|
||||
* re-minification for that bundle, so ambiguity is cheap to get wrong.
|
||||
*
|
||||
* Known blind spot: corruption that stays perfectly balanced (e.g. semantic-only
|
||||
* damage that still parses) returns "intact". Those do not crash the page and are
|
||||
* not what this guard targets.
|
||||
*
|
||||
* Known (harmless) false positives: a `/` after `}` is read as a regex, not
|
||||
* division, because telling a block `}` apart from an object-literal `}` needs a
|
||||
* full parser; so valid object-literal division such as `({}/2)` is reported
|
||||
* "broken". This generalizes to any object-literal `}` followed by `/` anywhere
|
||||
* in a source file (`f({}/2)`, `[{}/2]`, `var o={a:1}/2`), and because one
|
||||
* occurrence skips re-minification of that whole file's contribution to the
|
||||
* bundle, the cost is more than a single statement's worth of compression. It is
|
||||
* still fail-safe
|
||||
* (it only skips re-minification, never corrupts output), so the heuristic stays
|
||||
* simple rather than guess at block-vs-expression context; the new fallback
|
||||
* observability hook lets the real-world frequency be measured.
|
||||
*/
|
||||
class Js_Structure_Scanner {
|
||||
|
||||
/**
|
||||
* Lexer states.
|
||||
*/
|
||||
private const ST_CODE = 0;
|
||||
private const ST_STRING = 1; // Inside a '...' or "..." string.
|
||||
private const ST_TEMPLATE = 2; // Inside a `...` template literal.
|
||||
private const ST_REGEX = 3; // Inside a /.../ regex literal.
|
||||
private const ST_LINE_COMMENT = 4; // Inside a // comment.
|
||||
private const ST_BLOCK_COMMENT = 5; // Inside a /* */ comment.
|
||||
|
||||
/**
|
||||
* Upper bound (in bytes) on output we will fully scan per call.
|
||||
*
|
||||
* The scan only runs on a cache miss, but on hosts where the concatenation
|
||||
* cache is not writable the _jb_static path re-minifies (and would re-scan)
|
||||
* on every request, so the per-call cost must stay bounded. This is set high
|
||||
* enough to cover realistic modern bundles (Tailwind/SPA/plugin-heavy output
|
||||
* routinely runs a few MB) so the truncation signature is actually caught,
|
||||
* rather than silently bypassed for anything above an aggressive cap.
|
||||
*
|
||||
* For the rare bundle larger than this, the full lexer scan is skipped, but a
|
||||
* cheap gross-truncation backstop still runs when the original input is known
|
||||
* (see looks_broken()).
|
||||
*/
|
||||
private const MAX_SCAN_BYTES = 8388608; // 8 MB.
|
||||
|
||||
/**
|
||||
* Above MAX_SCAN_BYTES we cannot afford the full scan, but a minified output
|
||||
* that is dramatically smaller than its input is the truncation signature by
|
||||
* itself. Real-world minification shrinks JS by ~20-40%; an output below this
|
||||
* fraction of the original is treated as truncated. The threshold is
|
||||
* deliberately conservative so it only trips on gross truncation, never on
|
||||
* ordinary (even aggressive) minification.
|
||||
*
|
||||
* Blind spot: a size-preserving break above the cap (e.g. an unterminated
|
||||
* template at EOF, which drops only a byte or two) leaves the ratio normal and
|
||||
* is not caught -- and because no fallback fires, the observability hook does not
|
||||
* surface it either. Accepted because minification runs per source file, so the
|
||||
* cap only engages for a pathological multi-MB single source file, a vanishingly
|
||||
* rare input for this corruption.
|
||||
*/
|
||||
private const TRUNCATION_RATIO = 0.5;
|
||||
|
||||
/**
|
||||
* Upper bound on simultaneous open brackets / template-interpolation frames.
|
||||
*
|
||||
* $stack and $frames grow with structural nesting depth, which is bounded by
|
||||
* input bytes but not proportional to them: a small file of runaway `${`/`[`/`(`
|
||||
* openers can amplify into hundreds of MB of frame state and trip the PHP memory
|
||||
* limit. That fatal is NOT a \Throwable, so Minify::js()'s try/catch cannot catch
|
||||
* it -- it would white-screen the page, the exact failure this scanner exists to
|
||||
* prevent. Nesting this deep never occurs in legitimate output (real code nests a
|
||||
* few levels), so reaching the cap is itself a corruption/abuse signature: the
|
||||
* scan stops and returns "broken", routing the caller to the fail-safe original
|
||||
* bytes.
|
||||
*
|
||||
* Kept to a few thousand rather than higher: the check runs after each push, so
|
||||
* the cap also bounds peak frame state, and each interp frame is a small array.
|
||||
* A higher cap (e.g. 100k) lets ~150 KB of runaway `${` openers build ~22 MB of
|
||||
* state before tripping -- enough to OOM-fatal a low-memory shared host (Boost's
|
||||
* core audience) before the cap engages, reintroducing the white-screen. At a few
|
||||
* thousand the peak stays well under 1 MB while still sitting orders of magnitude
|
||||
* above any real nesting.
|
||||
*/
|
||||
private const MAX_NESTING_DEPTH = 2000;
|
||||
|
||||
/**
|
||||
* JS being scanned and its length.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $js;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $len;
|
||||
|
||||
/**
|
||||
* Current scan offset.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $pos = 0;
|
||||
|
||||
/**
|
||||
* Current lexer state (one of the ST_* constants).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $state = self::ST_CODE;
|
||||
|
||||
/**
|
||||
* The quote character that opened the current string (' or ").
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $string_quote = '';
|
||||
|
||||
/**
|
||||
* Stack of currently open brackets ('{', '(', '[').
|
||||
*
|
||||
* Coordinates with $frames: an `interp` frame snapshots count($stack) at
|
||||
* push time, and closes_interpolation() uses that snapshot to tell a '}'
|
||||
* that ends `${ ... }` apart from one that closes an ordinary block.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $stack = array();
|
||||
|
||||
/**
|
||||
* Template / interpolation frames.
|
||||
*
|
||||
* Each frame is one of:
|
||||
* - array( 'type' => 'template' ): inside the body of a `...` literal.
|
||||
* - array( 'type' => 'interp', 'depth' => int ): inside `${ ... }`; the
|
||||
* 'depth' field captures count($stack) at the moment the frame was
|
||||
* pushed, so closes_interpolation() can pair this '}' with the matching
|
||||
* '${' rather than a sibling code block. See scan_template() and
|
||||
* closes_interpolation().
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $frames = array();
|
||||
|
||||
/**
|
||||
* True once an extra or mismatched closing bracket has been seen.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $unmatched = false;
|
||||
|
||||
/**
|
||||
* True while inside a regex [...] character class.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $re_class = false;
|
||||
|
||||
/**
|
||||
* Last significant char seen in code state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $prev = '';
|
||||
|
||||
/**
|
||||
* The significant char before $prev.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $prev_prev = '';
|
||||
|
||||
/**
|
||||
* Last identifier/keyword seen in code state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $prev_word = '';
|
||||
|
||||
/**
|
||||
* Whether $prev_word was a member access (immediately after a '.').
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $prev_dot = false;
|
||||
|
||||
/**
|
||||
* Whether the given minified JS looks structurally broken/truncated.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param string $js Minified JS to inspect.
|
||||
* @param string|null $original_js The pre-minification input, when available. Used
|
||||
* only for the gross-truncation backstop on inputs
|
||||
* too large to scan in full (see MAX_SCAN_BYTES).
|
||||
*
|
||||
* @return bool True if it looks broken; false if it looks intact.
|
||||
*/
|
||||
public static function looks_broken( $js, $original_js = null ) {
|
||||
$js = (string) $js;
|
||||
|
||||
// Empty output is handled by the caller.
|
||||
if ( '' === $js ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Output larger than the scan budget is not lexed in full. Fall back to a
|
||||
// cheap size-delta check against the original: a minified output far smaller
|
||||
// than its input is the truncation signature even without a full scan. With
|
||||
// no original to compare against, assume intact.
|
||||
$len = strlen( $js );
|
||||
if ( $len > self::MAX_SCAN_BYTES ) {
|
||||
if ( null === $original_js ) {
|
||||
return false;
|
||||
}
|
||||
$original_len = strlen( (string) $original_js );
|
||||
return $original_len > 0 && $len < ( $original_len * self::TRUNCATION_RATIO );
|
||||
}
|
||||
|
||||
return ( new self( $js ) )->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $js Minified JS to inspect.
|
||||
*/
|
||||
private function __construct( $js ) {
|
||||
$this->js = $js;
|
||||
$this->len = strlen( $js );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the scan and return the verdict.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function run() {
|
||||
while ( $this->pos < $this->len ) {
|
||||
switch ( $this->state ) {
|
||||
case self::ST_CODE:
|
||||
$broken = $this->scan_code();
|
||||
break;
|
||||
case self::ST_STRING:
|
||||
$broken = $this->scan_string();
|
||||
break;
|
||||
case self::ST_TEMPLATE:
|
||||
$broken = $this->scan_template();
|
||||
break;
|
||||
case self::ST_REGEX:
|
||||
$broken = $this->scan_regex();
|
||||
break;
|
||||
case self::ST_LINE_COMMENT:
|
||||
$broken = $this->scan_line_comment();
|
||||
break;
|
||||
case self::ST_BLOCK_COMMENT:
|
||||
$broken = $this->scan_block_comment();
|
||||
break;
|
||||
default:
|
||||
return true; // Unknown state: fail safe.
|
||||
}
|
||||
if ( $broken ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Structural nesting this deep never occurs in legitimate output; stop
|
||||
// before unbounded $stack/$frames growth can exhaust memory (an
|
||||
// uncatchable fatal). The depth itself is a corruption signature, so the
|
||||
// fail-safe verdict is "broken".
|
||||
if ( count( $this->stack ) > self::MAX_NESTING_DEPTH
|
||||
|| count( $this->frames ) > self::MAX_NESTING_DEPTH ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->is_broken_at_eof();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one step while in code state.
|
||||
*
|
||||
* @return bool True to short-circuit as broken.
|
||||
*/
|
||||
private function scan_code() {
|
||||
$c = $this->js[ $this->pos ];
|
||||
|
||||
// `true`/`false` shortened to !0/!1 before a member access (e.g.
|
||||
// true.toString() -> !0.toString()) is invalid: `0.` is a numeric literal,
|
||||
// so the following identifier is a syntax error. This stays bracket-balanced,
|
||||
// so check it explicitly. Excludes the exponent case: `0.e5` / `0.e+5` is a
|
||||
// valid numeric literal, so `!0.e5` is valid and must not be flagged.
|
||||
if ( '!' === $c
|
||||
&& ( '0' === $this->peek( 1 ) || '1' === $this->peek( 1 ) )
|
||||
&& '.' === $this->peek( 2 )
|
||||
&& self::is_ident_start( $this->peek( 3 ) )
|
||||
&& ! $this->is_exponent_at( 3 ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( '/' === $c ) {
|
||||
$next = $this->peek();
|
||||
if ( '/' === $next ) {
|
||||
$this->state = self::ST_LINE_COMMENT;
|
||||
$this->pos += 2;
|
||||
return false;
|
||||
}
|
||||
if ( '*' === $next ) {
|
||||
$this->state = self::ST_BLOCK_COMMENT;
|
||||
$this->pos += 2;
|
||||
return false;
|
||||
}
|
||||
if ( $this->regex_allowed_here() ) {
|
||||
$this->state = self::ST_REGEX;
|
||||
$this->re_class = false;
|
||||
}
|
||||
$this->record_prev( '/' );
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( "'" === $c || '"' === $c ) {
|
||||
$this->state = self::ST_STRING;
|
||||
$this->string_quote = $c;
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( '`' === $c ) {
|
||||
$this->frames[] = array( 'type' => 'template' );
|
||||
$this->state = self::ST_TEMPLATE;
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( '{' === $c || '(' === $c || '[' === $c ) {
|
||||
$this->stack[] = $c;
|
||||
$this->record_prev( $c );
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( '}' === $c || ')' === $c || ']' === $c ) {
|
||||
// A '}' may close a template interpolation (${ ... }) rather than a
|
||||
// code block: the interpolation pushed its own '{' on the stack, so this
|
||||
// '}' closes the interpolation when that '{' is the current stack top.
|
||||
if ( '}' === $c && $this->closes_interpolation() ) {
|
||||
array_pop( $this->frames );
|
||||
array_pop( $this->stack ); // The '{' from ${.
|
||||
$this->state = self::ST_TEMPLATE;
|
||||
$this->record_prev( '`' );
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = ( '}' === $c ) ? '{' : ( ( ')' === $c ) ? '(' : '[' );
|
||||
if ( empty( $this->stack ) || end( $this->stack ) !== $expected ) {
|
||||
$this->unmatched = true;
|
||||
} else {
|
||||
array_pop( $this->stack );
|
||||
}
|
||||
$this->record_prev( $c );
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ctype_space( $c ) ) {
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_ident_char( $c ) ) {
|
||||
$start = $this->pos;
|
||||
while ( $this->pos < $this->len && self::is_ident_char( $this->js[ $this->pos ] ) ) {
|
||||
++$this->pos;
|
||||
}
|
||||
$this->prev_dot = ( '.' === $this->prev );
|
||||
$this->prev_prev = $this->prev;
|
||||
$this->prev = $this->js[ $this->pos - 1 ];
|
||||
$this->prev_word = substr( $this->js, $start, $this->pos - $start );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Any other significant char (operators like + - * . etc.).
|
||||
$this->record_prev( $c );
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one step while inside a '...' or "..." string.
|
||||
*
|
||||
* @return bool True to short-circuit as broken (unterminated string).
|
||||
*/
|
||||
private function scan_string() {
|
||||
$c = $this->js[ $this->pos ];
|
||||
if ( '\\' === $c ) {
|
||||
// Escape sequence, including a line continuation (`\` then a line
|
||||
// terminator). A CRLF continuation is three bytes (`\` + CR + LF), so
|
||||
// consume the trailing LF too -- otherwise it is left at the new position
|
||||
// and trips the raw-newline check below, falsely flagging a valid
|
||||
// continuation as broken. A lone CR or LF after `\` is the normal 2-byte
|
||||
// skip.
|
||||
if ( "\r" === $this->peek() && "\n" === $this->peek( 2 ) ) {
|
||||
$this->pos += 3;
|
||||
} else {
|
||||
$this->pos += 2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// A raw LF/CR inside a string literal is a syntax error (a real newline must
|
||||
// be escaped or a line continuation), so it never appears in valid minified
|
||||
// output -- only in truncated/corrupted output where the closing quote was
|
||||
// lost and a later quote happened to re-open the state. Checking ASCII LF/CR
|
||||
// is zero-false-positive and keeps the byte-oriented lexer simple.
|
||||
if ( "\n" === $c || "\r" === $c ) {
|
||||
return true; // Unterminated string literal.
|
||||
}
|
||||
if ( $c === $this->string_quote ) {
|
||||
$this->return_to_code( $c );
|
||||
}
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one step while inside a `...` template literal.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function scan_template() {
|
||||
$c = $this->js[ $this->pos ];
|
||||
if ( '\\' === $c ) {
|
||||
$this->pos += 2;
|
||||
return false;
|
||||
}
|
||||
if ( '`' === $c ) {
|
||||
$top = end( $this->frames );
|
||||
if ( $top && 'template' === $top['type'] ) {
|
||||
array_pop( $this->frames );
|
||||
}
|
||||
$this->return_to_code( '`' );
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
if ( '$' === $c && '{' === $this->peek() ) {
|
||||
$this->frames[] = array(
|
||||
'type' => 'interp',
|
||||
'depth' => count( $this->stack ),
|
||||
);
|
||||
$this->stack[] = '{'; // The '{' from ${.
|
||||
$this->return_to_code( '{' );
|
||||
$this->pos += 2;
|
||||
return false;
|
||||
}
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one step while inside a /.../ regex literal.
|
||||
*
|
||||
* @return bool True to short-circuit as broken (unterminated regex).
|
||||
*/
|
||||
private function scan_regex() {
|
||||
$c = $this->js[ $this->pos ];
|
||||
if ( '\\' === $c ) {
|
||||
// Unlike a string (where `\<LF>` is a valid line continuation), a line
|
||||
// terminator cannot be escaped inside a regex literal -- a backslash
|
||||
// immediately before a raw LF/CR is the truncation signature. Check the
|
||||
// escaped byte before skipping over the pair.
|
||||
$escaped = $this->peek();
|
||||
if ( "\n" === $escaped || "\r" === $escaped ) {
|
||||
return true; // Unterminated regex literal.
|
||||
}
|
||||
$this->pos += 2;
|
||||
return false;
|
||||
}
|
||||
// A raw line terminator (LF or CR) is invalid anywhere in a regex literal,
|
||||
// including inside a [...] character class, so this check sits above the
|
||||
// re_class branch.
|
||||
if ( "\n" === $c || "\r" === $c ) {
|
||||
return true; // Unterminated regex literal.
|
||||
}
|
||||
if ( $this->re_class ) {
|
||||
if ( ']' === $c ) {
|
||||
$this->re_class = false;
|
||||
}
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
if ( '[' === $c ) {
|
||||
$this->re_class = true;
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
if ( '/' === $c ) {
|
||||
$this->return_to_code( '/' );
|
||||
++$this->pos;
|
||||
while ( $this->pos < $this->len && ctype_alpha( $this->js[ $this->pos ] ) ) {
|
||||
++$this->pos; // Skip regex flags.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one step while inside a // line comment.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function scan_line_comment() {
|
||||
// CR, LF, and CRLF all end a line comment in JS; a bare CR must close it too,
|
||||
// otherwise code after a CR-only line ending is swallowed and broken input
|
||||
// can read as intact. (CRLF closes on the CR; the trailing LF is then a
|
||||
// no-op space in code state.)
|
||||
$c = $this->js[ $this->pos ];
|
||||
if ( "\n" === $c || "\r" === $c ) {
|
||||
$this->state = self::ST_CODE;
|
||||
}
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one step while inside a block comment.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function scan_block_comment() {
|
||||
if ( '*' === $this->js[ $this->pos ] && '/' === $this->peek() ) {
|
||||
$this->state = self::ST_CODE;
|
||||
$this->pos += 2;
|
||||
return false;
|
||||
}
|
||||
++$this->pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verdict once the whole input has been consumed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_broken_at_eof() {
|
||||
// A line comment running to EOF is valid (e.g. a trailing
|
||||
// `//# sourceMappingURL=...` with no final newline). Every other open state
|
||||
// at EOF is a genuinely unterminated construct.
|
||||
if ( self::ST_LINE_COMMENT === $this->state ) {
|
||||
$this->state = self::ST_CODE;
|
||||
}
|
||||
|
||||
if ( self::ST_CODE !== $this->state ) {
|
||||
return true; // Unterminated string/template/regex/block-comment.
|
||||
}
|
||||
if ( ! empty( $this->stack ) ) {
|
||||
return true; // Unbalanced brackets.
|
||||
}
|
||||
if ( ! empty( $this->frames ) ) {
|
||||
return true; // Unterminated template/interpolation.
|
||||
}
|
||||
|
||||
return $this->unmatched; // Saw an extra/mismatched closing bracket.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current '}' closes a template interpolation rather than a block.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function closes_interpolation() {
|
||||
$top = end( $this->frames );
|
||||
return $top && 'interp' === $top['type'] && ( $top['depth'] + 1 ) === count( $this->stack );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a `/` at the current position begins a regex literal (vs division).
|
||||
*
|
||||
* JavaScript cannot decide this locally; lexers approximate it from the
|
||||
* previous significant token: after a value (identifier, number, `)`, `]`,
|
||||
* postfix ++/--) `/` is division; after an operator/keyword that expects an
|
||||
* operand next, `/` begins a regex.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function regex_allowed_here() {
|
||||
$prev = $this->prev;
|
||||
|
||||
if ( '' === $prev ) {
|
||||
return true; // Start of input.
|
||||
}
|
||||
|
||||
// Postfix ++ / -- yields a value, so a following `/` is division.
|
||||
if ( ( '+' === $prev && '+' === $this->prev_prev ) || ( '-' === $prev && '-' === $this->prev_prev ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Punctuation/operators after which a regex literal is legal. Includes the
|
||||
// arithmetic/comparison binary operators so real minified expressions like
|
||||
// `x+"|"+/\d{1,2}/.source` (regex after a binary +) are not misread.
|
||||
static $puncts = array(
|
||||
'(' => 1,
|
||||
',' => 1,
|
||||
'=' => 1,
|
||||
':' => 1,
|
||||
'[' => 1,
|
||||
'!' => 1,
|
||||
'&' => 1,
|
||||
'|' => 1,
|
||||
'?' => 1,
|
||||
'{' => 1,
|
||||
';' => 1,
|
||||
'}' => 1,
|
||||
'+' => 1,
|
||||
'-' => 1,
|
||||
'*' => 1,
|
||||
'%' => 1,
|
||||
'<' => 1,
|
||||
'>' => 1,
|
||||
'~' => 1,
|
||||
'^' => 1,
|
||||
);
|
||||
if ( isset( $puncts[ $prev ] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// After a word: only the keywords below allow a regex, and only when the
|
||||
// word is not a property access (e.g. `a.return/b` is division).
|
||||
if ( self::is_ident_char( $prev ) ) {
|
||||
static $kw = array(
|
||||
'return' => 1,
|
||||
'throw' => 1,
|
||||
'typeof' => 1,
|
||||
'in' => 1,
|
||||
'of' => 1,
|
||||
'new' => 1,
|
||||
'do' => 1,
|
||||
'else' => 1,
|
||||
'void' => 1,
|
||||
'delete' => 1,
|
||||
'instanceof' => 1,
|
||||
'case' => 1,
|
||||
'yield' => 1,
|
||||
'await' => 1,
|
||||
);
|
||||
return ! $this->prev_dot && '' !== $this->prev_word && isset( $kw[ $this->prev_word ] );
|
||||
}
|
||||
|
||||
// Closing bracket ) ], a '.', etc. -> division.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the most recent significant token (without changing state).
|
||||
*
|
||||
* @param string $char The token char.
|
||||
*/
|
||||
private function record_prev( $char ) {
|
||||
$this->prev_prev = $this->prev;
|
||||
$this->prev = $char;
|
||||
$this->prev_word = '';
|
||||
$this->prev_dot = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return to code state, recording $char as the most recent token.
|
||||
*
|
||||
* @param string $char The token char (the closing delimiter / interpolation brace).
|
||||
*/
|
||||
private function return_to_code( $char ) {
|
||||
$this->state = self::ST_CODE;
|
||||
$this->record_prev( $char );
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead from the current position.
|
||||
*
|
||||
* @param int $offset How far ahead to peek (default the next char).
|
||||
*
|
||||
* @return string The char, or '' if out of range.
|
||||
*/
|
||||
private function peek( $offset = 1 ) {
|
||||
$i = $this->pos + $offset;
|
||||
return ( $i < $this->len ) ? $this->js[ $i ] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether $c can appear within a JS identifier.
|
||||
*
|
||||
* @param string $c Single character.
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_ident_char( $c ) {
|
||||
return '' !== $c && ( ctype_alnum( $c ) || '_' === $c || '$' === $c );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether $c can start a JS identifier.
|
||||
*
|
||||
* @param string $c Single character.
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_ident_start( $c ) {
|
||||
return '' !== $c && ( ctype_alpha( $c ) || '_' === $c || '$' === $c );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the chars at $offset begin a valid exponent part (the `e5` of the
|
||||
* numeric literal `0.e5`). Lets the !0/!1 member-access check tell a real,
|
||||
* broken member access (`!0.toString()`) apart from a valid exponent
|
||||
* literal (`!0.e5`), which is not a member access at all.
|
||||
*
|
||||
* @param int $offset Offset from the current position.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_exponent_at( $offset ) {
|
||||
$c = $this->peek( $offset );
|
||||
if ( 'e' !== $c && 'E' !== $c ) {
|
||||
return false;
|
||||
}
|
||||
$next = $this->peek( $offset + 1 );
|
||||
if ( '+' === $next || '-' === $next ) {
|
||||
$next = $this->peek( $offset + 2 );
|
||||
}
|
||||
return '' !== $next && ctype_digit( $next );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* Implement the minify class.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 0.2
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use MatthiasMullie\Minify\CSS as CSSMinifier;
|
||||
use MatthiasMullie\Minify\JS as JSMinifier;
|
||||
|
||||
/**
|
||||
* Class Minify
|
||||
*/
|
||||
class Minify {
|
||||
|
||||
/**
|
||||
* Reasons passed to the jetpack_boost_js_minify_fallback action. Public so hook
|
||||
* consumers can compare against Minify::FALLBACK_* instead of bare strings; the
|
||||
* string values are the stable wire format and must not change.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
public const FALLBACK_EXCEPTION = 'exception';
|
||||
public const FALLBACK_ERROR = 'error';
|
||||
public const FALLBACK_SCAN_ERROR = 'scan_error';
|
||||
public const FALLBACK_EMPTY_OUTPUT = 'empty_output';
|
||||
public const FALLBACK_LOOKS_BROKEN = 'looks_broken';
|
||||
|
||||
/**
|
||||
* Strips whitespace from JavaScript scripts.
|
||||
*
|
||||
* @param string $js Input JS string.
|
||||
*
|
||||
* @return string String with whitespace stripped.
|
||||
*/
|
||||
public static function js( $js ) {
|
||||
try {
|
||||
$minifier = new JSMinifier( $js );
|
||||
$minified_js = $minifier->minify();
|
||||
} catch ( \Exception $e ) {
|
||||
// Ordinary failure (e.g. a PCRE backtrack-limit hit on a huge bundle):
|
||||
// serve the original input rather than nothing.
|
||||
return self::fallback_js( $js, self::FALLBACK_EXCEPTION, $e );
|
||||
} catch ( \Error $e ) {
|
||||
// \Error subclasses (e.g. \TypeError, \ParseError) signal a genuine bug
|
||||
// rather than unsupported syntax. We still fall back -- a performance
|
||||
// optimization must never white-screen the page -- but report a distinct
|
||||
// reason so the condition is distinguishable from ordinary unsupported
|
||||
// syntax via the hook. (A true memory-limit fatal is not a \Throwable and
|
||||
// cannot be caught here at all; the scanner instead bounds its own nesting
|
||||
// depth so it never provokes one -- see Js_Structure_Scanner.)
|
||||
return self::fallback_js( $js, self::FALLBACK_ERROR, $e );
|
||||
}
|
||||
|
||||
// The bundled MatthiasMullie minifier is regex-based and ES5-era: it can
|
||||
// SILENTLY corrupt modern JS without throwing. The classic case is a `//`
|
||||
// inside a (nested) template literal being eaten as a line comment to
|
||||
// end-of-line, which drops the closing backtick and everything after it,
|
||||
// truncating the bundle -> "Unexpected end of input" in the browser. The
|
||||
// try/catch above never fires for this because nothing is thrown.
|
||||
//
|
||||
// As a safety net, structurally validate the output: if it looks broken,
|
||||
// fall back to the original (still concatenated, just not re-minified)
|
||||
// bytes. A slightly larger working bundle beats a smaller broken one.
|
||||
if ( '' === (string) $minified_js && '' !== (string) $js ) {
|
||||
return self::fallback_js( $js, self::FALLBACK_EMPTY_OUTPUT );
|
||||
}
|
||||
|
||||
// The structural scan runs outside the minifier try/catch above, so guard it
|
||||
// too: any \Throwable from the scan itself must degrade to the original rather
|
||||
// than white-screen the page. It reports the distinct 'scan_error' reason so a
|
||||
// scanner fault is never conflated with a minifier-level 'error' in the hook's
|
||||
// (released, immutable) reason contract. (Memory exhaustion is a fatal, not a
|
||||
// \Throwable, so it cannot be caught -- the scanner instead bounds its own
|
||||
// nesting depth to avoid provoking one.) Pass the original input so the scanner
|
||||
// can apply its gross-truncation backstop to bundles too large to scan in full.
|
||||
// See Js_Structure_Scanner.
|
||||
try {
|
||||
$looks_broken = Js_Structure_Scanner::looks_broken( $minified_js, $js );
|
||||
} catch ( \Throwable $e ) {
|
||||
return self::fallback_js( $js, self::FALLBACK_SCAN_ERROR, $e );
|
||||
}
|
||||
if ( $looks_broken ) {
|
||||
return self::fallback_js( $js, self::FALLBACK_LOOKS_BROKEN );
|
||||
}
|
||||
|
||||
return $minified_js;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the original (un-re-minified) JS when minification is declined, and
|
||||
* surface the reason so the safety net is observable in production.
|
||||
*
|
||||
* Observability policy: the jetpack_boost_js_minify_fallback action is the
|
||||
* always-on surface and fires on every fallback (including the abnormal
|
||||
* 'error' arm), so monitoring should hook it. The error_log calls are a debug
|
||||
* aid only and are gated behind WP_DEBUG: a recurring fallback on a
|
||||
* no-writable-cache host can fire every request, and unconditional logging
|
||||
* would flood the log without telling an operator anything the hook cannot.
|
||||
*
|
||||
* @param string $js The original JS that will be served.
|
||||
* @param string $reason One of the FALLBACK_* constants.
|
||||
* @param \Throwable|null $error The throwable, when the fallback was triggered by one.
|
||||
*
|
||||
* @return string The original JS, unchanged.
|
||||
*/
|
||||
private static function fallback_js( $js, $reason, $error = null ) {
|
||||
// js() has no type hint, so a caller can pass a non-string (the \Error-arm
|
||||
// test does exactly that); measure length defensively rather than assume.
|
||||
$bytes = is_string( $js ) ? strlen( $js ) : 0;
|
||||
$is_debug = defined( 'WP_DEBUG' ) && WP_DEBUG;
|
||||
|
||||
try {
|
||||
/**
|
||||
* Fires when Minify::js() declines its minified output and serves the
|
||||
* original JS instead. This is the always-on observability surface for
|
||||
* the minify safety net: it fires on every fallback, including the
|
||||
* abnormal 'error' / 'scan_error' arms (e.g. \TypeError / \ParseError),
|
||||
* so register a callback here to monitor how often -- and why -- it
|
||||
* engages. Callbacks must not throw; a throwing callback is swallowed so
|
||||
* it cannot break minification.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param string $reason Why the fallback fired: one of the Minify::FALLBACK_* values ('exception', 'error', 'scan_error', 'empty_output', 'looks_broken'). 'error' is a \Throwable raised by the minifier itself; 'scan_error' is a \Throwable raised by the structural scanner -- both carry it in $error.
|
||||
* @param int $bytes Length of the original JS being served, in bytes.
|
||||
* @param \Throwable|null $error The throwable when triggered by one, otherwise null. Its message may embed an internal filesystem path (e.g. an IOException), so consumers should not surface it unsanitized.
|
||||
*/
|
||||
do_action( 'jetpack_boost_js_minify_fallback', $reason, $bytes, $error );
|
||||
} catch ( \Throwable $hook_error ) {
|
||||
// A misbehaving hook callback must never turn a handled fallback into a
|
||||
// fatal error -- degrading gracefully to the original bundle is the whole
|
||||
// point of this method. Surface the hook failure only under WP_DEBUG.
|
||||
if ( $is_debug ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'Jetpack Boost: jetpack_boost_js_minify_fallback hook threw: ' . $hook_error->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $is_debug ) {
|
||||
$detail = $error instanceof \Throwable
|
||||
? sprintf( ' (%s: %s)', get_class( $error ), $error->getMessage() )
|
||||
: '';
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( sprintf( 'Jetpack Boost: Minify::js() fell back to original JS [reason=%s, bytes=%d]%s', $reason, $bytes, $detail ) );
|
||||
}
|
||||
|
||||
return $js;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minifies the supplied CSS code, returning its minified form.
|
||||
*/
|
||||
public static function css( $css ) {
|
||||
try {
|
||||
$minifier = new CSSMinifier( $css );
|
||||
$minified_css = $minifier->minify();
|
||||
} catch ( \Throwable $e ) {
|
||||
// Unlike js(), CSS uses a single \Throwable catch: there is no structural
|
||||
// validator for CSS output, and CSS minification has not shown the
|
||||
// silent-corruption failure mode that motivated splitting js()'s catch.
|
||||
return $css;
|
||||
}
|
||||
|
||||
return $minified_css;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack\My_Jetpack\Products;
|
||||
|
||||
class My_Jetpack {
|
||||
public static function get_product() {
|
||||
$product = Products::get_product_class( 'boost' );
|
||||
if ( ! $product ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
'tiers' => $product::get_tiers(),
|
||||
'features_by_tier' => $product::get_features_by_tier(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Implement nonce helper methods.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Class Nonce
|
||||
*/
|
||||
class Nonce {
|
||||
/**
|
||||
* This is a light clone of wp_create_nonce and wp_verify_nonce which skips the UID and cookie token parts,
|
||||
* so it can be used in anonymous HTTP callbacks. It is therefore not as secure, so be careful.
|
||||
*
|
||||
* @param string $action The action.
|
||||
*/
|
||||
public static function create( $action ) {
|
||||
return substr( wp_hash( wp_nonce_tick() . '|' . $action, 'nonce' ), -12, 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the nonce.
|
||||
*
|
||||
* @param string $nonce The nonce.
|
||||
* @param string $action The action.
|
||||
*/
|
||||
public static function verify( $nonce, $action ) {
|
||||
$i = wp_nonce_tick();
|
||||
|
||||
// Current nonce.
|
||||
$expected = substr( wp_hash( $i . '|' . $action, 'nonce' ), -12, 10 );
|
||||
if ( hash_equals( $expected, $nonce ) ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Nonce generated 12-24 hours ago.
|
||||
$expected = substr( wp_hash( ( $i - 1 ) . '|' . $action, 'nonce' ), -12, 10 );
|
||||
if ( hash_equals( $expected, $nonce ) ) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* Uses standard output buffering implemented to PHP
|
||||
* to allow for manipulating the output stream.
|
||||
*
|
||||
* The implementation allows for seamless text search and manipulation by
|
||||
* always taking into account two subsequent chunks of data.
|
||||
*
|
||||
* Example sequence of chunks sent to output (chunk size 4):
|
||||
*
|
||||
* ABCD EFGH IJKL MNOP QRST
|
||||
*
|
||||
* A standard output buffer callback handler would always receive only one
|
||||
* of those chunks, e.g. 'ABCD' and would be unable to match strings on
|
||||
* seams of individual chunks, e.g. 'DEF', because 'D' appears in chunk #1,
|
||||
* whereas 'EF' in chunk #2.
|
||||
*
|
||||
* This class solves this issue by utilizing a sliding window of size 2 chunks.
|
||||
* That means the callback receives:
|
||||
*
|
||||
* ABCDEFGH EFEGHIJKL IJKLMNOP MNOPQRST
|
||||
*
|
||||
* That allows for more advanced string manipulation even across chunk seams.
|
||||
* It is assumed any string searches are much shorter than a chunk size.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 0.2.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Class Output_Filter
|
||||
*/
|
||||
class Output_Filter {
|
||||
|
||||
/**
|
||||
* Output chunk size.
|
||||
*/
|
||||
const CHUNK_SIZE = 4096;
|
||||
|
||||
/**
|
||||
* List of callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $callbacks = array();
|
||||
|
||||
/**
|
||||
* One chunk always remains in the buffer to allow for cross-seam matching.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $buffered_chunk = '';
|
||||
|
||||
/**
|
||||
* Whether we allow the callbacks to filter incoming chunks of output.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $is_filtering = false;
|
||||
|
||||
/**
|
||||
* Add an output filtering callback.
|
||||
*
|
||||
* @param callable $callback Output filtering callback.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_callback( $callback ) {
|
||||
$this->callbacks[] = $callback;
|
||||
|
||||
if ( 1 === count( $this->callbacks ) ) {
|
||||
// Start filtering output now that we have some callbacks.
|
||||
$this->is_filtering = true;
|
||||
|
||||
ob_start(
|
||||
array( $this, 'tick' ),
|
||||
self::CHUNK_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processing a full output buffer.
|
||||
*
|
||||
* @param string $buffer Output buffer.
|
||||
* @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
|
||||
*
|
||||
* @return string Buffer data to be flushed to browser.
|
||||
*/
|
||||
public function tick( $buffer, $phase ) {
|
||||
// Bail early if not filtering.
|
||||
if ( ! $this->is_filtering ) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
// Check if this the first or last buffer. Use the $phase bitmask to figure it out.
|
||||
// $phase can contain multiple PHP_OUTPUT_HANDLER_* constants.
|
||||
// e.g.: PHP_OUTPUT_HANDLER_END = 8 (binary 1000), PHP_OUTPUT_HANDLER_START = 1 (binary 0001). Both = 9 (binary 1001).
|
||||
// Use bitwise AND to read individual flags from $phase.
|
||||
$is_first_chunk = ( $phase & PHP_OUTPUT_HANDLER_START ) > 0;
|
||||
$is_last_chunk = ( $phase & PHP_OUTPUT_HANDLER_END ) > 0;
|
||||
|
||||
// Don't handle the first chunk (unless it's also the last) - we want to output
|
||||
// one chunk behind the latest to allow for cross-seam matching.
|
||||
if ( $is_first_chunk && ! $is_last_chunk ) {
|
||||
$this->buffered_chunk = $buffer;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$buffer_start = $this->buffered_chunk;
|
||||
$buffer_end = $buffer;
|
||||
|
||||
foreach ( $this->callbacks as $callback ) {
|
||||
list( $buffer_start, $buffer_end ) = call_user_func( $callback, $buffer_start, $buffer_end );
|
||||
}
|
||||
$this->buffered_chunk = $buffer_end;
|
||||
$joint_buffer = $buffer_start . $buffer_end;
|
||||
|
||||
// If the second part of the buffer is the last chunk,
|
||||
// merge the buffer back together to ensure whole output.
|
||||
if ( $is_last_chunk ) {
|
||||
// If more buffer chunks arrive, don't apply callbacks to them.
|
||||
$this->is_filtering = false;
|
||||
|
||||
// Join remaining buffers and allow plugin to append anything to them.
|
||||
/**
|
||||
* Filter the Critical CSS output buffer
|
||||
*
|
||||
* @param string $joint_buffer The entire output buffer
|
||||
* @param string $buffer_start The top half of the buffer
|
||||
* @param string $buffer_end The bottom half of the buffer
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_output_filtering_last_buffer', $joint_buffer, $buffer_start, $buffer_end );
|
||||
}
|
||||
|
||||
// Send the first part of the whole buffer to the browser only,
|
||||
// because buffer_end will be manipulated in the next tick.
|
||||
return $buffer_start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack\Boost_Core\Lib\Boost_API;
|
||||
use Automattic\Jetpack\Boost_Core\Lib\Transient;
|
||||
|
||||
class Premium_Features {
|
||||
|
||||
const CLOUD_CSS = 'cloud-critical-css';
|
||||
const PERFORMANCE_HISTORY = 'performance-history';
|
||||
const IMAGE_CDN_LIAR = 'image-cdn-liar';
|
||||
const IMAGE_CDN_QUALITY = 'image-cdn-quality';
|
||||
const PRIORITY_SUPPORT = 'support';
|
||||
const PAGE_CACHE = 'page-cache';
|
||||
const CORNERSTONE_TEN_PAGES = 'cornerstone-10-pages';
|
||||
|
||||
const TRANSIENT_KEY = 'premium_features';
|
||||
|
||||
public static function has_feature( $feature ) {
|
||||
$features = self::get_features();
|
||||
|
||||
if ( is_array( $features ) ) {
|
||||
return in_array( $feature, $features, true );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function get_features() {
|
||||
$available_features = Transient::get( self::TRANSIENT_KEY, false );
|
||||
$all_features = array(
|
||||
self::CLOUD_CSS,
|
||||
self::IMAGE_CDN_LIAR,
|
||||
self::IMAGE_CDN_QUALITY,
|
||||
self::PERFORMANCE_HISTORY,
|
||||
self::PRIORITY_SUPPORT,
|
||||
self::CORNERSTONE_TEN_PAGES,
|
||||
);
|
||||
|
||||
if ( ! is_array( $available_features ) ) {
|
||||
$available_features = Boost_API::get( 'features' );
|
||||
if ( ! is_array( $available_features ) ) {
|
||||
$available_features = array();
|
||||
}
|
||||
Transient::set( self::TRANSIENT_KEY, $available_features, 3 * DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
$features = array();
|
||||
// Prepare a list of features after applying jetpack_boost_has_feature_* filter for each feature.
|
||||
foreach ( $all_features as $feature ) {
|
||||
/**
|
||||
* Filter the availability of a feature
|
||||
*
|
||||
* @param bool $has_feature if the feature is available
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
if ( apply_filters( "jetpack_boost_has_feature_{$feature}", in_array( $feature, $available_features, true ) ) ) {
|
||||
$features[] = $feature;
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
public static function has_any() {
|
||||
return count( self::get_features() ) > 0;
|
||||
}
|
||||
|
||||
public static function clear_cache() {
|
||||
Transient::delete( self::TRANSIENT_KEY );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack\My_Jetpack\Products\Boost;
|
||||
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
|
||||
use Automattic\Jetpack\Status;
|
||||
|
||||
class Premium_Pricing {
|
||||
/**
|
||||
* Get an object containing the yearly pricing information for Jetpack Boost.
|
||||
*
|
||||
* Used by Jetpack_Boost js constants and data sync.
|
||||
*/
|
||||
public static function get_yearly_pricing() {
|
||||
$yearly_pricing = Wpcom_Products::get_product_pricing( Boost::UPGRADED_TIER_PRODUCT_SLUG );
|
||||
|
||||
if ( empty( $yearly_pricing ) ) {
|
||||
// In offline mode, we don't have access to the pricing data and it's not an error.
|
||||
if ( ! ( new Status() )->is_offline_mode() ) {
|
||||
Analytics::record_user_event( 'upgrade_price_missing', array( 'error_message' => 'Missing pricing information on benefits interstitial page.' ) );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'priceBefore' => $yearly_pricing['full_price'],
|
||||
'priceAfter' => $yearly_pricing['discount_price'],
|
||||
'currencyCode' => $yearly_pricing['currency_code'],
|
||||
'isIntroductoryOffer' => $yearly_pricing['is_introductory_offer'] === true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Setup;
|
||||
|
||||
class Setup {
|
||||
|
||||
protected static $instances = array();
|
||||
|
||||
/**
|
||||
* This method takes a `Has_Setup` instance and registers the setup action.
|
||||
* In addition, it will keep track of all the instances passed to it,
|
||||
*
|
||||
* This is useful if a plugin needs the instance
|
||||
* to modify the behavior at a certain hook that
|
||||
* Jetpack Boost is using.
|
||||
*
|
||||
* The use case would be something like this:
|
||||
* ```
|
||||
* $instance = my_get_instance_method( Setup::get_instances() );
|
||||
* remove_action( 'wp_footer', array( $instance, 'foobar' ) );
|
||||
* ```
|
||||
*
|
||||
* @param Has_Setup $instance
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add( Has_Setup $instance ) {
|
||||
$instance->setup();
|
||||
|
||||
self::$instances[] = $instance;
|
||||
}
|
||||
|
||||
public static function get_instances() {
|
||||
return self::$instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param class-string<T> $class_name
|
||||
* @return T|null
|
||||
*/
|
||||
public static function get_instance_of( $class_name ) {
|
||||
foreach ( self::get_instances() as $instance ) {
|
||||
if ( $instance instanceof $class_name ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchReturn -- Phan isn't inferring the type correctly from the `instanceof $class_name` like it's supposed to.
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Site_Health.
|
||||
*
|
||||
* Displays performance issues in WordPress site health page.
|
||||
*/
|
||||
class Site_Health {
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! has_filter( 'site_status_tests', array( __CLASS__, 'add_check' ) ) ) {
|
||||
add_filter( 'site_status_tests', array( __CLASS__, 'add_check' ), 99 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add site-health page tests.
|
||||
*
|
||||
* @param array $checks Core checks.
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function add_check( $checks ) {
|
||||
$checks['direct']['jetpack_boost_checks'] = array(
|
||||
'label' => __( 'Jetpack Boost checks', 'jetpack-boost' ),
|
||||
'test' => array( __CLASS__, 'do_checks' ),
|
||||
);
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do site-health page checks
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function do_checks() {
|
||||
$health = new Boost_Health();
|
||||
$total_issues = $health->get_total_issues();
|
||||
$issues = $health->get_all_issues();
|
||||
|
||||
/**
|
||||
* Default, no issues found
|
||||
*/
|
||||
$result = array(
|
||||
'label' => __( 'No issues found', 'jetpack-boost' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => __( 'Performance', 'jetpack-boost' ),
|
||||
'color' => 'gray',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'Jetpack Boost did not find any known performance issues with your site.', 'jetpack-boost' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'jetpack_boost_checks',
|
||||
);
|
||||
|
||||
/**
|
||||
* If issues found.
|
||||
*/
|
||||
if ( $total_issues ) {
|
||||
$result['status'] = 'critical';
|
||||
/* translators: $d is the number of performance issues found. */
|
||||
$result['label'] = sprintf( _n( 'Your site is affected by %d performance issue', 'Your site is affected by %d performance issues', $total_issues, 'jetpack-boost' ), $total_issues );
|
||||
$result['description'] = __( 'Jetpack Boost detected the following performance issues with your site:', 'jetpack-boost' );
|
||||
|
||||
foreach ( $issues as $issue ) {
|
||||
$result['description'] .= '<p>';
|
||||
$result['description'] .= "<span class='dashicons dashicons-warning' style='color: crimson;'></span>  ";
|
||||
$result['description'] .= wp_kses( $issue, array( 'a' => array( 'href' => array() ) ) ); // Only allow a href HTML tags.
|
||||
$result['description'] .= '</p>';
|
||||
}
|
||||
$result['description'] .= '<p>';
|
||||
$result['description'] .= sprintf(
|
||||
wp_kses(
|
||||
/* translators: Link to Jetpack Boost. */
|
||||
__( 'Visit <a href="%s">Boost settings page</a> for more information.', 'jetpack-boost' ),
|
||||
array(
|
||||
'a' => array( 'href' => array() ),
|
||||
)
|
||||
),
|
||||
esc_url( admin_url( 'admin.php?page=jetpack-boost' ) )
|
||||
);
|
||||
$result['description'] .= '</p>';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
class Site_Urls {
|
||||
|
||||
public static function get( $limit = 100 ) {
|
||||
$core_urls = self::get_wp_core_urls();
|
||||
$post_urls = self::cleanup_post_urls(
|
||||
self::get_post_urls( $limit ),
|
||||
wp_list_pluck(
|
||||
$core_urls,
|
||||
'url'
|
||||
)
|
||||
);
|
||||
|
||||
$urls = array_slice(
|
||||
array_merge(
|
||||
$core_urls,
|
||||
$post_urls
|
||||
),
|
||||
0,
|
||||
$limit
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the list of site URLs used by the Image Size Analysis.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param array $urls An array of URLs.
|
||||
* @param int $limit The maximum number of URLs that should be returned.
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_site_urls', $urls, $limit );
|
||||
}
|
||||
|
||||
private static function get_wp_core_urls() {
|
||||
$urls = array();
|
||||
|
||||
$front_page = get_option( 'page_on_front' );
|
||||
if ( ! empty( $front_page ) ) {
|
||||
$urls['core_front_page'] = array(
|
||||
'url' => get_permalink( $front_page ),
|
||||
'modified' => get_post_modified_time( 'Y-m-d H:i:s', false, $front_page ),
|
||||
'group' => 'core_front_page',
|
||||
);
|
||||
} else {
|
||||
$urls['core_front_page'] = array(
|
||||
'url' => home_url( '/' ),
|
||||
'modified' => current_time( 'Y-m-d H:i:s' ),
|
||||
'group' => 'core_front_page',
|
||||
);
|
||||
}
|
||||
|
||||
$posts_page = get_option( 'page_for_posts' );
|
||||
if ( ! empty( $posts_page ) ) {
|
||||
$urls['core_posts_page'] = array(
|
||||
'url' => get_permalink( $posts_page ),
|
||||
'modified' => get_post_modified_time( 'Y-m-d H:i:s', false, $posts_page ),
|
||||
'group' => 'other',
|
||||
);
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
private static function get_post_urls( $limit ) {
|
||||
global $wpdb;
|
||||
|
||||
$public_post_types = self::get_public_post_types();
|
||||
$post_types_placeholders = implode(
|
||||
',',
|
||||
array_fill(
|
||||
0,
|
||||
count( $public_post_types ),
|
||||
'%s'
|
||||
)
|
||||
);
|
||||
|
||||
$prepare_values = $public_post_types;
|
||||
$prepare_values[] = $limit;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT ID, post_modified FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type IN ({$post_types_placeholders}) ORDER BY post_modified DESC LIMIT 0, %d",
|
||||
$prepare_values
|
||||
)
|
||||
);
|
||||
|
||||
$urls = array();
|
||||
foreach ( $results as $result ) {
|
||||
$urls[ 'post_id_' . $result->ID ] = array(
|
||||
'url' => get_permalink( $result->ID ),
|
||||
'modified' => get_post_modified_time( 'Y-m-d H:i:s', false, $result ),
|
||||
'group' => self::get_post_group( $result ),
|
||||
);
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicate URLs from the $post_urls list
|
||||
* based on the additional URLs.
|
||||
*
|
||||
* @param array $post_urls List of URLs to cleanup.
|
||||
* @param array $additional_urls List of URLs to lookup while cleaning.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function cleanup_post_urls( $post_urls, $additional_urls ) {
|
||||
$clean = array();
|
||||
|
||||
foreach ( $post_urls as $key => $item ) {
|
||||
if ( in_array( $item['url'], $additional_urls, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$clean[ $key ] = $item;
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private static function get_public_post_types() {
|
||||
$post_types = get_post_types(
|
||||
array(
|
||||
'public' => true,
|
||||
)
|
||||
);
|
||||
unset( $post_types['attachment'] );
|
||||
|
||||
return array_values(
|
||||
array_filter(
|
||||
$post_types,
|
||||
'is_post_type_viewable'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the group for the post.
|
||||
*
|
||||
* @param \WP_Post $p Post object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_post_group( $p ) {
|
||||
$post_type = get_post_type( $p->ID );
|
||||
if ( 'post' === $post_type || 'page' === $post_type ) {
|
||||
return 'singular_' . $post_type;
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
|
||||
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS\Critical_CSS;
|
||||
|
||||
class Status implements Entry_Can_Get, Entry_Can_Set {
|
||||
|
||||
/**
|
||||
* Slug of the optimization module which is currently being toggled
|
||||
*
|
||||
* @var string $slug
|
||||
*/
|
||||
protected $slug;
|
||||
|
||||
/**
|
||||
* A map of modules whose status are synced.
|
||||
*
|
||||
* @var array[] $status_sync_map
|
||||
*/
|
||||
protected $status_sync_map;
|
||||
|
||||
/**
|
||||
* @var string $option_name
|
||||
*/
|
||||
protected $option_name;
|
||||
|
||||
public function __construct( $slug ) {
|
||||
$this->slug = $slug;
|
||||
$module_slug = str_replace( '_', '-', $this->slug );
|
||||
$this->option_name = 'jetpack_boost_status_' . $module_slug;
|
||||
|
||||
$this->status_sync_map = array(
|
||||
Cloud_CSS::get_slug() => array(
|
||||
Critical_CSS::get_slug(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function get( $_fallback = false ) {
|
||||
return get_option( $this->option_name, false );
|
||||
}
|
||||
|
||||
public function set( $value ) {
|
||||
return update_option( $this->option_name, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the module is toggled.
|
||||
*
|
||||
* Called by Modules and triggered by the `jetpack_ds_set` action.
|
||||
*/
|
||||
public function on_update( $new_status ) {
|
||||
$this->update_mapped_modules( $new_status );
|
||||
$this->track_module_status( (bool) $new_status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update modules which are to follow the status of the current module.
|
||||
*
|
||||
* For example: critical-css module status should be synced with cloud-css module.
|
||||
*
|
||||
* @param mixed $new_status
|
||||
* @return void
|
||||
*/
|
||||
protected function update_mapped_modules( $new_status ) {
|
||||
if ( ! isset( $this->status_sync_map[ $this->slug ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modules_instance = Setup::get_instance_of( Modules_Setup::class );
|
||||
|
||||
// The moduleInstance will be there. But check just in case.
|
||||
if ( $modules_instance !== null ) {
|
||||
// Remove the action temporarily to avoid infinite loop.
|
||||
remove_action( 'jetpack_boost_module_status_updated', array( $modules_instance, 'on_module_status_update' ) );
|
||||
}
|
||||
|
||||
foreach ( $this->status_sync_map[ $this->slug ] as $mapped_module_slug ) {
|
||||
$mapped_status = new Status( $mapped_module_slug );
|
||||
$mapped_status->set( $new_status );
|
||||
}
|
||||
|
||||
// The moduleInstance will be there. But check just in case.
|
||||
if ( $modules_instance !== null ) {
|
||||
add_action( 'jetpack_boost_module_status_updated', array( $modules_instance, 'on_module_status_update' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
protected function track_module_status( $status ) {
|
||||
Analytics::record_user_event(
|
||||
'set_module_status',
|
||||
array(
|
||||
'module' => $this->slug,
|
||||
'status' => $status,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
/**
|
||||
* Cache for Jetpack Boost that uses CPTs to store the data.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
/**
|
||||
* Class Storage_Post_type
|
||||
*/
|
||||
class Storage_Post_Type {
|
||||
|
||||
/**
|
||||
* The name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* Storage_Post_type constructor.
|
||||
*
|
||||
* @param string $name The name.
|
||||
*/
|
||||
public function __construct( $name ) {
|
||||
$this->name = sanitize_title( $name );
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post type slug.
|
||||
*/
|
||||
public function post_type_slug() {
|
||||
return 'jb_store_' . $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static initialization.
|
||||
*/
|
||||
private function init() {
|
||||
// Check if post type already registered.
|
||||
if ( post_type_exists( $this->post_type_slug() ) ) {
|
||||
return;
|
||||
}
|
||||
register_post_type(
|
||||
$this->post_type_slug(),
|
||||
array(
|
||||
'description' => 'Cache entries for the Jetpack Boost plugin.',
|
||||
'public' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => false,
|
||||
'can_export' => false,
|
||||
'delete_with_user' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache entry using a CPT.
|
||||
*
|
||||
* @param string $key Cache key name.
|
||||
* @param mixed $value Cache value.
|
||||
* @param int $expiry Cache expiration in seconds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set( $key, $value, $expiry = 0 ) {
|
||||
$data_post_data = array(
|
||||
'post_type' => $this->post_type_slug(),
|
||||
'post_title' => $key,
|
||||
'post_name' => $key,
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$data_post = $this->get_post_by_name( $key );
|
||||
$expiry_timestamp = 0;
|
||||
|
||||
if ( $expiry ) {
|
||||
$expiry_timestamp = time() + $expiry;
|
||||
}
|
||||
|
||||
$value = array(
|
||||
'data' => $value,
|
||||
'expiry' => $expiry_timestamp,
|
||||
);
|
||||
$data_post_data['post_content'] = base64_encode( maybe_serialize( $value ) ); // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
// Update an existing data post if we have one or create a new one.
|
||||
if ( $data_post ) {
|
||||
$data_post_data['ID'] = $data_post->ID;
|
||||
wp_update_post( $data_post_data );
|
||||
} else {
|
||||
wp_insert_post( $data_post_data );
|
||||
}
|
||||
|
||||
delete_transient( $this->post_type_slug() . '_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cache entry using a CPT.
|
||||
*
|
||||
* @param string $key Cache key name.
|
||||
* @param mixed $default Default value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( $key, $default ) {
|
||||
$cached = get_transient( $this->post_type_slug() . '_' . $key );
|
||||
if ( $cached ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$data_post = $this->get_post_by_name( $key );
|
||||
|
||||
if ( ! $data_post ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array(
|
||||
* 'data' => mixed,
|
||||
* 'expiry' => int,
|
||||
* )
|
||||
*/
|
||||
|
||||
// phpcs:disable
|
||||
$value = maybe_unserialize( base64_decode( $data_post->post_content ) );
|
||||
// phpcs:enable
|
||||
|
||||
if ( isset( $value['expiry'] ) && intval( $value['expiry'] ) > 0 ) {
|
||||
if ( time() > intval( $value['expiry'] ) ) {
|
||||
// The cache entry expired. Clear it.
|
||||
$this->delete( $key );
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! isset( $value['data'] ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
set_transient( $this->post_type_slug() . '_' . $key, $value['data'], HOUR_IN_SECONDS );
|
||||
|
||||
return $value['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a cache entry from a CPT.
|
||||
*
|
||||
* @param string $key Cache key name.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete( $key ) {
|
||||
$data_post = $this->get_post_by_name( $key );
|
||||
|
||||
// Delete the post.
|
||||
if ( $data_post ) {
|
||||
wp_delete_post( $data_post->ID, true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single WP post based on the `post_name` property.
|
||||
*
|
||||
* Note: `post_name` is indexed in the DB.
|
||||
*
|
||||
* @see https://codex.wordpress.org/Database_Description#Indexes_6
|
||||
*
|
||||
* @param string $post_name Post name.
|
||||
*
|
||||
* @return bool|\WP_Post
|
||||
*/
|
||||
public function get_post_by_name( $post_name ) {
|
||||
$post_query = new \WP_Query(
|
||||
array(
|
||||
'name' => $post_name,
|
||||
'post_type' => $this->post_type_slug(),
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'ignore_sticky_posts' => true,
|
||||
'no_found_rows' => true,
|
||||
'update_post_meta_cache' => false,
|
||||
'update_post_term_cache' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $post_query->have_posts() ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! $post_query->posts[0] instanceof \WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $post_query->posts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data stored in post types. On systems which support it, this
|
||||
* will use wp_cache_flush_group and a db query to efficiently flush the
|
||||
* cache. Otherwise, it will fall back to deleting each item.
|
||||
*/
|
||||
public function clear() {
|
||||
if (
|
||||
function_exists( 'wp_cache_flush_group' ) &&
|
||||
function_exists( 'wp_cache_supports' ) &&
|
||||
wp_cache_supports( 'flush_group' )
|
||||
) {
|
||||
$this->clear_bulk();
|
||||
} else {
|
||||
$this->clear_manually();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data stored in post types using wp_cache_flush_group and a db
|
||||
* query. This is more efficient than deleting each item individually.
|
||||
* Make sure that wp_cache_supports( 'flush_group' ) returns true before
|
||||
* calling this method.
|
||||
*/
|
||||
private function clear_bulk() {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$wpdb->delete(
|
||||
$wpdb->posts,
|
||||
array( 'post_type' => $this->post_type_slug() ),
|
||||
array( '%s' )
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
|
||||
'_transient_' . $this->post_type_slug() . '_%'
|
||||
)
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
|
||||
'_transient_timeout_' . $this->post_type_slug() . '_%'
|
||||
)
|
||||
);
|
||||
|
||||
wp_cache_flush_group( $this->post_type_slug() );
|
||||
wp_cache_flush_group( 'options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data stored in post types by deleting each item individually.
|
||||
* This is less efficient than using wp_cache_flush_group and a db query,
|
||||
* but works on all systems.
|
||||
*/
|
||||
private function clear_manually() {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => $this->post_type_slug(),
|
||||
'posts_per_page' => -1,
|
||||
)
|
||||
);
|
||||
|
||||
$keys = array();
|
||||
foreach ( $posts as $post ) {
|
||||
wp_delete_post( $post->ID, true );
|
||||
wp_cache_delete( $post->post_name, $this->post_type_slug() );
|
||||
$keys[] = '_transient_' . $this->post_type_slug() . '_' . $post->post_name;
|
||||
$keys[] = '_transient_timeout_' . $this->post_type_slug() . '_' . $post->post_name;
|
||||
}
|
||||
|
||||
if ( empty( $keys ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$key = $this->post_type_slug() . '_' . $post->post_name;
|
||||
delete_transient( $key );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
class Super_Cache_Config_Compatibility {
|
||||
public static function is_compatible() {
|
||||
if ( ! self::include_supercache_config() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( self::is_mobile_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_late_init_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_rejected_cookies_configured() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_cache_restrictions_configured() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_preload_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_no_cache_for_get_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_save_headers_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_make_known_anon_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_dynamic_cache_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_clear_on_post_edit_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_front_page_checks_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_extra_pages_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_extra_acceptable_files_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_extra_rejected_uris_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( self::is_extra_rejected_user_agents_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function include_supercache_config() {
|
||||
return include WPCACHECONFIGPATH . '/wp-cache-config.php';
|
||||
}
|
||||
|
||||
private static function is_mobile_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_mobile_enabled'] );
|
||||
}
|
||||
|
||||
private static function is_late_init_enabled() {
|
||||
return ! empty( $GLOBALS['wp_super_cache_late_init'] );
|
||||
}
|
||||
|
||||
private static function is_rejected_cookies_configured() {
|
||||
return ! empty( $GLOBALS['wpsc_rejected_cookies'] );
|
||||
}
|
||||
|
||||
private static function is_cache_restrictions_configured() {
|
||||
return isset( $GLOBALS['wp_cache_not_logged_in'] ) && $GLOBALS['wp_cache_not_logged_in'] !== 2;
|
||||
}
|
||||
|
||||
private static function is_preload_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_preload_on'] );
|
||||
}
|
||||
|
||||
private static function is_no_cache_for_get_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_no_cache_for_get'] );
|
||||
}
|
||||
|
||||
private static function is_save_headers_enabled() {
|
||||
return ! empty( $GLOBALS['wpsc_save_headers'] );
|
||||
}
|
||||
|
||||
private static function is_make_known_anon_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_make_known_anon'] );
|
||||
}
|
||||
|
||||
private static function is_dynamic_cache_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_mfunc_enabled'] );
|
||||
}
|
||||
|
||||
private static function is_clear_on_post_edit_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_clear_on_post_edit'] );
|
||||
}
|
||||
|
||||
private static function is_front_page_checks_enabled() {
|
||||
return ! empty( $GLOBALS['wp_cache_front_page_checks'] );
|
||||
}
|
||||
|
||||
private static function is_extra_pages_enabled() {
|
||||
return is_array( $GLOBALS['wp_cache_pages'] ) && array_sum( $GLOBALS['wp_cache_pages'] );
|
||||
}
|
||||
|
||||
private static function is_extra_acceptable_files_enabled() {
|
||||
$default_cache_acceptable_files = array( 'wp-comments-popup.php', 'wp-links-opml.php', 'wp-locations.php' );
|
||||
return self::is_array_value_changed( $default_cache_acceptable_files, $GLOBALS['cache_acceptable_files'] );
|
||||
}
|
||||
|
||||
private static function is_extra_rejected_uris_enabled() {
|
||||
$default_cache_rejected_uri = array( 'wp-.*\\.php', 'index\\.php' );
|
||||
return self::is_array_value_changed( $default_cache_rejected_uri, $GLOBALS['cache_rejected_uri'] );
|
||||
}
|
||||
|
||||
private static function is_extra_rejected_user_agents_enabled() {
|
||||
return self::is_array_value_changed( array( '' ), $GLOBALS['cache_rejected_user_agent'] );
|
||||
}
|
||||
|
||||
private static function is_array_value_changed( $default, $current ) {
|
||||
return is_array( $current ) && array_diff( $current, $default );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib;
|
||||
|
||||
class Super_Cache_Tracking {
|
||||
public static function setup() {
|
||||
add_action( 'jb_cache_moved_to_wpsc', array( __CLASS__, 'track_move_to_wpsc' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Track when site owner moves cache to WPSC.
|
||||
*/
|
||||
public static function track_move_to_wpsc() {
|
||||
Analytics::record_user_event( 'moved_cache_to_wpsc' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Cornerstone;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema;
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Setup;
|
||||
use Automattic\Jetpack_Boost\Data_Sync\Cornerstone_Pages_Entry;
|
||||
use Automattic\Jetpack_Boost\Lib\Premium_Features;
|
||||
class Cornerstone_Pages implements Has_Setup {
|
||||
|
||||
const PREMIUM_MAX_PAGES = 10;
|
||||
const FREE_MAX_PAGES = 1;
|
||||
|
||||
public function setup() {
|
||||
$this->register_ds_stores();
|
||||
|
||||
add_filter( 'jetpack_boost_critical_css_providers', array( $this, 'remove_ccss_front_page_provider' ), 10, 2 );
|
||||
add_filter( 'display_post_states', array( $this, 'add_display_post_states' ), 10, 2 );
|
||||
add_action( 'init', array( $this, 'set_default_pages' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default pages for the first time Boost is running on the website.
|
||||
*/
|
||||
public function set_default_pages() {
|
||||
// Since the DS store always returns an empty array, we can't know if the user
|
||||
// wants an empty list of pages or this is the first time Boost is running on the website
|
||||
// and we need to actually load the default pages.
|
||||
$raw_pages = \get_option( 'jetpack_boost_ds_cornerstone_pages_list' );
|
||||
if ( $raw_pages === false ) {
|
||||
jetpack_boost_ds_set( 'cornerstone_pages_list', $this->default_pages() );
|
||||
}
|
||||
}
|
||||
|
||||
private function register_ds_stores() {
|
||||
$schema = Schema::as_array( Schema::as_string() )->fallback( array() );
|
||||
jetpack_boost_register_option( 'cornerstone_pages_list', $schema, new Cornerstone_Pages_Entry( 'cornerstone_pages_list' ) );
|
||||
jetpack_boost_register_readonly_option( 'cornerstone_pages_properties', array( $this, 'get_properties' ) );
|
||||
}
|
||||
|
||||
public function remove_ccss_front_page_provider( $providers ) {
|
||||
$filtered_providers = array();
|
||||
|
||||
foreach ( $providers as $provider ) {
|
||||
if ( $provider['key'] !== 'core_front_page' ) {
|
||||
$filtered_providers[] = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered_providers;
|
||||
}
|
||||
|
||||
private function default_pages() {
|
||||
$max_pages = $this->get_max_pages();
|
||||
$yoast_cornerstone_pages = $this->get_yoast_cornerstone_pages();
|
||||
$woocommerce_pages = $this->get_woocommerce_pages();
|
||||
|
||||
$urls = array_unique( array_merge( $woocommerce_pages, $yoast_cornerstone_pages ) );
|
||||
$urls = array_map( 'untrailingslashit', $urls );
|
||||
|
||||
// An empty string represents the home page.
|
||||
// Remove it if it's in the list since the home page is a predefined page.
|
||||
$urls = array_filter( $urls );
|
||||
|
||||
return array_slice( $urls, 0, $max_pages );
|
||||
}
|
||||
|
||||
private function get_yoast_cornerstone_pages() {
|
||||
$urls = array();
|
||||
if ( ! function_exists( 'wpseo_init' ) ) {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
$max_pages = $this->get_max_pages();
|
||||
$yoast_cornerstone_content = get_posts(
|
||||
array(
|
||||
'meta_key' => '_yoast_wpseo_is_cornerstone',
|
||||
'meta_value' => '1',
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => $max_pages,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $yoast_cornerstone_content as $post ) {
|
||||
$permalink = get_permalink( $post->ID );
|
||||
if ( $permalink ) {
|
||||
$relative_permalink = $this->make_relative_url( $permalink );
|
||||
$urls[] = $relative_permalink;
|
||||
}
|
||||
}
|
||||
return $urls;
|
||||
}
|
||||
|
||||
private function get_woocommerce_pages() {
|
||||
$urls = array();
|
||||
if ( ! function_exists( 'wc_get_page_id' ) ) {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
$shop_page_id = \wc_get_page_id( 'shop' );
|
||||
if ( $shop_page_id ) {
|
||||
$url = get_permalink( $shop_page_id );
|
||||
if ( $url ) {
|
||||
$relative_url = $this->make_relative_url( $url );
|
||||
|
||||
if ( $relative_url ) {
|
||||
$urls[] = $relative_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
private function make_relative_url( $url ) {
|
||||
if ( is_string( $url ) && strpos( $url, home_url() ) === 0 ) {
|
||||
$url = substr( $url, strlen( home_url() ) );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function get_properties() {
|
||||
$properties = array(
|
||||
'max_pages' => $this->get_max_pages(),
|
||||
'max_pages_premium' => static::PREMIUM_MAX_PAGES,
|
||||
'default_pages' => array(),
|
||||
'predefined_pages' => Cornerstone_Utils::get_predefined_list(),
|
||||
);
|
||||
|
||||
// We need this to ensure we don't include the home page in the default pages since an empty array is returned when no default pages are found.
|
||||
$default_pages = $this->default_pages();
|
||||
|
||||
if ( ! empty( $default_pages ) ) {
|
||||
$properties['default_pages'] = array_map( 'home_url', $default_pages );
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
public function add_display_post_states( $post_states, $post ) {
|
||||
if ( Cornerstone_Utils::is_cornerstone_page( $post->ID ) ) {
|
||||
$post_states[] = __( 'Cornerstone Page', 'jetpack-boost' );
|
||||
}
|
||||
|
||||
return $post_states;
|
||||
}
|
||||
|
||||
private function get_max_pages() {
|
||||
return Premium_Features::has_feature( Premium_Features::CORNERSTONE_TEN_PAGES ) ? static::PREMIUM_MAX_PAGES : static::FREE_MAX_PAGES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Cornerstone;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Cornerstone_Provider;
|
||||
|
||||
class Cornerstone_Utils {
|
||||
|
||||
/**
|
||||
* Get the list of cornerstone pages.
|
||||
*
|
||||
* @return string[] The relative URLs of all the cornerstone pages.
|
||||
*/
|
||||
public static function get_list() {
|
||||
/**
|
||||
* Filters the list of cornerstone pages. This list includes the predefined and custom pages.
|
||||
* If you want to change the list of custom pages, use `jetpack_boost_cornerstone_pages_list` instead.
|
||||
*
|
||||
* @since 4.4.0-beta1
|
||||
*
|
||||
* @param string[] $urls The absolute URLs of all the cornerstone pages.
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_cornerstone_pages_list_complete', array_merge( self::get_predefined_list(), self::get_custom_list() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of Cornerstone Pages that the user has added to the custom list.
|
||||
*
|
||||
* @return string[] The absolute URLs of the cornerstone pages.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function get_custom_list() {
|
||||
$pages = jetpack_boost_ds_get( 'cornerstone_pages_list' );
|
||||
|
||||
// Bail early if no pages are found.
|
||||
if ( empty( $pages ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return self::maybe_trailing_slash_urls( $pages );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of Cornerstone Pages that the user cannot remove.
|
||||
*
|
||||
* @return string[] The absolute URLs of the cornerstone pages.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function get_predefined_list() {
|
||||
return self::maybe_trailing_slash_urls( array( home_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is a cornerstone page.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
* @return bool True if the URL is a cornerstone page, false otherwise.
|
||||
*/
|
||||
public static function is_cornerstone_page_by_url( $url ) {
|
||||
$cornerstone_pages = self::get_list();
|
||||
if ( empty( $cornerstone_pages ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cornerstone_pages = array_map( 'untrailingslashit', $cornerstone_pages );
|
||||
return in_array( self::sanitize_url( $url ), $cornerstone_pages, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a URL to make it a compatible cornerstone page URL.
|
||||
*
|
||||
* @param string $url The URL to sanitize.
|
||||
* @return string The sanitized URL.
|
||||
*/
|
||||
public static function sanitize_url( $url ) {
|
||||
return untrailingslashit( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider key for a given URL.
|
||||
*
|
||||
* @param string $url The URL to get the provider key for.
|
||||
* @return string The provider key.
|
||||
*/
|
||||
public static function get_provider_key( $url ) {
|
||||
return Cornerstone_Provider::get_provider_key( self::sanitize_url( $url ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare provider data for a given URL.
|
||||
* This is usually sent to the Cloud API.
|
||||
*
|
||||
* @param string $url The URL to prepare provider data for.
|
||||
* @return array The provider data.
|
||||
*/
|
||||
public static function prepare_provider_data( $url ) {
|
||||
return array(
|
||||
'key' => self::get_provider_key( $url ),
|
||||
// Send the URL verbatim (do not sanitize/untrailingslashit it). The cloud analyzer
|
||||
// fetches this exact URL, so it must keep the site's canonical trailing-slash form to
|
||||
// avoid edge/WAF 301s or 403s that break analysis. The `key` is intentionally still
|
||||
// stripped so storage/retrieval stays slash-agnostic. Do not "simplify" url back to
|
||||
// self::sanitize_url( $url ).
|
||||
'url' => $url,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page is a cornerstone page.
|
||||
*
|
||||
* @return bool True if the current page is a cornerstone page, false otherwise.
|
||||
*
|
||||
* @since 3.13.1
|
||||
*/
|
||||
public static function is_current_page_cornerstone() {
|
||||
return self::is_cornerstone_page_by_url( Cornerstone_Provider::get_request_url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post ID is a cornerstone page.
|
||||
*
|
||||
* @param int $post_id The ID of the post to check.
|
||||
* @return bool True if the post is a cornerstone page, false otherwise.
|
||||
*/
|
||||
public static function is_cornerstone_page( $post_id ) {
|
||||
return self::is_cornerstone_page_by_url( get_permalink( $post_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds trailing slashes to URLs if the current permalink structure requires it.
|
||||
*
|
||||
* @param string[] $urls The URLs to process.
|
||||
* @return string[] The processed URLs.
|
||||
*/
|
||||
public static function maybe_trailing_slash_urls( $urls ) {
|
||||
$permalink_structure = \get_option( 'permalink_structure' );
|
||||
|
||||
// If permalink structure ends with slash, add trailing slashes.
|
||||
if ( $permalink_structure && substr( $permalink_structure, -1 ) === '/' ) {
|
||||
$urls = array_map( 'trailingslashit', $urls );
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
class Admin_Bar_Compatibility {
|
||||
|
||||
/**
|
||||
* Enforces the admin bar stylesheet to load late and synchronously
|
||||
* when the admin bar is present on the page.
|
||||
*/
|
||||
public static function init() {
|
||||
|
||||
// Force the Admin Bar to render in the footer.
|
||||
remove_action( 'wp_body_open', 'wp_admin_bar_render', '0' );
|
||||
|
||||
add_filter( 'jetpack_boost_async_style', array( __CLASS__, 'enable_asynchronous_admin_bar' ), 10, 2 );
|
||||
add_action( 'wp_before_admin_bar_render', array( __CLASS__, 'force_admin_bar_stylesheet' ) );
|
||||
add_action( 'wp_head', array( __CLASS__, 'dequeue_admin_bar' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the admin bar CSS synchronously.
|
||||
*
|
||||
* @param bool $is_async Whether admin bar is async.
|
||||
* @param string $handle Asset handle.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function enable_asynchronous_admin_bar( $is_async, $handle ) {
|
||||
|
||||
if ( 'admin-bar' === $handle ) {
|
||||
$is_async = false;
|
||||
}
|
||||
|
||||
return $is_async;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dequeue the admin bar stylesheet, so that it's not printed early.
|
||||
*
|
||||
* @see wp_head
|
||||
*/
|
||||
public static function dequeue_admin_bar() {
|
||||
wp_dequeue_style( 'admin-bar' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the admin bar stylesheet to print right before the admin bar markup.
|
||||
*
|
||||
* @see wp_before_admin_bar_render
|
||||
*/
|
||||
public static function force_admin_bar_stylesheet() {
|
||||
wp_print_styles( 'admin-bar' );
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* Critical CSS Invalidator
|
||||
*
|
||||
* Reset critical CSS when existing critical css values are stale.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
use Automattic\Jetpack_Boost\Admin\Regenerate_Admin_Notice;
|
||||
use Automattic\Jetpack_Boost\Lib\Boost_Health;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Source_Providers;
|
||||
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS_Followup;
|
||||
|
||||
/**
|
||||
* Handler for invalidating Critical CSS; both Cloud and Local. Watches relevant
|
||||
* hooks and clears data when necessary. Also sends out its own action
|
||||
* (after_critical_css_invalidate) so that the Cloud or Local implementations of
|
||||
* Critical CSS can respond to the invalidation.
|
||||
*/
|
||||
class Critical_CSS_Invalidator {
|
||||
public static function init() {
|
||||
add_action( 'jetpack_boost_deactivate', array( __CLASS__, 'reset_data' ) );
|
||||
add_action( 'jetpack_boost_environment_changed', array( __CLASS__, 'handle_environment_change' ) );
|
||||
add_filter( 'jetpack_boost_total_problem_count', array( __CLASS__, 'update_boost_problem_count' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Critical CSS data.
|
||||
* For Cloud CSS, we need to make sure there are providers in the state,
|
||||
* otherwise Cloud CSS cannot be stored after it is generated by the cloud.
|
||||
*/
|
||||
public static function reset_data() {
|
||||
$storage = new Critical_CSS_Storage();
|
||||
$storage->clear();
|
||||
|
||||
$state = new Critical_CSS_State();
|
||||
$state->clear();
|
||||
|
||||
if ( self::is_cloud_css() ) {
|
||||
$state->prepare_for_generation( ( new Source_Providers() )->get_provider_sources() );
|
||||
$state->save();
|
||||
|
||||
// Clear the regenerate flag so things are nice and tidy.
|
||||
jetpack_boost_ds_delete( 'critical_css_suggest_regenerate' );
|
||||
// Also clear the admin notice flag, so the notice doesn't show up in case
|
||||
// the user is reverted to free Boost.
|
||||
Regenerate_Admin_Notice::dismiss();
|
||||
|
||||
}
|
||||
|
||||
Cloud_CSS_Followup::unschedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to environment changes; deciding whether or not to clear Critical CSS data.
|
||||
*/
|
||||
public static function handle_environment_change( $is_major_change ) {
|
||||
if ( $is_major_change ) {
|
||||
self::reset_data();
|
||||
|
||||
/**
|
||||
* Indicate that all existing critical CSS has been invalidated.
|
||||
*/
|
||||
do_action( 'jetpack_boost_critical_css_invalidated' );
|
||||
}
|
||||
}
|
||||
|
||||
public static function update_boost_problem_count( $count ) {
|
||||
$css_needs_regeneration = Boost_Health::critical_css_needs_regeneration();
|
||||
if ( $css_needs_regeneration ) {
|
||||
++$count;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public static function is_cloud_css() {
|
||||
$optimizations = ( new Modules_Setup() )->get_status();
|
||||
return isset( $optimizations[ Cloud_CSS::get_slug() ] ) && $optimizations[ Cloud_CSS::get_slug() ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
class Critical_CSS_State {
|
||||
|
||||
const GENERATION_STATES = array(
|
||||
'not_generated' => 'not_generated',
|
||||
'pending' => 'pending',
|
||||
'generated' => 'generated',
|
||||
'error' => 'error',
|
||||
);
|
||||
|
||||
const PROVIDER_STATES = array(
|
||||
'pending' => 'pending',
|
||||
'success' => 'success',
|
||||
'error' => 'error',
|
||||
);
|
||||
public $state;
|
||||
|
||||
public function __construct() {
|
||||
$this->state = jetpack_boost_ds_get( 'critical_css_state' );
|
||||
}
|
||||
|
||||
public function clear() {
|
||||
jetpack_boost_ds_delete( 'critical_css_state' );
|
||||
}
|
||||
|
||||
public function save() {
|
||||
$this->state['updated'] = microtime( true );
|
||||
jetpack_boost_ds_set( 'critical_css_state', $this->state );
|
||||
|
||||
if ( $this->is_generated() ) {
|
||||
/**
|
||||
* Fires when critical CSS has successfully been generated.
|
||||
*/
|
||||
do_action( 'jetpack_boost_critical_css_generated' );
|
||||
}
|
||||
}
|
||||
|
||||
public function set_error( $message ) {
|
||||
if ( empty( $message ) ) {
|
||||
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
'Critical CSS: set_error() called with empty message'
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->state['status_error'] = $message;
|
||||
$this->state['status'] = self::GENERATION_STATES['error'];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set_provider_error_dismissed( $provider_key, $error_type, $dismissed ) {
|
||||
if ( empty( $this->state['providers'] ) ) {
|
||||
return new WP_Error( 'invalid_provider_key', 'No providers exist' );
|
||||
}
|
||||
|
||||
$provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
|
||||
if ( $provider_index === false ) {
|
||||
return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
|
||||
}
|
||||
|
||||
if ( ! isset( $this->state['providers'][ $provider_index ]['dismissed_errors'] ) ) {
|
||||
$this->state['providers'][ $provider_index ]['dismissed_errors'] = array();
|
||||
}
|
||||
|
||||
if ( $dismissed ) {
|
||||
if ( ! in_array( $error_type, $this->state['providers'][ $provider_index ]['dismissed_errors'], true ) ) {
|
||||
$this->state['providers'][ $provider_index ]['dismissed_errors'][] = $error_type;
|
||||
}
|
||||
} else {
|
||||
$key = array_search( $error_type, $this->state['providers'][ $provider_index ]['dismissed_errors'], true );
|
||||
if ( $key !== false ) {
|
||||
unset( $this->state['providers'][ $provider_index ]['dismissed_errors'][ $key ] );
|
||||
$this->state['providers'][ $provider_index ]['dismissed_errors'] = array_values( $this->state['providers'][ $provider_index ]['dismissed_errors'] );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a provider's state. The provider must already exist in the state to be updated.
|
||||
*
|
||||
* @param string $provider_key The provider key.
|
||||
* @param array $state An array to overlay over the current state.
|
||||
* @return bool|WP_Error True on success, WP_Error on failure.
|
||||
*/
|
||||
private function update_provider_state( $provider_key, $state ) {
|
||||
if ( empty( $this->state['providers'] ) ) {
|
||||
return new WP_Error( 'invalid_provider_key', 'No providers exist' );
|
||||
}
|
||||
|
||||
$provider_index = array_search( $provider_key, array_column( $this->state['providers'], 'key' ), true );
|
||||
if ( $provider_index === false ) {
|
||||
return new WP_Error( 'invalid_provider_key', 'Invalid provider key' );
|
||||
}
|
||||
|
||||
$this->state['providers'][ $provider_index ] = array_merge(
|
||||
$this->state['providers'][ $provider_index ],
|
||||
$state
|
||||
);
|
||||
|
||||
$this->maybe_set_generated();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a provider's state to error.
|
||||
*
|
||||
* @param string $provider_key The provider key.
|
||||
* @param array $errors A list of errors to store with this provider.
|
||||
* @return bool|WP_Error True on success, WP_Error on failure.
|
||||
*/
|
||||
public function set_provider_errors( $provider_key, $errors ) {
|
||||
return $this->update_provider_state(
|
||||
$provider_key,
|
||||
array(
|
||||
'status' => self::PROVIDER_STATES['error'],
|
||||
'errors' => $errors,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a provider's state to success.
|
||||
*
|
||||
* @param string $provider_key The provider key.
|
||||
* @return bool|WP_Error True on success, WP_Error on failure.
|
||||
*/
|
||||
public function set_provider_success( $provider_key ) {
|
||||
return $this->update_provider_state(
|
||||
$provider_key,
|
||||
array(
|
||||
'status' => self::PROVIDER_STATES['success'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state to generated if all providers are done. Should be called wherever
|
||||
* a provider's state is updated.
|
||||
*/
|
||||
private function maybe_set_generated() {
|
||||
if ( empty( $this->state['providers'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$provider_states = array_column( $this->state['providers'], 'status' );
|
||||
$is_done = ! in_array( self::GENERATION_STATES['pending'], $provider_states, true );
|
||||
|
||||
if ( $is_done ) {
|
||||
$this->state['status'] = self::GENERATION_STATES['generated'];
|
||||
}
|
||||
}
|
||||
|
||||
public function has_errors() {
|
||||
// Check if any of the providers have errors as well.
|
||||
$any_provider_has_error = in_array(
|
||||
'error',
|
||||
array_unique(
|
||||
wp_list_pluck(
|
||||
$this->state['providers'],
|
||||
'status'
|
||||
)
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
return self::GENERATION_STATES['error'] === $this->state['status'] || $any_provider_has_error;
|
||||
}
|
||||
|
||||
public function get_error_message() {
|
||||
return $this->state['status_error'] ?? null;
|
||||
}
|
||||
|
||||
public function is_generated() {
|
||||
return self::GENERATION_STATES['generated'] === $this->state['status'];
|
||||
}
|
||||
|
||||
public function is_requesting() {
|
||||
return self::GENERATION_STATES['pending'] === $this->state['status'];
|
||||
}
|
||||
|
||||
public function prepare_request() {
|
||||
$this->state = array(
|
||||
'status' => self::GENERATION_STATES['pending'],
|
||||
'providers' => array(),
|
||||
'created' => microtime( true ),
|
||||
'updated' => microtime( true ),
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set_pending_providers( $providers ) {
|
||||
foreach ( $providers as $key => $provider ) {
|
||||
$providers[ $key ]['status'] = self::PROVIDER_STATES['pending'];
|
||||
}
|
||||
$this->state['providers'] = $providers;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add providers to the state, sets their status to pending
|
||||
* and sets the generation status to pending.
|
||||
*
|
||||
* @param array $providers The providers to include in the state and set as pending.
|
||||
* @return $this
|
||||
*/
|
||||
public function prepare_for_generation( $providers ) {
|
||||
$this->set_pending_providers( $providers );
|
||||
$this->state['status'] = self::GENERATION_STATES['pending'];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fresh state
|
||||
*/
|
||||
public function get() {
|
||||
$this->state = jetpack_boost_ds_get( 'critical_css_state' );
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function has_pending_provider( $needles = array() ) {
|
||||
if ( empty( $this->state['providers'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$providers = $this->state['providers'];
|
||||
foreach ( $providers as $provider ) {
|
||||
if (
|
||||
! empty( $provider['key'] )
|
||||
&& ! empty( $provider['status'] )
|
||||
&& self::PROVIDER_STATES['pending'] === $provider['status']
|
||||
&& in_array( $provider['key'], $needles, true )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Critical CSS storage.
|
||||
*
|
||||
* @link https://automattic.com
|
||||
* @since 1.0.0
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Storage_Post_Type;
|
||||
|
||||
/**
|
||||
* Critical CSS Storage class
|
||||
*/
|
||||
class Critical_CSS_Storage {
|
||||
|
||||
/**
|
||||
* Storage post type.
|
||||
*
|
||||
* @var Storage_Post_Type
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Critical_CSS_Storage constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->storage = new Storage_Post_Type( 'css' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Critical CSS for a specific provider.
|
||||
*
|
||||
* @param string $key Provider key.
|
||||
* @param string $value Critical CSS.
|
||||
*/
|
||||
public function store_css( $key, $value ) {
|
||||
$this->storage->set(
|
||||
$key,
|
||||
array(
|
||||
'css' => $value,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the whole Critical CSS storage.
|
||||
*/
|
||||
public function clear() {
|
||||
$this->storage->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Critical CSS for specific provider keys.
|
||||
*
|
||||
* @param array $provider_keys Provider keys.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function get_css( $provider_keys ) {
|
||||
foreach ( $provider_keys as $key ) {
|
||||
$data = $this->storage->get( $key, false );
|
||||
if ( $data && $data['css'] ) {
|
||||
return array(
|
||||
'key' => $key,
|
||||
'css' => $data['css'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* Class that's responsible for rendering
|
||||
* Critical CSS on the site front-end.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
class Display_Critical_CSS {
|
||||
|
||||
/**
|
||||
* @var string The Critical CSS to display.
|
||||
*/
|
||||
protected $css;
|
||||
|
||||
/**
|
||||
* @param string $css
|
||||
*/
|
||||
public function __construct( $css ) {
|
||||
$this->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 ( ! $this->css ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$supported_loading_methods = array( 'async', 'deferred' );
|
||||
|
||||
/**
|
||||
* 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 supported, do not alter the stylesheet loading.
|
||||
if ( ! in_array( $method, $supported_loading_methods, true ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Update the stylesheet markup for supported loading methods using WordPress HTML API.
|
||||
$processor = new \WP_HTML_Tag_Processor( $html );
|
||||
if ( ! $processor->next_tag( 'link' ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Only process if this is a stylesheet link tag.
|
||||
if ( 'stylesheet' !== $processor->get_attribute( 'rel' ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Set the new attributes based on the selected method.
|
||||
$processor->set_attribute( 'media', 'not all' );
|
||||
$processor->set_attribute( 'data-media', $media );
|
||||
if ( 'async' === $method ) {
|
||||
$processor->set_attribute( 'onload', "this.media=this.dataset.media; delete this.dataset.media; this.removeAttribute( 'onload' );" );
|
||||
}
|
||||
|
||||
// Prepend the original HTML stylesheet tag within the noscript tag
|
||||
// to support the rendering of the stylesheet when JavaScript is disabled.
|
||||
return '<noscript>' . $html . '</noscript>' . $processor->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the critical CSS to the page.
|
||||
*/
|
||||
public function display_critical_css() {
|
||||
$critical_css = $this->css;
|
||||
|
||||
if ( ! $critical_css ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
echo '<style id="jetpack-boost-critical-css">';
|
||||
|
||||
// Ensure the CSS cannot terminate the style element early.
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo self::neutralize_style_closing_tags( $critical_css );
|
||||
|
||||
echo '</style>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutralize any closing </style tag so CSS can be printed inside a <style>
|
||||
* element without breaking out of it.
|
||||
*
|
||||
* This is NOT a general-purpose CSS sanitizer: it does exactly one thing,
|
||||
* which is to stop the CSS from terminating the surrounding <style> element.
|
||||
* It deliberately avoids wp_strip_all_tags(), which corrupts valid CSS values
|
||||
* that contain markup - e.g. `background-image: url("data:image/svg+xml,<svg ...></svg>")`.
|
||||
*
|
||||
* Per the HTML rawtext tokenizer, a <style> element can only be terminated by
|
||||
* the literal sequence `</style` (case-insensitive). Escaping its forward slash
|
||||
* to `<\/style` defeats the tokenizer - `</` must be immediately followed by the
|
||||
* tag name, and `<\` is treated as literal text - so the markup stays inert. The
|
||||
* replacement string contains no `</style` substring, so a single left-to-right
|
||||
* pass cannot reconstruct the sequence (including from nested input like
|
||||
* `<</style/style`), and the transform is idempotent.
|
||||
*
|
||||
* Inside CSS strings and url() tokens `\/` is a valid escape for `/`, so
|
||||
* legitimate quoted values keep their meaning. (A literal `</style` outside a
|
||||
* quoted string does not occur in well-formed CSS; the security guarantee takes
|
||||
* priority there regardless.)
|
||||
*
|
||||
* @param string $css CSS to neutralize.
|
||||
* @return string CSS that cannot terminate the surrounding <style> element.
|
||||
*/
|
||||
public static function neutralize_style_closing_tags( $css ) {
|
||||
return str_ireplace( '</style', '<\/style', $css );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <style> block to media="not all" to deactivate it.
|
||||
*/
|
||||
public function onload_flip_stylesheets() {
|
||||
/*
|
||||
Unminified version of footer script.
|
||||
|
||||
?>
|
||||
<script>
|
||||
window.addEventListener( 'load', function() {
|
||||
|
||||
// Flip all media="not all" links to media="all".
|
||||
document.querySelectorAll( 'link' ).forEach(
|
||||
function( link ) {
|
||||
if ( link.media === 'not all' && link.dataset.media ) {
|
||||
link.media = link.dataset.media;
|
||||
delete link.dataset.media;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Turn off Critical CSS style block with media="not all".
|
||||
var element = document.getElementById( 'jetpack-boost-critical-css' );
|
||||
if ( element ) {
|
||||
element.media = 'not all';
|
||||
}
|
||||
|
||||
} );
|
||||
</script>
|
||||
<?php
|
||||
*/
|
||||
|
||||
// Minified version of footer script. See above comment for unminified version.
|
||||
?>
|
||||
<script>window.addEventListener( 'load', function() {
|
||||
document.querySelectorAll( 'link' ).forEach( function( e ) {'not all' === e.media && e.dataset.media && ( e.media = e.dataset.media, delete e.dataset.media );} );
|
||||
var e = document.getElementById( 'jetpack-boost-critical-css' );
|
||||
e && ( e.media = 'not all' );
|
||||
} );</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS\CSS_Proxy;
|
||||
|
||||
class Generator {
|
||||
|
||||
const GENERATE_QUERY_ACTION = 'jb-generate-critical-css';
|
||||
|
||||
public static function init() {
|
||||
$generator = new static();
|
||||
if ( static::is_generating_critical_css() ) {
|
||||
add_action( 'wp_head', array( $generator, 'display_generate_meta' ), 0 );
|
||||
$generator->force_logged_out_render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if page is loaded to generate critical CSS
|
||||
*
|
||||
* phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
*/
|
||||
public static function is_generating_critical_css() {
|
||||
return isset( $_GET[ self::GENERATE_QUERY_ACTION ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Critical CSS status block, adding in local generation nonces (if applicable).
|
||||
* i.e.: Call this method to supply enough Critical CSS status to kick off local generation,
|
||||
* such as in response to a request-generate API call or during page initialization.
|
||||
*/
|
||||
public function get_generation_metadata() {
|
||||
$status = array();
|
||||
|
||||
// Add a user-bound nonce to use when proxying CSS for Critical CSS generation.
|
||||
$status['proxy_nonce'] = wp_create_nonce( CSS_Proxy::NONCE_ACTION );
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a <meta> tag used to verify this is a valid page to generate Critical CSS with.
|
||||
*/
|
||||
public function display_generate_meta() {
|
||||
?>
|
||||
<meta name="<?php echo esc_attr( self::GENERATE_QUERY_ACTION ); ?>" content="true"/>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS;
|
||||
|
||||
use Automattic\Jetpack_Boost\Admin\Regenerate_Admin_Notice;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Source_Providers;
|
||||
use Automattic\Jetpack_Boost\Modules\Modules_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS_Followup;
|
||||
|
||||
class Regenerate {
|
||||
/** @var Critical_CSS_State */
|
||||
private $state;
|
||||
|
||||
public function is_cloud_css() {
|
||||
$optimizations = ( new Modules_Setup() )->get_status();
|
||||
return isset( $optimizations[ Cloud_CSS::get_slug() ] ) && $optimizations[ Cloud_CSS::get_slug() ];
|
||||
}
|
||||
|
||||
public function start() {
|
||||
// Get Critical CSS Source URLs
|
||||
$source_providers = new Source_Providers();
|
||||
$providers = $source_providers->get_provider_sources();
|
||||
|
||||
// Store those URLs in the Critical CSS State
|
||||
$this->state = new Critical_CSS_State();
|
||||
$this->state->prepare_request()
|
||||
->set_pending_providers( $providers )
|
||||
->save();
|
||||
|
||||
// Get the data
|
||||
$data = $this->state->get();
|
||||
|
||||
if ( $this->is_cloud_css() ) {
|
||||
// If this is a cloud CSS request, we need to trigger the generation
|
||||
// of the CSS and return the URL to the CSS file.
|
||||
$cloud_css = new Cloud_CSS();
|
||||
$cloud_css->regenerate_cloud_css( Cloud_CSS::REGENERATE_REASON_USER_REQUEST, $cloud_css->get_all_providers() );
|
||||
Cloud_CSS_Followup::schedule();
|
||||
}
|
||||
|
||||
// Clear previous Critical CSS From storage
|
||||
$storage = new Critical_CSS_Storage();
|
||||
$storage->clear();
|
||||
|
||||
// Dismiss admin notices
|
||||
Regenerate_Admin_Notice::dismiss();
|
||||
jetpack_boost_ds_delete( 'critical_css_suggest_regenerate' );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function get_state() {
|
||||
return $this->state;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Regenerate;
|
||||
|
||||
/**
|
||||
* Critical CSS Action: request regeneration.
|
||||
*/
|
||||
class Regenerate_CSS implements Data_Sync_Action {
|
||||
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $_data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $_data, $_request ) {
|
||||
$regenerate = new Regenerate();
|
||||
$regenerate->start();
|
||||
|
||||
$state = $regenerate->get_state();
|
||||
|
||||
return array(
|
||||
'success' => ! $state->has_errors(),
|
||||
'state' => $state->get(),
|
||||
'errors' => $state->get_error_message(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage;
|
||||
|
||||
/**
|
||||
* Critical CSS Action: Set CSS for a provider.
|
||||
*/
|
||||
class Set_Provider_CSS implements Data_Sync_Action {
|
||||
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $data, $_request ) {
|
||||
$state = new Critical_CSS_State();
|
||||
|
||||
if ( empty( $data['key'] ) || empty( $data['css'] ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'state' => $state->get(),
|
||||
'error' => 'Invalid data',
|
||||
);
|
||||
}
|
||||
|
||||
$provider_key = sanitize_key( $data['key'] );
|
||||
$css = $this->unmask_content( $data['css'] );
|
||||
|
||||
$storage = new Critical_CSS_Storage();
|
||||
$storage->store_css( $provider_key, $css );
|
||||
|
||||
$state->set_provider_success( $provider_key );
|
||||
$state->save();
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'state' => $state->get(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmasks the content.
|
||||
*
|
||||
* @param string $content The content to unmask.
|
||||
* @return string The unmasked content.
|
||||
*/
|
||||
private function unmask_content( $content ) {
|
||||
return str_replace( '__JB_XMLNS__', 'xmlns', $content );
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
|
||||
|
||||
/**
|
||||
* Critical CSS Action: Update whether or not to show a provider which is in an error state.
|
||||
*/
|
||||
class Set_Provider_Error_Dismissed implements Data_Sync_Action {
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $data, $_request ) {
|
||||
$state = new Critical_CSS_State();
|
||||
|
||||
foreach ( $data as $item ) {
|
||||
if ( empty( $item['provider'] ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'state' => $state->get(),
|
||||
'error' => 'Invalid data',
|
||||
);
|
||||
}
|
||||
|
||||
$provider_key = sanitize_key( $item['provider'] );
|
||||
$dismissed = ! empty( $item['dismissed'] );
|
||||
|
||||
$state->set_provider_error_dismissed( $provider_key, $item['error_type'], $dismissed );
|
||||
}
|
||||
|
||||
$state->save();
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'state' => $state->get(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
|
||||
|
||||
/**
|
||||
* Critical CSS Action: Store errors for a provider.
|
||||
*/
|
||||
class Set_Provider_Errors implements Data_Sync_Action {
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $data, $_request ) {
|
||||
$state = new Critical_CSS_State();
|
||||
|
||||
if ( empty( $data['key'] ) || empty( $data['errors'] ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'state' => $state->get(),
|
||||
'error' => 'Invalid data',
|
||||
);
|
||||
}
|
||||
|
||||
$provider_key = sanitize_key( $data['key'] );
|
||||
$errors = $data['errors'];
|
||||
|
||||
$state->set_provider_errors( $provider_key, $errors );
|
||||
$state->save();
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'state' => $state->get(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Registers data sync for both the Critical CSS module and the Cloud CSS module. Both of these modules cannot be available at the same time.
|
||||
*/
|
||||
class Data_Sync_Schema {
|
||||
/**
|
||||
* Represents a set of errors that can be stored for a single Provider Key in a Critical CSS state block.
|
||||
*/
|
||||
public static function critical_css_provider_error() {
|
||||
return Schema::as_array(
|
||||
Schema::as_assoc_array(
|
||||
array(
|
||||
'url' => Schema::as_string(),
|
||||
'message' => Schema::as_string(),
|
||||
'type' => Schema::as_string(),
|
||||
'meta' => Schema::any_json_data()->nullable(),
|
||||
)
|
||||
)->fallback(
|
||||
array(
|
||||
'url' => '',
|
||||
'message' => '',
|
||||
'type' => '',
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function critical_css_state() {
|
||||
return Schema::as_assoc_array(
|
||||
array(
|
||||
'providers' => Schema::as_array(
|
||||
Schema::as_assoc_array(
|
||||
array(
|
||||
'key' => Schema::as_string(),
|
||||
'label' => Schema::as_string(),
|
||||
'urls' => Schema::as_array( Schema::as_string() ),
|
||||
'success_ratio' => Schema::as_float(),
|
||||
'status' => Schema::enum( array( 'success', 'pending', 'error', 'validation-error' ) )->fallback( 'validation-error' ),
|
||||
'dismissed_errors' => Schema::as_array( Schema::as_string() )->nullable(),
|
||||
'errors' => self::critical_css_provider_error()->nullable(),
|
||||
)
|
||||
)
|
||||
)->nullable(),
|
||||
'status' => Schema::enum( array( 'not_generated', 'generated', 'pending', 'error' ) )->fallback( 'not_generated' ),
|
||||
'created' => Schema::as_float()->nullable(),
|
||||
'updated' => Schema::as_float()->nullable(),
|
||||
'status_error' => Schema::as_string()->nullable(),
|
||||
)
|
||||
)->fallback(
|
||||
array(
|
||||
'providers' => array(),
|
||||
'status' => 'not_generated',
|
||||
'created' => null,
|
||||
'updated' => null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function critical_css_meta() {
|
||||
return Schema::as_assoc_array(
|
||||
array(
|
||||
'proxy_nonce' => Schema::as_string()->nullable(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function critical_css_suggest_regenerate() {
|
||||
return Schema::enum(
|
||||
array(
|
||||
'1', // Old versions of Boost stored a boolean in the DB.
|
||||
'page_saved',
|
||||
'post_saved',
|
||||
'switched_theme',
|
||||
'plugin_change',
|
||||
'cornerstone_page_saved',
|
||||
'cornerstone_pages_list_updated',
|
||||
)
|
||||
)->nullable();
|
||||
}
|
||||
|
||||
public static function critical_css_set_provider() {
|
||||
return Schema::as_assoc_array(
|
||||
array(
|
||||
'key' => Schema::as_string(),
|
||||
'css' => Schema::as_string(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function critical_css_set_provider_errors() {
|
||||
return Schema::as_assoc_array(
|
||||
array(
|
||||
'key' => Schema::as_string(),
|
||||
'errors' => self::critical_css_provider_error(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function critical_css_set_provider_errors_dismissed() {
|
||||
return Schema::as_array(
|
||||
Schema::as_assoc_array(
|
||||
array(
|
||||
'provider' => Schema::as_string(),
|
||||
'error_type' => Schema::as_string(),
|
||||
'dismissed' => Schema::as_boolean(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Archive_Provider;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Cornerstone_Provider;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Post_ID_Provider;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Provider;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Singular_Post_Provider;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\Taxonomy_Provider;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers\WP_Core_Provider;
|
||||
|
||||
class Source_Providers {
|
||||
|
||||
/**
|
||||
* Variable used to cache the CSS string during the page request.
|
||||
* This is here because `get_critical_css` is called multiple
|
||||
* times in `style_loader_tag` hook (on each CSS file).
|
||||
*
|
||||
* @var null|false|string
|
||||
*/
|
||||
protected $request_cached_css;
|
||||
|
||||
/**
|
||||
* Stores the Critical CSS key used for rendering the current page if any.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
protected $current_critical_css_key;
|
||||
|
||||
/**
|
||||
* List of all the Critical CSS Types.
|
||||
*
|
||||
* The order is important because searching for critical CSS will stop as soon as a value is found.
|
||||
* So finding Critical CSS by post ID is attempted before searching for a common Singular Post critical CSS.
|
||||
*
|
||||
* @var Provider[]
|
||||
*/
|
||||
protected $providers = array(
|
||||
Cornerstone_Provider::class,
|
||||
Post_ID_Provider::class,
|
||||
WP_Core_Provider::class,
|
||||
Singular_Post_Provider::class,
|
||||
Archive_Provider::class,
|
||||
Taxonomy_Provider::class,
|
||||
);
|
||||
|
||||
public function get_providers() {
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider which controls a given key.
|
||||
*/
|
||||
public function get_provider_for_key( $key ) {
|
||||
foreach ( $this->providers as $provider ) {
|
||||
if ( $provider::owns_key( $key ) ) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all critical CSS storage keys that are available for the current request.
|
||||
* Caches the result.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_current_request_css_keys() {
|
||||
static $keys = null;
|
||||
if ( null !== $keys ) {
|
||||
return $keys;
|
||||
}
|
||||
|
||||
$keys = array();
|
||||
|
||||
foreach ( $this->providers as $provider ) {
|
||||
$provider_keys = $provider::get_current_storage_keys();
|
||||
if ( empty( $provider_keys ) ) {
|
||||
continue;
|
||||
}
|
||||
$keys = array_merge( $keys, $provider_keys );
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get critical CSS for the current request.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function get_current_request_css() {
|
||||
if ( null !== $this->request_cached_css ) {
|
||||
return $this->request_cached_css;
|
||||
}
|
||||
|
||||
$storage = new Critical_CSS_Storage();
|
||||
$data = $storage->get_css( $this->get_current_request_css_keys() );
|
||||
if ( false === $data ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->request_cached_css = $data['css'];
|
||||
$this->current_critical_css_key = $data['key'];
|
||||
|
||||
return $this->request_cached_css;
|
||||
}
|
||||
|
||||
public function get_current_critical_css_key() {
|
||||
return $this->current_critical_css_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get providers sources.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_provider_sources( $context_posts = array() ) {
|
||||
$sources = array();
|
||||
$flat_core_and_cornerstone_urls = array();
|
||||
|
||||
$wp_core_provider_urls = WP_Core_Provider::get_critical_source_urls( $context_posts );
|
||||
foreach ( $wp_core_provider_urls as $urls ) {
|
||||
$flat_core_and_cornerstone_urls = array_merge( $flat_core_and_cornerstone_urls, $urls );
|
||||
}
|
||||
$cornerstone_provider_urls = Cornerstone_Provider::get_critical_source_urls( $context_posts );
|
||||
foreach ( $cornerstone_provider_urls as $urls ) {
|
||||
$flat_core_and_cornerstone_urls = array_merge( $flat_core_and_cornerstone_urls, $urls );
|
||||
}
|
||||
$flat_core_and_cornerstone_urls = array_values( array_unique( $flat_core_and_cornerstone_urls ) );
|
||||
|
||||
foreach ( $this->get_providers() as $provider ) {
|
||||
$provider_name = $provider::get_provider_name();
|
||||
|
||||
// For each provider,
|
||||
// Gather a list of URLs that are going to be used as Critical CSS source.
|
||||
foreach ( $provider::get_critical_source_urls( $context_posts ) as $group => $urls ) {
|
||||
if ( empty( $urls ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This removes core and cornerstone URLs from the list of URLs,
|
||||
// so they don't belong to two separate groups.
|
||||
if ( ! in_array( $provider, array( WP_Core_Provider::class, Cornerstone_Provider::class ), true ) ) {
|
||||
$urls = array_values( array_diff( $urls, $flat_core_and_cornerstone_urls ) );
|
||||
}
|
||||
|
||||
if ( empty( $urls ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$urls = $this->make_absolute_urls( $urls );
|
||||
|
||||
$key = $provider_name . '_' . $group;
|
||||
|
||||
// For each provider
|
||||
// Track the state and errors in a state array.
|
||||
$sources[] = array(
|
||||
'key' => $key,
|
||||
'label' => $provider::describe_key( $key ),
|
||||
/**
|
||||
* Filters the URLs used by Critical CSS for each provider.
|
||||
*
|
||||
* @param array $urls The list of URLs to be used to generate critical CSS
|
||||
* @param string $provider The provider name.
|
||||
* @since 1.0.0
|
||||
*/
|
||||
'urls' => apply_filters( 'jetpack_boost_critical_css_urls', $urls, $provider ),
|
||||
'success_ratio' => $provider::get_success_ratio(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the list of Critical CSS source providers.
|
||||
*
|
||||
* @param array $sources The list of Critical CSS source providers.
|
||||
* @since 3.6.0
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_critical_css_providers', $sources );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make URLs absolute.
|
||||
*
|
||||
* @param array $urls The list of URLs to make absolute.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function make_absolute_urls( $urls ) {
|
||||
$absolute_urls = array();
|
||||
foreach ( $urls as $url ) {
|
||||
if ( class_exists( '\WP_Http' ) && method_exists( '\WP_Http', 'make_absolute_url' ) ) {
|
||||
$absolute_urls[] = \WP_Http::make_absolute_url( $url, home_url() );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( stripos( $url, home_url() ) === 0 ) {
|
||||
$absolute_urls[] = $url;
|
||||
} else {
|
||||
$absolute_urls[] = home_url( $url );
|
||||
}
|
||||
}
|
||||
|
||||
return $absolute_urls;
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/**
|
||||
* Archive provider class.
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
/**
|
||||
* Class Archive_Provider
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
|
||||
*/
|
||||
class Archive_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* Provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $name = 'archive';
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_critical_source_urls( $context_posts = array() ) {
|
||||
$links = array();
|
||||
$context_post_types = wp_list_pluck( $context_posts, 'post_type' );
|
||||
|
||||
$post_types = self::get_post_types();
|
||||
if ( ! empty( $context_post_types ) ) {
|
||||
$post_types = array_intersect( $post_types, $context_post_types );
|
||||
}
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$link = get_post_type_archive_link( $post_type );
|
||||
|
||||
if ( ! empty( $link ) ) {
|
||||
$links[ $post_type ][] = $link;
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_current_storage_keys() {
|
||||
if ( ! is_archive() ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// For example: "archive_post".
|
||||
return array( self::$name . '_' . get_post_type() );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_keys() {
|
||||
return self::get_post_types();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_edit_url( $_provider_key ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function describe_key( $provider_key ) {
|
||||
$post_type = substr( $provider_key, strlen( static::$name ) + 1 );
|
||||
|
||||
switch ( $post_type ) {
|
||||
case 'post':
|
||||
return __( 'Post archive view', 'jetpack-boost' );
|
||||
|
||||
case 'page':
|
||||
return __( 'Page archive view', 'jetpack-boost' );
|
||||
|
||||
default:
|
||||
return __( 'Archive page for custom post type', 'jetpack-boost' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post types that need Critical CSS.
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
public static function get_post_types() {
|
||||
$post_types = get_post_types(
|
||||
array(
|
||||
'public' => true,
|
||||
'has_archive' => true,
|
||||
),
|
||||
'objects'
|
||||
);
|
||||
unset( $post_types['attachment'] );
|
||||
|
||||
$post_types = array_filter( $post_types, 'is_post_type_viewable' );
|
||||
|
||||
$provider_post_types = array();
|
||||
// Generate a name => name array for backwards compatibility.
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$provider_post_types[ $post_type->name ] = $post_type->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the post types used for Critical CSS
|
||||
*
|
||||
* @param array $post_types The array of post types to be used
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
return apply_filters(
|
||||
'jetpack_boost_critical_css_post_types_archives',
|
||||
apply_filters_deprecated(
|
||||
'jetpack_boost_critical_css_post_types',
|
||||
array(
|
||||
$provider_post_types,
|
||||
),
|
||||
'3.4.0',
|
||||
'jetpack_boost_critical_css_post_types_archives'
|
||||
),
|
||||
$post_types
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_success_ratio() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Provider for the cornerstone pages
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
|
||||
|
||||
/**
|
||||
* Class Cornerstone_Provider
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers
|
||||
*/
|
||||
class Cornerstone_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static $name = 'cornerstone';
|
||||
|
||||
/**
|
||||
* Get the providers for cornerstone pages.
|
||||
*
|
||||
* @param array $_context_posts Context posts, not used. Cornerstone pages are always available.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_critical_source_urls( $_context_posts = array() ) {
|
||||
$urls = Cornerstone_Utils::get_list();
|
||||
|
||||
$groups = array();
|
||||
foreach ( $urls as $url ) {
|
||||
$groups[ self::get_hash_for_url( $url ) ] = array( $url );
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public static function get_provider_key( $url ) {
|
||||
return self::$name . '_' . self::get_hash_for_url( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current storage keys for cornerstone pages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_current_storage_keys() {
|
||||
$current_url = self::get_request_url();
|
||||
return array( self::get_provider_key( $current_url ) );
|
||||
}
|
||||
|
||||
public static function get_request_url() {
|
||||
global $wp;
|
||||
|
||||
// If pretty parmalinks are enabled, use the request. Otherwise, use the query vars.
|
||||
if ( get_option( 'permalink_structure' ) ) {
|
||||
return home_url( $wp->request );
|
||||
}
|
||||
|
||||
return add_query_arg( $wp->query_vars, home_url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keys for cornerstone pages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_keys() {
|
||||
$urls = Cornerstone_Utils::get_list();
|
||||
|
||||
return array_map( array( __CLASS__, 'get_hash_for_url' ), $urls );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function get_hash_for_url( $url ) {
|
||||
// Remove the home_url from the beginning of the URL.
|
||||
$home_url = home_url();
|
||||
if ( stripos( $url, $home_url ) === 0 ) {
|
||||
$url = substr( $url, strlen( $home_url ) );
|
||||
}
|
||||
|
||||
$url = ltrim( $url, '/' );
|
||||
$url = untrailingslashit( $url );
|
||||
|
||||
$hash = hash( 'md5', $url );
|
||||
|
||||
return substr( $hash, 0, 8 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function describe_key( $_key ) {
|
||||
return __( 'Cornerstone page', 'jetpack-boost' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function get_edit_url( $key ) {
|
||||
$hash = substr( $key, strlen( self::$name ) + 1 );
|
||||
|
||||
$source_urls = self::get_critical_source_urls();
|
||||
|
||||
if ( ! isset( $source_urls[ $hash ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$post_id = url_to_postid( $source_urls[ $hash ][0] );
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_edit_post_link( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function get_success_ratio() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* The Post ID provider class.
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
/**
|
||||
* Class Post_ID_Provider
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
|
||||
*/
|
||||
class Post_ID_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* Post Ids storage key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const STORAGE_KEY = 'jetpack_boost_critical_css_post_ids';
|
||||
|
||||
/**
|
||||
* Provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $name = 'post_id';
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_critical_source_urls( $context_posts = array() ) {
|
||||
$results = array();
|
||||
$query = self::get_posts();
|
||||
$context_post_ids = wp_list_pluck( $context_posts, 'ID' );
|
||||
|
||||
if ( false === $query ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ( $query->posts as $post ) {
|
||||
if ( empty( $context_post_ids ) || in_array( $post->ID, $context_post_ids, true ) ) {
|
||||
$url = get_permalink( $post );
|
||||
if ( ! empty( $url ) ) {
|
||||
$results[ $post->ID ] = array( $url );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_current_storage_keys() {
|
||||
if ( ! is_singular() ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// For example: "post_id_123".
|
||||
return array( self::$name . '_' . get_the_ID() );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function describe_key( $provider_key ) {
|
||||
$post_id = (int) substr( $provider_key, strlen( self::$name ) + 1 );
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
/* translators: %d is the id of a post which cannot be found. */
|
||||
return sprintf( __( 'Post %d', 'jetpack-boost' ), $post_id );
|
||||
}
|
||||
|
||||
return $post->post_title;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_edit_url( $provider_key ) {
|
||||
$post_id = (int) substr( $provider_key, strlen( self::$name ) + 1 );
|
||||
|
||||
return get_edit_post_link( $post_id, 'link' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key that can be used to identify the current page, if any exists.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_current_page_key() {
|
||||
$keys = static::get_current_storage_keys();
|
||||
|
||||
if ( count( $keys ) > 0 ) {
|
||||
return $keys[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post ids.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_post_ids() {
|
||||
// Store the IDs somewhere.
|
||||
return get_option( self::STORAGE_KEY, array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add post id to storage.
|
||||
*
|
||||
* @param int $post_id Post Id.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function add_post_id( $post_id ) {
|
||||
$post_ids = static::get_post_ids();
|
||||
|
||||
if ( in_array( $post_id, $post_ids, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post_ids[] = (int) $post_id;
|
||||
|
||||
return update_option( self::STORAGE_KEY, $post_ids );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_keys() {
|
||||
return self::get_post_ids();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WP_Query to gather sample posts.
|
||||
*
|
||||
* @return false|\WP_Query
|
||||
*/
|
||||
public static function get_posts() {
|
||||
$ids = self::get_post_ids();
|
||||
|
||||
if ( ! $ids ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new \WP_Query(
|
||||
array(
|
||||
'post__in' => $ids,
|
||||
'posts_per_page' => count( $ids ),
|
||||
'post_status' => array( 'publish' ),
|
||||
'post_type' => 'any',
|
||||
'no_found_rows' => true,
|
||||
'ignore_sticky_posts' => true,
|
||||
'update_post_term_cache' => false,
|
||||
'update_post_meta_cache' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_success_ratio() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract Critical CSS provider class.
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
/**
|
||||
* Class Provider
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
|
||||
*/
|
||||
abstract class Provider {
|
||||
|
||||
/**
|
||||
* The name of the provider.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @access protected
|
||||
* @var string $name The name of the provider
|
||||
*/
|
||||
protected static $name;
|
||||
|
||||
/**
|
||||
* Each provider must return a list of URLs to generate CSS from.
|
||||
*
|
||||
* @param \WP_Post[] $context_posts The posts to generate CSS from.
|
||||
* @return array
|
||||
*/
|
||||
abstract public static function get_critical_source_urls( $context_posts = array() );
|
||||
|
||||
/**
|
||||
* What key should this provider look for during the current request?
|
||||
* Used in the front-end to determine where the CSS
|
||||
* might be stored for the current request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public static function get_current_storage_keys();
|
||||
|
||||
/**
|
||||
* Returns a list of all keys that this provider can provide, regardless
|
||||
* of the current URL.
|
||||
*/
|
||||
abstract public static function get_keys();
|
||||
|
||||
/**
|
||||
* Get a human-displayable string describing the given provider key.
|
||||
*
|
||||
* @param string $provider_key the key to describe.
|
||||
*/
|
||||
abstract public static function describe_key( $provider_key );
|
||||
|
||||
/**
|
||||
* Get the URL of the edit page for the given provider key.
|
||||
*
|
||||
* @param string $provider_key the key to edit.
|
||||
*/
|
||||
abstract public static function get_edit_url( $provider_key );
|
||||
|
||||
/**
|
||||
* Returns true if the key looks like it belongs to this provider.
|
||||
*
|
||||
* @param string $key The key.
|
||||
*/
|
||||
public static function owns_key( $key ) {
|
||||
return strncmp( static::$name, $key, strlen( static::$name ) ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the provider.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_provider_name() {
|
||||
return static::$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ratio of valid urls from the provider source urls
|
||||
* for the Critical CSS generation to be considered successful.
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
abstract public static function get_success_ratio();
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* Critical CSS Provider for singular posts.
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
/**
|
||||
* Class Singular_Post_Provider
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
|
||||
*/
|
||||
class Singular_Post_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* Provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $name = 'singular';
|
||||
|
||||
/**
|
||||
* Max number of posts to query.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const MAX_URLS = 10;
|
||||
|
||||
/**
|
||||
* Minimum number of posts to have Critical CSS generated in order for the whole process to be successful.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const MIN_SUCCESS_URLS = 5;
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_critical_source_urls( $context_posts = array() ) {
|
||||
$links = array();
|
||||
$context_post_types = wp_list_pluck( $context_posts, 'post_type' );
|
||||
|
||||
$post_types = self::get_post_types();
|
||||
if ( ! empty( $context_post_types ) ) {
|
||||
$post_types = array_intersect( $post_types, $context_post_types );
|
||||
}
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$query = self::post_type_query( $post_type );
|
||||
|
||||
foreach ( $query->posts as $post ) {
|
||||
$url = get_permalink( $post );
|
||||
if ( ! empty( $url ) ) {
|
||||
$links[ $post_type ][] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_current_storage_keys() {
|
||||
if ( ! is_singular() ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// For example: "singular_post".
|
||||
return array( self::$name . '_' . get_post_type() );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_keys() {
|
||||
return array_keys( self::get_post_types() );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_edit_url( $_provider_key ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function describe_key( $provider_key ) {
|
||||
$post_type = substr( $provider_key, strlen( static::$name ) + 1 );
|
||||
|
||||
switch ( $post_type ) {
|
||||
case 'post':
|
||||
return __( 'Single post view', 'jetpack-boost' );
|
||||
|
||||
case 'page':
|
||||
return __( 'Single page view', 'jetpack-boost' );
|
||||
|
||||
case 'product':
|
||||
return __( 'Single product view', 'jetpack-boost' );
|
||||
|
||||
default:
|
||||
return __( 'Custom post type', 'jetpack-boost' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post types that need Critical CSS.
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
public static function get_post_types() {
|
||||
$post_types = get_post_types(
|
||||
array(
|
||||
'public' => true,
|
||||
),
|
||||
'objects'
|
||||
);
|
||||
unset( $post_types['attachment'] );
|
||||
|
||||
$post_types = array_filter( $post_types, 'is_post_type_viewable' );
|
||||
|
||||
$provider_post_types = array();
|
||||
// Generate a name => name array for backwards compatibility.
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$provider_post_types[ $post_type->name ] = $post_type->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the post types used for Critical CSS
|
||||
*
|
||||
* @param array $post_types The array of post types to be used
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
return apply_filters(
|
||||
'jetpack_boost_critical_css_post_types_singular',
|
||||
apply_filters_deprecated(
|
||||
'jetpack_boost_critical_css_post_types',
|
||||
array(
|
||||
$provider_post_types,
|
||||
),
|
||||
'3.4.0',
|
||||
'jetpack_boost_critical_css_post_types_singular'
|
||||
),
|
||||
$post_types
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WP_Query to gather sample posts.
|
||||
*
|
||||
* @param string $post_type post type.
|
||||
*
|
||||
* @return \WP_Query
|
||||
*/
|
||||
public static function post_type_query( $post_type ) {
|
||||
/**
|
||||
* Filters the WP_Query parameters used to gather sample posts
|
||||
*
|
||||
* @param array $args The arguments that will be used by WP_Query
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$args = apply_filters(
|
||||
'jetpack_boost_critical_css_post_type_query',
|
||||
array(
|
||||
'orderby' => 'ID',
|
||||
'post_type' => $post_type,
|
||||
'posts_per_page' => static::MAX_URLS,
|
||||
'post_status' => array( 'publish' ),
|
||||
'no_found_rows' => true,
|
||||
'update_post_term_cache' => false,
|
||||
'update_post_meta_cache' => false,
|
||||
)
|
||||
);
|
||||
|
||||
return new \WP_Query( $args );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_success_ratio() {
|
||||
return static::MIN_SUCCESS_URLS / static::MAX_URLS;
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* Provides taxonomy support for critical CSS
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
/**
|
||||
* Class Taxonomy_Provider
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
|
||||
*/
|
||||
class Taxonomy_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* Provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $name = 'taxonomy';
|
||||
|
||||
/**
|
||||
* Max number of posts to query.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const MAX_URLS = 10;
|
||||
|
||||
/**
|
||||
* Minimum number of posts to have Critical CSS generated in order for the whole process to be successful.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
const MIN_SUCCESS_URLS = 5;
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_critical_source_urls( $context_posts = array() ) {
|
||||
$results = array();
|
||||
|
||||
$taxonomies = self::get_available_taxonomies();
|
||||
if ( ! empty( $context_posts ) ) {
|
||||
$context_post_types = array_unique( wp_list_pluck( $context_posts, 'post_type' ) );
|
||||
$context_taxonomies = get_object_taxonomies( $context_post_types, 'names' );
|
||||
$taxonomies = array_intersect( $taxonomies, $context_taxonomies );
|
||||
}
|
||||
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$terms = self::get_terms( $taxonomy );
|
||||
|
||||
if ( ! $terms ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $terms as $term ) {
|
||||
$url = get_term_link( $term, $taxonomy );
|
||||
if ( ! is_wp_error( $url ) && ! empty( $url ) ) {
|
||||
$results[ $taxonomy ][] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_current_storage_keys() {
|
||||
if ( ! is_category() && ! is_tax() ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
if ( ! get_queried_object() ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// For example: "taxonomy_category".
|
||||
return array( self::$name . '_' . get_queried_object()->taxonomy );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_keys() {
|
||||
return array_keys(
|
||||
array_filter(
|
||||
self::get_available_taxonomies(),
|
||||
function ( $taxonomy ) {
|
||||
return ! empty( Taxonomy_Provider::get_terms( $taxonomy ) );
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function describe_key( $provider_key ) {
|
||||
$taxonomy = substr( $provider_key, strlen( static::$name ) + 1 );
|
||||
|
||||
switch ( $taxonomy ) {
|
||||
case 'category':
|
||||
return __( 'Category view', 'jetpack-boost' );
|
||||
|
||||
default:
|
||||
return __( 'View for custom taxonomy', 'jetpack-boost' );
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_edit_url( $_provider_key ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which taxonomies should Critical CSS be generated for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_available_taxonomies() {
|
||||
$taxonomies = get_taxonomies(
|
||||
array(
|
||||
'public' => true,
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
'objects'
|
||||
);
|
||||
|
||||
$taxonomies = array_filter( $taxonomies, 'is_taxonomy_viewable' );
|
||||
|
||||
$provider_taxonomies = array();
|
||||
// Generate a name => name array for backwards compatibility.
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$provider_taxonomies[ $taxonomy->name ] = $taxonomy->name;
|
||||
}
|
||||
|
||||
return $provider_taxonomies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a couple sample terms for a taxonomy.
|
||||
*
|
||||
* @param string $taxonomy Taxonomy.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_terms( $taxonomy ) {
|
||||
/**
|
||||
* Filters the WP_Term_Query args to get a sample of terms for a taxonomy
|
||||
*
|
||||
* @param array $args The arguments that will be used by WP_Term_Query
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$args = apply_filters(
|
||||
'jetpack_boost_critical_css_terms_query',
|
||||
array(
|
||||
'fields' => 'ids',
|
||||
'taxonomy' => $taxonomy,
|
||||
'orderby' => 'term_order',
|
||||
'number' => static::MAX_URLS,
|
||||
'hide_empty' => true,
|
||||
'hierarchical' => false,
|
||||
'update_term_meta_cache' => false,
|
||||
)
|
||||
);
|
||||
|
||||
return ( new \WP_Term_Query( $args ) )->terms;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_success_ratio() {
|
||||
return static::MIN_SUCCESS_URLS / static::MAX_URLS;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* Provides core support for critical CSS
|
||||
*
|
||||
* @package automattic/jetpack-boost
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Providers;
|
||||
|
||||
/**
|
||||
* Class WP_Core_Provider.
|
||||
*
|
||||
* @package Automattic\Jetpack_Boost\Modules\Critical_CSS\Providers
|
||||
*/
|
||||
class WP_Core_Provider extends Provider {
|
||||
|
||||
/**
|
||||
* Provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $name = 'core';
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_critical_source_urls( $context_posts = array() ) {
|
||||
$urls = array();
|
||||
|
||||
$front_page = (int) get_option( 'page_on_front' );
|
||||
$posts_page = (int) get_option( 'page_for_posts' );
|
||||
|
||||
if ( ! empty( $front_page ) && empty( $context_posts ) ) {
|
||||
$permalink = get_permalink( $front_page );
|
||||
if ( ! empty( $permalink ) ) {
|
||||
$urls['front_page'] = array( $permalink );
|
||||
}
|
||||
}
|
||||
|
||||
$context_post_types = wp_list_pluck( $context_posts, 'post_type' );
|
||||
$context_post_ids = wp_list_pluck( $context_posts, 'ID' );
|
||||
|
||||
// The blog page is only in context if the context posts include a 'post' post_type.
|
||||
// Or, if the blog page itself is in context.
|
||||
if ( empty( $context_post_types ) || in_array( 'post', $context_post_types, true ) || in_array( $posts_page, $context_post_ids, true ) ) {
|
||||
if ( ! empty( $posts_page ) ) {
|
||||
$permalink = get_permalink( $posts_page );
|
||||
if ( ! empty( $permalink ) ) {
|
||||
$urls['posts_page'] = array( $permalink );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $front_page && ! isset( $urls['posts_page'] ) ) {
|
||||
$urls['posts_page'] = array( home_url( '/' ) );
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_keys() {
|
||||
$keys = array( 'posts_page' );
|
||||
|
||||
if ( ! empty( get_option( 'page_on_front' ) ) ) {
|
||||
$keys[] = 'front_page';
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_current_storage_keys() {
|
||||
if ( is_home() ) {
|
||||
$key = 'posts_page';
|
||||
} elseif ( is_front_page() ) {
|
||||
$key = 'front_page';
|
||||
}
|
||||
|
||||
if ( ! isset( $key ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// For example: "core_posts_page".
|
||||
return array( self::$name . '_' . $key );
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_edit_url( $provider_key ) {
|
||||
if ( $provider_key === 'core_front_page' ) {
|
||||
$front_page_id = get_option( 'page_on_front' );
|
||||
if ( ! empty( $front_page_id ) ) {
|
||||
return get_edit_post_link( $front_page_id, 'link' );
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function describe_key( $provider_key ) {
|
||||
$page = substr( $provider_key, strlen( static::$name ) + 1 );
|
||||
|
||||
switch ( $page ) {
|
||||
case 'posts_page':
|
||||
return __( 'Posts page', 'jetpack-boost' );
|
||||
|
||||
case 'front_page':
|
||||
return __( 'Front page', 'jetpack-boost' );
|
||||
|
||||
default:
|
||||
return $provider_key;
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public static function get_success_ratio() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
/**
|
||||
* Takes care of cleaning up options created during concatenation.
|
||||
*
|
||||
* @since 4.1.2
|
||||
*/
|
||||
class Cleanup_Stored_Paths {
|
||||
|
||||
/**
|
||||
* The maximum number of options to process in a single batch.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $max_options_to_process = 50;
|
||||
|
||||
/**
|
||||
* The key of the option that stores the ID of the last processed option.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $last_processed_option_key = 'jetpack_boost_cleanup_concat_paths_last_processed_option_id';
|
||||
|
||||
/**
|
||||
* Whether to schedule a followup cleanup.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $should_schedule_followup = false;
|
||||
|
||||
/**
|
||||
* Schedules the start of the cleanup.
|
||||
*/
|
||||
public static function setup_schedule() {
|
||||
if ( false === wp_next_scheduled( 'jetpack_boost_minify_cron_cleanup_concat_paths' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'jetpack_boost_minify_cron_cleanup_concat_paths' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks the callbacks for the cleanup.
|
||||
*/
|
||||
public static function add_cleanup_actions() {
|
||||
add_action( 'jetpack_boost_minify_cron_cleanup_concat_paths', array( __CLASS__, 'run_cleanup' ) );
|
||||
add_action( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( __CLASS__, 'run_cleanup' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cleanup schedules.
|
||||
*/
|
||||
public static function clear_schedules() {
|
||||
wp_unschedule_hook( 'jetpack_boost_minify_cron_cleanup_concat_paths' );
|
||||
wp_unschedule_hook( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the cleanup for the first batch,
|
||||
* and if there are more entries to process,
|
||||
* schedules the cleanup for the next batch.
|
||||
*/
|
||||
public static function run_cleanup() {
|
||||
$cleanup = new Cleanup_Stored_Paths();
|
||||
$can_continue = $cleanup->cleanup_stored_paths_batch();
|
||||
if ( ! $can_continue ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $cleanup->should_schedule_followup ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'batch' arg is necessary, to tell WP that this is a unique event
|
||||
// and allow it to be run within 10 minutes of the last.
|
||||
// See https://developer.wordpress.org/reference/functions/wp_schedule_single_event/#description
|
||||
if ( ! wp_next_scheduled( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( 'batch' ) ) ) {
|
||||
wp_schedule_single_event( time(), 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( 'batch' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up expired stored paths.
|
||||
*
|
||||
* @return bool True if there are more entries to process, false if not.
|
||||
*/
|
||||
public function cleanup_stored_paths_batch() {
|
||||
$stored_paths = $this->get_stored_paths();
|
||||
if ( ! $stored_paths ) {
|
||||
// Cleanup after the cleanup.
|
||||
delete_option( $this->last_processed_option_key );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Used to tell the cleanup to skip the entries that were checked in the previous run.
|
||||
// Avoids processing the same entries over and over again.
|
||||
$last_processed_option_id = false;
|
||||
|
||||
foreach ( $stored_paths as $option ) {
|
||||
$value = maybe_unserialize( $option['option_value'] );
|
||||
if ( ! is_array( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $value['expire'] <= time() ) {
|
||||
$this->delete_static_file_by_hash( str_replace( 'jb_transient_concat_paths_', '', $option['option_name'] ) );
|
||||
delete_option( $option['option_name'] );
|
||||
}
|
||||
|
||||
$last_processed_option_id = $option['option_id'];
|
||||
}
|
||||
|
||||
update_option( $this->last_processed_option_key, $last_processed_option_id, false );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the static files by hash.
|
||||
*
|
||||
* @param string $hash The hash of the file.
|
||||
* @return void
|
||||
*/
|
||||
private function delete_static_file_by_hash( $hash ) {
|
||||
// Since we don't have a way to know if this file is JS or CSS,
|
||||
// we delete both.
|
||||
|
||||
$js_file_path = jetpack_boost_get_minify_file_path( $hash . '.min.js' );
|
||||
if ( file_exists( $js_file_path ) ) {
|
||||
wp_delete_file( $js_file_path );
|
||||
}
|
||||
|
||||
$css_file_path = jetpack_boost_get_minify_file_path( $hash . '.min.css' );
|
||||
if ( file_exists( $css_file_path ) ) {
|
||||
wp_delete_file( $css_file_path );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the stored paths to process, and skips the ones that were checked in the previous run.
|
||||
* Also sets a flag that determines if there should be a followup cleanup or not.
|
||||
*
|
||||
* @return array The stored paths.
|
||||
*/
|
||||
private function get_stored_paths() {
|
||||
$last_processed_option_id = get_option( $this->last_processed_option_key );
|
||||
|
||||
global $wpdb;
|
||||
$query = "SELECT * FROM {$wpdb->options} WHERE option_name LIKE 'jb_transient_concat_paths_%'";
|
||||
if ( $last_processed_option_id ) {
|
||||
$query .= $wpdb->prepare( ' AND option_id > %d', $last_processed_option_id );
|
||||
}
|
||||
|
||||
// Add 1 to use it as a flag to know if there are more entries to process.
|
||||
$max_options_to_process_offset = $this->max_options_to_process + 1;
|
||||
$query .= $wpdb->prepare( ' ORDER BY option_id ASC LIMIT %d', $max_options_to_process_offset );
|
||||
|
||||
$stored_paths = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
// If the number of stored paths is equal to the offset of max options to process,
|
||||
// it means that there are more entries to process, so a followup cleanup is needed.
|
||||
if ( count( $stored_paths ) === $max_options_to_process_offset ) {
|
||||
$this->should_schedule_followup = true;
|
||||
|
||||
// Since 1 was added to the limit, it needs to be removed from the list.
|
||||
array_pop( $stored_paths );
|
||||
}
|
||||
|
||||
return $stored_paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
use WP_Styles;
|
||||
|
||||
// Disable complaints about enqueuing stylesheets, as this class alters the way enqueuing them works.
|
||||
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
|
||||
|
||||
/**
|
||||
* Replacement for, and subclass of WP_Styles - used to control the way that styles are enqueued and output.
|
||||
*/
|
||||
class Concatenate_CSS extends WP_Styles {
|
||||
private $dependency_path_mapping;
|
||||
private $old_styles;
|
||||
|
||||
public $allow_gzip_compression;
|
||||
|
||||
public function __construct( $styles ) {
|
||||
if ( empty( $styles ) || ! ( $styles instanceof WP_Styles ) ) {
|
||||
$this->old_styles = new WP_Styles();
|
||||
} else {
|
||||
$this->old_styles = $styles;
|
||||
}
|
||||
|
||||
// Unset all the object properties except our private copy of the styles object.
|
||||
// We have to unset everything so that the overload methods talk to $this->old_styles->whatever
|
||||
// instead of $this->whatever.
|
||||
foreach ( array_keys( get_object_vars( $this ) ) as $key ) {
|
||||
if ( 'old_styles' === $key ) {
|
||||
continue;
|
||||
}
|
||||
unset( $this->$key );
|
||||
}
|
||||
|
||||
$this->dependency_path_mapping = new Dependency_Path_Mapping(
|
||||
/**
|
||||
* Filter the URL of the site the plugin will be concatenating CSS or JS on
|
||||
*
|
||||
* @param string $url URL of the page with CSS or JS to concatonate.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
apply_filters( 'page_optimize_site_url', $this->base_url )
|
||||
);
|
||||
}
|
||||
|
||||
public function do_items( $handles = false, $group = false ) {
|
||||
$handles = false === $handles ? $this->queue : (array) $handles;
|
||||
$stylesheets = array();
|
||||
/**
|
||||
* Filter the URL of the site the plugin will be concatenating CSS or JS on
|
||||
*
|
||||
* @param string $url URL of the page with CSS or JS to concatonate.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$siteurl = apply_filters( 'page_optimize_site_url', $this->base_url );
|
||||
|
||||
$this->all_deps( $handles );
|
||||
|
||||
$stylesheet_group_index = 0;
|
||||
// Merge CSS into a single file
|
||||
$concat_group = 'concat';
|
||||
// Concat group on top (first array element gets processed earlier)
|
||||
$stylesheets[ $concat_group ] = array();
|
||||
|
||||
foreach ( $this->to_do as $key => $handle ) {
|
||||
$obj = $this->registered[ $handle ];
|
||||
/**
|
||||
* Filter the style source URL
|
||||
*
|
||||
* @param string $url URL of the page with CSS or JS to concatonate.
|
||||
* @param string $handle handle to CSS file.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$obj->src = apply_filters( 'style_loader_src', $obj->src, $obj->handle );
|
||||
|
||||
// Core is kind of broken and returns "true" for src of "colors" handle
|
||||
// http://core.trac.wordpress.org/attachment/ticket/16827/colors-hacked-fixed.diff
|
||||
// http://core.trac.wordpress.org/ticket/20729
|
||||
$css_url = $obj->src;
|
||||
if ( 'colors' === $obj->handle && true === $css_url ) {
|
||||
$css_url = wp_style_loader_src( $css_url, $obj->handle );
|
||||
}
|
||||
|
||||
$css_url = jetpack_boost_enqueued_to_absolute_url( $css_url );
|
||||
$css_url_parsed = wp_parse_url( $css_url );
|
||||
$extra = $obj->extra;
|
||||
|
||||
// Don't concat by default
|
||||
$do_concat = false;
|
||||
|
||||
// Only try to concat static css files
|
||||
if ( str_contains( $css_url_parsed['path'], '.css' ) ) {
|
||||
$do_concat = true;
|
||||
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => Maybe Not Static File %s -->\n", esc_html( $handle ), esc_html( $obj->src ) );
|
||||
}
|
||||
|
||||
// Don't try to concat styles which are loaded conditionally (like IE stuff)
|
||||
if ( isset( $extra['conditional'] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => Has Conditional -->\n", esc_html( $handle ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
|
||||
// Don't concat rtl stuff for now until concat supports it correctly
|
||||
if ( $do_concat && 'rtl' === $this->text_direction && ! empty( $extra['rtl'] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => Is RTL -->\n", esc_html( $handle ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
|
||||
// Don't try to concat externally hosted scripts
|
||||
$is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $css_url );
|
||||
if ( $do_concat && ! $is_internal_uri ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => External URL: %s -->\n", esc_html( $handle ), esc_url( $css_url ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
|
||||
if ( $do_concat ) {
|
||||
// Resolve paths and concat styles that exist in the filesystem
|
||||
$css_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $css_url );
|
||||
if ( false === $css_realpath ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => Invalid Path %s -->\n", esc_html( $handle ), esc_html( $css_realpath ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip concating CSS from exclusion list
|
||||
$exclude_list = jetpack_boost_page_optimize_css_exclude_list();
|
||||
foreach ( $exclude_list as $exclude ) {
|
||||
if ( $do_concat && $handle === $exclude ) {
|
||||
$do_concat = false;
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => Excluded option -->\n", esc_html( $handle ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter that allows plugins to disable concatenation of certain stylesheets.
|
||||
*
|
||||
* @param bool $do_concat if true, then perform concatenation
|
||||
* @param string $handle handle to CSS file
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
if ( $do_concat && ! apply_filters( 'css_do_concat', $do_concat, $handle ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat CSS %s => Filtered `false` -->\n", esc_html( $handle ) );
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Filter that allows plugins to disable concatenation of certain stylesheets.
|
||||
*
|
||||
* @param bool $do_concat if true, then perform concatenation
|
||||
* @param string $handle handle to CSS file
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$do_concat = apply_filters( 'css_do_concat', $do_concat, $handle );
|
||||
|
||||
if ( true === $do_concat ) {
|
||||
$media = $obj->args;
|
||||
if ( empty( $media ) ) {
|
||||
$media = 'all';
|
||||
}
|
||||
|
||||
$stylesheets[ $concat_group ][ $media ][ $handle ] = $css_url_parsed['path'];
|
||||
$this->done[] = $handle;
|
||||
} else {
|
||||
++$stylesheet_group_index;
|
||||
$stylesheets[ $stylesheet_group_index ]['noconcat'][] = $handle;
|
||||
++$stylesheet_group_index;
|
||||
}
|
||||
unset( $this->to_do[ $key ] );
|
||||
}
|
||||
|
||||
foreach ( $stylesheets as $_idx => $stylesheets_group ) {
|
||||
foreach ( $stylesheets_group as $media => $css ) {
|
||||
$href = '';
|
||||
if ( 'noconcat' === $media ) {
|
||||
foreach ( $css as $handle ) {
|
||||
if ( $this->do_item( $handle, $group ) ) {
|
||||
$this->done[] = $handle;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} elseif ( count( $css ) > 0 ) {
|
||||
// Split the CSS into groups of max files, we are also chunking the handles to match the CSS groups and depending on order to be maintained.
|
||||
$css_groups = array_chunk( $css, jetpack_boost_minify_concat_max_files(), true );
|
||||
|
||||
foreach ( $css_groups as $css_group ) {
|
||||
$file_name = jetpack_boost_page_optimize_generate_concat_path( $css_group, $this->dependency_path_mapping );
|
||||
|
||||
if ( get_site_option( 'jetpack_boost_static_minification' ) ) {
|
||||
$href = jetpack_boost_get_minify_url( $file_name . '.min.css' );
|
||||
} else {
|
||||
$href = $siteurl . jetpack_boost_get_static_prefix() . '??' . $file_name;
|
||||
}
|
||||
|
||||
$this->print_style_tag( $href, array_keys( $css_group ), $media );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->done;
|
||||
}
|
||||
|
||||
private function print_style_tag( $href, $handles, $media ) {
|
||||
$css_id = sanitize_title_with_dashes( $media ) . '-css-' . md5( $href );
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
$style_tag = "<link data-handles='" . esc_attr( implode( ',', $handles ) ) . "' rel='stylesheet' id='$css_id' href='$href' type='text/css' media='$media' />";
|
||||
} else {
|
||||
$style_tag = "<link rel='stylesheet' id='$css_id' href='$href' type='text/css' media='$media' />";
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the style loader HTML tag for page optimize.
|
||||
*
|
||||
* @param string $style_tag style loader tag
|
||||
* @param array $handles handles of CSS files
|
||||
* @param string $href link to CSS file
|
||||
* @param string $media media attribute of the link.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$style_tag = apply_filters( 'page_optimize_style_loader_tag', $style_tag, $handles, $href, $media );
|
||||
|
||||
/**
|
||||
* Filter the stylesheet tag. For example: making it deferred when using Critical CSS.
|
||||
*
|
||||
* @param string $style_tag stylesheet tag
|
||||
* @param array $handles handles of CSS files
|
||||
* @param string $href link to CSS file
|
||||
* @param string $media media attribute of the link.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$style_tag = apply_filters( 'style_loader_tag', $style_tag, implode( ',', $handles ), $href, $media );
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $style_tag . "\n";
|
||||
|
||||
array_map( array( $this, 'print_inline_style' ), $handles );
|
||||
}
|
||||
|
||||
public function __isset( $key ) {
|
||||
return isset( $this->old_styles->$key );
|
||||
}
|
||||
|
||||
public function __unset( $key ) {
|
||||
unset( $this->old_styles->$key );
|
||||
}
|
||||
|
||||
public function &__get( $key ) {
|
||||
return $this->old_styles->$key;
|
||||
}
|
||||
|
||||
public function __set( $key, $value ) {
|
||||
$this->old_styles->$key = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
use WP_Scripts;
|
||||
|
||||
// Disable complaints about enqueuing scripts, as this class alters the way enqueuing them works.
|
||||
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
|
||||
|
||||
/**
|
||||
* Replacement for, and subclass of WP_Scripts - used to control the way that scripts are enqueued and output.
|
||||
*/
|
||||
class Concatenate_JS extends WP_Scripts {
|
||||
private $dependency_path_mapping;
|
||||
private $old_scripts;
|
||||
|
||||
public $allow_gzip_compression;
|
||||
|
||||
public function __construct( $scripts ) {
|
||||
if ( empty( $scripts ) || ! ( $scripts instanceof WP_Scripts ) ) {
|
||||
$this->old_scripts = new WP_Scripts();
|
||||
} else {
|
||||
$this->old_scripts = $scripts;
|
||||
}
|
||||
|
||||
// Unset all the object properties except our private copy of the scripts object.
|
||||
// We have to unset everything so that the overload methods talk to $this->old_scripts->whatever
|
||||
// instead of $this->whatever.
|
||||
foreach ( array_keys( get_object_vars( $this ) ) as $key ) {
|
||||
if ( 'old_scripts' === $key ) {
|
||||
continue;
|
||||
}
|
||||
unset( $this->$key );
|
||||
}
|
||||
|
||||
$this->dependency_path_mapping = new Dependency_Path_Mapping(
|
||||
/**
|
||||
* Filter the URL of the site the plugin will be concatenating CSS or JS on
|
||||
*
|
||||
* @param bool $url URL of the page with CSS or JS to concatonate.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
apply_filters( 'page_optimize_site_url', $this->base_url )
|
||||
);
|
||||
}
|
||||
|
||||
protected function has_inline_content( $handle ) {
|
||||
$before_output = $this->get_data( $handle, 'before' );
|
||||
if ( ! empty( $before_output ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$after_output = $this->get_data( $handle, 'after' );
|
||||
if ( ! empty( $after_output ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// JavaScript translations
|
||||
$has_translations = ! empty( $this->registered[ $handle ]->textdomain );
|
||||
if ( $has_translations ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override for WP_Scripts::do_item() - this is the method that actually outputs the scripts.
|
||||
*/
|
||||
public function do_items( $handles = false, $group = false ) {
|
||||
$handles = false === $handles ? $this->queue : (array) $handles;
|
||||
$javascripts = array();
|
||||
/**
|
||||
* Filter the URL of the site the plugin will be concatenating CSS or JS on
|
||||
*
|
||||
* @param bool $url URL of the page with CSS or JS to concatonate.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$siteurl = apply_filters( 'page_optimize_site_url', $this->base_url );
|
||||
$this->all_deps( $handles );
|
||||
$level = 0;
|
||||
|
||||
$using_strict = false;
|
||||
$strict_count = 0;
|
||||
foreach ( $this->to_do as $key => $handle ) {
|
||||
$script_is_strict = false;
|
||||
if ( in_array( $handle, $this->done, true ) || ! isset( $this->registered[ $handle ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 0 === $group && $this->groups[ $handle ] > 0 ) {
|
||||
$this->in_footer[] = $handle;
|
||||
unset( $this->to_do[ $key ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $this->registered[ $handle ]->src ) { // Defines a group.
|
||||
if ( $this->has_inline_content( $handle ) ) {
|
||||
++$level;
|
||||
$javascripts[ $level ]['type'] = 'do_item';
|
||||
$javascripts[ $level ]['handle'] = $handle;
|
||||
++$level;
|
||||
unset( $this->to_do[ $key ] );
|
||||
} else {
|
||||
// if there are localized items, echo them
|
||||
$this->print_extra_script( $handle );
|
||||
$this->done[] = $handle;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( false === $group && in_array( $handle, $this->in_footer, true ) ) {
|
||||
$this->in_footer = array_diff( $this->in_footer, (array) $handle );
|
||||
}
|
||||
|
||||
$obj = $this->registered[ $handle ];
|
||||
$js_url = jetpack_boost_enqueued_to_absolute_url( $obj->src );
|
||||
$js_url_parsed = wp_parse_url( $js_url );
|
||||
|
||||
// Don't concat by default
|
||||
$do_concat = false;
|
||||
|
||||
// Only try to concat static js files
|
||||
if ( str_contains( $js_url_parsed['path'], '.js' ) ) {
|
||||
// Previously, the value of this variable was determined by a function.
|
||||
// Now, since concatenation is always enabled when the module is active,
|
||||
// the value will always be true for static files.
|
||||
$do_concat = true;
|
||||
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Maybe Not Static File %s -->\n", esc_html( $handle ), esc_html( $obj->src ) );
|
||||
}
|
||||
|
||||
// Don't try to concat externally hosted scripts
|
||||
$is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $js_url );
|
||||
if ( $do_concat && ! $is_internal_uri ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => External URL: %s -->\n", esc_html( $handle ), esc_url( $js_url ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
|
||||
$js_realpath = false;
|
||||
if ( $do_concat ) {
|
||||
// Resolve paths and concat scripts that exist in the filesystem
|
||||
$js_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $js_url );
|
||||
if ( false === $js_realpath ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Invalid Path %s -->\n", esc_html( $handle ), esc_html( $js_realpath ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $do_concat && $this->has_inline_content( $handle ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Has Inline Content -->\n", esc_html( $handle ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
}
|
||||
|
||||
// Skip core scripts that use Strict Mode
|
||||
if ( $do_concat && ( 'react' === $handle || 'react-dom' === $handle ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Has Strict Mode (Core) -->\n", esc_html( $handle ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
$script_is_strict = true;
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
} elseif ( $do_concat && preg_match_all( '/^[\',"]use strict[\',"];/Uims', file_get_contents( $js_realpath ), $matches ) ) {
|
||||
// Skip third-party scripts that use Strict Mode
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Has Strict Mode (Third-Party) -->\n", esc_html( $handle ) );
|
||||
}
|
||||
$do_concat = false;
|
||||
$script_is_strict = true;
|
||||
} else {
|
||||
$script_is_strict = false;
|
||||
}
|
||||
|
||||
// Skip concating scripts from exclusion list
|
||||
$exclude_list = jetpack_boost_page_optimize_js_exclude_list();
|
||||
foreach ( $exclude_list as $exclude ) {
|
||||
if ( $do_concat && $handle === $exclude ) {
|
||||
$do_concat = false;
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Excluded option -->\n", esc_html( $handle ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** This filter is documented in wp-includes/class-wp-scripts.php */
|
||||
$js_url = esc_url_raw( apply_filters( 'script_loader_src', $js_url, $handle ) );
|
||||
if ( ! $js_url ) {
|
||||
$do_concat = false;
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => No URL -->\n", esc_html( $handle ) );
|
||||
}
|
||||
} elseif ( 'module' === $this->get_script_type( $handle, $js_url ) ) {
|
||||
$do_concat = false;
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Module Script -->\n", esc_html( $handle ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter that allows plugins to disable concatenation of certain scripts.
|
||||
*
|
||||
* @param bool $do_concat if true, then perform concatenation
|
||||
* @param string $handle handle to JS file
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
if ( $do_concat && ! apply_filters( 'js_do_concat', $do_concat, $handle ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
printf( "\n<!-- No Concat JS %s => Filtered `false` -->\n", esc_html( $handle ) );
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Filter that allows plugins to disable concatenation of certain scripts.
|
||||
*
|
||||
* @param bool $do_concat if true, then perform concatenation
|
||||
* @param string $handle handle to JS file
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$do_concat = apply_filters( 'js_do_concat', $do_concat, $handle );
|
||||
|
||||
if ( true === $do_concat ) {
|
||||
|
||||
// If the number of files in the group is greater than the maximum, start a new group.
|
||||
if ( isset( $javascripts[ $level ] ) && count( $javascripts[ $level ]['handles'] ) >= jetpack_boost_minify_concat_max_files() ) {
|
||||
++$level;
|
||||
}
|
||||
|
||||
if ( ! isset( $javascripts[ $level ] ) ) {
|
||||
$javascripts[ $level ]['type'] = 'concat';
|
||||
}
|
||||
|
||||
$javascripts[ $level ]['paths'][] = $js_url_parsed['path'];
|
||||
$javascripts[ $level ]['handles'][] = $handle;
|
||||
|
||||
} else {
|
||||
++$level;
|
||||
$javascripts[ $level ]['type'] = 'do_item';
|
||||
$javascripts[ $level ]['handle'] = $handle;
|
||||
++$level;
|
||||
}
|
||||
unset( $this->to_do[ $key ] );
|
||||
|
||||
if ( $using_strict !== $script_is_strict ) {
|
||||
if ( $script_is_strict ) {
|
||||
$using_strict = true;
|
||||
$strict_count = 0;
|
||||
} else {
|
||||
$using_strict = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $script_is_strict ) {
|
||||
++$strict_count;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $javascripts ) ) {
|
||||
return $this->done;
|
||||
}
|
||||
|
||||
foreach ( $javascripts as $js_array ) {
|
||||
if ( 'do_item' === $js_array['type'] ) {
|
||||
if ( $this->do_item( $js_array['handle'], $group ) ) {
|
||||
$this->done[] = $js_array['handle'];
|
||||
}
|
||||
} elseif ( 'concat' === $js_array['type'] ) {
|
||||
array_map( array( $this, 'print_extra_script' ), $js_array['handles'] );
|
||||
|
||||
if ( isset( $js_array['paths'] ) && count( $js_array['paths'] ) > 1 ) {
|
||||
$file_name = jetpack_boost_page_optimize_generate_concat_path( $js_array['paths'], $this->dependency_path_mapping );
|
||||
|
||||
if ( get_site_option( 'jetpack_boost_static_minification' ) ) {
|
||||
$href = jetpack_boost_get_minify_url( $file_name . '.min.js' );
|
||||
} else {
|
||||
$href = $siteurl . jetpack_boost_get_static_prefix() . '??' . $file_name;
|
||||
}
|
||||
} elseif ( isset( $js_array['paths'] ) && is_array( $js_array['paths'] ) ) {
|
||||
$href = jetpack_boost_page_optimize_cache_bust_mtime( $js_array['paths'][0], $siteurl );
|
||||
}
|
||||
|
||||
$this->done = array_merge( $this->done, $js_array['handles'] );
|
||||
|
||||
// Print before/after scripts from wp_inline_scripts() and concatenated script tag
|
||||
if ( isset( $js_array['extras']['before'] ) ) {
|
||||
foreach ( $js_array['extras']['before'] as $inline_before ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $inline_before;
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $href ) ) {
|
||||
$handles = implode( ',', $js_array['handles'] );
|
||||
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
$tag = "<script data-handles='" . esc_attr( $handles ) . "' type='text/javascript' src='" . esc_url( $href ) . "'></script>\n";
|
||||
} else {
|
||||
$tag = "<script type='text/javascript' src='" . esc_url( $href ) . "'></script>\n";
|
||||
}
|
||||
|
||||
if ( is_array( $js_array['handles'] ) && count( $js_array['handles'] ) === 1 ) {
|
||||
/**
|
||||
* Filters the HTML script tag of an enqueued script
|
||||
* A copy of the core filter of the same name. https://developer.wordpress.org/reference/hooks/script_loader_tag/
|
||||
* Because we have a single script, let's apply the `script_loader_tag` filter as core does in `do_item()`.
|
||||
* That way, we interfere less with plugin and theme script filtering. For example, without this filter,
|
||||
* there is a case where we block the TwentyTwenty theme from adding async/defer attributes.
|
||||
* https://github.com/Automattic/page-optimize/pull/44
|
||||
*
|
||||
* @param string $tag Script tag for the enqueued script.
|
||||
* @param string $handle The script's registered handle.
|
||||
* @param string $href URL of the script.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
$tag = apply_filters( 'script_loader_tag', $tag, $js_array['handles'][0], $href );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $tag;
|
||||
}
|
||||
|
||||
if ( isset( $js_array['extras']['after'] ) ) {
|
||||
foreach ( $js_array['extras']['after'] as $inline_after ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $inline_after;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do_action( 'js_concat_did_items', $javascripts );
|
||||
|
||||
return $this->done;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the script.
|
||||
* module, text/javascript, etc. False if the script tag is invalid,
|
||||
* or the type is not set.
|
||||
*
|
||||
* @since 4.1.2
|
||||
*
|
||||
* @param string $handle The script's registered handle.
|
||||
* @param string $src The script's source URL.
|
||||
*
|
||||
* @return string|false The type of the script. False if the script tag is invalid,
|
||||
* or the type is not set.
|
||||
*/
|
||||
private function get_script_type( $handle, $src ) {
|
||||
$script_tag_attr = array(
|
||||
'src' => $src,
|
||||
'id' => "$handle-js",
|
||||
);
|
||||
|
||||
// Get the script tag and allow plugins to filter it.
|
||||
$script_tag = wp_get_script_tag( $script_tag_attr );
|
||||
|
||||
// This is a workaround to get the type of the script without outputting it.
|
||||
$script_tag = apply_filters( 'script_loader_tag', $script_tag, $handle, $src );
|
||||
$processor = new \WP_HTML_Tag_Processor( $script_tag );
|
||||
|
||||
// If for some reason the script tag isn't valid, bail.
|
||||
if ( ! $processor->next_tag() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $processor->get_attribute( 'type' );
|
||||
}
|
||||
|
||||
public function __isset( $key ) {
|
||||
return isset( $this->old_scripts->$key );
|
||||
}
|
||||
|
||||
public function __unset( $key ) {
|
||||
unset( $this->old_scripts->$key );
|
||||
}
|
||||
|
||||
public function &__get( $key ) {
|
||||
return $this->old_scripts->$key;
|
||||
}
|
||||
|
||||
public function __set( $key, $value ) {
|
||||
$this->old_scripts->$key = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
/**
|
||||
* Configuration management for the minification system.
|
||||
*/
|
||||
class Config {
|
||||
/**
|
||||
* Get the directory path for storing static cache files.
|
||||
*/
|
||||
public static function get_static_cache_dir_path() {
|
||||
return WP_CONTENT_DIR . '/boost-cache/static';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory path for storing cache files.
|
||||
*/
|
||||
public static function get_legacy_cache_dir_path() {
|
||||
if ( defined( 'PAGE_OPTIMIZE_CACHE_DIR' ) ) {
|
||||
if ( empty( \PAGE_OPTIMIZE_CACHE_DIR ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \PAGE_OPTIMIZE_CACHE_DIR;
|
||||
}
|
||||
|
||||
return WP_CONTENT_DIR . '/cache/page_optimize';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WordPress ABSPATH, with support for custom configuration.
|
||||
*/
|
||||
public static function get_abspath() {
|
||||
return defined( 'PAGE_OPTIMIZE_ABSPATH' ) ? \PAGE_OPTIMIZE_ABSPATH : \ABSPATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static cache can be used.
|
||||
*/
|
||||
public static function can_use_static_cache() {
|
||||
$cache_dir = static::get_static_cache_dir_path();
|
||||
|
||||
if ( ! static::ensure_dir_exists( $cache_dir ) ) {
|
||||
static::log_error(
|
||||
sprintf(
|
||||
/* translators: a filesystem path to a directory */
|
||||
__( "Disabling concatenate static cache. Unable to create cache directory '%s'.", 'jetpack-boost' ),
|
||||
$cache_dir
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! static::is_dir_writable( $cache_dir ) ) {
|
||||
static::log_error(
|
||||
sprintf(
|
||||
/* translators: a filesystem path to a directory */
|
||||
__( "Disabling concatenate static cache. Unable to write to cache directory '%s'.", 'jetpack-boost' ),
|
||||
$cache_dir
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache can be used.
|
||||
*/
|
||||
public static function can_use_cache() {
|
||||
$cache_dir = static::get_legacy_cache_dir_path();
|
||||
|
||||
if ( empty( $cache_dir ) ) {
|
||||
static::log_error( __( 'Disabling page-optimize cache. Cache directory not defined.', 'jetpack-boost' ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! static::ensure_dir_exists( $cache_dir ) ) {
|
||||
static::log_error(
|
||||
sprintf(
|
||||
/* translators: a filesystem path to a directory */
|
||||
__( "Disabling page-optimize cache. Unable to create cache directory '%s'.", 'jetpack-boost' ),
|
||||
$cache_dir
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! static::is_dir_writable( $cache_dir ) ) {
|
||||
static::log_error(
|
||||
sprintf(
|
||||
/* translators: a filesystem path to a directory */
|
||||
__( "Disabling page-optimize cache. Unable to write to cache directory '%s'.", 'jetpack-boost' ),
|
||||
$cache_dir
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a directory exists.
|
||||
*
|
||||
* @param string $dir The directory to check.
|
||||
* @return bool True if the directory exists, false otherwise.
|
||||
*/
|
||||
private static function ensure_dir_exists( $dir ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
|
||||
if ( ! is_dir( $dir ) && ! mkdir( $dir, 0775, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a directory is writable.
|
||||
*
|
||||
* @param string $dir The directory to check.
|
||||
* @return bool True if the directory is writable, false otherwise.
|
||||
*/
|
||||
private static function is_dir_writable( $dir ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
|
||||
if ( ! is_dir( $dir ) || ! is_writable( $dir ) || ! is_executable( $dir ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message if WP_DEBUG is enabled.
|
||||
*
|
||||
* @param string $message The error message to log.
|
||||
*/
|
||||
private static function log_error( $message ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( $message );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
/**
|
||||
* This is a class to map script and style URLs to local filesystem paths.
|
||||
* This is necessary when we are deciding what we can concatenate and when
|
||||
* actually building the concatenation.
|
||||
*/
|
||||
class Dependency_Path_Mapping {
|
||||
/**
|
||||
* Save entire site URL so we can check whether other URLs are based on it (internal URLs)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $site_url;
|
||||
|
||||
/**
|
||||
* Save URI path and dir for mapping URIs to filesystem paths
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $site_uri_path = null;
|
||||
public $site_dir = null;
|
||||
public $content_uri_path = null;
|
||||
public $content_dir = null;
|
||||
public $plugin_uri_path = null;
|
||||
public $plugin_dir = null;
|
||||
|
||||
public function __construct(
|
||||
// Expose URLs and DIRs for unit test
|
||||
$site_url = null, // default site URL is determined dynamically
|
||||
$site_dir = null,
|
||||
$content_url = WP_CONTENT_URL,
|
||||
$content_dir = WP_CONTENT_DIR,
|
||||
$plugin_url = WP_PLUGIN_URL,
|
||||
$plugin_dir = WP_PLUGIN_DIR
|
||||
) {
|
||||
if ( null === $site_dir ) {
|
||||
$site_dir = Config::get_abspath();
|
||||
}
|
||||
|
||||
if ( null === $site_url ) {
|
||||
$site_url = is_multisite() ? get_site_url( get_current_blog_id() ) : get_site_url();
|
||||
}
|
||||
$site_url = trailingslashit( $site_url );
|
||||
$this->site_url = $site_url;
|
||||
$this->site_uri_path = wp_parse_url( $site_url, PHP_URL_PATH );
|
||||
$this->site_dir = trailingslashit( $site_dir );
|
||||
|
||||
// Only resolve content URLs if they are under the site URL
|
||||
if ( $this->is_internal_uri( $content_url ) ) {
|
||||
$this->content_uri_path = wp_parse_url( trailingslashit( $content_url ), PHP_URL_PATH );
|
||||
$this->content_dir = trailingslashit( $content_dir );
|
||||
}
|
||||
|
||||
// Only resolve plugin URLs if they are under the site URL
|
||||
if ( $this->is_internal_uri( $plugin_url ) ) {
|
||||
$this->plugin_uri_path = wp_parse_url( trailingslashit( $plugin_url ), PHP_URL_PATH );
|
||||
$this->plugin_dir = trailingslashit( $plugin_dir );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the full URL of a script/style dependency, return its local filesystem path.
|
||||
*/
|
||||
public function dependency_src_to_fs_path( $src ) {
|
||||
if ( ! $this->is_internal_uri( $src ) ) {
|
||||
// If a URI is not internal, we can have no confidence
|
||||
// we are resolving to the correct file.
|
||||
return false;
|
||||
}
|
||||
|
||||
$src_parts = wp_parse_url( $src );
|
||||
if ( false === $src_parts ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $src_parts['path'] ) ) {
|
||||
// We can't find anything to resolve
|
||||
return false;
|
||||
}
|
||||
$path = $src_parts['path'];
|
||||
|
||||
if ( empty( $src_parts['host'] ) ) {
|
||||
// With no host, this is a path relative to the WordPress root
|
||||
$fs_path = "{$this->site_dir}{$path}";
|
||||
|
||||
return file_exists( $fs_path ) ? $fs_path : false;
|
||||
}
|
||||
|
||||
return $this->uri_path_to_fs_path( $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URI path of a script/style resource, return its local filesystem path.
|
||||
*/
|
||||
public function uri_path_to_fs_path( $uri_path ) {
|
||||
if ( 1 === preg_match( '#(?:^|/)\.\.?(?:/|$)#', $uri_path ) ) {
|
||||
// Reject relative paths
|
||||
return false;
|
||||
}
|
||||
|
||||
// The plugin URI path may be contained within the content URI path, so we check it before the content URI.
|
||||
// And both the plugin and content URI paths must be contained within the site URI path,
|
||||
// so we check them before checking the site URI.
|
||||
if ( isset( $this->plugin_uri_path ) && static::is_descendant_uri( $this->plugin_uri_path, $uri_path ) ) {
|
||||
$file_path = $this->plugin_dir . substr( $uri_path, strlen( $this->plugin_uri_path ) );
|
||||
} elseif ( isset( $this->content_uri_path ) && static::is_descendant_uri( $this->content_uri_path, $uri_path ) ) {
|
||||
$file_path = $this->content_dir . substr( $uri_path, strlen( $this->content_uri_path ) );
|
||||
} elseif ( static::is_descendant_uri( $this->site_uri_path, $uri_path ) ) {
|
||||
$file_path = $this->site_dir . substr( $uri_path, strlen( $this->site_uri_path ) );
|
||||
}
|
||||
|
||||
if ( isset( $file_path ) && file_exists( $file_path ) ) {
|
||||
return $file_path;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a URI is internal, contained by this site.
|
||||
*
|
||||
* This method helps ensure we only resolve to local FS paths.
|
||||
*/
|
||||
public function is_internal_uri( $uri ) {
|
||||
if ( jetpack_boost_page_optimize_starts_with( '/', $uri ) && ! jetpack_boost_page_optimize_starts_with( '//', $uri ) ) {
|
||||
// Absolute paths are internal because they are based on the site dir (typically ABSPATH),
|
||||
// and this looks like an absolute path.
|
||||
return true;
|
||||
}
|
||||
|
||||
// To be internal, a URL must have the same scheme, host, and port as the site URL
|
||||
// and start with the same path as the site URL.
|
||||
return static::is_descendant_uri( $this->site_url, $uri );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a path is descended from the given directory path.
|
||||
*
|
||||
* Does not handle relative paths.
|
||||
*/
|
||||
public static function is_descendant_uri( $dir_path, $candidate ) {
|
||||
// Ensure a trailing slash to avoid false matches like
|
||||
// "/wp-content/resource" being judged a descendant of "/wp".
|
||||
$dir_path = trailingslashit( $dir_path );
|
||||
|
||||
return jetpack_boost_page_optimize_starts_with( $dir_path, $candidate );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
use Automattic\Jetpack\Boost_Core\Lib\Cacheable;
|
||||
|
||||
/**
|
||||
* Store the hash of the path string so that the concatenated file can be constructed
|
||||
* on-demand.
|
||||
*
|
||||
* We are setting the transient for a long time because, if the content of the hash is not accessible, the concatenated file
|
||||
* will not be able to construct itself. Because of page caching, there may be situations where this function will not
|
||||
* get called for a long time, and we still want to serve the concatenated file in those cases. Having the value in
|
||||
* a Boost Transient ensures that the value is cleaned up when Boost is uninstalled.
|
||||
*/
|
||||
final class File_Paths extends Cacheable {
|
||||
private $paths;
|
||||
private $mtime;
|
||||
private $cache_buster;
|
||||
|
||||
protected const DEFAULT_EXPIRY = MONTH_IN_SECONDS;
|
||||
|
||||
public function set( $paths, $mtime, $cache_buster ) {
|
||||
$this->paths = $paths;
|
||||
$this->mtime = $mtime;
|
||||
$this->cache_buster = $cache_buster;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return array(
|
||||
'paths' => $this->paths,
|
||||
'mtime' => $this->mtime,
|
||||
'cache_buster' => $this->cache_buster,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of file paths that belong to the same hash.
|
||||
*
|
||||
* @return array Array of file paths relative to the webroot.
|
||||
*/
|
||||
public function get_paths() {
|
||||
return $this->paths;
|
||||
}
|
||||
|
||||
public static function jsonUnserialize( $data ) {
|
||||
$instance = new self();
|
||||
$instance->set( $data['paths'], $data['mtime'], $data['cache_buster'] );
|
||||
return $instance;
|
||||
}
|
||||
|
||||
protected function generate_cache_id() {
|
||||
$hash = md5( implode( ',', $this->paths ) . '_' . $this->mtime . '_' . $this->cache_buster );
|
||||
|
||||
// Keep cache id small as option keys have a character limit.
|
||||
return substr( $hash, 0, 10 );
|
||||
}
|
||||
|
||||
protected static function cache_prefix() {
|
||||
return 'concat_paths_';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Lib\Minify;
|
||||
|
||||
class Utils {
|
||||
|
||||
/**
|
||||
* Indicates whether to use the WordPress functions.
|
||||
*
|
||||
* @var bool $use_wp Whether to use WordPress functions.
|
||||
*/
|
||||
private $use_wp;
|
||||
|
||||
/**
|
||||
* Utils constructor.
|
||||
*
|
||||
* @param bool $use_wp Whether to use WordPress functions. Default is true.
|
||||
*/
|
||||
public function __construct( $use_wp = true ) {
|
||||
$this->use_wp = $use_wp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a value to JSON.
|
||||
*
|
||||
* @param mixed $value The value to encode.
|
||||
* @param int $flags Options to be passed to json_encode(). Default 0.
|
||||
* @param int $depth Maximum depth to walk through $value. Must be greater than 0.
|
||||
*
|
||||
* @return string|false The JSON-encoded string, or false on failure.
|
||||
*/
|
||||
public function json_encode( $value, $flags = 0, $depth = 512 ) {
|
||||
if ( $this->use_wp ) {
|
||||
return wp_json_encode( $value, $flags, $depth );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
return json_encode( $value, $flags, $depth );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes slashes from a string or an array of strings.
|
||||
*
|
||||
* @param mixed $value The string or array of strings to remove slashes from.
|
||||
*
|
||||
* @return mixed The string or array of strings with slashes removed.
|
||||
*/
|
||||
public function unslash( $value ) {
|
||||
if ( $this->use_wp ) {
|
||||
return wp_unslash( $value );
|
||||
}
|
||||
|
||||
return is_string( $value ) ? stripslashes( $value ) : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URL and returns its components.
|
||||
*
|
||||
* @param string $url The URL to parse.
|
||||
* @param int $component Optional. The specific component to retrieve.
|
||||
* Use one of the PHP_URL_* constants. Default is -1 (all components).
|
||||
*
|
||||
* @return mixed|array|string|null The parsed URL component(s), or the entire URL string if $component is -1.
|
||||
*/
|
||||
public function parse_url( $url, $component = -1 ) {
|
||||
if ( $this->use_wp ) {
|
||||
return wp_parse_url( $url, $component );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
|
||||
return parse_url( $url, $component );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Cleanup_Stored_Paths;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Config;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Dependency_Path_Mapping;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\File_Paths;
|
||||
use Automattic\Jetpack_Boost\Modules\Module;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Minify\Minify_CSS;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Minify\Minify_JS;
|
||||
|
||||
/**
|
||||
* Get an extra cache key for requests. We can manually bump this when we want
|
||||
* to ensure a new version of Jetpack Boost never reuses old cached URLs.
|
||||
*/
|
||||
function jetpack_boost_minify_cache_buster() {
|
||||
// Bumped to 2 so the JS truncation fix actually reaches already-affected sites:
|
||||
// the cached concat bundle is served before any rebuild, so a site that cached a
|
||||
// corrupted bundle would keep shipping it (until its source mtime changed) even
|
||||
// after upgrade. Changing the buster invalidates those cache keys and forces a
|
||||
// rebuild through the new structural-validation/fallback path.
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a file should be treated as already minified (so the serving path must
|
||||
* not re-minify it).
|
||||
*
|
||||
* This is the load-bearing skip for assets shipped as `.min.js` / `.min.css`
|
||||
* (e.g. Boost's own image-guide bundle, which webpack/Terser has already
|
||||
* minified): re-running the ES5-era PHP minifier over modern, already-minified
|
||||
* JS can silently corrupt it. The mime type already routes JS and CSS down
|
||||
* separate branches, so matching both extensions here is safe for either.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*
|
||||
* @param string $fullpath Absolute path to the file being served.
|
||||
*
|
||||
* @return bool True if the filename indicates already-minified content.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_is_already_minified( $fullpath ) {
|
||||
return (bool) preg_match( '/\.min\.(css|js)$/', (string) $fullpath );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of maximum files that can be concatenated in a group.
|
||||
*/
|
||||
function jetpack_boost_minify_concat_max_files() {
|
||||
|
||||
/**
|
||||
* Filter the number of maximum files that can be concatenated in a group.
|
||||
*
|
||||
* @param int $max_files The maximum number of files that can be concatenated.
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_minify_concat_max_files', 150 );
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that the cache cleanup cron job is only run once per day, espicially for multisite.
|
||||
*/
|
||||
function jetpack_boost_should_run_daily_network_cron_job( $hook ) {
|
||||
// If we see it's been executed within 24 hours, don't run
|
||||
if ( get_site_option( 'jetpack_boost_' . $hook . '_last_run', 0 ) > time() - DAY_IN_SECONDS ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_site_option( 'jetpack_boost_' . $hook . '_last_run', time() );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that the cache cleanup cron job is only run once per day, espicially for multisite.
|
||||
*/
|
||||
function jetpack_boost_minify_cron_cache_cleanup() {
|
||||
if ( ! jetpack_boost_should_run_daily_network_cron_job( 'minify_cron_cache_cleanup' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
jetpack_boost_legacy_minify_cache_cleanup();
|
||||
jetpack_boost_minify_cache_cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup minify cache files stored using the legacy method.
|
||||
*
|
||||
* @param int $file_age The age of files to purge, in seconds.
|
||||
*/
|
||||
function jetpack_boost_legacy_minify_cache_cleanup( $file_age = DAY_IN_SECONDS ) {
|
||||
// If file_age is not an int, set it to the default.
|
||||
// $file_age can be an empty string if calling this from an action: https://core.trac.wordpress.org/ticket/14881
|
||||
$file_age = is_int( $file_age ) ? $file_age : DAY_IN_SECONDS;
|
||||
|
||||
$cache_folder = Config::get_legacy_cache_dir_path();
|
||||
|
||||
if ( ! is_dir( $cache_folder ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cache_files = glob( $cache_folder . '/page-optimize-cache-*' );
|
||||
|
||||
jetpack_boost_delete_expired_files( $cache_files, $file_age );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup obsolete files in static cache folder.
|
||||
*
|
||||
* @param int $file_age The age of files to purge, in seconds.
|
||||
*/
|
||||
function jetpack_boost_minify_cache_cleanup( $file_age = DAY_IN_SECONDS ) {
|
||||
// Explicitly cast to int as do_action() can pass a non-int value. https://core.trac.wordpress.org/ticket/14881
|
||||
$file_age = is_int( $file_age ) ? $file_age : DAY_IN_SECONDS;
|
||||
|
||||
/*
|
||||
* Cleanup obsolete files in static cache folder.
|
||||
* If $file_age is 0, we can skip this as we will delete all files anyway.
|
||||
*/
|
||||
if ( $file_age !== 0 ) {
|
||||
// Cleanup obsolete files in static cache folder
|
||||
jetpack_boost_minify_remove_stale_static_files();
|
||||
}
|
||||
|
||||
$cache_files = glob( Config::get_static_cache_dir_path() . '/*.min.*' );
|
||||
|
||||
jetpack_boost_delete_expired_files( $cache_files, $file_age );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete expired files based on access time or file age.
|
||||
*
|
||||
* Only delete files which are either 2x $file_age or have not been accessed in $file_age seconds.
|
||||
*
|
||||
* @param array $files The files to delete.
|
||||
* @param int $file_age The age of files to purge, in seconds.
|
||||
*/
|
||||
function jetpack_boost_delete_expired_files( $files, $file_age ) {
|
||||
foreach ( $files as $file ) {
|
||||
if ( ! is_file( $file ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $file_age === 0 ) {
|
||||
wp_delete_file( $file );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete files that haven't been accessed in $file_age seconds,
|
||||
// or files that are older than 2 * $file_age regardless of access time
|
||||
if ( ( time() - $file_age ) > fileatime( $file ) ) {
|
||||
wp_delete_file( $file );
|
||||
} elseif ( ( time() - ( 2 * $file_age ) ) > filemtime( $file ) ) {
|
||||
wp_delete_file( $file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear scheduled cron events for minification.
|
||||
*
|
||||
* Removes the cache cleanup cron job and the 404 tester cron job.
|
||||
*/
|
||||
function jetpack_boost_minify_clear_scheduled_events() {
|
||||
wp_unschedule_hook( 'jetpack_boost_minify_cron_cache_cleanup' );
|
||||
wp_unschedule_hook( 'jetpack_boost_404_tester_cron' );
|
||||
Cleanup_Stored_Paths::clear_schedules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation hook - unschedule cronjobs and purge cache.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_deactivate() {
|
||||
// Cleanup all cache files even if they are 0 seconds old
|
||||
jetpack_boost_legacy_minify_cache_cleanup( 0 );
|
||||
jetpack_boost_minify_cache_cleanup( 0 );
|
||||
|
||||
jetpack_boost_minify_clear_scheduled_events();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert enqueued home-relative URLs to absolute ones.
|
||||
*
|
||||
* Enqueued script URLs which start with / are relative to WordPress' home URL.
|
||||
* i.e.: "/wp-includes/x.js" should be "WP_HOME/wp-includes/x.js".
|
||||
*
|
||||
* Note: this method uses home_url, so should only be used plugin-side when
|
||||
* generating concatenated URLs.
|
||||
*/
|
||||
function jetpack_boost_enqueued_to_absolute_url( $url ) {
|
||||
if ( str_starts_with( $url, '/' ) ) {
|
||||
return home_url( $url );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of JS handles to exclude from minification.
|
||||
*
|
||||
* Administrators can append extra handles for the current request only via the
|
||||
* `jb-minify-js-excludes` GET parameter (comma-separated handles). See
|
||||
* jetpack_boost_page_optimize_merge_debug_excludes() for details.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_js_exclude_list() {
|
||||
return jetpack_boost_page_optimize_merge_debug_excludes(
|
||||
jetpack_boost_ds_get( 'minify_js_excludes' ),
|
||||
'jb-minify-js-excludes'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of CSS handles to exclude from minification.
|
||||
*
|
||||
* Administrators can append extra handles for the current request only via the
|
||||
* `jb-minify-css-excludes` GET parameter (comma-separated handles). See
|
||||
* jetpack_boost_page_optimize_merge_debug_excludes() for details.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_css_exclude_list() {
|
||||
return jetpack_boost_page_optimize_merge_debug_excludes(
|
||||
jetpack_boost_ds_get( 'minify_css_excludes' ),
|
||||
'jb-minify-css-excludes'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging aid: merge per-request exclude handles from a GET parameter into a
|
||||
* saved concatenate/minify exclude list.
|
||||
*
|
||||
* Lets administrators test "would excluding handle X fix this page?" by loading
|
||||
* the page with e.g. `?jb-minify-js-excludes=jquery-core,my-plugin-script`
|
||||
* without editing the saved settings, and without needing a staging site.
|
||||
*
|
||||
* Safety properties:
|
||||
* - Only honored for logged-in users with the `manage_options` capability.
|
||||
* current_user_can() is available during normal page rendering (the exclude
|
||||
* lists are read while scripts/styles are printed at wp_head/wp_footer); the
|
||||
* function_exists() guard defensively no-ops in any pre-WordPress context
|
||||
* (e.g. the page-cache layer) where it is not yet loaded.
|
||||
* - Each handle is validated against a strict allowlist (alphanumerics, dash,
|
||||
* underscore and dot); anything else is ignored entirely. Case is preserved
|
||||
* because script/style handles are matched case-sensitively downstream.
|
||||
* - The number of handles processed is capped to bound the work this debug aid
|
||||
* can be made to do per request.
|
||||
* - The merged list is never persisted — nothing is written to the data sync
|
||||
* store or options; the merge only affects the current request.
|
||||
* - No Page Cache interaction: logged-in users always bypass Boost's page cache
|
||||
* (Request::is_cacheable() returns false), so pages rendered with these debug
|
||||
* parameters are neither served from nor written to the cache.
|
||||
*
|
||||
* Residual note: changing the exclude list changes concatenate-bundle membership,
|
||||
* so each distinct combination an admin renders generates its own concat-path
|
||||
* transient (and static file when static minification is on), just like any new
|
||||
* page does. These are reclaimed by the normal Cleanup_Stored_Paths schedule.
|
||||
* The parameter is intentionally nonce-free so it can be shared as a plain URL,
|
||||
* so this stays capability-gated rather than CSRF-gated.
|
||||
*
|
||||
* @since 4.6.2
|
||||
*
|
||||
* @param mixed $excludes The saved exclude list; non-array values are treated as an empty list.
|
||||
* @param string $param The GET parameter to read extra handles from.
|
||||
*
|
||||
* @return array The exclude list, with any valid per-request handles appended.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_merge_debug_excludes( $excludes, $param ) {
|
||||
if ( ! is_array( $excludes ) ) {
|
||||
$excludes = array();
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only debugging parameter; capability-gated below and never persisted.
|
||||
if ( ! isset( $_GET[ $param ] ) || ! is_string( $_GET[ $param ] ) ) {
|
||||
return $excludes;
|
||||
}
|
||||
|
||||
// Only administrators may use the debug parameters.
|
||||
if ( ! function_exists( 'current_user_can' ) || ! current_user_can( 'manage_options' ) ) {
|
||||
return $excludes;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each handle is validated against a strict allowlist below.
|
||||
$raw_value = wp_unslash( $_GET[ $param ] );
|
||||
|
||||
// Bound the work: a real debug session never needs more than a handful of
|
||||
// handles, so process at most the first 100. The excess is dropped rather
|
||||
// than discarding the whole value, so a partial list still applies.
|
||||
$raw_handles = array_slice( explode( ',', $raw_value ), 0, 100 );
|
||||
|
||||
$extra_handles = array();
|
||||
foreach ( $raw_handles as $handle ) {
|
||||
$handle = trim( $handle );
|
||||
|
||||
/*
|
||||
* Allowlist of characters valid in script/style handles. Case is preserved
|
||||
* (not lowercased) because handles are matched case-sensitively downstream,
|
||||
* so lowercasing would silently fail to exclude a handle registered with
|
||||
* uppercase letters. Handles containing any other character are ignored.
|
||||
*/
|
||||
if ( $handle !== '' && preg_match( '/^[a-zA-Z0-9_.\-]+$/', $handle ) ) {
|
||||
$extra_handles[] = $handle;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $extra_handles ) ) {
|
||||
return $excludes;
|
||||
}
|
||||
|
||||
return array_values( array_unique( array_merge( $excludes, $extra_handles ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a string starts with another string.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_starts_with( $prefix, $str ) {
|
||||
$prefix_length = strlen( $prefix );
|
||||
if ( strlen( $str ) < $prefix_length ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return substr( $str, 0, $prefix_length ) === $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers whether the plugin should provide concat resource URIs
|
||||
* that are relative to a common ancestor directory. Assuming a common ancestor
|
||||
* allows us to skip resolving resource URIs to filesystem paths later on.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_use_concat_base_dir() {
|
||||
return defined( 'PAGE_OPTIMIZE_CONCAT_BASE_DIR' ) && file_exists( PAGE_OPTIMIZE_CONCAT_BASE_DIR );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a filesystem path relative to a configured base path for resources
|
||||
* that will be concatenated. Assuming a common ancestor allows us to skip
|
||||
* resolving resource URIs to filesystem paths later on.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_remove_concat_base_prefix( $original_fs_path ) {
|
||||
$abspath = Config::get_abspath();
|
||||
|
||||
// Always check longer path first
|
||||
if ( strlen( $abspath ) > strlen( PAGE_OPTIMIZE_CONCAT_BASE_DIR ) ) {
|
||||
$longer_path = $abspath;
|
||||
$shorter_path = PAGE_OPTIMIZE_CONCAT_BASE_DIR;
|
||||
} else {
|
||||
$longer_path = PAGE_OPTIMIZE_CONCAT_BASE_DIR;
|
||||
$shorter_path = $abspath;
|
||||
}
|
||||
|
||||
$prefix_abspath = trailingslashit( $longer_path );
|
||||
if ( jetpack_boost_page_optimize_starts_with( $prefix_abspath, $original_fs_path ) ) {
|
||||
return substr( $original_fs_path, strlen( $prefix_abspath ) );
|
||||
}
|
||||
|
||||
$prefix_basedir = trailingslashit( $shorter_path );
|
||||
if ( jetpack_boost_page_optimize_starts_with( $prefix_basedir, $original_fs_path ) ) {
|
||||
return substr( $original_fs_path, strlen( $prefix_basedir ) );
|
||||
}
|
||||
|
||||
// If we end up here, this is a resource we shouldn't have tried to concat in the first place
|
||||
return '/page-optimize-resource-outside-base-path/' . basename( $original_fs_path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a cronjob for the 404 tester, if one isn't already scheduled.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_schedule_404_tester() {
|
||||
if ( false === wp_next_scheduled( 'jetpack_boost_404_tester_cron' ) ) {
|
||||
wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'jetpack_boost_404_tester_cron' );
|
||||
|
||||
// Run the test immediately, so the settings page can show the result.
|
||||
jetpack_boost_404_tester();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a cronjob for cache cleanup, if one isn't already scheduled.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_schedule_cache_cleanup() {
|
||||
// If caching is on, and job isn't queued for current cache folder
|
||||
if ( false === wp_next_scheduled( 'jetpack_boost_minify_cron_cache_cleanup' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'jetpack_boost_minify_cron_cache_cleanup' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether it's safe to minify for the duration of this HTTP request. Checks
|
||||
* for things like page-builder editors, etc.
|
||||
*
|
||||
* @return bool True if we don't want to minify/concatenate CSS/JS for this request.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_bail() {
|
||||
static $should_bail = null;
|
||||
if ( null !== $should_bail ) {
|
||||
return $should_bail;
|
||||
}
|
||||
|
||||
$should_bail = false;
|
||||
|
||||
// Bail if this is an admin page
|
||||
if ( is_admin() ) {
|
||||
$should_bail = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bail if we're in customizer
|
||||
global $wp_customize;
|
||||
if ( isset( $wp_customize ) ) {
|
||||
$should_bail = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bail if Divi theme is active, and we're in the Divi Front End Builder
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! empty( $_GET['et_fb'] ) && 'Divi' === wp_get_theme()->get_template() ) {
|
||||
$should_bail = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bail if we're editing pages in Brizy Editor
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( class_exists( 'Brizy_Editor' ) && method_exists( 'Brizy_Editor', 'prefix' ) && ( isset( $_GET[ Brizy_Editor::prefix( '-edit-iframe' ) ] ) || isset( $_GET[ Brizy_Editor::prefix( '-edit' ) ] ) ) ) {
|
||||
$should_bail = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bail in elementor preview
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( isset( $_GET['elementor-preview'] ) ) {
|
||||
$should_bail = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return $should_bail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a URL with a cache-busting query string based on the file's mtime.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_cache_bust_mtime( $path, $siteurl ) {
|
||||
static $dependency_path_mapping;
|
||||
|
||||
// Absolute paths should dump the path component of siteurl.
|
||||
if ( str_starts_with( $path, '/' ) ) {
|
||||
$parts = wp_parse_url( $siteurl );
|
||||
$siteurl = $parts['scheme'] . '://' . $parts['host'];
|
||||
}
|
||||
|
||||
$url = $siteurl . $path;
|
||||
|
||||
if ( str_contains( $url, '?m=' ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( ! isset( $parts['path'] ) || empty( $parts['path'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if ( empty( $dependency_path_mapping ) ) {
|
||||
$dependency_path_mapping = new Dependency_Path_Mapping();
|
||||
}
|
||||
|
||||
$file = $dependency_path_mapping->dependency_src_to_fs_path( $url );
|
||||
|
||||
$mtime = false;
|
||||
if ( file_exists( $file ) ) {
|
||||
$mtime = filemtime( $file );
|
||||
}
|
||||
|
||||
if ( ! $mtime ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if ( ! str_contains( $url, '?' ) ) {
|
||||
$q = '';
|
||||
} else {
|
||||
list( $url, $q ) = explode( '?', $url, 2 );
|
||||
if ( strlen( $q ) ) {
|
||||
$q = '&' . $q;
|
||||
}
|
||||
}
|
||||
|
||||
return "$url?m={$mtime}{$q}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL prefix for static minify/concat resources. Defaults to /_jb_static/, but can be
|
||||
* overridden by defining JETPACK_BOOST_STATIC_PREFIX.
|
||||
*/
|
||||
function jetpack_boost_get_static_prefix() {
|
||||
$prefix = defined( 'JETPACK_BOOST_STATIC_PREFIX' ) ? JETPACK_BOOST_STATIC_PREFIX : '/_jb_static/';
|
||||
|
||||
if ( ! str_starts_with( $prefix, '/' ) ) {
|
||||
$prefix = '/' . $prefix;
|
||||
}
|
||||
|
||||
return trailingslashit( $prefix );
|
||||
}
|
||||
|
||||
function jetpack_boost_get_minify_url( $file_name = '' ) {
|
||||
return content_url( '/boost-cache/static/' . $file_name );
|
||||
}
|
||||
|
||||
function jetpack_boost_get_minify_file_path( $file_name = '' ) {
|
||||
return WP_CONTENT_DIR . '/boost-cache/static/' . $file_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects requests within the `/_jb_static/` directory, and serves minified content.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function jetpack_boost_minify_serve_concatenated() {
|
||||
// Potential improvement: Make concat URL dir configurable
|
||||
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_path = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) )[0];
|
||||
$prefix = jetpack_boost_get_static_prefix();
|
||||
if ( $prefix === substr( $request_path, -strlen( $prefix ), strlen( $prefix ) ) ) {
|
||||
require_once __DIR__ . '/functions-service-fallback.php';
|
||||
jetpack_boost_page_optimize_service_request();
|
||||
exit( 0 ); // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though jetpack_boost_page_optimize_service_request() itself never returns.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run during activation of any minify module.
|
||||
*
|
||||
* This handles scheduling cache cleanup, and setting up the cronjob to periodically test for the 404 handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function jetpack_boost_minify_activation() {
|
||||
// Schedule a cronjob for cache cleanup, if one isn't already scheduled.
|
||||
jetpack_boost_page_optimize_schedule_cache_cleanup();
|
||||
|
||||
Cleanup_Stored_Paths::setup_schedule();
|
||||
|
||||
// Setup the cronjob to periodically test for the 404 handler.
|
||||
jetpack_boost_404_setup();
|
||||
}
|
||||
|
||||
function jetpack_boost_minify_is_enabled() {
|
||||
$minify_css = new Module( new Minify_CSS() );
|
||||
$minify_js = new Module( new Minify_JS() );
|
||||
|
||||
return $minify_css->is_enabled() || $minify_js->is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run during initialization of any minify module.
|
||||
*
|
||||
* Run during every page load if any minify module is active.
|
||||
*/
|
||||
function jetpack_boost_minify_init() {
|
||||
add_action( 'jetpack_boost_minify_cron_cache_cleanup', 'jetpack_boost_minify_cron_cache_cleanup' );
|
||||
Cleanup_Stored_Paths::add_cleanup_actions();
|
||||
|
||||
if ( jetpack_boost_page_optimize_bail() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable Jetpack Site Accelerator CDN for static JS/CSS, if we're minifying this page.
|
||||
add_filter( 'jetpack_force_disable_site_accelerator', '__return_true' );
|
||||
}
|
||||
|
||||
function jetpack_boost_page_optimize_generate_concat_path( $url_paths, $dependency_path_mapping ) {
|
||||
$fs_paths = array();
|
||||
foreach ( $url_paths as $url_path ) {
|
||||
$fs_paths[] = $dependency_path_mapping->uri_path_to_fs_path( $url_path );
|
||||
}
|
||||
|
||||
$mtime = max( array_map( 'filemtime', $fs_paths ) );
|
||||
if ( jetpack_boost_page_optimize_use_concat_base_dir() ) {
|
||||
$paths = array_map( 'jetpack_boost_page_optimize_remove_concat_base_prefix', $fs_paths );
|
||||
} else {
|
||||
$paths = $url_paths;
|
||||
}
|
||||
|
||||
$file_paths = new File_Paths();
|
||||
$file_paths->set( $paths, $mtime, jetpack_boost_minify_cache_buster() );
|
||||
$file_paths->store();
|
||||
|
||||
return $file_paths->get_cache_id();
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Minify;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Config;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Dependency_Path_Mapping;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\File_Paths;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Utils;
|
||||
|
||||
function jetpack_boost_page_optimize_types() {
|
||||
return array(
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle serving a minified / concatenated file from the virtual _jb_static dir.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
function jetpack_boost_page_optimize_service_request() {
|
||||
$use_wp = defined( 'JETPACK_BOOST_CONCAT_USE_WP' ) && JETPACK_BOOST_CONCAT_USE_WP;
|
||||
$utils = new Utils( $use_wp );
|
||||
|
||||
// We handle the cache here, tell other caches not to.
|
||||
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
|
||||
define( 'DONOTCACHEPAGE', true );
|
||||
}
|
||||
|
||||
$use_cache = Config::can_use_cache();
|
||||
$cache_file = '';
|
||||
$cache_file_meta = '';
|
||||
|
||||
if ( $use_cache ) {
|
||||
$cache_dir = Config::get_legacy_cache_dir_path();
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $utils->unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$request_uri_hash = md5( $request_uri );
|
||||
$cache_file = $cache_dir . "/page-optimize-cache-$request_uri_hash";
|
||||
$cache_file_meta = $cache_dir . "/page-optimize-cache-meta-$request_uri_hash";
|
||||
|
||||
// Serve an existing file.
|
||||
if ( file_exists( $cache_file ) ) {
|
||||
if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
if ( strtotime( $utils->unslash( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) < filemtime( $cache_file ) ) {
|
||||
header( 'HTTP/1.1 304 Not Modified' );
|
||||
exit( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( file_exists( $cache_file_meta ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$meta = json_decode( file_get_contents( $cache_file_meta ), true );
|
||||
if ( ! empty( $meta ) && ! empty( $meta['headers'] ) ) {
|
||||
foreach ( $meta['headers'] as $header ) {
|
||||
header( $header );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$etag = '"' . md5( file_get_contents( $cache_file ) ) . '"';
|
||||
|
||||
// Check if we're on Atomic and take advantage of the Atomic Edge Cache.
|
||||
if ( defined( 'ATOMIC_CLIENT_ID' ) ) {
|
||||
header( 'A8c-Edge-Cache: cache' );
|
||||
}
|
||||
header( 'X-Page-Optimize: cached' );
|
||||
header( 'Cache-Control: max-age=' . 31536000 );
|
||||
header( 'ETag: ' . $etag );
|
||||
|
||||
echo file_get_contents( $cache_file ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- We need to trust this unfortunately.
|
||||
die( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
// Existing file not available; generate new content.
|
||||
$output = jetpack_boost_page_optimize_build_output();
|
||||
$content = $output['content'];
|
||||
$headers = $output['headers'];
|
||||
|
||||
foreach ( $headers as $header ) {
|
||||
header( $header );
|
||||
}
|
||||
// Check if we're on Atomic and take advantage of the Atomic Edge Cache.
|
||||
if ( defined( 'ATOMIC_CLIENT_ID' ) ) {
|
||||
header( 'A8c-Edge-Cache: cache' );
|
||||
}
|
||||
header( 'X-Page-Optimize: uncached' );
|
||||
header( 'Cache-Control: max-age=' . 31536000 );
|
||||
header( 'ETag: "' . md5( $content ) . '"' );
|
||||
|
||||
echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
|
||||
|
||||
// Cache the generated data, if available.
|
||||
if ( $use_cache ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
|
||||
file_put_contents( $cache_file, $content );
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
|
||||
file_put_contents( $cache_file_meta, wp_json_encode( array( 'headers' => $headers ), JSON_UNESCAPED_SLASHES ) );
|
||||
}
|
||||
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip matching parent paths off a string. Returns $path without $parent_path.
|
||||
*/
|
||||
function jetpack_boost_strip_parent_path( $parent_path, $path ) {
|
||||
$trimmed_parent = ltrim( $parent_path, '/' );
|
||||
$trimmed_path = ltrim( $path, '/' );
|
||||
|
||||
if ( substr( $trimmed_path, 0, strlen( $trimmed_parent ) ) === $trimmed_parent ) {
|
||||
$trimmed_path = substr( $trimmed_path, strlen( $trimmed_parent ) );
|
||||
}
|
||||
|
||||
return str_starts_with( $trimmed_path, '/' ) ? $trimmed_path : '/' . $trimmed_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a combined and minified output for the current request. This is run regardless of the
|
||||
* type of content being fetched; JavaScript or CSS, so it must handle either.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_build_output() {
|
||||
$use_wp = defined( 'JETPACK_BOOST_CONCAT_USE_WP' ) && JETPACK_BOOST_CONCAT_USE_WP;
|
||||
$utils = new Utils( $use_wp );
|
||||
|
||||
$jetpack_boost_page_optimize_types = jetpack_boost_page_optimize_types();
|
||||
|
||||
// Config
|
||||
$concat_max_files = jetpack_boost_minify_concat_max_files();
|
||||
$concat_unique = true;
|
||||
|
||||
// Main
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? $utils->unslash( $_SERVER['REQUEST_METHOD'] ) : 'GET';
|
||||
if ( ! in_array( $method, array( 'GET', 'HEAD' ), true ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
// Ensure the path follows one of these forms:
|
||||
// /_jb_static/??/foo/bar.css,/foo1/bar/baz.css?m=293847g
|
||||
// -- or --
|
||||
// /_jb_static/??-eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM=
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $utils->unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$args = $utils->parse_url( $request_uri, PHP_URL_QUERY );
|
||||
if ( ! $args || ! str_contains( $args, '?' ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
$args = substr( $args, strpos( $args, '?' ) + 1 );
|
||||
|
||||
$args = jetpack_boost_page_optimize_get_file_paths( $args );
|
||||
|
||||
// args contain something like array( '/foo/bar.css', '/foo1/bar/baz.css' )
|
||||
if ( 0 === count( $args ) || count( $args ) > $concat_max_files ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
// If we're in a subdirectory context, use that as the root.
|
||||
// We can't assume that the root serves the same content as the subdir.
|
||||
$subdir_path_prefix = '';
|
||||
$request_path = $utils->parse_url( $request_uri, PHP_URL_PATH );
|
||||
$_static_index = strpos( $request_path, jetpack_boost_get_static_prefix() );
|
||||
if ( $_static_index > 0 ) {
|
||||
$subdir_path_prefix = substr( $request_path, 0, $_static_index );
|
||||
}
|
||||
unset( $request_path, $_static_index );
|
||||
|
||||
$last_modified = 0;
|
||||
$pre_output = '';
|
||||
$output = '';
|
||||
|
||||
foreach ( $args as $uri ) {
|
||||
$fullpath = jetpack_boost_page_optimize_get_path( $uri );
|
||||
|
||||
if ( ! file_exists( $fullpath ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 404 );
|
||||
}
|
||||
|
||||
$mime_type = jetpack_boost_page_optimize_get_mime_type( $fullpath );
|
||||
if ( ! in_array( $mime_type, $jetpack_boost_page_optimize_types, true ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
if ( $concat_unique ) {
|
||||
if ( ! isset( $last_mime_type ) ) {
|
||||
$last_mime_type = $mime_type;
|
||||
}
|
||||
|
||||
if ( $last_mime_type !== $mime_type ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
}
|
||||
|
||||
$stat = stat( $fullpath );
|
||||
if ( false === $stat ) {
|
||||
jetpack_boost_page_optimize_status_exit( 500 );
|
||||
}
|
||||
|
||||
if ( $stat['mtime'] > $last_modified ) {
|
||||
$last_modified = $stat['mtime'];
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$buf = file_get_contents( $fullpath );
|
||||
if ( false === $buf ) {
|
||||
jetpack_boost_page_optimize_status_exit( 500 );
|
||||
}
|
||||
|
||||
if ( 'text/css' === $mime_type ) {
|
||||
$dirpath = jetpack_boost_strip_parent_path( $subdir_path_prefix, dirname( $uri ) );
|
||||
|
||||
// url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
|
||||
$buf = jetpack_boost_page_optimize_relative_path_replace( $buf, $dirpath );
|
||||
|
||||
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
// This regex changes things like AlphaImageLoader(...src='relative/path/to/file'...) to AlphaImageLoader(...src='/absolute/path/to/file'...)
|
||||
$buf = preg_replace(
|
||||
'/(Microsoft.AlphaImageLoader\s*\([^\)]*src=(?:\'|")?)([^\/\'"\s\)](?:(?<!http:|https:).)*)\)/isU',
|
||||
'$1' . ( $dirpath === '/' ? '/' : $dirpath . '/' ) . '$2)',
|
||||
$buf
|
||||
);
|
||||
|
||||
// The @charset rules must be on top of the output
|
||||
if ( str_starts_with( $buf, '@charset' ) ) {
|
||||
$buf = preg_replace_callback(
|
||||
'/(?P<charset_rule>@charset\s+[\'"][^\'"]+[\'"];)/i',
|
||||
function ( $match ) use ( &$pre_output ) {
|
||||
if ( str_starts_with( $pre_output, '@charset' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$pre_output = $match[0] . "\n" . $pre_output;
|
||||
|
||||
return '';
|
||||
},
|
||||
$buf
|
||||
);
|
||||
}
|
||||
|
||||
// Move the @import rules on top of the concatenated output.
|
||||
// Only @charset rule are allowed before them.
|
||||
if ( str_contains( $buf, '@import' ) ) {
|
||||
$buf = preg_replace_callback(
|
||||
'/(?P<pre_path>@import\s+(?:url\s*\()?[\'"\s]*)(?P<path>[^\'"\s](?:https?:\/\/.+\/?)?.+?)(?P<post_path>[\'"\s\)]*;)/i',
|
||||
function ( $match ) use ( $dirpath, &$pre_output ) {
|
||||
if ( ! str_starts_with( $match['path'], 'http' ) && '/' !== $match['path'][0] ) {
|
||||
$pre_output .= $match['pre_path'] . ( $dirpath === '/' ? '/' : $dirpath . '/' ) .
|
||||
$match['path'] . $match['post_path'] . "\n";
|
||||
} else {
|
||||
$pre_output .= $match[0] . "\n";
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
$buf
|
||||
);
|
||||
}
|
||||
|
||||
// If filename indicates it's already minified, don't minify it again.
|
||||
if ( ! jetpack_boost_page_optimize_is_already_minified( $fullpath ) ) {
|
||||
// Minify CSS.
|
||||
$buf = Minify::css( $buf );
|
||||
}
|
||||
$output .= "$buf";
|
||||
} else {
|
||||
// If filename indicates it's already minified, don't minify it again.
|
||||
if ( ! jetpack_boost_page_optimize_is_already_minified( $fullpath ) ) {
|
||||
// Minify JS
|
||||
$buf = Minify::js( $buf );
|
||||
}
|
||||
|
||||
$output .= "$buf;\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Don't let trailing whitespace ruin everyone's day. Seems to get stripped by batcache
|
||||
// resulting in ns_error_net_partial_transfer errors.
|
||||
$output = rtrim( $output );
|
||||
|
||||
$headers = array(
|
||||
'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT',
|
||||
"Content-Type: $mime_type",
|
||||
);
|
||||
|
||||
return array(
|
||||
'headers' => $headers,
|
||||
'content' => $pre_output . $output,
|
||||
);
|
||||
}
|
||||
|
||||
function jetpack_boost_page_optimize_get_file_paths( $args ) {
|
||||
$paths = File_Paths::get( $args );
|
||||
if ( $paths ) {
|
||||
$args = $paths->get_paths();
|
||||
} else {
|
||||
// Kept for backward compatibility in case cached page is still referring to old formal asset URLs.
|
||||
|
||||
// It's a base64 encoded list of file path.
|
||||
// e.g.: /_jb_static/??-eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM=
|
||||
if ( '-' === $args[0] ) {
|
||||
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$args = @gzuncompress( base64_decode( substr( $args, 1 ) ) );
|
||||
}
|
||||
|
||||
// It's an unencoded comma-separated list of file paths.
|
||||
// /foo/bar.css,/foo1/bar/baz.css?m=293847g
|
||||
$version_string_pos = strpos( $args, '?' );
|
||||
if ( false !== $version_string_pos ) {
|
||||
$args = substr( $args, 0, $version_string_pos );
|
||||
}
|
||||
// /foo/bar.css,/foo1/bar/baz.css
|
||||
$args = explode( ',', $args );
|
||||
}
|
||||
|
||||
if ( ! is_array( $args ) ) {
|
||||
// Invalid data, abort!
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit with a given HTTP status code.
|
||||
*
|
||||
* @param int $status HTTP status code.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
function jetpack_boost_page_optimize_status_exit( $status ) {
|
||||
http_response_code( $status );
|
||||
exit( 0 ); // This is a workaround, until a bug in phan is fixed - https://github.com/phan/phan/issues/4888
|
||||
}
|
||||
|
||||
function jetpack_boost_page_optimize_get_mime_type( $file ) {
|
||||
$jetpack_boost_page_optimize_types = jetpack_boost_page_optimize_types();
|
||||
|
||||
$lastdot_pos = strrpos( $file, '.' );
|
||||
if ( false === $lastdot_pos ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ext = substr( $file, $lastdot_pos + 1 );
|
||||
if ( ! isset( $jetpack_boost_page_optimize_types[ $ext ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $jetpack_boost_page_optimize_types[ $ext ];
|
||||
}
|
||||
|
||||
function jetpack_boost_page_optimize_relative_path_replace( $buf, $dirpath ) {
|
||||
// url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
|
||||
$buf = preg_replace(
|
||||
'/(:?\s*url\s*\()\s*(?:\'|")?\s*([^\/\'"\s\)](?:(?<!data:|http:|https:|[\(\'"]#|%23).)*)[\'"\s]*\)/isU',
|
||||
'$1' . ( $dirpath === '/' ? '/' : $dirpath . '/' ) . '$2)',
|
||||
$buf
|
||||
);
|
||||
|
||||
return $buf;
|
||||
}
|
||||
|
||||
function jetpack_boost_page_optimize_get_path( $uri ) {
|
||||
static $dependency_path_mapping;
|
||||
|
||||
if ( ! strlen( $uri ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
if ( str_contains( $uri, '..' ) || str_contains( $uri, "\0" ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
if ( defined( 'PAGE_OPTIMIZE_CONCAT_BASE_DIR' ) ) {
|
||||
$path = realpath( PAGE_OPTIMIZE_CONCAT_BASE_DIR . "/$uri" );
|
||||
|
||||
if ( false === $path ) {
|
||||
$path = realpath( Config::get_abspath() . "/$uri" );
|
||||
}
|
||||
} else {
|
||||
if ( empty( $dependency_path_mapping ) ) {
|
||||
$dependency_path_mapping = new Dependency_Path_Mapping();
|
||||
}
|
||||
$path = $dependency_path_mapping->uri_path_to_fs_path( $uri );
|
||||
}
|
||||
|
||||
if ( false === $path ) {
|
||||
jetpack_boost_page_optimize_status_exit( 404 );
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
use Automattic\Jetpack_Boost\Admin\Config as Boost_Admin_Config;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Config;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\File_Paths;
|
||||
use Automattic\Jetpack_Boost\Lib\Minify\Utils;
|
||||
|
||||
if ( ! defined( 'JETPACK_BOOST_STATIC_CACHE_404_TESTER_PATH' ) ) {
|
||||
define( 'JETPACK_BOOST_STATIC_CACHE_404_TESTER_PATH', '/wp-content/boost-cache/static/testing_404.js' );
|
||||
}
|
||||
|
||||
function jetpack_boost_handle_minify_request( $request_uri ) {
|
||||
// We handle the cache here, tell other caches not to.
|
||||
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
|
||||
define( 'DONOTCACHEPAGE', true );
|
||||
}
|
||||
|
||||
$output = jetpack_boost_build_minify_output( $request_uri );
|
||||
$content = $output['content'];
|
||||
$headers = $output['headers'];
|
||||
|
||||
foreach ( $headers as $header ) {
|
||||
header( $header );
|
||||
}
|
||||
|
||||
// Check if we're on Atomic and take advantage of the Atomic Edge Cache.
|
||||
if ( defined( 'ATOMIC_CLIENT_ID' ) ) {
|
||||
header( 'A8c-Edge-Cache: cache' );
|
||||
}
|
||||
|
||||
header( 'X-Page-Optimize: uncached' );
|
||||
header( 'Cache-Control: max-age=' . 31536000 );
|
||||
header( 'ETag: "' . md5( $content ) . '"' );
|
||||
|
||||
echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
|
||||
|
||||
// Cache the generated data, if possible.
|
||||
$use_cache = Config::can_use_static_cache();
|
||||
if ( $use_cache ) {
|
||||
$file_parts = jetpack_boost_minify_get_file_parts( $request_uri );
|
||||
if ( is_array( $file_parts ) && isset( $file_parts['file_name'] ) && isset( $file_parts['file_extension'] ) ) {
|
||||
$cache_dir = Config::get_static_cache_dir_path();
|
||||
$cache_file_path = $cache_dir . '/' . $file_parts['file_name'] . '.min.' . $file_parts['file_extension'];
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
|
||||
file_put_contents( $cache_file_path, $content );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using a crafted request, we can check if is_404() is working in wp-content/
|
||||
* The constant JETPACK_BOOST_STATIC_CACHE_404_TESTER_PATH is the path to the file that will be requested.
|
||||
*/
|
||||
function jetpack_boost_check_404_handler( $request_uri ) {
|
||||
if ( ! str_contains( strtolower( $request_uri ), JETPACK_BOOST_STATIC_CACHE_404_TESTER_PATH ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_404() ) {
|
||||
if ( ! is_dir( Config::get_static_cache_dir_path() ) ) {
|
||||
mkdir( Config::get_static_cache_dir_path(), 0775, true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
|
||||
}
|
||||
file_put_contents( Config::get_static_cache_dir_path() . '/404', '1' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
|
||||
return true;
|
||||
} else {
|
||||
wp_delete_file( Config::get_static_cache_dir_path() . '/404' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that the 404 tester is only run once per day, espicially for multisite.
|
||||
*/
|
||||
function jetpack_boost_404_tester_cron() {
|
||||
// If we see it's been executed within 24 hours, don't run
|
||||
if ( ! jetpack_boost_should_run_daily_network_cron_job( '404_tester' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
jetpack_boost_404_tester();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to test if is_404() is working in wp-content/
|
||||
* It sends a request to a non-existent URL, that will execute the 404 handler
|
||||
* in jetpack_boost_check_404_handler().
|
||||
* Define the constant JETPACK_BOOST_DISABLE_404_TESTER to disable this.
|
||||
* The constant JETPACK_BOOST_STATIC_CACHE_404_TESTER_PATH is the path to the file that will be requested.
|
||||
*
|
||||
* This function is called when the Minify_CSS or Minify_JS module is activated, and once per day.
|
||||
*/
|
||||
function jetpack_boost_404_tester() {
|
||||
if ( defined( 'JETPACK_BOOST_DISABLE_404_TESTER' ) && JETPACK_BOOST_DISABLE_404_TESTER ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$minification_enabled = '';
|
||||
wp_remote_get( home_url( JETPACK_BOOST_STATIC_CACHE_404_TESTER_PATH ) );
|
||||
if ( file_exists( Config::get_static_cache_dir_path() . '/404' ) ) {
|
||||
wp_delete_file( Config::get_static_cache_dir_path() . '/404' );
|
||||
$minification_enabled = 1;
|
||||
} else {
|
||||
$minification_enabled = 0;
|
||||
}
|
||||
update_site_option( 'jetpack_boost_static_minification', $minification_enabled );
|
||||
|
||||
return $minification_enabled;
|
||||
}
|
||||
|
||||
add_action( 'jetpack_boost_404_tester_cron', 'jetpack_boost_404_tester_cron' );
|
||||
|
||||
/**
|
||||
* Setup the 404 tester.
|
||||
*
|
||||
* Schedule the 404 tester if the concatenation modules
|
||||
* haven't been toggled since this feature was released.
|
||||
* Only run this in wp-admin to avoid excessive updates to the option.
|
||||
*/
|
||||
function jetpack_boost_404_setup() {
|
||||
// If we're on Atomic or Woa, don't setup the 404 tester.
|
||||
if ( in_array( Boost_Admin_Config::get_hosting_provider(), array( 'atomic', 'woa' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_admin() && get_site_option( 'jetpack_boost_static_minification', 'na' ) === 'na' ) {
|
||||
update_site_option( 'jetpack_boost_static_minification', 0 ); // Add a default value if not set to avoid an extra SQL query.
|
||||
}
|
||||
|
||||
jetpack_boost_page_optimize_schedule_404_tester();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to clean up the static cache folder.
|
||||
* It removes files with the file extension passed in the $file_extension parameter.
|
||||
*
|
||||
* @param string $file_extension The file extension to clean up.
|
||||
*/
|
||||
function jetpack_boost_page_optimize_cleanup_cache( $file_extension ) {
|
||||
$files = glob( Config::get_static_cache_dir_path() . "/*.min.{$file_extension}" );
|
||||
foreach ( $files as $file ) {
|
||||
wp_delete_file( $file );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to clean up the static cache folder.
|
||||
* It removes files that are stale and no longer needed.
|
||||
* A file is considered stale if it's older than the files it depends on.
|
||||
*/
|
||||
function jetpack_boost_minify_remove_stale_static_files() {
|
||||
$concat_files = glob( Config::get_static_cache_dir_path() . '/*.min.*' );
|
||||
foreach ( $concat_files as $concat_file ) {
|
||||
if ( ! file_exists( $concat_file ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_mtime = filemtime( $concat_file );
|
||||
$file_parts = pathinfo( $concat_file );
|
||||
$hash = substr( $file_parts['basename'], 0, strpos( $file_parts['basename'], '.' ) );
|
||||
$paths = File_Paths::get( $hash );
|
||||
if ( $paths ) {
|
||||
$args = $paths->get_paths();
|
||||
if ( ! is_array( $args ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the site path relative to the webroot.
|
||||
$site_url_path = wp_parse_url( site_url(), PHP_URL_PATH );
|
||||
if ( ! $site_url_path ) {
|
||||
$site_url_path = '';
|
||||
}
|
||||
|
||||
// Get the webroot path by removing the site path from the ABSPATH. In case it's a subdirectory install, webroot is different from ABSPATH.
|
||||
$webroot = substr( ABSPATH, 0, - strlen( $site_url_path ) - 1 );
|
||||
|
||||
foreach ( $args as $dependency_filename ) {
|
||||
if ( ! file_exists( $webroot . $dependency_filename ) || filemtime( $webroot . $dependency_filename ) > $file_mtime ) {
|
||||
wp_delete_file( $concat_file ); // remove the file from the cache because it's stale.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function jetpack_boost_build_minify_output( $request_uri ) {
|
||||
$utils = new Utils();
|
||||
$jetpack_boost_page_optimize_types = jetpack_boost_page_optimize_types();
|
||||
|
||||
// Config
|
||||
$concat_max_files = jetpack_boost_minify_concat_max_files();
|
||||
$concat_unique = true;
|
||||
|
||||
$file_parts = jetpack_boost_minify_get_file_parts( $request_uri );
|
||||
if ( ! $file_parts ) {
|
||||
jetpack_boost_page_optimize_status_exit( 404 );
|
||||
}
|
||||
|
||||
$file_paths = jetpack_boost_page_optimize_get_file_paths( $file_parts['file_name'] );
|
||||
|
||||
// file_paths contain something like array( '/foo/bar.css', '/foo1/bar/baz.css' )
|
||||
if ( count( $file_paths ) > $concat_max_files ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
// If we're in a subdirectory context, use that as the root.
|
||||
// We can't assume that the root serves the same content as the subdir.
|
||||
$subdir_path_prefix = '';
|
||||
$request_path = $utils->parse_url( $request_uri, PHP_URL_PATH );
|
||||
$_static_index = strpos( $request_path, jetpack_boost_get_static_prefix() );
|
||||
if ( $_static_index > 0 ) {
|
||||
$subdir_path_prefix = substr( $request_path, 0, $_static_index );
|
||||
}
|
||||
unset( $request_path, $_static_index );
|
||||
|
||||
$last_modified = 0;
|
||||
$pre_output = '';
|
||||
$output = '';
|
||||
|
||||
$mime_type = '';
|
||||
|
||||
foreach ( $file_paths as $uri ) {
|
||||
$fullpath = jetpack_boost_page_optimize_get_path( $uri );
|
||||
|
||||
if ( ! file_exists( $fullpath ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 404 );
|
||||
}
|
||||
|
||||
$mime_type = jetpack_boost_page_optimize_get_mime_type( $fullpath );
|
||||
if ( ! in_array( $mime_type, $jetpack_boost_page_optimize_types, true ) ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
|
||||
if ( $concat_unique ) {
|
||||
if ( ! isset( $last_mime_type ) ) {
|
||||
$last_mime_type = $mime_type;
|
||||
}
|
||||
|
||||
if ( $last_mime_type !== $mime_type ) {
|
||||
jetpack_boost_page_optimize_status_exit( 400 );
|
||||
}
|
||||
}
|
||||
|
||||
$stat = stat( $fullpath );
|
||||
if ( false === $stat ) {
|
||||
jetpack_boost_page_optimize_status_exit( 500 );
|
||||
}
|
||||
|
||||
if ( $stat['mtime'] > $last_modified ) {
|
||||
$last_modified = $stat['mtime'];
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$buf = file_get_contents( $fullpath );
|
||||
if ( false === $buf ) {
|
||||
jetpack_boost_page_optimize_status_exit( 500 );
|
||||
}
|
||||
|
||||
if ( 'text/css' === $mime_type ) {
|
||||
$dirpath = jetpack_boost_strip_parent_path( $subdir_path_prefix, dirname( $uri ) );
|
||||
|
||||
// url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
|
||||
$buf = jetpack_boost_page_optimize_relative_path_replace( $buf, $dirpath );
|
||||
|
||||
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
// This regex changes things like AlphaImageLoader(...src='relative/path/to/file'...) to AlphaImageLoader(...src='/absolute/path/to/file'...)
|
||||
$buf = preg_replace(
|
||||
'/(Microsoft.AlphaImageLoader\s*\([^\)]*src=(?:\'|")?)([^\/\'"\s\)](?:(?<!http:|https:).)*)\)/isU',
|
||||
'$1' . ( $dirpath === '/' ? '/' : $dirpath . '/' ) . '$2)',
|
||||
$buf
|
||||
);
|
||||
|
||||
// The @charset rules must be on top of the output
|
||||
if ( str_starts_with( $buf, '@charset' ) ) {
|
||||
$buf = preg_replace_callback(
|
||||
'/(?P<charset_rule>@charset\s+[\'"][^\'"]+[\'"];)/i',
|
||||
function ( $match ) use ( &$pre_output ) {
|
||||
if ( str_starts_with( $pre_output, '@charset' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$pre_output = $match[0] . "\n" . $pre_output;
|
||||
|
||||
return '';
|
||||
},
|
||||
$buf
|
||||
);
|
||||
}
|
||||
|
||||
// Move the @import rules on top of the concatenated output.
|
||||
// Only @charset rule are allowed before them.
|
||||
if ( str_contains( $buf, '@import' ) ) {
|
||||
$buf = preg_replace_callback(
|
||||
'/(?P<pre_path>@import\s+(?:url\s*\()?[\'"\s]*)(?P<path>[^\'"\s](?:https?:\/\/.+\/?)?.+?)(?P<post_path>[\'"\s\)]*;)/i',
|
||||
function ( $match ) use ( $dirpath, &$pre_output ) {
|
||||
if ( ! str_starts_with( $match['path'], 'http' ) && '/' !== $match['path'][0] ) {
|
||||
$pre_output .= $match['pre_path'] . ( $dirpath === '/' ? '/' : $dirpath . '/' ) .
|
||||
$match['path'] . $match['post_path'] . "\n";
|
||||
} else {
|
||||
$pre_output .= $match[0] . "\n";
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
$buf
|
||||
);
|
||||
}
|
||||
|
||||
// If filename indicates it's already minified, don't minify it again.
|
||||
if ( ! jetpack_boost_page_optimize_is_already_minified( $fullpath ) ) {
|
||||
// Minify CSS.
|
||||
$buf = Minify::css( $buf );
|
||||
}
|
||||
$output .= "$buf";
|
||||
} else {
|
||||
// If filename indicates it's already minified, don't minify it again.
|
||||
if ( ! jetpack_boost_page_optimize_is_already_minified( $fullpath ) ) {
|
||||
// Minify JS
|
||||
$buf = Minify::js( $buf );
|
||||
}
|
||||
|
||||
$output .= "$buf;\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Don't let trailing whitespace ruin everyone's day. Seems to get stripped by batcache
|
||||
// resulting in ns_error_net_partial_transfer errors.
|
||||
$output = rtrim( $output );
|
||||
|
||||
$headers = array(
|
||||
'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT',
|
||||
"Content-Type: $mime_type",
|
||||
);
|
||||
|
||||
return array(
|
||||
'headers' => $headers,
|
||||
'content' => $pre_output . $output,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file name and extension from the request URI.
|
||||
*
|
||||
* @param string $request_uri The request URI.
|
||||
* @return array|false The file name and extension, or false if the request URI is invalid.
|
||||
*/
|
||||
function jetpack_boost_minify_get_file_parts( $request_uri ) {
|
||||
$utils = new Utils();
|
||||
$request_uri = $utils->unslash( $request_uri );
|
||||
|
||||
$file_path = $utils->parse_url( $request_uri, PHP_URL_PATH );
|
||||
if ( $file_path === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_info = pathinfo( $file_path );
|
||||
|
||||
$minify_path = $utils->parse_url( jetpack_boost_get_minify_url(), PHP_URL_PATH );
|
||||
if ( trailingslashit( $file_info['dirname'] ) !== $minify_path ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed_extensions = array_keys( jetpack_boost_page_optimize_types() );
|
||||
if ( ! isset( $file_info['extension'] ) || ! in_array( $file_info['extension'], $allowed_extensions, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The base name (without the extension) might contain ".min".
|
||||
// Example - 777873a36e.min
|
||||
$file_name_parts = explode( '.', $file_info['basename'] );
|
||||
$file_name = $file_name_parts[0];
|
||||
|
||||
return array(
|
||||
'file_name' => $file_name,
|
||||
'file_extension' => $file_info['extension'] ?? '',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Load minify library code.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function jetpack_boost_minify_load_library(): void {
|
||||
require_once JETPACK_BOOST_DIR_PATH . '/app/lib/minify/class-utils.php';
|
||||
require_once JETPACK_BOOST_DIR_PATH . '/app/lib/minify/class-config.php';
|
||||
require_once JETPACK_BOOST_DIR_PATH . '/app/lib/minify/class-dependency-path-mapping.php';
|
||||
require_once JETPACK_BOOST_DIR_PATH . '/app/lib/minify/functions-helpers.php';
|
||||
require_once JETPACK_BOOST_DIR_PATH . '/app/lib/minify/functions-service-fallback.php';
|
||||
require_once JETPACK_BOOST_DIR_PATH . '/app/lib/minify/functions-service.php';
|
||||
}
|
||||
jetpack_boost_minify_load_library();
|
||||
|
||||
add_action(
|
||||
'template_redirect',
|
||||
function () {
|
||||
// Only intercept 404 requests.
|
||||
if ( ! is_404() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the path for the current request.
|
||||
$request_uri = remove_query_arg( 'JB_NONEXISTENTQUERY_ARG' );
|
||||
if ( ! str_contains( strtolower( $request_uri ), '/wp-content/boost-cache/static/' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
jetpack_boost_check_404_handler( $request_uri );
|
||||
|
||||
// Send a status 200 header, otherwise the browser will return a 404.
|
||||
status_header( 200 );
|
||||
jetpack_boost_handle_minify_request( $request_uri );
|
||||
exit( 0 );
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user