initial
This commit is contained in:
@@ -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