File "class-minify.php"
Full Path: /home/digimqhe/flashdigi.uk/comment-content/cgi-bin/core/modules/class-minify.php
File size: 63.85 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Minify module.
*
* @package Hummingbird\Core\Modules
*/
namespace Hummingbird\Core\Modules;
use Hummingbird\Core\Filesystem;
use Hummingbird\Core\Module;
use Hummingbird\Core\Settings;
use Hummingbird\Core\Utils;
use WP_Customize_Manager;
use WP_Scripts;
use WP_Styles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Minify
*/
class Minify extends Module {
/**
* List of groups to be processed at the end of the request
*
* @var array
*/
private $group_queue = array();
/**
* Source collector.
*
* @var Minify\Sources_Collector
*/
public $sources_collector;
/**
* Error controller.
*
* @var Minify\Errors_Controller
*/
public $errors_controller;
/**
* Houskeeper module.
*
* @var Minify\Housekeeper
*/
public $housekeeper;
/**
* Minify scanner.
*
* @var Minify\Scanner
*/
public $scanner;
/**
* Counter that will name scripts/styles slugs
*
* @var int
*/
private static $counter = 0;
/**
* Assets that have been already parsed.
*
* @var array $done
*/
public $done = array(
'scripts' => array(),
'styles' => array(),
);
/**
* Assets that go to footer.
*
* @var array $to_footer
*/
public $to_footer = array(
'styles' => array(),
'scripts' => array(),
);
/**
* Exclusion list.
*
* @since 2.7.2 Added 'lodash' script. It has an inlined script 'window.lodash = _.noConflict();' that prevents
* errors in browser console. Without that line, many core WordPress scripts will error out.
* @see https://incsub.atlassian.net/browse/HUM-404
*
* @var array $exclude_combine
*/
private $exclude_combine = array( 'lodash' );
/**
* Google fonts collection.
*
* @since 3.0.0
* @var array $fonts
*/
private $fonts = array();
/**
* Transient expiration timeout.
*
* @var string
*/
const AO_TRANSIENT_EXPIRATION = 60;
/**
* Transient name.
*
* @var string
*/
const AO_TRANSIENT_NAME = 'wphb-processing';
/**
* Cached result of the safe mode preview check.
*
* @var bool|null
*/
private $previewing_safe_mode = null;
/**
* Initializes the module. Always executed even if the module is deactivated.
*
* We need the scanner module to be always active, because HB uses is_scanning to detect
* if there is a scan going on.
*/
public function init() {
$this->scanner = new Minify\Scanner();
add_filter( 'wp_hummingbird_is_active_module_minify', array( $this, 'minify_module_status' ) );
add_filter( 'wphb_block_resource', array( $this, 'filter_resource_block' ), 10, 5 );
add_filter( 'wphb_minify_resource', array( $this, 'filter_resource_minify' ), 10, 4 );
add_filter( 'wphb_combine_resource', array( $this, 'filter_resource_combine' ), 10, 3 );
add_filter( 'wphb_defer_resource', array( $this, 'filter_resource_defer' ), 10, 3 );
add_filter( 'wphb_inline_resource', array( $this, 'filter_resource_inline' ), 10, 3 );
add_filter( 'wphb_preload_resource', array( $this, 'filter_resource_preload' ), 10, 3 );
add_filter( 'wphb_async_resource', array( $this, 'filter_resource_async' ), 10, 3 );
add_filter( 'wphb_send_resource_to_footer', array( $this, 'filter_resource_to_footer' ), 10, 3 );
add_filter( 'wphb_cdn_resource', array( $this, 'filter_resource_cdn' ), 10, 3 );
add_filter( 'wphb_minify_scan_url', array( $this, 'maybe_append_safe_mode_query_arg' ) );
add_filter( 'wphb_get_settings_for_module_minify', array( $this, 'maybe_serve_safe_mode_minify_settings' ) );
// Remove files from AO UI.
add_filter( 'wphb_minification_display_enqueued_file', array( $this, 'exclude_from_ao_ui' ), 10, 3 );
// Remove -rtl from CDN links.
add_filter( 'style_loader_tag', array( $this, 'remove_rtl_prefix_on_cdn' ) );
if ( $this->previewing_safe_mode() ) {
add_action(
'template_redirect',
function () {
ob_start( array( $this, 'add_safe_mode_param_to_links' ) );
}
);
add_filter( 'wphb_block_resource', array( $this, 'exclude_essential_safe_mode_scripts' ), 10, 2 );
add_filter( 'wphb_minify_resource', array( $this, 'exclude_essential_safe_mode_scripts' ), 10, 2 );
add_filter( 'wphb_combine_resource', array( $this, 'exclude_essential_safe_mode_scripts' ), 10, 2 );
}
add_action( 'admin_notices', array( $this, 'safe_mode_notice' ) );
}
/**
* Initializes Minify module
*/
public function init_module_action() {
$this->housekeeper = new Minify\Housekeeper();
$this->housekeeper->init();
$this->errors_controller = new Minify\Errors_Controller();
$this->sources_collector = new Minify\Sources_Collector();
}
/**
* Delete files attached to a `minify` group.
*
* @param int $post_id Post ID.
*/
public function on_delete_post( $post_id ) {
$group = Minify\Minify_Group::get_instance_by_post_id( $post_id );
if ( ( $group instanceof Minify\Minify_Group ) && $group->file_id ) {
if ( $group->get_file_path() && file_exists( $group->get_file_path() ) ) {
wp_delete_file( $group->get_file_path() );
}
wp_cache_delete( 'wphb_minify_groups' );
}
}
/**
* Execute the module actions. Executed when module is active.
*/
public function run() {
global $wp_customize, $pagenow;
$this->init_module_action();
add_action( 'init', array( $this, 'register_cpts' ) );
add_action( 'before_delete_post', array( $this, 'on_delete_post' ), 10 );
// Process the queue through WP Cron.
add_action( 'wphb_minify_process_queue', array( $this, 'process_queue' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_critical_css' ), 5 );
// Optimize fonts.
add_action( 'wphb_process_fonts', array( $this, 'process_fonts' ) );
// Disable module on login pages.
add_action( 'login_init', array( $this, 'disable_minify_on_page' ) );
$avoid_minify = filter_input( INPUT_GET, 'avoid-minify', FILTER_VALIDATE_BOOLEAN );
if ( $avoid_minify || 'wp-login.php' === $pagenow || $this->disable_minify_for_safe_mode() ) {
$this->disable_minify_on_page();
}
if ( is_admin() || is_customize_preview() || ( $wp_customize instanceof WP_Customize_Manager ) || apply_filters( 'wphb_do_not_run_ao_files', false ) ) {
return;
}
// Only minify on front.
add_filter( 'print_styles_array', array( $this, 'filter_styles' ), 5 );
add_filter( 'print_scripts_array', array( $this, 'filter_scripts' ), 5 );
add_action( 'wp_footer', array( $this, 'trigger_process_queue_cron' ), 10000 );
add_filter( 'wp_resource_hints', array( $this, 'prefetch_cdn_dns' ), 99, 2 );
// Google fonts optimization.
$this->fonts = Settings::get_setting( 'fonts', 'minify' );
if ( $this->fonts ) {
add_filter( 'style_loader_tag', array( $this, 'preload_fonts' ), 10, 3 );
}
}
/**
* Disable module on login pages. Fix conflicts with Defender masked login and LoginPress.
*
* @since 2.7.1
*/
public function disable_minify_on_page() {
add_filter( 'wp_hummingbird_is_active_module_' . $this->get_slug(), '__return_false' );
}
/**
* Register a new CPT for Assets groups
*/
public static function register_cpts() {
$labels = array(
'name' => 'WPHB Minify Groups',
'singular_name' => 'WPHB Minify Group',
);
$args = array(
'labels' => $labels,
'description' => 'WPHB Minify Groups (internal use)',
'public' => false,
'publicly_queryable' => false,
'show_ui' => false,
'show_in_menu' => false,
'query_var' => false,
'rewrite' => false,
'capability_type' => 'post',
'has_archive' => false,
'hierarchical' => false,
'supports' => array(),
);
register_post_type( 'wphb_minify_group', $args );
}
/**
* Used in tests.
*
* @return array
*/
public function get_queue_to_process() {
return $this->group_queue;
}
/**
* Filter styles
*
* @param array $handles List of styles slugs.
*
* @return array
*/
public function filter_styles( $handles ) {
return $this->filter_enqueues_list( $handles, 'styles' );
}
/**
* Filter scripts
*
* @param array $handles List of scripts slugs.
*
* @return array
*/
public function filter_scripts( $handles ) {
return $this->filter_enqueues_list( $handles, 'scripts' );
}
/**
* Filter the sources
*
* We'll collect those styles/scripts that are going to be
* processed by WP Hummingbird and return those that will
* be processed by WordPress
*
* @param array $handles List of scripts/styles slugs.
* @param string $type scripts|styles.
*
* @return array List of handles that will be processed by WordPress
*/
public function filter_enqueues_list( $handles, $type ) {
if ( ! $this->is_active() ) {
// Asset optimization is not active, return the handles.
return $handles;
}
if ( $this->errors_controller->is_server_error() ) {
// There seem to be an error in our severs, do not minify.
return $handles;
}
if ( 'styles' === $type ) {
global $wp_styles;
$wp_dependencies = $wp_styles;
} elseif ( 'scripts' === $type ) {
global $wp_scripts;
$wp_dependencies = $wp_scripts;
} else {
return $handles;
}
// Nothing to do, return the handles.
if ( empty( $handles ) ) {
return $handles;
}
$return_to_wp = array();
// Collect handles information to use in admin later.
foreach ( $handles as $key => $handle ) {
/**
* Not registered for some reason - return to WP.
*
* This has been added and removed from time to time. Not sure if this is the best way to do it, so I
* will try to history of commits.
*
* @since 2.7.1 Reverted the previous fix.
* @see https://incsub.atlassian.net/browse/HUM-294
*
* @since 2.7.2 Brought this back in a new updated way.
* @see https://incsub.atlassian.net/browse/HUM-482
*/
if ( ! isset( $wp_dependencies->registered[ $handle ] ) ) {
$return_to_wp = array_merge( $return_to_wp, array( $handle ) );
unset( $handles[ $key ] );
continue;
}
/**
* Filter to prevent adding a handle to the collection.
*
* @var bool false
* @var string $handle Source slug.
* @var string $source_url Source URL.
* @var string $type scripts|styles
*/
if ( apply_filters( 'wphb_dont_add_handle_to_collection', false, $handle, $wp_dependencies->registered[ $handle ]->src, $type ) ) {
$return_to_wp = array_merge( $return_to_wp, array( $handle ) );
unset( $handles[ $key ] );
continue;
}
// Only show items that have a handle and a source.
if ( ! empty( $wp_dependencies->registered[ $handle ]->src ) ) {
$this->sources_collector->add_to_collection( $wp_dependencies->registered[ $handle ], $type );
}
// If we aren't in footer, remove handles that need to go to footer.
if ( self::is_in_header() && ! self::is_in_footer() && isset( $wp_dependencies->groups[ $handle ] ) && $wp_dependencies->groups[ $handle ] ) {
$this->to_footer[ $type ][] = $handle;
unset( $handles[ $key ] );
}
}
$handles = array_values( $handles );
if ( self::is_in_footer() && ! empty( $this->to_footer[ $type ] ) ) {
// This is done to remove script, that are dequeued later.
$this->to_footer[ $type ] = array_intersect( $this->to_footer[ $type ], $handles );
// Header sent us some handles to be moved to footer.
$handles = array_unique( array_merge( $handles, $this->to_footer[ $type ] ) );
}
// Group dependencies by attributes like args, extra, etc.
$_groups = $this->group_dependencies_by_attributes( $handles, $wp_dependencies, $type );
// Create a Groups list object.
$groups_list = new Minify\Minify_Groups_List( $type );
array_map( array( $groups_list, 'add_group' ), $_groups );
unset( $_groups );
/**
* WARNING: This is dangerous, it can fall into an infinite loop if not treated with love and care.
* I've added a safety mechanism to try and counter infinite loops.
*/
$loop_counter = 0;
$loop_limit = apply_filters( 'wphb_group_split_loop_limit', 300 );
do {
$loop_counter++;
$needs_additional_splitting = $this->maybe_split_groups( $groups_list, $type );
if ( $loop_limit === $loop_counter ) {
set_transient( 'wphb_infinite_loop_warning', true, 3600 );
error_log( '[Hummingbird] Minify group infinite loop detected. Safety mechanism invoked, breaking out of loop.' );
break;
}
} while ( $needs_additional_splitting );
// Set the groups handles, as we need all of them before processing.
foreach ( $groups_list->get_groups() as $group ) {
$handles = $group->get_handles();
if ( count( $handles ) === 1 ) {
// Just one handle, let's keep the handle name as the group ID.
$group->group_id = $handles[0];
} else {
$group->group_id = 'wphb-' . ++self::$counter;
}
foreach ( $handles as $handle ) {
$this->done[ $type ][] = $handle;
}
}
if ( 'scripts' === $type ) {
$this->attach_scripts_localization( $groups_list, $wp_dependencies );
}
$this->attach_inline_attribute( $groups_list, $wp_dependencies );
// Parse dependencies, load files and mark groups as ready,process or only-handles
// Watch out! Groups must not be changed after this point!
$groups_list->preprocess_groups();
/**
* Minify group.
*
* @var Minify\Minify_Group $group
*/
foreach ( $groups_list->get_groups() as $group ) {
$group_status = $groups_list->get_group_status( $group->hash );
$deps = $groups_list->get_group_dependencies( $group->hash );
// The group has its file and is ready to be enqueued.
if ( 'ready' === $group_status ) {
$group->enqueue( self::is_in_footer(), $deps );
$return_to_wp = array_merge( $return_to_wp, array( $group->group_id ) );
} else {
// The group has not yet a file attached, or it cannot be processed for some reason.
foreach ( $group->get_handles() as $handle ) {
$group->enqueue_one_handle( $handle, self::is_in_footer(), $deps );
$return_to_wp = array_merge( $return_to_wp, array( $handle ) );
}
if ( 'process' === $group_status ) {
// Add the group to the queue to be processed later.
if ( $group->should_process_group() ) {
$this->group_queue[] = $group;
}
}
}
}
return $return_to_wp;
}
/**
* Try to split the groups. Recursive function.
*
* The idea behind this is that when groups are split, we need to check those new groups if they need to be
* split even further.
*
* This might be a minor performance hog on larger installs with a lot of settings in asset optimization.
* I have tested on a relatively small site (29 assets) with three assets set to be split up, and did not notice
* a significant difference in performance. This whole part took 1.59ms (xdebug enabled, worst score out of several
* runs) compared to 1.47ms without recursive functionality (best score out of several runs). Which is, worst
* case scenario, about 0.12ms per extra split run.
*
* @since 3.1.0
*
* @param Minify\Minify_Groups_List $groups_list Group list.
* @param string $type Scripts|styles.
*
* @return bool True when we need to do another pass, false when nothing else to split.
*/
private function maybe_split_groups( &$groups_list, $type ) {
// Time to split the groups if we're not combining some of them.
foreach ( $groups_list->get_groups() as $group ) {
/**
* Minify group.
*
* @var Minify\Minify_Group $group
*/
$dont_enqueue_list = $group->get_dont_enqueue_list();
if ( $dont_enqueue_list ) {
// There are one or more handles that should not be enqueued.
$group->remove_handles( $dont_enqueue_list );
if ( 'styles' === $type ) {
wp_dequeue_style( $dont_enqueue_list );
} else {
wp_dequeue_script( $dont_enqueue_list );
}
}
// No need to split a single group.
$handles = $group->get_handles();
if ( 1 === count( $handles ) ) {
continue;
}
$dont_combine_list = $group->get_dont_combine_list();
if ( $dont_combine_list ) {
$split_group = $this->get_splitted_group_structure_by( 'combine', $group );
$groups_list->split_group( $group->hash, $split_group );
return true;
}
$defer = $group->get_defer_list();
if ( 'scripts' === $type && $defer && $handles !== $defer ) {
$split_group = $this->get_splitted_group_structure_by( 'defer', $group );
$groups_list->split_group( $group->hash, $split_group );
return true;
}
$async = $group->get_async_list();
if ( 'scripts' === $type && $async && $handles !== $async ) {
$split_group = $this->get_splitted_group_structure_by( 'async', $group );
$groups_list->split_group( $group->hash, $split_group );
return true;
}
$inline = $group->get_inline_list();
if ( 'styles' === $type && $inline && $handles !== $inline ) {
$split_group = $this->get_splitted_group_structure_by( 'inline', $group );
$groups_list->split_group( $group->hash, $split_group );
return true;
}
$preload = $group->get_preload_list();
if ( $preload && $handles !== $preload ) {
$split_group = $this->get_splitted_group_structure_by( 'preload', $group );
$groups_list->split_group( $group->hash, $split_group );
return true;
}
}
return false;
}
/**
* Create a new group structure based on $by parameter
*
* This will allow later to split groups into new groups based on combination/deferring...
*
* @param string $by combine|defer|minify...
* @param Minify\Minify_Group $group Minify group.
* @param bool $value Value to apply if the handle should be done.
*
* @return array New structure
*/
private function get_splitted_group_structure_by( $by, $group, $value = true ) {
$handles = $group->get_handles();
// Here we'll save sources that don't need to be minified/combine/deferred...
// Then we'll extract those handles from the group, and we'll create
// a new group for them keeping the groups order.
$group_todos = array();
foreach ( $handles as $handle ) {
$value = absint( $value );
$not_value = absint( ! $value );
$group_todos[ $handle ] = $group->should_do_handle( $handle, $by ) ? $value : $not_value;
}
// Now split groups if needed based on $by value
// We need to keep always the order, ALWAYS
// This will save the new split group structure.
$split_group = array();
$last_status = null;
foreach ( $group_todos as $handle => $status ) {
// Last minify status will be the first one by default.
if ( is_null( $last_status ) ) {
$last_status = $status;
}
// Set the split groups to the last element.
end( $split_group );
if ( $last_status === $status && 0 !== $status ) {
$current_key = key( $split_group );
if ( ! $current_key ) {
// Current key can be NULL, set to 0.
$current_key = 0;
}
if ( ! isset( $split_group[ $current_key ] ) || ! is_array( $split_group[ $current_key ] ) ) {
$split_group[ $current_key ] = array();
}
$split_group[ $current_key ][] = $handle;
} else {
// Create a new group.
$split_group[] = array( $handle );
}
$last_status = $status;
}
return $split_group;
}
/**
* Group dependencies by alt, title, rtl, conditional and args attributes.
*
* This is a very-very fragile function. When making changes, please provide a detailed comment on why a
* change has been made.
*
* @param array $handles Handles array.
* @param WP_Scripts|WP_Styles $wp_dependencies List of dependencies.
* @param string $type Asset type: 'scripts' or 'styles'.
*
* @return array
*/
private function group_dependencies_by_attributes( $handles, $wp_dependencies, $type ) {
$groups = array();
$prev_differentiators_hash = false;
/**
* TODO: we only compare the current group with the previous group but what if two assets have the same attributes but they don't exists right beside each other in the list?
*/
foreach ( $handles as $handle ) {
$registered_dependency = isset( $wp_dependencies->registered[ $handle ] ) ? $wp_dependencies->registered[ $handle ] : false;
if ( ! $registered_dependency ) {
continue;
}
if ( ! self::is_in_footer() ) {
/**
* Filter the resource (move to footer)
*
* @usedby wphb_filter_resource_to_footer()
*
* @var bool $send_resource_to_footer
* @var string $handle Source slug
* @var string $type scripts|styles
* @var string $source_url Source URL
*/
if ( apply_filters( 'wphb_send_resource_to_footer', false, $handle, $type, $wp_dependencies->registered[ $handle ]->src ) ) {
// Move this to footer, do not take this handle in account for this iteration.
$this->to_footer[ $type ][] = $handle;
continue;
}
}
/**
* We'll group by these extras $wp_style->extras and $wp_style->args (args is no more than a string, confusing)
* If previous group has the same values, we'll add this dep it to that group
* otherwise, a new group will be created.
*/
$group_extra_differentiators = array( 'alt', 'title', 'rtl', 'conditional' );
$group_differentiators = array( 'args' );
// We'll create a hash for all differentiators.
// TODO: extract a method for generating hash
$differentiators_hash = array();
foreach ( $group_extra_differentiators as $differentiator ) {
if ( isset( $registered_dependency->extra[ $differentiator ] ) ) {
if ( is_bool( $registered_dependency->extra[ $differentiator ] ) && $registered_dependency->extra[ $differentiator ] ) {
$differentiators_hash[] = 'true';
} elseif ( is_bool( $registered_dependency->extra[ $differentiator ] ) && ! $registered_dependency->extra[ $differentiator ] ) {
$differentiators_hash[] = 'false';
} else {
$differentiators_hash[] = (string) $registered_dependency->extra[ $differentiator ];
}
} else {
$differentiators_hash[] = '';
}
}
foreach ( $group_differentiators as $differentiator ) {
if ( isset( $registered_dependency->$differentiator ) ) {
if ( is_bool( $registered_dependency->$differentiator ) && $registered_dependency->$differentiator ) {
$differentiators_hash[] = 'true';
} elseif ( is_bool( $registered_dependency->$differentiator ) && ! $registered_dependency->$differentiator ) {
$differentiators_hash[] = 'false';
} else {
$differentiators_hash[] = (string) $registered_dependency->$differentiator;
}
} else {
$differentiators_hash[] = '';
}
}
$differentiators_hash = implode( '-', $differentiators_hash );
// Now compare the hash with the previous one
// If they are the same, do not create a new group.
if ( $differentiators_hash !== $prev_differentiators_hash ) {
$new_group = new Minify\Minify_Group();
$new_group->set_type( $type );
foreach ( $registered_dependency->extra as $key => $value ) {
$new_group->add_extra( $key, $value );
}
// We'll treat this later.
$new_group->delete_extra( 'after' );
$new_group->delete_extra( 'before' );
$new_group->delete_extra( 'data' );
$new_group->set_args( $registered_dependency->args );
/**
* A bit of explanation behind this. Originally, we were only checking to see if the
* $registered_dependency->src was present. But at some point there were conflicts with themes/plugins
* that were enqueueing an asset with an empty source (just to inline something). That was first noticed
* with WP core mediaelement, with a fix introduced in 2.0. Then later on in 2.0.1 this lead to a more
* general approach of checking if there were some extra attributes for the asset.
*
* @since 2.0.0 This is not a perfect fix, but it works. 'mediaelement' script does not have a source
* file, but has an inline script with _wpmejsSettings variable. Without it, media
* elements do not function properly. So we do not exclude such a script.
* @since 2.0.1 Instead of checking for 'mediaelement', we check if there are extra attributes
* with $registered_dependency->extra
*/
if ( $registered_dependency->src || 0 < count( $registered_dependency->extra ) ) {
$new_group->add_handle( $handle, $registered_dependency->src, $registered_dependency->ver );
// Add dependencies.
$new_group->add_handle_dependency( $handle, $wp_dependencies->registered[ $handle ]->deps );
}
$groups[] = $new_group;
} else {
end( $groups );
$last_key = key( $groups );
$groups[ $last_key ]->add_handle( $handle, $registered_dependency->src, $registered_dependency->ver );
// Add dependencies.
$groups[ $last_key ]->add_handle_dependency( $handle, $registered_dependency->deps );
reset( $groups );
}
$prev_differentiators_hash = $differentiators_hash;
}
// Remove group without handles.
$return = array();
foreach ( $groups as $key => $group ) {
if ( $group->get_handles() ) {
$return[ $key ] = $group;
}
}
return $return;
}
/**
* Attach inline scripts/styles to groups
*
* Extract all deps that has inline scripts/styles (added by wp_add_inline_script/style functions)
* then it will add those extras to the groups
*
* @param Minify\Minify_Groups_List $groups_list Group list.
* @param WP_Scripts|WP_Styles $wp_dependencies List of dependencies.
*/
private function attach_inline_attribute( &$groups_list, $wp_dependencies ) {
$registered = $wp_dependencies->registered;
$extras = wp_list_pluck( $registered, 'extra' );
$after = wp_list_pluck( array_filter( $extras, array( $this, 'filter_after_after_attribute' ) ), 'after' );
$before = wp_list_pluck( array_filter( $extras, array( $this, 'filter_after_before_attribute' ) ), 'before' );
array_map(
function( $group ) use ( $groups_list, $after, $before ) {
/**
* Minify group.
*
* @var Minify\Minify_Group $group
*/
array_map(
function( $handle ) use ( $after, $group, $before ) {
if ( isset( $after[ $handle ] ) ) {
// Add!
$group->add_after( $after[ $handle ] );
}
if ( isset( $before[ $handle ] ) ) {
// Add!
$group->add_before( $before[ $handle ] );
}
},
$group->get_handles()
);
},
$groups_list->get_groups()
);
}
/**
* Attach localization scripts to groups
*
* @param Minify\Minify_Groups_List $groups_list Group list.
* @param WP_Scripts|WP_Styles $wp_dependencies List of dependencies.
*/
private function attach_scripts_localization( &$groups_list, $wp_dependencies ) {
$registered = $wp_dependencies->registered;
$extra = wp_list_pluck( $registered, 'extra' );
$data = wp_list_pluck(
array_filter(
$extra,
function( $attr ) {
if ( isset( $attr['data'] ) ) {
return $attr['data'];
}
return false;
}
),
'data'
);
array_map(
function( $group ) use ( $groups_list, $data ) {
/**
* Minify group.
*
* @var Minify\Minify_Group $group
*/
array_map(
function( $handle ) use ( $data, $group ) {
if ( isset( $data[ $handle ] ) ) {
$group->add_data( $data[ $handle ] ); // Add!
}
},
$group->get_handles()
);
},
$groups_list->get_groups()
);
}
/**
* Filter a list of dependencies returning their 'after' attribute inside 'extra' list
*
* @internal
*
* @param array $attr Attributes array.
*
* @return bool
*/
public function filter_after_after_attribute( $attr ) {
if ( isset( $attr['after'] ) ) {
return $attr['after'];
}
return false;
}
/**
* Filter a list of dependencies returning their 'before' attribute inside 'extra' list
*
* @internal
*
* @param array $attr Attributes array.
*
* @return bool
*/
public function filter_after_before_attribute( $attr ) {
if ( isset( $attr['before'] ) ) {
return $attr['before'];
}
return false;
}
/**
* Return if we are processing the header
*
* @return bool
* @since 2.6.0
*/
public static function is_in_header() {
return doing_action( 'wp_head' ) || doing_action( 'wp_print_header_scripts' );
}
/**
* Return if we are processing the footer
*
* @return bool
*/
public static function is_in_footer() {
return doing_action( 'wp_footer' ) || doing_action( 'wp_print_footer_scripts' );
}
/**
* Trigger the action to process the queue
*/
public function trigger_process_queue_cron() {
// Trigger the queue through WP CRON, so we don't waste load time.
$this->sources_collector->save_collection();
$queue = $this->get_queue_to_process();
$this->add_items_to_persistent_queue( $queue );
$queue = $this->get_pending_persistent_queue();
if ( empty( $queue ) ) {
return;
}
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
$this->process_queue();
} else {
self::schedule_process_queue_cron();
}
}
/**
* Process the queue: Minify and combine files
*/
public function process_queue() {
// Process the queue.
if ( get_transient( self::AO_TRANSIENT_NAME ) ) {
// Still processing. Try again.
if ( ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) {
self::schedule_process_queue_cron();
}
return;
}
$queue = $this->get_pending_persistent_queue();
set_transient( self::AO_TRANSIENT_NAME, true, self::AO_TRANSIENT_EXPIRATION );
// Process 10 groups max in a request.
$count = 0;
$new_queue = $queue;
foreach ( $queue as $key => $item ) {
if ( $count >= 8 ) {
break;
}
if ( ! ( $item instanceof Minify\Minify_Group ) ) {
continue;
}
if ( $item->should_generate_file() ) {
$result = $item->process_group();
if ( is_wp_error( $result ) ) {
$this->errors_controller->add_server_error( $result );
}
}
$this->remove_item_from_persistent_queue( $item->hash );
unset( $new_queue[ $key ] );
$count++;
}
if ( ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) {
if ( ! empty( $new_queue ) ) {
// Still needs processing.
self::schedule_process_queue_cron();
}
}
if ( empty( $new_queue ) ) {
// Finish processing.
delete_transient( self::AO_TRANSIENT_NAME );
// Update AO completion date.
self::update_ao_completion_time();
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
/**
* Unfortunately, during cron we are not able to detect the first page load, so it will get cached.
* Page load -> page caching and cron are triggered at the same time, but this is the limitation,
* that page cache will not have the wphb-processing transient at this stage. To counter this,
* we will purge all cache when Asset Optimization is done.
*
* @since 3.0.0
* @see Page_Cache::cache_request() for transient check without cron.
*/
do_action( 'wphb_clear_page_cache' );
}
} else {
// Refresh transient.
set_transient( self::AO_TRANSIENT_NAME, true, self::AO_TRANSIENT_EXPIRATION );
}
}
/**
* Schedule queue process through WP Cron.
*/
public static function schedule_process_queue_cron() {
if ( ! wp_next_scheduled( 'wphb_minify_process_queue' ) ) {
wp_schedule_single_event( time(), 'wphb_minify_process_queue' );
}
}
/**
* Save a list of groups to a persistent option in database.
*
* If a timeout happens during groups processing, we won't lose the data needed to process the rest of groups.
*
* @param array $items Array of items.
*/
private function add_items_to_persistent_queue( $items ) {
// Nothing to be added.
if ( empty( $items ) ) {
return;
}
$current_queue = $this->get_pending_persistent_queue();
if ( empty( $current_queue ) ) {
update_option( 'wphb_process_queue', $items, 'no' );
return;
}
$updated = false;
$current_queue_hashes = wp_list_pluck( $current_queue, 'hash' );
foreach ( $items as $item ) {
if ( ! in_array( $item->hash, $current_queue_hashes, true ) ) {
$updated = true;
$current_queue[] = $item;
}
}
if ( $updated ) {
update_option( 'wphb_process_queue', $current_queue, 'no' );
}
}
/**
* Remove a group from the persistent queue
*
* @param string $hash Item hash.
*/
private function remove_item_from_persistent_queue( $hash ) {
$queue = $this->get_pending_persistent_queue();
$items = wp_list_filter(
$queue,
array(
'hash' => $hash,
)
);
if ( ! $items ) {
return;
}
$keys = array_keys( $items );
foreach ( $keys as $key ) {
unset( $queue[ $key ] );
}
$queue = array_values( $queue );
if ( empty( $queue ) ) {
$this->delete_pending_persistent_queue();
return;
}
update_option( 'wphb_process_queue', $queue, 'no' );
}
/**
* Get the list of groups that are yet pending to be processed
*/
public function get_pending_persistent_queue() {
return get_option( 'wphb_process_queue', array() );
}
/**
* Deletes the persistent queue completely
*/
public function delete_pending_persistent_queue() {
delete_option( 'wphb_process_queue' );
wp_cache_delete( 'wphb_process_queue', 'options' );
}
/**
* Implement abstract parent method for clearing cache.
*
* Clear the module cache.
*
* @param bool $reset_settings If set to true will set Asset Optimization settings to default (that includes files positions).
* @param bool $reset_minify Reset minify settings.
* @param bool $keep_collection Keep collections. If removed, will require to visit the homepage.
*
* @return bool
*/
public function clear_cache( $reset_settings = true, $reset_minify = true, $keep_collection = false ) {
$this->clear_files();
// Reset AO completion time.
self::update_ao_completion_time( true );
if ( $reset_settings ) {
// This one when cleared will trigger a new scan.
if ( ! $keep_collection ) {
Minify\Sources_Collector::clear_collection();
}
$options = $this->get_options();
$default_options = Settings::get_default_settings();
// Reset the minification settings.
if ( $reset_minify ) {
$options['dont_minify'] = $default_options['minify']['dont_minify'];
$options['dont_combine'] = $default_options['minify']['dont_combine'];
}
$options['block'] = $default_options['minify']['block'];
$options['position'] = $default_options['minify']['position'];
$options['defer'] = $default_options['minify']['defer'];
$options['inline'] = $default_options['minify']['inline'];
$options['fonts'] = $default_options['minify']['fonts'];
$options['preload'] = $default_options['minify']['preload'];
$options['async'] = $default_options['minify']['async'];
$this->update_options( $options );
}
// Clear the pending process queue.
self::clear_pending_process_queue();
$this->scanner->reset_scan();
Minify\Errors_Controller::clear_errors();
return true;
}
/**
* Clear pending queue.
*/
public static function clear_pending_process_queue() {
delete_transient( 'wphb_infinite_loop_warning' );
delete_option( 'wphb_process_queue' );
wp_cache_delete( 'wphb_process_queue', 'options' );
delete_transient( self::AO_TRANSIENT_NAME );
}
/**
* Update AO completion time.
*
* @param bool $reset Reset completed time.
*/
public static function update_ao_completion_time( $reset = false ) {
if ( ! Utils::is_ao_status_bar_enabled() ) {
return;
}
$get_date_time = date_i18n( get_option( 'date_format' ) ) . ' @ ' . date_i18n( get_option( 'time_format' ) );
$get_date_time = $reset ? '' : $get_date_time;
// Update setting.
Settings::update_setting( 'ao_completed_time', $get_date_time, 'minify' );
}
/**
* Disable minification module.
*/
public function disable() {
$this->toggle_service( false );
$this->clear_cache();
$this->delete_safe_mode();
// Delete notices if they are there.
delete_option( 'wphb-minification-files-scanned' );
delete_site_option( 'wphb-notice-minification-optimized-show' );
// Clear cron events.
if ( wp_next_scheduled( 'wphb_minify_clear_files' ) ) {
wp_clear_scheduled_hook( 'wphb_minify_clear_files' );
}
}
/**
* Reset to default settings for minification module.
*
* @since 2.6.0
*/
public function reset_minification_settings() {
$default = Settings::get_default_settings();
$minify_default = $default[ $this->get_slug() ];
// Settings that need to be reset.
$ao_settings = array( 'do_assets', 'view', 'type', 'use_cdn', 'nocdn', 'delay_js', 'delay_js_timeout', 'delay_js_exclusions', 'delay_js_files_exclusion', 'delay_js_post_types_exclusion', 'delay_js_post_urls_exclusion', 'delay_js_plugins_themes_exclusion', 'delay_js_ads_tracker_exclusion', 'delay_js_exclude_inline_js', 'delay_js_keywords_advanced_view', 'critical_css', 'critical_css_type', 'critical_css_remove_type', 'critical_css_mode', 'critical_page_types', 'critical_skipped_custom_post_types', 'above_fold_load_stylesheet_method', 'critical_css_files_exclusion', 'critical_css_post_urls_exclusion', 'critical_css_plugins_themes_exclusion', 'critical_css_keywords', 'font_optimization', 'font_swap', 'font_display_value', 'preload_fonts_mode' );
// These settings are only valid for single sites or network admin.
if ( ! is_multisite() || is_network_admin() ) {
$ao_settings = array_merge( $ao_settings, array( 'file_path', 'log' ) );
}
$ao_settings_default = array_intersect_key( $minify_default, array_flip( $ao_settings ) );
// Get current settings for minify.
$minify_settings = $this->get_options();
$minify_settings = array_merge( $minify_settings, $ao_settings_default );
// Reset Critical css.
self::save_css( '' );
self::save_css( '', 'manual-critical' );
$this->update_options( $minify_settings );
}
/**
* *************************
* FILTERS
***************************/
/**
* Filter module status.
*
* @param bool $current Current status.
*
* @return bool
*/
public function minify_module_status( $current ) {
if ( ! apply_filters( 'wphb_should_minify_on_page', true ) ) {
return false;
}
$options = $this->get_options();
if ( false === $options['enabled'] ) {
return false;
}
if ( is_multisite() ) {
$current = $options['minify_blog'];
} else {
$current = $options['enabled'];
}
return $current;
}
/**
* Filter blocker resources.
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_block( $value, $handle, $type ) {
$options = $this->get_options();
$blocked = $options['block'][ $type ];
if ( in_array( $handle, $blocked, true ) ) {
return true;
}
return $value;
}
/**
* Filter minified resources.
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
* @param string $url Script URL.
*
* @return bool
*/
public function filter_resource_minify( $value, $handle, $type, $url ) {
$options = $this->get_options();
$minify = $options['dont_minify'][ $type ];
if ( is_array( $minify ) && in_array( $handle, $minify, true ) ) {
return false;
}
// If handle is already available in error, then ignore the handle.
if ( $this->errors_controller->get_handle_error( $handle, $type ) ) {
return false;
}
// Filter already minified resources.
if ( ! empty( $url ) && preg_match( '/\.min\.(css|js)/', basename( $url ) ) ) {
return false;
}
return $value;
}
/**
* Filter combine resources.
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_combine( $value, $handle, $type ) {
$options = $this->get_options();
$combine = $options['dont_combine'][ $type ];
$delay_js = $options['delay_js'];
if ( true === $delay_js && 'scripts' === $type ) {
return false;
}
/**
* Filter to disable the combine.
*
* @param array $value Whether to disable the combine or not, default false.
* @param array $handle Resource handle.
* @param string $type Script or style..
*/
if ( apply_filters( 'wphb_dont_combine_handles', false, $handle, $type ) ) {
return false;
}
if ( $this->errors_controller->get_handle_error( $handle, $type ) ) {
return false;
}
if ( in_array( $handle, $combine, true ) ) {
return false;
}
if ( in_array( $handle, $this->exclude_combine, true ) ) {
return false;
}
return $value;
}
/**
* Filter defer resources.
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_defer( $value, $handle, $type ) {
$options = $this->get_options();
$defer = $options['defer'][ $type ];
if ( ! in_array( $handle, $defer, true ) ) {
return $value;
}
return true;
}
/**
* Filter inline resources.
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_inline( $value, $handle, $type ) {
$options = $this->get_options();
$defer = $options['inline'][ $type ];
if ( ! in_array( $handle, $defer, true ) ) {
return $value;
}
return true;
}
/**
* Filter move to footer resources.
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_to_footer( $value, $handle, $type ) {
$options = $this->get_options();
$to_footer = $options['position'][ $type ];
if ( ! in_array( $handle, $to_footer, true ) ) {
return $value;
}
return true;
}
/**
* Filter out assets from using CDN.
*
* @since 2.4.0
*
* @param bool $value Current CDN status.
* @param array $handles Array of handles (or single handle).
* @param string $type Scripts or styles.
*
* @return bool
*/
public function filter_resource_cdn( $value, $handles, $type ) {
$options = Settings::get_setting( 'nocdn', 'minify' );
foreach ( $handles as $handle ) {
if ( in_array( $handle, $options[ $type ], true ) ) {
$value = false;
}
}
return $value;
}
/**
* Exclude files from the AO list.
*
* @since 2.7.2
*
* @param bool $action Exclude or not.
* @param array|string $handle Handle.
* @param string $type Asset type: styles or scripts.
*
* @return bool
*/
public function exclude_from_ao_ui( $action, $handle, $type ) {
if ( is_array( $handle ) && isset( $handle['handle'] ) ) {
$handle = $handle['handle'];
}
if ( 'scripts' === $type && in_array( $handle, $this->exclude_combine, true ) ) {
return false;
}
return $action;
}
/**
* Filter preload resources.
*
* @since 3.1.0
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_preload( $value, $handle, $type ) {
$options = $this->get_options();
if ( ! in_array( $handle, $options['preload'][ $type ], true ) ) {
return $value;
}
return true;
}
/**
* Filter async resources.
*
* @since 3.1.0
*
* @param bool $value Current value.
* @param string $handle Resource handle.
* @param string $type Script or style.
*
* @return bool
*/
public function filter_resource_async( $value, $handle, $type ) {
$options = $this->get_options();
if ( ! in_array( $handle, $options['async'][ $type ], true ) ) {
return $value;
}
return true;
}
/**
* *************************
* HELPER FUNCTIONS
***************************/
/**
* Clear cache for selected file.
*
* @since 1.9.2
*
* @param string $handle Handle.
* @param string $type Type.
*/
public function clear_file( $handle, $type ) {
$groups = Minify\Minify_Group::get_groups_from_handle( $handle, $type );
foreach ( $groups as $group ) {
if ( 'wphb_minify_group' === get_post_type( $group->file_id ) ) {
Utils::get_module( 'minify' )->log( 'Deleting (in clear_file function) the minify group file id : ' . $group->file_id );
wp_delete_post( $group->file_id );
}
}
}
/**
* Clear minified group files
*/
public function clear_files() {
$groups = Minify\Minify_Group::get_minify_groups();
foreach ( $groups as $group ) {
// This will also delete the file. See WP_Hummingbird\Core\Modules\Minify::on_delete_post().
if ( 'wphb_minify_group' === get_post_type( $group->ID ) ) {
Utils::get_module( 'minify' )->log( 'Deleting (in clear_files function) the minify group ID : ' . $group->ID );
wp_delete_post( $group->ID );
}
}
wp_cache_delete( 'wphb_minify_groups' );
// Clear all the page cache.
do_action( 'wphb_clear_page_cache' );
}
/**
* Get all resources collected
*
* This collection is displayed in minification admin page
*/
public function get_resources_collection() {
$collection = Minify\Sources_Collector::get_collection();
$posts = Minify\Minify_Group::get_minify_groups();
foreach ( $posts as $post ) {
$group = Minify\Minify_Group::get_instance_by_post_id( $post->ID );
if ( ! $group ) {
continue;
}
foreach ( $group->get_handles() as $handle ) {
if ( isset( $collection[ $group->type ][ $handle ] ) ) {
$collection[ $group->type ][ $handle ]['original_size'] = $group->get_handle_original_size( $handle );
$collection[ $group->type ][ $handle ]['compressed_size'] = $group->get_handle_compressed_size( $handle );
$collection[ $group->type ][ $handle ]['file_url'] = $group->get_file_url();
}
}
}
return $collection;
}
/**
* Init minification scan.
*/
public function init_scan() {
$this->clear_cache( false );
// Activate minification if is not.
$this->toggle_service( true );
// Init scan.
$this->scanner->init_scan();
}
/**
* Toggle minification.
*
* @param bool $value Value for minification. Accepts boolean value: true or false.
* @param bool $network Value for network. Default: false.
*/
public function toggle_service( $value, $network = false ) {
$options = $this->get_options();
if ( is_multisite() ) {
if ( $network ) {
// Updating for the whole network.
$options['enabled'] = $value;
// If deactivated for whole network, also deactivate CDN.
if ( false === $value ) {
$options['use_cdn'] = false;
$options['log'] = false;
}
} else {
// Updating on subsite.
if ( ! $options['enabled'] ) {
// Asset optimization is turned down for the whole network, do not activate it per site.
$options['minify_blog'] = false;
} else {
$options['minify_blog'] = $value;
}
}
} else {
$options['enabled'] = $value;
}
$this->update_options( $options );
}
/**
* Toggle CDN helper function.
*
* @param bool $value CDN status to set.
*/
public function toggle_cdn( $value ) {
$options = $this->get_options();
$options['use_cdn'] = $value;
$this->update_options( $options );
}
/**
* Get CDN status.
*
* @since 1.5.2
* @return bool
*/
public function get_cdn_status() {
$options = $this->get_options();
return $options['use_cdn'];
}
/**
* Enqueue critical CSS file (css above the fold).
*
* @since 1.8
*/
public function enqueue_critical_css() {
// If critical css is enable return early.
if ( Utils::get_module( 'critical_css' )->is_active() ) {
return;
}
$assets_dir = Filesystem::critical_assets_dir();
$file = $assets_dir['path'] . 'critical.css';
if ( ! file_exists( $file ) ) {
return;
}
$content = file_get_contents( $file );
if ( empty( $content ) ) {
return;
}
$url = $assets_dir['url'] . 'critical.css';
wp_register_style( 'wphb-critical-css', $url, array(), filemtime( $file ) );
wp_enqueue_style( 'wphb-critical-css' );
}
/**
* Get css file content for critical css file.
*
* @since 1.8
*
* @param string $filename CSS filename.
* @return string
*/
public static function get_css( $filename = 'critical' ) {
$assets_dir = Filesystem::critical_assets_dir();
$file = $assets_dir['path'] . $filename . '.css';
if ( file_exists( $file ) ) {
return file_get_contents( $file );
}
return '';
}
/**
* Save critical css file (css above the fold).
*
* @since 1.8
*
* @param string $content CSS content.
* @param string $filename CSS filename.
*
* @return array
*/
public static function save_css( $content, $filename = 'critical' ) {
if ( ! is_string( $content ) ) {
return array(
'success' => false,
'message' => __( 'Unsupported content', 'wphb' ),
);
}
$fs = Filesystem::instance();
if ( is_wp_error( $fs->status ) ) {
return array(
'success' => false,
'message' => __( 'Error saving file', 'wphb' ),
);
}
$assets_dir = Filesystem::critical_assets_dir();
$file = $assets_dir['path'] . $filename . '.css';
$content = trim( $content );
if ( ! empty( $content ) ) {
$status = $fs->write( $file, $content );
if ( is_wp_error( $status ) ) {
return array(
'success' => false,
'message' => $status->get_error_message(),
);
}
} else {
if ( file_exists( $file ) ) {
unlink( $file );
}
}
return array(
'success' => true,
'message' => __( 'Settings updated', 'wphb' ),
);
}
/**
* Return a list of fields used on the wp_postmeta table.
*
* @since 2.7.0
*
* @return array
*/
public static function get_postmeta_fields() {
return array(
'_handles',
'_handle_urls',
'_handle_versions',
'_extra',
'_args',
'_type',
'_dont_minify',
'_dont_combine',
'_dont_enqueue',
'_defer',
'_inline',
'_preload',
'_async',
'_handle_dependencies',
'_handle_original_sizes',
'_handle_compressed_sizes',
'_hash',
'_file_id',
'_url',
'_expires',
);
}
/**
* CDN does not support -rtl suffixes, so we remove those from style links
*
* @since 2.7.2
*
* @param string $rtl_tag Style tag.
*
* @return string
*/
public function remove_rtl_prefix_on_cdn( $rtl_tag ) {
// If not from Hummingbird CDN - skip.
if ( false === strpos( $rtl_tag, 'hb.wpmucdn.com' ) ) {
return $rtl_tag;
}
// If does not contain -rtl prefix - skip.
if ( false === strpos( $rtl_tag, '-rtl.' ) ) {
return $rtl_tag;
}
return str_replace( '-rtl.', '.', $rtl_tag );
}
/**
* Replace Google fonts with a preloaded version.
*
* @since 3.0.0
*
* @param string $tag The link tag for the enqueued style.
* @param string $handle The style's registered handle.
* @param string $href The stylesheet's source URL.
*
* @return string
*/
public function preload_fonts( $tag, $handle, $href ) {
if ( ! in_array( $handle, $this->fonts, true ) ) {
return $tag;
}
$fonts = '<link rel="preload" as="style" href="' . $href . '" />';
$fonts .= str_replace( "media='all'", "media='print' onload='this.media="all"'", $tag );
return $fonts;
}
/**
* Add CDN URL to header for better speed.
*
* @since 3.1.0
*
* @param array $hints URLs to print for resource hints.
* @param string $relation_type The relation type the URLs are printed.
*
* @return array
*/
public function prefetch_cdn_dns( $hints, $relation_type ) {
// Add only if CDN active.
if ( 'dns-prefetch' === $relation_type && $this->get_cdn_status() ) {
$hints[] = '//hb.wpmucdn.com';
}
return $hints;
}
/**
* Auto optimize all fonts after scans.
*
* @since 3.3.0
*/
public function process_fonts() {
$options = $this->get_options();
$collection = Minify\Sources_Collector::get_collection();
if ( ! isset( $collection['styles'] ) ) {
return;
}
$updated = false;
foreach ( $collection['styles'] as $item ) {
if ( ! isset( $item['src'] ) || false === strpos( $item['src'], 'fonts.googleapis.com' ) ) {
continue;
}
$key = array_search( $item['handle'], $options['fonts'], true );
// Add new font to optimization array.
if ( false === $key ) {
array_push( $options['fonts'], $item['handle'] );
$updated = true;
}
}
if ( $updated ) {
$this->update_options( $options );
}
}
/**
* Filter through enable/disable switchers.
*
* @since 3.4.0
*
* @param array $asset Asset details.
* @param string $type Asset type: scripts|styles.
*
* @return array
*/
private function get_disabled_switchers( $asset, $type ) {
$error = $this->errors_controller->get_handle_error( $asset['handle'], $type );
$disable_switchers = $error ? $error['disable'] : array();
/**
* Allows enable/disable switchers in minification page.
*
* @param array $disable_switchers List of switchers disabled for an item ( include, minify, combine).
* @param array $item Info about the current item.
* @param string $type Type of the current item (scripts|styles).
*/
$disable_switchers = apply_filters( 'wphb_minification_disable_switchers', $disable_switchers, $asset, $type );
// Disable inline for assets larger than 4 kb.
if ( 'styles' === $type && apply_filters( 'wphb_inline_limit_kb', 4.0 ) < (float) $asset['originalSize'] && ! in_array( 'inline', $disable_switchers, true ) ) {
$disable_switchers[] = 'inline';
}
return $disable_switchers;
}
/**
* Process collection.
*
* @since 3.4.0
*
* @return array
*/
public function get_processed_collection() {
$collection = $this->get_resources_collection();
// This will be used for filtering.
$theme = wp_get_theme();
$plugins = get_option( 'active_plugins', array() );
if ( is_multisite() ) {
foreach ( get_site_option( 'active_sitewide_plugins', array() ) as $plugin => $item ) {
$plugins[] = $plugin;
}
}
foreach ( $collection as $type => $assets ) {
if ( is_array( $assets ) || is_object( $assets ) ) {
foreach ( $assets as $handle => $asset ) {
/**
* Filter minification enqueued files items displaying.
*
* @param bool $display If set to true, display the item. Default false.
* @param array $item Item data.
* @param string $type Type of the current item (scripts|styles).
*/
if ( ! apply_filters( 'wphb_minification_display_enqueued_file', true, $asset, $type ) ) {
unset( $collection[ $type ][ $handle ] );
continue;
}
// Remove unused fields.
unset( $asset['args'] );
unset( $asset['deps'] );
unset( $asset['extra'] );
unset( $asset['textdomain'] );
unset( $asset['translations_path'] );
unset( $asset['ver'] );
$settings = array(
'component' => '',
'extension' => 'OTHER',
'filter' => '',
'isLocal' => Minify\Minify_Group::is_src_local( $asset['src'] ),
);
$asset['compressedSize'] = isset( $asset['compressed_size'] ) ? $asset['compressed_size'] : false;
unset( $asset['compressed_size'] );
// Get original file size for local files that don't have it set for some reason.
if ( ! isset( $asset['original_size'] ) && file_exists( Utils::src_to_path( $asset['src'] ) ) ) {
$asset['original_size'] = number_format_i18n( filesize( Utils::src_to_path( $asset['src'] ) ) / 1000, 1 );
}
// With remote assets we can't easily get the file size without doing extra remote queries.
if ( isset( $asset['original_size'] ) ) {
$asset['originalSize'] = $asset['original_size'];
unset( $asset['original_size'] );
} else {
$asset['originalSize'] = false;
}
if ( isset( $asset['file_url'] ) ) {
$asset['fileUrl'] = empty( $asset['file_url'] )
? ''
: $asset['file_url'];
unset( $asset['file_url'] );
}
$settings['disableSwitchers'] = $this->get_disabled_switchers( $asset, $type );
if ( preg_match( '/wp-content\/themes\/(.*)\//', $asset['src'], $matches ) ) {
$settings['component'] = 'theme';
$settings['filter'] = $theme->get( 'Name' );
} elseif ( preg_match( '/wp-content\/plugins\/([\w\-_]*)\//', $asset['src'], $matches ) ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
// The source comes from a plugin.
foreach ( $plugins as $active_plugin ) {
if ( stristr( $active_plugin, $matches[1] ) ) {
// It seems that we found the plugin but let's double-check.
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $active_plugin );
if ( $plugin_data['Name'] ) {
// Found plugin, add it as a filter.
$settings['filter'] = $plugin_data['Name'];
}
break;
}
}
$settings['component'] = 'plugin';
}
$extension = pathinfo( $asset['src'], PATHINFO_EXTENSION );
if ( false !== strpos( $asset['src'], 'fonts.googleapis.com' ) ) {
$settings['extension'] = 'FONT';
} elseif ( $extension && preg_match( '/(css)\??[a-zA-Z=0-9]*/', $extension ) ) {
$settings['extension'] = 'CSS';
} elseif ( $extension && preg_match( '/(js)\??[a-zA-Z=0-9]*/', $extension ) ) {
$settings['extension'] = 'JS';
}
// Add settings to the asset.
$asset['settings'] = $settings;
// If this is a Google font - move to fonts section.
if ( 'FONT' === $settings['extension'] ) {
unset( $collection[ $type ][ $handle ] );
$collection['fonts'][ $handle ] = $asset;
} else {
$collection[ $type ][ $handle ] = $asset;
}
}
}
}
// Get minify stats data.
$dashboard_data = Utils::get_ao_stats_data();
$collection['dashboard_data'] = $dashboard_data;
return $collection;
}
/**
* Returns true if safe mode is active, and we are *not* in the safe mode preview.
*
* @since 3.4.0
*
* @return bool
*/
private function disable_minify_for_safe_mode() {
if ( is_admin() ) {
return false;
}
if ( ! self::get_safe_mode_status() ) {
return false;
}
$status = $this->previewing_safe_mode();
return true !== $status;
}
public function maybe_append_safe_mode_query_arg( $url ) {
if ( self::get_safe_mode_status() ) {
$url = add_query_arg( 'minify-safe', 'true', $url );
}
return $url;
}
public function maybe_serve_safe_mode_minify_settings( $settings ) {
if ( $this->previewing_safe_mode() ) {
return array_merge( $settings, $this->get_safe_mode_settings() );
}
return $settings;
}
public static function get_safe_mode_status() {
$value = self::get_safe_mode_option_value();
return $value['status'];
}
public function set_safe_mode_status( $status ) {
$value = self::get_safe_mode_option_value();
$value['status'] = $status;
$this->set_safe_mode_option_value( $value );
}
/**
* @return array
*/
public function get_safe_mode_settings() {
$value = self::get_safe_mode_option_value();
return $value['settings'];
}
public function set_safe_mode_settings( $settings ) {
$value = self::get_safe_mode_option_value();
$value['settings'] = $settings;
$this->set_safe_mode_option_value( $value );
}
public function delete_safe_mode() {
Settings::delete( 'wphb_safe_mode' );
}
public function reset_safe_mode() {
$this->set_safe_mode_option_value( array(
'status' => false,
'settings' => array(),
) );
}
private function set_safe_mode_option_value( $value ) {
Settings::update( 'wphb_safe_mode', $value );
return $value;
}
private static function get_safe_mode_option_value() {
$raw_value = Settings::get( 'wphb_safe_mode', array() );
$value = array();
$value['status'] = ! empty( $raw_value['status'] );
$value['settings'] = empty( $raw_value['settings'] ) || ! is_array( $raw_value['settings'] )
? array()
: $raw_value['settings'];
return $value;
}
/**
* @return mixed
*/
private function previewing_safe_mode() {
if ( null === $this->previewing_safe_mode ) {
$safe_mode_status = self::get_safe_mode_status();
$query_param_value = filter_input( INPUT_GET, 'minify-safe', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
$this->previewing_safe_mode = $safe_mode_status && true === $query_param_value;
}
return $this->previewing_safe_mode;
}
public function add_safe_mode_param_to_links( $content ) {
if ( ! preg_match( '/(?=<body).*<\/body>/is', $content, $body ) ) {
return $content;
}
$body = $body[0];
$links = array();
$safe_mode_links = array();
foreach ( $this->find_internal_links( $body ) as $link ) {
if ( empty( $link ) || ! is_string( $link ) ) {
continue;
}
$delimiter = '~';
$link_pattern = "$delimiter" . preg_quote( $link, $delimiter ) . "(?=\s*[\"'])$delimiter";
$links[] = $link_pattern;
$safe_mode_links[] = $this->is_frontend_link( $link )
? esc_url_raw( add_query_arg( 'minify-safe', 'true', $link ) )
: $link;
}
$safe_mode_body = preg_replace( $links, $safe_mode_links, $body );
if ( ! empty( $safe_mode_body ) ) {
$content = str_replace( $body, $safe_mode_body, $content );
}
return $content;
}
private function find_internal_links( $content ) {
$links = array();
$elements = $this->get_tags( $content, 'a' );
if ( ! $elements || ! is_a( $elements, '\DOMNodeList' ) ) {
return $links;
}
for ( $i = 0; $i < $elements->length; $i ++ ) {
/**
* @var $element \DOMElement
*/
$element = $elements->item( $i );
if ( ! $element && ! is_a( $element, '\DOMElement' ) ) {
continue;
}
$attribute = $element->getAttribute( 'href' );
if ( ! empty( $attribute ) && $this->is_internal_link( $attribute ) ) {
$links[ $attribute ] = $attribute;
}
}
return array_values( array_unique( $links ) );
}
private function get_tags( $markup, $tag ) {
if ( ! class_exists( '\DOMDocument' ) || ! function_exists( 'libxml_use_internal_errors' ) ) {
return false;
}
$document = new \DOMDocument();
$internalErrors = libxml_use_internal_errors( true );
$html = $document->loadHTML( $markup, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
libxml_use_internal_errors( $internalErrors );
return $html ? $document->getElementsByTagName( $tag ) : false;
}
private function is_internal_link( $link ) {
return parse_url( $link, PHP_URL_HOST ) === parse_url( home_url(), PHP_URL_HOST );
}
private function is_frontend_link( $link ) {
return ! $this->is_admin_link( $link )
&& ! $this->is_asset_link( $link );
}
private function is_admin_link( $link ) {
$admin_url = untrailingslashit( admin_url() );
$admin_url_parts = explode( '/', $admin_url );
if ( empty( $admin_url_parts ) || empty( $link ) ) {
return false;
}
$last_part_index = count( $admin_url_parts ) - 1;
$admin_identifier = $admin_url_parts[ $last_part_index ]; // This is usually going to be wp-admin but not always (e.g. due to defender)
return mb_strpos( trailingslashit( $link ), "/$admin_identifier/" ) !== false;
}
private function get_wp_media_extensions() {
$extensions = array();
foreach ( wp_get_ext_types() as $type_extensions ) {
$extensions = array_merge(
$extensions,
$type_extensions
);
}
return $extensions;
}
private function is_asset_link( $url ) {
foreach ( $this->get_wp_media_extensions() as $extension ) {
if ( str_ends_with( $url, ".$extension" ) ) {
return true;
}
}
return false;
}
public function exclude_essential_safe_mode_scripts( $block, $handle ) {
if ( $handle === 'wphb-global' ) {
return false;
}
return $block;
}
public function safe_mode_notice() {
if ( ! self::get_safe_mode_status() || ! current_user_can( Utils::get_admin_capability() ) ) {
return;
}
$current_screen = get_current_screen();
if ( $current_screen && str_ends_with( $current_screen->id, 'wphb-minification' ) ) {
// Don't show on the minification page itself
return;
}
$message = esc_html__( "We've noticed that you have Safe Mode active in Hummingbird Asset Optimization. Keeping safe mode active for a long period of time may cause page load delays on your live site. We recommend that you review your changes and publish them to live, or disable safe mode.", 'wphb' );
$disable_safe_mode_url = admin_url( 'admin.php?page=wphb-minification&action=disable_safe_mode' );
?>
<div class="notice notice-warning">
<p><?php echo wp_kses_post( $message ); ?></p>
<div style="margin-bottom: 10px; display:flex; align-items:center;">
<a class="button button-primary"
href="<?php echo esc_attr( $disable_safe_mode_url ); ?>">
<?php esc_html_e( 'Disable safe mode', 'wphb' ); ?>
</a>
</div>
</div>
<?php
}
}