This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -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 = '&amp;' . $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 );
}
);