initial
This commit is contained in:
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync;
|
||||
use Automattic\Jetpack_Boost\Admin\Regenerate_Admin_Notice;
|
||||
use Automattic\Jetpack_Boost\Contracts\Changes_Output_After_Activation;
|
||||
use Automattic\Jetpack_Boost\Contracts\Feature;
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Data_Sync;
|
||||
use Automattic\Jetpack_Boost\Contracts\Needs_To_Be_Ready;
|
||||
use Automattic\Jetpack_Boost\Contracts\Optimization;
|
||||
use Automattic\Jetpack_Boost\Data_Sync\Critical_CSS_Meta_Entry;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Admin_Bar_Compatibility;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Invalidator;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync\Data_Sync_Schema;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions\Regenerate_CSS;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions\Set_Provider_CSS;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions\Set_Provider_Error_Dismissed;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Data_Sync_Actions\Set_Provider_Errors;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Display_Critical_CSS;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Generator;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Source_Providers\Source_Providers;
|
||||
use Automattic\Jetpack_Boost\Lib\Premium_Features;
|
||||
|
||||
class Critical_CSS implements Feature, Changes_Output_After_Activation, Optimization, Has_Data_Sync, Needs_To_Be_Ready {
|
||||
|
||||
/**
|
||||
* Critical CSS storage class instance.
|
||||
*
|
||||
* @var Critical_CSS_Storage
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Critical CSS Provider Paths.
|
||||
*
|
||||
* @var Source_Providers
|
||||
*/
|
||||
protected $paths;
|
||||
|
||||
/**
|
||||
* Prepare module. This is run irrespective of the module activation status.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->storage = new Critical_CSS_Storage();
|
||||
$this->paths = new Source_Providers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the module is ready and already serving critical CSS.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_ready() {
|
||||
return ( new Critical_CSS_State() )->is_generated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action names that will be triggered when the module is ready and serving critical CSS.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_change_output_action_names() {
|
||||
return array( 'jetpack_boost_critical_css_invalidated', 'jetpack_boost_critical_css_generated' );
|
||||
}
|
||||
|
||||
public static function is_available() {
|
||||
return true !== Premium_Features::has_feature( Premium_Features::CLOUD_CSS );
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only run if Critical CSS module has been activated.
|
||||
*/
|
||||
public function setup() {
|
||||
add_action( 'wp', array( $this, 'display_critical_css' ) );
|
||||
add_filter( 'jetpack_boost_total_problem_count', array( $this, 'update_total_problem_count' ) );
|
||||
|
||||
Generator::init();
|
||||
Critical_CSS_Invalidator::init();
|
||||
CSS_Proxy::init();
|
||||
|
||||
// Admin Notices
|
||||
Regenerate_Admin_Notice::init();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function register_data_sync( Data_Sync $instance ) {
|
||||
$instance->register( 'critical_css_state', Data_Sync_Schema::critical_css_state() );
|
||||
$instance->register( 'critical_css_meta', Data_Sync_Schema::critical_css_meta(), new Critical_CSS_Meta_Entry() );
|
||||
$instance->register( 'critical_css_suggest_regenerate', Data_Sync_Schema::critical_css_suggest_regenerate() );
|
||||
$instance->register_action( 'critical_css_state', 'request-regenerate', Schema::as_void(), new Regenerate_CSS() );
|
||||
$instance->register_action( 'critical_css_state', 'set-provider-css', Data_Sync_Schema::critical_css_set_provider(), new Set_Provider_CSS() );
|
||||
$instance->register_action( 'critical_css_state', 'set-provider-errors', Data_Sync_Schema::critical_css_set_provider_errors(), new Set_Provider_Errors() );
|
||||
$instance->register_action( 'critical_css_state', 'set-provider-errors-dismissed', Data_Sync_Schema::critical_css_set_provider_errors_dismissed(), new Set_Provider_Error_Dismissed() );
|
||||
}
|
||||
|
||||
public static function get_slug() {
|
||||
return 'critical_css';
|
||||
}
|
||||
|
||||
public function display_critical_css() {
|
||||
// Don't look for Critical CSS in the dashboard.
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
// Don't display Critical CSS when generating Critical CSS.
|
||||
if ( Generator::is_generating_critical_css() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show Critical CSS in customizer previews.
|
||||
if ( is_customize_preview() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Critical CSS to show.
|
||||
$critical_css = $this->paths->get_current_request_css();
|
||||
if ( ! $critical_css ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
|
||||
$critical_css = "/* Critical CSS Key: {$this->paths->get_current_critical_css_key()} */\n" . $critical_css;
|
||||
}
|
||||
|
||||
$display = new Display_Critical_CSS( $critical_css );
|
||||
add_action( 'wp_head', array( $display, 'display_critical_css' ), 0 );
|
||||
add_filter( 'style_loader_tag', array( $display, 'asynchronize_stylesheets' ), 10, 4 );
|
||||
add_action( 'wp_footer', array( $display, 'onload_flip_stylesheets' ) );
|
||||
|
||||
// Ensure admin bar compatibility.
|
||||
Admin_Bar_Compatibility::init();
|
||||
}
|
||||
|
||||
public function update_total_problem_count( $count ) {
|
||||
return ( new Critical_CSS_State() )->has_errors() ? ++$count : $count;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
|
||||
use Automattic\Jetpack_Boost\Lib\Critical_CSS\Display_Critical_CSS;
|
||||
|
||||
/**
|
||||
* Add an ajax endpoint to proxy external CSS files.
|
||||
*/
|
||||
class CSS_Proxy {
|
||||
const NONCE_ACTION = 'jb-generate-proxy-nonce';
|
||||
|
||||
public static function init() {
|
||||
$instance = new self();
|
||||
|
||||
if ( is_admin() ) {
|
||||
add_action( 'wp_ajax_boost_proxy_css', array( $instance, 'handle_css_proxy' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler to handle proxying of external CSS resources.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_css_proxy() {
|
||||
|
||||
// Verify valid nonce.
|
||||
if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), self::NONCE_ACTION ) ) {
|
||||
wp_die( '', 400 );
|
||||
}
|
||||
|
||||
// Make sure currently logged in as admin.
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( '', 400 );
|
||||
}
|
||||
|
||||
// Reject any request made when not generating.
|
||||
if ( ! ( new Critical_CSS_State() )->is_requesting() ) {
|
||||
wp_die( '', 400 );
|
||||
}
|
||||
|
||||
// Validate URL and fetch.
|
||||
$proxy_url = filter_var( wp_unslash( $_POST['proxy_url'] ?? '' ), FILTER_VALIDATE_URL );
|
||||
if ( ! wp_http_validate_url( $proxy_url ) ) {
|
||||
die( 'Invalid URL' );
|
||||
}
|
||||
|
||||
$url_path = wp_parse_url( $proxy_url, PHP_URL_PATH );
|
||||
if ( ! $url_path || substr( strtolower( $url_path ), -4 ) !== '.css' ) {
|
||||
wp_die( 'Invalid CSS file URL', 400 );
|
||||
}
|
||||
|
||||
$css = $this->get_proxied_css( $proxy_url );
|
||||
|
||||
if ( $css ) {
|
||||
$this->serve_proxied_css( $css );
|
||||
die( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the proxied CSS body with the appropriate headers.
|
||||
*
|
||||
* Separated from handle_css_proxy() so the output (and its </style
|
||||
* neutralization) is testable without the request teardown (die()).
|
||||
*
|
||||
* @param string $css CSS body to serve.
|
||||
*/
|
||||
protected function serve_proxied_css( $css ) {
|
||||
if ( ! headers_sent() ) {
|
||||
header( 'Content-type: text/css' );
|
||||
header( 'X-Content-Type-Options: nosniff' );
|
||||
}
|
||||
|
||||
/*
|
||||
* Outputting proxied CSS contents unescaped. Do not strip tags here;
|
||||
* valid CSS values may contain markup (e.g. inline SVGs in data: URIs),
|
||||
* and stripping them corrupts the CSS fed to the generator. The
|
||||
* text/css + nosniff headers stop a browser from sniffing this body as
|
||||
* HTML; neutralizing </style is defense-in-depth in case the body is
|
||||
* ever embedded inside a <style> element downstream.
|
||||
*/
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo Display_Critical_CSS::neutralize_style_closing_tags( $css );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the CSS for a proxied URL from cache, or by fetching and caching it.
|
||||
*
|
||||
* Separated from handle_css_proxy() so the cache/fetch logic is unit-testable
|
||||
* without the surrounding request teardown (die()).
|
||||
*
|
||||
* @param string $proxy_url Validated external CSS URL.
|
||||
* @return string The CSS body, or '' when there is none to serve.
|
||||
*/
|
||||
protected function get_proxied_css( $proxy_url ) {
|
||||
$cache_key = 'jb_css_proxy_' . md5( $proxy_url );
|
||||
$response = get_transient( $cache_key );
|
||||
|
||||
if ( is_array( $response ) && isset( $response['error'] ) ) {
|
||||
wp_die( esc_html( $response['error'] ), 400 );
|
||||
}
|
||||
|
||||
if ( is_string( $response ) ) {
|
||||
// Cache hit: the transient stores the CSS body from a previous
|
||||
// successful fetch. Without this branch a cached body was ignored
|
||||
// (the handler saw no CSS), so it returned nothing to the Critical
|
||||
// CSS generator.
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Anything other than a missing entry (false) has been handled above.
|
||||
if ( false !== $response ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$response = wp_safe_remote_get( $proxy_url );
|
||||
|
||||
// A transport failure must fail loudly with a 5xx. Falling through would
|
||||
// either mis-cache it as a bad content type or (via die()) emit an HTTP
|
||||
// 200 the Critical CSS generator would happily consume as a stylesheet,
|
||||
// silently corrupting that provider's Critical CSS.
|
||||
if ( is_wp_error( $response ) ) {
|
||||
wp_die( '', 502 );
|
||||
}
|
||||
|
||||
// Likewise, only a 2xx response is usable: an upstream 404/500 served with
|
||||
// a text/css content type must not be cached and served as valid CSS.
|
||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||
if ( $status_code < 200 || $status_code >= 300 ) {
|
||||
wp_die( '', 502 );
|
||||
}
|
||||
|
||||
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
|
||||
if ( strpos( $content_type, 'text/css' ) === false ) {
|
||||
set_transient( $cache_key, array( 'error' => 'Invalid content type. Expected CSS.' ), HOUR_IN_SECONDS );
|
||||
wp_die( 'Invalid content type. Expected CSS.', 400 );
|
||||
}
|
||||
|
||||
$css = wp_remote_retrieve_body( $response );
|
||||
|
||||
// Only cache a non-empty body. Caching an empty string would make
|
||||
// is_string() cache hits replay it for the full TTL, pinning empty
|
||||
// CSS for an hour after a single transient empty/failed fetch.
|
||||
if ( '' !== $css ) {
|
||||
set_transient( $cache_key, $css, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user