This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,353 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\Admin\Cache;
use Sphere\Debloat\Admin\OptionsData;
/**
* Admin initialization.
*
* @author asadkn
* @since 1.0.0
*/
class Admin
{
/**
* @var Sphere\Debloat\Admin\Cache
*/
protected $cache;
/**
* Setup hooks
*/
public function init()
{
$this->cache = new Cache;
$this->cache->init();
add_action('cmb2_admin_init', [$this, 'setup_options']);
// Enqueue at a lower priority to be after CMB2.
add_action('admin_enqueue_scripts', [$this, 'register_assets'], 99);
// Page to delete cache.
add_action('admin_menu', function() {
add_submenu_page(
'',
'Delete Cache',
'Delete Cache',
'manage_options',
'debloat-delete-cache',
[$this, 'delete_cache']
);
});
// Empty cache on save.
add_action('cmb2_save_options-page_fields', [$this, '_delete_cache']);
/**
* Fix: CMB2 doesn't save unchecked making default => true impossible.
*/
add_filter('cmb2_sanitize_checkbox', function($override, $value) {
return is_null($value) ? '0' : $value;
}, 20, 2);
// Custom CMB2 field for manual callback
add_action('cmb2_render_manual', function($field) {
// Add attributes to an empty span for cmb2-conditional
if (!empty($field->args['attributes'])) {
printf('<meta name="%s" %s />',
$field->args('id'),
\CMB2_Utils::concat_attrs($field->args('attributes'))
);
}
if (!empty($field->args['render_html']) && is_callable($field->args['render_html'])) {
call_user_func($field->args['render_html'], $field);
}
if (!empty($field->args['desc'])) {
echo '<p class="cmb2-metabox-description">' . esc_html($field->args['desc']) . '</p>';
}
});
}
/**
* Register admin assets
*/
public function register_assets()
{
// Specific assets for option pages only
if (!empty($_GET['page']) && strpos($_GET['page'], 'debloat_options') !== false) {
wp_enqueue_script(
'debloat-cmb2-conditionals',
Plugin::get_instance()->dir_url . 'js/admin/cmb2-conditionals.js',
['jquery'],
Plugin::VERSION
);
wp_enqueue_script(
'debloat-options',
Plugin::get_instance()->dir_url . 'js/admin/options.js',
['jquery', 'debloat-cmb2-conditionals'],
Plugin::VERSION
);
wp_enqueue_style(
'debloat-admin-cmb2',
Plugin::get_instance()->dir_url . 'css/admin/cmb2.css',
['cmb2-styles'],
Plugin::VERSION
);
}
}
/**
* Delete Cache page.
*/
public function delete_cache()
{
check_admin_referer('debloat_delete_cache');
$this->_delete_cache();
echo '
<h2>Clearing Cache</h2>
<p>Caches cleared. You may also have to clear your cache plugins.</p>
<a href="' . esc_url(admin_url('admin.php?page=debloat_options')) . '">Back to Options</a>';
}
/**
* Callback: Delete the cache.
*
* @access private
*/
public function _delete_cache()
{
$this->cache->empty();
/**
* Hook after deleting cache.
*/
do_action('debloat/after_delete_cache');
}
/**
* Setup admin options with CMB2
*/
public function setup_options()
{
// Configure admin options
$options = new_cmb2_box([
'id' => 'debloat_options',
'title' => esc_html__('Debloat Plugin Settings', 'debloat'),
'object_types' => ['options-page'],
'option_key' => 'debloat_options',
'parent_slug' => 'options-general.php',
'menu_title' => esc_html__('Debloat: Optimize', 'debloat'),
'tab_group' => 'debloat_options',
'tab_title' => esc_html__('Optimize CSS', 'debloat'),
'classes' => 'sphere-cmb2-wrap',
'display_cb' => [$this, 'render_options_page'],
]);
$this->add_options(
OptionsData::get_css(),
$options
);
// Configure admin options
$js_options = new_cmb2_box([
'id' => 'debloat_options_js',
'title' => esc_html__('Optimize JS', 'debloat'),
'object_types' => ['options-page'],
'option_key' => 'debloat_options_js',
'parent_slug' => 'debloat_options',
'menu_title' => esc_html__('Optimize JS', 'debloat'),
'tab_group' => 'debloat_options',
'tab_title' => esc_html__('Optimize JS', 'debloat'),
'classes' => 'sphere-cmb2-wrap',
'display_cb' => [$this, 'render_options_page'],
]);
$this->add_options(
OptionsData::get_js(),
$js_options
);
// Configure admin options
$general_options = new_cmb2_box([
'id' => 'debloat_options_general',
'title' => esc_html__('General Settings', 'debloat'),
'object_types' => ['options-page'],
'option_key' => 'debloat_options_general',
'parent_slug' => 'debloat_options',
'menu_title' => esc_html__('General Settings', 'debloat'),
'tab_group' => 'debloat_options',
'tab_title' => esc_html__('General Settings', 'debloat'),
'display_cb' => [$this, 'render_options_page'],
'classes' => 'sphere-cmb2-wrap'
]);
$this->add_options(
OptionsData::get_general(),
$general_options
);
do_action('debloat/admin/after_options', $options);
}
/**
* Add options to CMB2 array.
*
* @param array $options
* @param \CMB2 $object
* @return void
*/
protected function add_options($options, $object)
{
return array_map(
function($option) use ($object) {
if (isset($option['attributes']['data-conditional-id'])) {
$condition = &$option['attributes']['data-conditional-id'];
if (is_array($condition)) {
$condition = json_encode($condition);
}
}
$field_id = $object->add_field($option);
if ($option['type'] === 'group') {
$this->add_option_group($option, $field_id, $object);
}
},
$options
);
}
protected function add_option_group($option, $group_id, $object)
{
if ($option['id'] === 'allow_conditionals_data') {
$object->add_group_field($group_id, [
'id' => 'type',
'name' => esc_html__('Condition Type', 'debloat'),
'type' => 'radio',
'default' => 'class',
'options' => [
'class' => esc_html__('Class - If a class (in "condition match") exists in HTML, keep classes matching "selector match".', 'debloat'),
'prefix' => esc_html__('Prefix - Condition matches the first class and keeps all the used child classes. Example: .s-dark will keep .s-dark .site-header.', 'debloat'),
],
]);
$object->add_group_field($group_id, [
'id' => 'match',
'name' => esc_html__('Condition Match', 'debloat'),
'desc' => esc_html__('Required. Usually a single class, example:', 'debloat') . '<code>.my-class</code>',
'type' => 'text',
'default' => '',
]);
$object->add_group_field($group_id, [
'id' => 'search',
'name' => esc_html__('Selectors Match', 'debloat'),
'desc' => esc_html__('Enter one per line. See example matchings in "Always Keep Selectors" above.', 'debloat'),
'type' => 'textarea_small',
'default' => '',
'attributes' => [
'data-conditional-id' => json_encode([$group_id, 'type']),
'data-conditional-value' => 'class'
]
]);
}
}
public function render_options_page($hookup)
{
?>
<div class="cmb2-options-page debloat-options option-<?php echo esc_attr( sanitize_html_class( $hookup->option_key ) ); ?>">
<div class="wrap">
<?php if ( $hookup->cmb->prop( 'title' ) ) : ?>
<h2><?php echo wp_kses_post( $hookup->cmb->prop( 'title' ) ); ?></h2>
<?php endif; ?>
</div>
<div class="wrap"><?php $hookup->options_page_tab_nav_output(); ?></div>
<div class="debloat-inner-wrap">
<form class="cmb-form debloat-options-form" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="POST" id="<?php echo esc_attr($hookup->cmb->cmb_id); ?>" enctype="multipart/form-data" encoding="multipart/form-data">
<input type="hidden" name="action" value="<?php echo esc_attr( $hookup->option_key ); ?>">
<div class="sphere-cmb2-wrap debloat-intro-info">
<div class="cmb2-wrap cmb2-metabox">
<div class="cmb-row">
<h3>Important: Debloat Plugin</h3>
<p>
This plugin is for advanced users. The features "Remove Unused CSS" and "Delay JS" are especially for advanced users only.
</p>
<ol>
<li>Use a cache plugin like W3 Total Cache, WP Super Cache, etc. <strong>Required</strong> for Remove Unused CSS feature.</li>
<li>Do <strong>NOT</strong> enable minification, CSS, or JS optimization via another plugin.</li>
<li>If your theme doesn't have it built-in, use a Lazyload plugin for images.</li>
</ol>
</div>
</div>
</div>
<?php $hookup->options_page_metabox(); ?>
<?php submit_button( esc_attr( $hookup->cmb->prop( 'save_button' ) ), 'primary', 'submit-cmb' ); ?>
</form>
<div class="debloat-sidebar">
<?php $this->cache_info(); ?>
</div>
</div>
</div>
<?php
}
public function cache_info()
{
$js_cache = Plugin::file_cache()->get_stats('js');
$css_cache = Plugin::file_cache()->get_stats('css');
// Number of css sheets in cache.
$css_sheets = count($this->cache->get_transients());
?>
<div class="sphere-cmb2-wrap debloat-cache-info">
<div class="cmb2-wrap cmb2-metabox">
<div class="cmb-row cmb-type-title">
<div class="cmb-td">
<h3 class="cmb2-metabox-title">
<?php esc_html_e('Cache Stats', 'debloat'); ?>
</h3>
</div>
</div>
<div class="cmb-row">
<?php if (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG): ?>
<p><strong>Minification Disabled</strong>: SCRIPT_DEBUG is enabled (likely in wp-config.php).</p>
<?php endif; ?>
<div class="cache-stats">
<p><?php printf(esc_html__('Minified CSS files: %d', 'debloat'), $css_cache); ?></p>
<p><?php printf(esc_html__('Minified JS files: %d', 'debloat'), $js_cache); ?></p>
<p><?php printf(esc_html__('Processed CSS Sheets: %d', 'debloat'), $css_sheets); ?></p>
</div>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=debloat-delete-cache'), 'debloat_delete_cache'); ?>"
class="button button-secondary" style="margin-top: 10px;">
<?php echo esc_html('Empty All Cache', 'debloat'); ?>
</a>
</p>
</div>
</div>
</div>
<?php
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace Sphere\Debloat\Admin;
use Sphere\Debloat\Plugin;
/**
* Cache clear, stats and similar for admin area.
*
* @author asadkn
* @since 1.0.0
*/
class Cache
{
protected $deleting = false;
public function init()
{
$this->register_clear_hooks();
}
/**
* Register hooks to clear cache.
*
* @return void
*/
public function register_clear_hooks()
{
/**
* Use this hook to clear caches externally.
*/
add_action('debloat/empty_caches', [$this, 'empty']);
/**
* Other plugin hooks to empty cache on.
*/
$hooks = [
// WP Rocket.
'after_rocket_clean_domain',
// W3 Total Cache.
'w3tc_flush_all',
// SGF plugin font cache delete.
'sgf/after_delete_cache',
];
foreach ($hooks as $hook) {
add_action($hook, [$this, 'empty']);
}
}
/**
* Get all the debloat cache transients.
*
* @return array
*/
public function get_transients()
{
global $wpdb;
return (array) $wpdb->get_results(
"SELECT `option_name` FROM {$wpdb->options} WHERE `option_name` LIKE '_transient_debloat_sheet_cache_%'",
ARRAY_A
);
}
/**
* Delete all the debloat cache transients.
*
* @return void
*/
protected function delete_transients()
{
foreach ($this->get_transients() as $transient) {
$transient = str_replace('_transient_', '', $transient['option_name']);
\delete_transient($transient);
}
}
/**
* Delete all types of caches.
*
* @return void
*/
public function empty()
{
// Already clearing the caches. Needed as we hook into plugins clear methods,
// but also call them ourselves on cache clear.
if ($this->deleting) {
return;
}
// One of the hooks set via register_clear_hooks() may fire.
$this->deleting = true;
$this->delete_transients();
// Delete files cache.
Plugin::file_cache()->delete_cache('js');
Plugin::file_cache()->delete_cache('css');
// Clear cache plugins.
$this->empty_cache_plugins();
$this->deleting = false;
}
/**
* Empty external caches from plugins and hosts.
*
* @return void
*/
protected function empty_cache_plugins()
{
// WP Super Cache.
if (function_exists('wp_cache_clear_cache')) {
wp_cache_clear_cache(
is_multisite() ? get_current_blog_id() : 0
);
}
// W3 Total Cache.
if (function_exists('w3tc_pgcache_flush')) {
w3tc_pgcache_flush();
}
// WP Rocket.
if (function_exists('rocket_clean_domain')) {
rocket_clean_domain();
}
// WP Fastest Cache.
if (function_exists('wpfc_clear_all_cache')) {
wpfc_clear_all_cache();
}
// Swift Performance plugin.
if (class_exists('\Swift_Performance_Cache') && is_callable(['\Swift_Performance_Cache', 'clear_all_cache'])) {
\Swift_Performance_Cache::clear_all_cache();
}
// LiteSpeed Cache.
if (class_exists('\LiteSpeed_Cache_API') && is_callable(['\LiteSpeed_Cache_API', 'purge_all'])) {
\LiteSpeed_Cache_API::purge_all();
}
// Cache Enabler.
if (class_exists('\Cache_Enabler') && is_callable(['\Cache_Enabler', 'clear_total_cache'])) {
\Cache_Enabler::clear_total_cache();
}
// Comet cache.
if (class_exists('\comet_cache') && is_callable(['\comet_cache', 'clear'])) {
\comet_cache::clear();
}
// RT Nginx Helper plugin.
if (defined('NGINX_HELPER_BASENAME')) {
do_action('rt_nginx_helper_purge_all');
}
// Hummingbird
if (class_exists('\Hummingbird\WP_Hummingbird') && is_callable(['\Hummingbird\WP_Hummingbird', 'flush_cache'])) {
\Hummingbird\WP_Hummingbird::flush_cache();
}
// Pagely.
if (class_exists('\PagelyCachePurge') && is_callable(['\PagelyCachePurge', 'purgeAll'])) {
\PagelyCachePurge::purgeAll();
}
// WPEngine
if (class_exists('\WpeCommon')) {
is_callable(['\WpeCommon', 'purge_memcached']) && \WpeCommon::purge_memcached();
is_callable(['\WpeCommon', 'purge_varnish_cache']) && \WpeCommon::purge_varnish_cache();
}
// SiteGround.
if (function_exists('sg_cachepress_purge_cache')) {
sg_cachepress_purge_cache();
}
}
}

View File

@ -0,0 +1,446 @@
<?php
namespace Sphere\Debloat\Admin;
/**
* Options data.
*/
class OptionsData
{
/**
* Common shared data and options.
*
* @param string $key
* @return array
*/
public static function get_common($key = '')
{
$_common = [];
$_common['enable_on'] = [
'all' => esc_html__('All Pages', 'debloat'),
'single' => esc_html__('Single Post/Article', 'debloat'),
'pages' => esc_html__('Pages', 'delobat'),
'home' => esc_html__('Homepage', 'delobat'),
'archives' => esc_html__('Archives', 'delobat'),
'categories' => esc_html__('Categories', 'delobat'),
'search' => esc_html__('Search', 'delobat'),
];
return $key ? $_common[$key] : $_common;
}
public static function get_css()
{
$options = [];
$options[] = [
'name' => esc_html__('Optimize CSS', 'debloat'),
// 'description' => 'foo',
'type' => 'title',
'id' => '_optimize_css',
];
$options[] = [
'id' => 'optimize_css',
'name' => esc_html__('Fix Render-Blocking CSS', 'debloat'),
'desc' => esc_html__('Enable CSS Optimizations to fix Render-blocking CSS.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
];
$options[] = [
'id' => 'optimize_css_to_inline',
'name' => esc_html__('Inline Optimized CSS', 'debloat'),
'desc' => esc_html__('Inline the CSS to prevent flash of unstyled content. Highly recommended.', 'debloat'),
'type' => 'checkbox',
'default' => 1,
'attributes' => ['data-conditional-id' => 'optimize_css'],
];
$options[] = [
'id' => 'optimize_gfonts_inline',
'name' => esc_html__('Inline Google Fonts CSS', 'debloat'),
'desc' => esc_html__('Inline the Google Fonts CSS for a big boost on FCP and slight on LCP on mobile. Highly recommended.', 'debloat'),
'type' => 'checkbox',
'default' => 1,
'attributes' => ['data-conditional-id' => 'optimize_css'],
];
$options[] = [
'id' => 'optimize_css_minify',
'name' => esc_html__('Minify CSS', 'debloat'),
'desc' => esc_html__('Minify CSS to reduced the CSS size.', 'debloat'),
'type' => 'checkbox',
'default' => 1,
'attributes' => ['data-conditional-id' => 'optimize_css'],
];
$options[] = [
'id' => 'optimize_css_excludes',
'name' => esc_html__('Exclude Styles', 'debloat'),
'desc' =>
esc_html__('Enter one per line to exclude certain CSS files from this optimizations. Examples:', 'debloat')
. ' <code>id:my-css-id</code>
<br /><code>wp-content/themes/my-theme/style.css</code>
<br /><code>wp-content/themes/my-theme*</code>
',
'type' => 'textarea_small',
'default' => '',
'attributes' => ['data-conditional-id' => 'optimize_css'],
];
$options[] = [
'id' => 'integrations_css',
'name' => esc_html__('Enable Plugin Integrations', 'debloat'),
'desc' => esc_html__('Special pre-made rules for CSS, specific to plugins, are applied if enabled.', 'debloat'),
'type' => 'multicheck_inline',
'options' => [
'elementor' => 'Elementor',
'wpbakery' => 'WPBakery Page Builder',
],
'default' => ['elementor', 'wpbakery'],
'select_all_button' => false,
];
$options[] = [
'id' => 'optimize_gfonts',
'name' => esc_html__('Optimize Google Fonts', 'debloat'),
'desc' => esc_html__('Add preconnect hints and add display swap for Google Fonts.', 'debloat'),
'type' => 'checkbox',
'default' => 1,
];
$options[] = [
'name' => esc_html__('Optimize CSS: Remove Unused', 'debloat'),
// 'description' => 'foo',
'type' => 'title',
'id' => '_remove_unused',
];
$options[] = [
'id' => 'remove_css',
'name' => esc_html__('Remove Unused CSS', 'debloat'),
'desc' => esc_html__('This is an expensive process. DO NOT use without a cache plugin.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
];
$options[] = [
'id' => 'remove_css_all',
'name' => esc_html__('Remove from All Stylesheets', 'debloat'),
'desc' => esc_html__('WARNING: Only use if you are sure your plugins and themes dont add classes using JS. May also be enabled when delay loading all the original CSS.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
'attributes' => ['data-conditional-id' => 'remove_css'],
];
$options[] = [
'id' => 'remove_css_plugins',
'name' => esc_html__('Enable for Plugins CSS', 'debloat'),
'desc' => esc_html__('Removed unused CSS on all plugins CSS files.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
'attributes' => [
'data-conditional-id' => [
['key' => 'remove_css'],
['key' => 'remove_css_all', 'value' => 'off'],
]
],
];
$options[] = [
'id' => 'remove_css_theme',
'name' => esc_html__('Enable for Theme CSS', 'debloat'),
'desc' => esc_html__('Removed unused CSS from all theme CSS files.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
'attributes' => [
'data-conditional-id' => [
['key' => 'remove_css'],
['key' => 'remove_css_all', 'value' => 'off'],
]
],
];
$options[] = [
'id' => 'remove_css_includes',
'name' => esc_html__('Target Stylesheets', 'debloat'),
'desc' =>
esc_html__('Will remove unused CSS from these targets. You may use an ID or the part of the URL. Examples:', 'debloat')
. ' <code>id:my-css-id</code>
<br /><code>wp-content/themes/my-theme/style.css</code>
<br /><code>wp-content/themes/my-theme*</code>: All theme stylesheets.
<br /><code>plugins/plugin-slug/*</code>: All stylesheets for plugin-slug.
',
'type' => 'textarea_small',
'default' => 'id:wp-block-library',
'attributes' => [
'data-conditional-id' => [
['key' => 'remove_css'],
['key' => 'remove_css_all', 'value' => 'off'],
]
],
];
$options[] = [
'id' => 'remove_css_excludes',
'name' => esc_html__('Exclude Stylesheets', 'debloat'),
'desc' =>
esc_html__('Enter one per line to exclude certain CSS files from this optimizations. Examples:', 'debloat')
. ' <code>id:my-css-id</code>
<br /><code>wp-content/themes/my-theme/style.css</code>
<br /><code>wp-content/themes/my-theme*</code>
',
'type' => 'textarea_small',
'default' => '',
'attributes' => ['data-conditional-id' => 'remove_css'],
];
$options[] = [
'id' => 'allow_css_selectors',
'name' => esc_html__('Always Keep Selectors', 'debloat'),
'desc' =>
esc_html__('Enter one per line. Partial or full matches for selectors (if any of these keywords found, the selector will be kept). Examples:', 'debloat')
. ' <code>.myclass</code>
<br /><code>.myclass*</code>: Will match selectors starting with .myclass, .myclass-xyz, .myclass_xyz etc.
<br /><code>.myclass *</code>: Selectors starting with .myclass, .myclass .sub-class and so on.
<br /><code>*.myclass *</code>: For matching .xyz .myclass, .myclass, .xyz .myclass .xyz and so on.
',
'type' => 'textarea_small',
'default' => '',
'attributes' => ['data-conditional-id' => 'remove_css'],
];
$options[] = [
'id' => 'allow_css_conditionals',
'name' => esc_html__('Advanced: Conditionally Keep Selectors', 'debloat'),
'desc' => 'Add advanced conditions.',
'type' => 'checkbox',
'default' => 0,
'attributes' => ['data-conditional-id' => 'remove_css'],
];
$options[] = [
'id' => 'allow_conditionals_data',
'name' => '',
'desc' => 'Keep selector if a certain condition is true. For example, condition type class with match <code>.mfp-lightbox</code> can be used to search for <code>.mfp-</code> to keep all the CSS selectors that have .mfp- in selector.',
'type' => 'group',
'default' => [],
'attributes' => ['data-conditional-id' => 'remove_css'],
'options' => [
'group_title' => 'Condition {#}',
'add_button' => esc_html__('Add Condition', 'debloat'),
'remove_button' => esc_html__('Remove', 'debloat'),
'closed' => true,
]
];
$options[] = [
'id' => 'remove_css_on',
'name' => esc_html__('Remove CSS On', 'debloat'),
'desc' => esc_html__('Pages where unused CSS should be removed.', 'debloat'),
'type' => 'multicheck',
'options' => self::get_common('enable_on'),
'default' => ['all'],
'select_all_button' => false,
'attributes' => ['data-conditional-id' => 'remove_css'],
];
$options[] = [
'id' => 'delay_css_load',
'name' => esc_html__('Delay load Original CSS', 'debloat'),
'desc' => esc_html__('Delay-loading all of the original CSS might be needed in situations where there are too many JS-based CSS classes that are added later such as sliders, that you cannot track down and add to exclusions right now. Or on pages that may have Auto-load Next Post.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
'attributes' => ['data-conditional-id' => 'remove_css'],
];
$options[] = [
'id' => 'delay_css_on',
'name' => esc_html__('Delay load Original On', 'debloat'),
'desc' => esc_html__('Pages where original CSS should be delayed load.', 'debloat'),
'type' => 'multicheck',
'options' => self::get_common('enable_on'),
'default' => ['all'],
'select_all_button' => false,
'attributes' => ['data-conditional-id' => 'delay_css_load'],
];
return $options;
}
public static function get_js()
{
$options = [];
/**
* Optimize JS
*/
$options[] = [
'name' => esc_html__('Optimize JS', 'debloat'),
// 'description' => 'foo',
'type' => 'title',
'id' => '_defer_js',
];
$options[] = [
'id' => 'defer_js',
'name' => esc_html__('Defer Javascript', 'debloat'),
'desc' => esc_html__('Delay JS execution till HTML is loaded to fix Render-Blocking JS issues.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
];
$options[] = [
'id' => 'defer_js_excludes',
'name' => esc_html__('Exclude Scripts', 'debloat'),
'desc' => esc_html__('Enter one per line to exclude certain JS files from being deferred.', 'debloat'),
'type' => 'textarea_small',
'default' => '',
'attributes' => ['data-conditional-id' => 'defer_js'],
];
$options[] = [
'id' => 'defer_js_inline',
'name' => esc_html__('Defer Inline JS', 'debloat'),
'desc' => sprintf(
'%s<p><strong>%s</strong> %s</p>',
esc_html__('Defer all inline JS.', 'debloat'),
esc_html__('Note:', 'debloat'),
esc_html__('Normally not needed. All correct dependent inline scripts are deferred by default. Enable if inline JS not enqueued using WordPress enqueue functions.', 'debloat')
),
'type' => 'checkbox',
'default' => 0,
'attributes' => ['data-conditional-id' => 'defer_js'],
];
$options[] = [
'id' => 'minify_js',
'name' => esc_html__('Minify Javascript', 'debloat'),
'desc' => esc_html__('Minify all the deferred or delayed JS files.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
];
$options[] = [
'id' => 'integrations_js',
'name' => esc_html__('Enable Plugin Integrations', 'debloat'),
'desc' => esc_html__('Special pre-made rules for javascript, specific to plugins, are applied if enabled.', 'debloat'),
'type' => 'multicheck_inline',
'options' => [
'elementor' => 'Elementor',
'wpbakery' => 'WPBakery Page Builder',
],
'default' => ['elementor', 'wpbakery'],
'select_all_button' => false,
];
/**
* Delay JS
*/
$options[] = [
'name' => esc_html__('Delay Load JS', 'debloat'),
// 'description' => 'foo',
'type' => 'title',
'id' => '_delay_js',
];
$options[] = [
'id' => 'delay_js',
'name' => esc_html__('Delay Javascript', 'debloat'),
'desc' => esc_html__('Delay execution of the targeted JS files until user interaction.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
];
$options[] = [
'id' => 'delay_js_max',
'name' => esc_html__('Maximum Delay (in seconds)', 'debloat'),
'desc' => esc_html__('Max seconds to wait for interaction until delayed JS is loaded anyways.', 'debloat'),
'type' => 'text_small',
'default' => '',
'attributes' => [
'type' => 'number',
'min' => 0,
'data-conditional-id' => 'delay_js'
],
];
$options[] = [
'id' => 'delay_js_all',
'name' => esc_html__('Delay All Scripts', 'debloat'),
'desc' => esc_html__('CAREFUL. Delays all JS files. Its better to target scripts manually below. If there are scripts that setup sliders/carousels, animations, or other similar things, these won\'t be setup until the first user interaction.', 'debloat'),
'type' => 'checkbox',
'default' => 0,
'attributes' => ['data-conditional-id' => 'delay_js'],
];
$options[] = [
'id' => 'delay_js_includes',
'name' => esc_html__('Target Scripts', 'debloat'),
'desc' =>
esc_html__('Will delay from these scripts. You may use an ID, part of the URL, or any code for inline scripts. One per line. Examples:', 'debloat')
. ' <code>id:my-js-id</code>
<br /><code>my-theme/js-file.js</code>
<br /><code>wp-content/themes/my-theme/*</code>: All theme JS files.
<br /><code>plugins/plugin-slug/*</code>: All JS files for plugin-slug.
',
'type' => 'textarea_small',
'default' => implode("\n", [
'twitter.com/widgets.js',
'gtm.js',
'id:google_gtagjs'
]),
'attributes' => [
'data-conditional-id' => [
['key' => 'delay_js'],
['key' => 'delay_js_all', 'value' => 'off'],
]
],
];
$options[] = [
'id' => 'delay_js_excludes',
'name' => esc_html__('Exclude Scripts', 'debloat'),
'desc' =>
esc_html__('Enter one per line to exclude certain scripts from this optimizations. Examples:', 'debloat')
. '<code>id:my-js-id</code>
<br /><code>my-theme/js-file.js</code>
<br /><code>wp-content/themes/my-theme/*</code>: All theme JS files.
<br /><code>someStringInJs</code>: Exclude by some text in inline JS tag.
',
'type' => 'textarea_small',
'default' => '',
'attributes' => ['data-conditional-id' => 'delay_js'],
];
$options[] = [
'id' => 'delay_js_adsense',
'name' => esc_html__('Delay Google Ads', 'debloat'),
'desc' => esc_html__('Delay Google Adsense until first interaction. Note: This may not be ideal if you have ads that are in header.', 'debloat'),
'type' => 'checkbox',
'default' => 1,
'attributes' => ['data-conditional-id' => 'delay_js'],
];
return $options;
}
public static function get_general()
{
$options = [];
$options[] = [
'name' => esc_html__('Disable for Admins', 'debloat'),
'desc' => esc_html__('Disable processing for logged in admin users or any user with capability "manage_options". (Useful if using a pagebuilder that conflicts)', 'debloat'),
'id' => 'disable_for_admins',
'type' => 'checkbox',
'default' => 0,
];
return $options;
}
public static function get_all()
{
return array_merge(
self::get_css(),
self::get_js()
);
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Sphere\Debloat;
/**
* Autoloader for loading classes off defined namespaces or a class map.
*
* @author asadkn
* @since 1.0.0
*/
class Autoloader
{
public $class_map;
public $namespaces = [];
public function __construct($namespaces = null, $prepend = false)
{
if (is_array($namespaces)) {
$this->namespaces = $namespaces;
}
spl_autoload_register(array($this, 'load'), true, $prepend);
}
/**
* Autoloader the class either using a class map or via conversion of
* class name to file.
*
* @param string $class
*/
public function load($class)
{
if (isset($this->class_map[$class])) {
$file = $this->class_map[$class];
}
else {
foreach ($this->namespaces as $namespace => $dir) {
if (strpos($class, $namespace) !== false) {
$file = $this->get_file_path($class, $namespace, $dir);
break;
}
}
}
if (!empty($file)) {
require_once $file;
}
}
/**
* Get file path to include.
*
* Examples:
*
* Bunyad_Theme_Foo_Bar to inc/foo/bar/bar.php (fallback to inc/foo/bar.php)
* Bunyad\Blocks\FooBar to blocks/foo-bar/foo-bar.php (fallback to inc/foo-bar.php)
*
* @return string Relative path to the file from the theme dir
*/
public function get_file_path($class, $prefix = '', $path = '')
{
// Remove namespace and convert underscore as a namespace delim.
$class = str_replace($prefix, '', $class);
$class = str_replace('_', '\\', $class);
// Split to convert CamelCase.
$parts = explode('\\', $class);
foreach ($parts as $key => $part) {
$test = substr($part, 1);
// Convert CamelCase to Camel-Case
if (strtolower($test) !== $test) {
$part = preg_replace('/(.)(?=[A-Z])/u', '$1-', $part);
}
$parts[$key] = $part;
}
$name = strtolower(array_pop($parts));
$path = $path . '/' . strtolower(
implode('/', $parts)
);
$path = trailingslashit($path);
// Preferred and fallback file path.
$pref_file = $path . "{$name}/{$name}.php";
$alt_file = $path . "{$name}.php";
// Try with directory path pattern first.
if (file_exists($pref_file)) {
return $pref_file;
}
else if (file_exists($alt_file)) {
return $alt_file;
}
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Sphere\Debloat\Base;
use Sphere\Debloat\Plugin;
/**
* Base class for scripts and stylesheets.
*
* @uses Plugin::file_system()
*
* @author asadkn
* @since 1.0.0
*/
abstract class Asset
{
/**
* @var string
*/
public $id;
/**
* @var string
*/
public $orig_url;
public $url;
public $minified_url;
/**
* Whether the asset is local or at a remote URL.
* Note: Defaults to null for lazy init.
*
* @var boolean
*/
private $is_remote = null;
/**
* Return URL to get content from. Minified URL if present.
*
* @return void
*/
public function get_content_url()
{
return $this->minified_url ?: $this->url;
}
/**
* Render attributes using key values.
*
* @param array $orig_attrs Key value pair of attributes.
* @param array $safe List of attributes pre-escaped.
* @return array Array of attributes.
*/
public function render_attrs($orig_attrs, $safe = [])
{
$attrs = [];
foreach ($orig_attrs as $key => $value) {
// For true, no value is needed in HTML.
if ($value === true) {
$attrs[] = $key;
continue;
}
// Adding previously escaped, but escape again just in case it was done using
// different quotes originally.
$value = !in_array($key, $safe) ? esc_attr($value) : $value;
$attrs[] = sprintf('%s="%s"', $key, $value);
}
return $attrs;
}
/**
* Whether the asset is on a remote location.
*
* @return boolean
*/
public function is_remote()
{
if ($this->is_remote || $this->minified_url) {
return true;
}
if ($this->is_remote === null && !Plugin::file_system()->url_to_local($this->url)) {
$this->is_remote = true;
}
return $this->is_remote;
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\OptimizeCss\Stylesheet;
use Sphere\Debloat\OptimizeJs\Script;
/**
* Delay load assets singleton.
*
* @author asadkn
* @since 1.0.0
*/
class DelayLoad
{
public $enabled = true;
public $use_js = false;
public $js_type;
public $preload = [];
/**
* Note: Should be used a singleton.
*/
public function __construct()
{
$this->register_hooks();
}
public function register_hooks()
{
// add_action('wp_enqueue_scripts', [$this, 'register_assets']);
}
/**
* Enable injection of required scripts and preloads, as needed.
*
* @param null|true $use_js Whether to inject delay/defer JS. null keeps the current.
* @return void
*/
public function enable($use_js = null)
{
$this->enabled = true;
if ($use_js === true) {
$this->use_js = $use_js;
}
}
public function disable()
{
$this->enabled = false;
}
/**
* Enqueue a preload.
*
* @param Stylesheet|Script $asset
* @return void
*/
public function add_preload($asset)
{
$this->preload[] = $asset;
}
/**
* Add extras to the provided HTML buffer.
*
* @param string $html
* @return string
*/
public function render($html)
{
if (!$this->enabled) {
return $html;
}
$js = $this->render_js();
$preloads = $this->render_preloads();
$append = $preloads . $js;
// Add to body or at the end.
// Note: Has to be at the end to ensure all <script> tags with defer has been added.
if (strpos($html, '</body>') !== false) {
$html = str_replace('</body>', $append . "\n</body>", $html);
} else {
$html .= $append;
}
return $html;
}
protected function render_preloads()
{
$preloads = [];
foreach ($this->preload as $preload) {
$media = 'all';
$type = ($preload instanceof Stylesheet) ? 'style' : 'script';
if ($type === 'style') {
$media = $preload->media ?: $media;
}
$preloads[] = sprintf(
'<link rel="prefetch" href="%1$s" as="%2$s" media="%3$s" />',
esc_url($preload->get_content_url()),
$type,
esc_attr($media)
);
}
return implode('', $preloads);
}
/**
* Get JS enqueues and inline scripts as needed.
*
* @return string
*/
protected function render_js()
{
if (!$this->use_js) {
return '';
}
$js = '';
$min = Plugin::get_instance()->env !== 'dev' ? '.min' : '';
// Defer JS is always inline.
$js .= sprintf(
'<script data-cfasync="false">%s</script>',
Plugin::file_system()->get_contents(
Plugin::get_instance()->dir_path . 'inc/delay-load/js/defer-load'. $min .'.js'
)
);
// Delay load JS comes after, just in case, to not mess up readyState.
if ($this->js_type === 'inline') {
$js .= sprintf(
'<script data-cfasync="false">%s</script>',
Plugin::file_system()->get_contents(
Plugin::get_instance()->dir_path . 'inc/delay-load/js/delay-load'. $min .'.js'
)
);
} else {
$js .= sprintf(
'<script type="text/javascript" src="%s" data-cfasync="false"></script>',
esc_url(Plugin::get_instance()->dir_url . 'inc/delay-load/js/delay-load'. $min .'.js?ver=' . Plugin::VERSION)
);
}
if (!$js) {
return '';
}
/**
* Add configs.
*/
$configs = [
'cssDelayType' => Plugin::options()->delay_css_type,
'jsDelayType' => Plugin::options()->delay_js_type,
'jsDelayMax' => Plugin::options()->delay_js_max
];
$js = sprintf(
'<script>var debloatConfig = %1$s;</script>%2$s',
json_encode($configs),
$js
);
return $js;
}
/**
* Check for conditionals for delay load.
*
* @return boolean
*/
public function should_delay_css()
{
if (!Plugin::options()->delay_css_load) {
return false;
}
$valid = Plugin::process()->check_enabled(Plugin::options()->delay_css_on);
return apply_filters('debloat/should_delay_css', $valid);
}
}

View File

@ -0,0 +1,36 @@
/**
* Debloat plugin's defer load.
* @preserve
* @copyright asadkn 2021
*/
"use strict";
(() => {
const n = true;
const e = [ ...document.querySelectorAll("script[defer]") ];
if (e.length && document.readyState !== "complete") {
let t = document.readyState;
Object.defineProperty(document, "readyState", {
configurable: true,
get() {
return t;
},
set(e) {
return t = e;
}
});
let e = false;
document.addEventListener("DOMContentLoaded", () => {
t = "interactive";
n && console.log("DCL Ready.");
e = true;
document.dispatchEvent(new Event("readystatechange"));
e = false;
});
document.addEventListener("readystatechange", () => {
if (!e && t === "interactive") {
t = "complete";
}
});
}
})();

View File

@ -0,0 +1 @@
"use strict";(()=>{if([...document.querySelectorAll("script[defer]")].length&&"complete"!==document.readyState){let t=document.readyState;Object.defineProperty(document,"readyState",{configurable:!0,get(){return t},set(e){return t=e}});let e=!1;document.addEventListener("DOMContentLoaded",()=>{t="interactive",e=!0,document.dispatchEvent(new Event("readystatechange")),e=!1}),document.addEventListener("readystatechange",()=>{e||"interactive"!==t||(t="complete")})}})();

View File

@ -0,0 +1,254 @@
/**
* Delay load functionality of debloat plugin.
* @preserve
* @copyright asadkn 2021
*/
"use strict";
(() => {
const r = window.debloatConfig || {};
const s = true;
let c = [];
const d = {
HTMLDocument: document.addEventListener.bind(document),
Window: window.addEventListener.bind(window)
};
const n = {};
let a;
let o = false;
let i = false;
let l = false;
let u = false;
let e = false;
let f = [];
let t = [];
function m() {
h();
w();
document.addEventListener("debloat-load-css", () => w(true));
document.addEventListener("debloat-load-js", () => h(true));
}
function h(e) {
f = [ ...document.querySelectorAll("script[data-debloat-delay]") ];
if (f.length) {
E();
y("js", e);
}
}
function w(e) {
t = [ ...document.querySelectorAll("link[data-debloat-delay]") ];
if (t.length) {
y("css", e);
}
}
function y(t, n) {
t = t || "js";
const o = n ? "onload" : r[t + "DelayType"] || "onload";
const a = t === "js" ? p : g;
if (t === "js") {
n || o === "onload" ? v() : D(v);
}
switch (o) {
case "onload":
D(() => a(n));
break;
case "interact":
let e = false;
const s = [ "mousemove", "mousedown", "keydown", "touchstart", "wheel" ];
const c = () => {
if (e) {
return;
}
e = true;
t === "js" ? O(() => setTimeout(a, 2)) : a();
};
s.forEach(e => {
document.addEventListener(e, c, {
passive: true,
once: true
});
});
if (t === "js" && r.jsDelayMax) {
O(() => setTimeout(c, r.jsDelayMax * 1e3));
}
break;
case "custom-delay":
D(() => {
const e = parseInt(element.dataset.customDelay) * 1e3;
setTimeout(a, e);
});
break;
}
}
function g() {
t.forEach(e => b(e));
}
function p(e) {
v();
if (!e) {
l = true;
a = document.readyState;
let t = "loading";
Object.defineProperty(document, "readyState", {
configurable: true,
get() {
return t;
},
set(e) {
return t = e;
}
});
}
let t;
const n = new Promise(e => t = e);
const o = () => {
if (!f.length) {
t();
return;
}
const e = b(f.shift());
e.then(o);
};
o();
n.then(j).catch(e => {
console.error(e);
j();
});
setTimeout(() => !c.length || j(), 45e3);
}
function v(o) {
if (e) {
return;
}
e = true;
f.forEach(e => {
const t = e.src || e.dataset.src;
if (!t) {
return;
}
const n = document.createElement("link");
Object.assign(n, {
rel: o || "preload",
as: "script",
href: t,
...e.crossOrigin && {
crossOrigin: e.crossOrigin
}
});
document.head.append(n);
});
}
function b(t) {
let e;
const n = t.dataset.src;
const o = t => {
return new Promise(e => {
t.addEventListener("load", e);
t.addEventListener("error", e);
});
};
if (n) {
const s = document.createElement("script");
e = o(s);
t.getAttributeNames().forEach(e => {
e === "src" || (s[e] = t[e]);
});
s.async = false;
s.src = n;
t.parentNode.replaceChild(s, t);
} else if (t.type && t.type === "text/debloat-script") {
t.type = t.dataset.type || "text/javascript";
t.text += " ";
}
const a = t.dataset.href;
if (a) {
e = o(t);
t.href = a;
}
[ "debloatDelay", "src" ].forEach(e => {
t.dataset[e] = "";
delete t.dataset[e];
});
return e || Promise.resolve();
}
function E() {
if (o) {
return;
}
o = true;
const e = (t, e) => {
e.addEventListener(t, e => n[t] = e);
};
e("DOMContentLoaded", document);
e("load", window);
e("readystatechange", document);
e("pageshow", window);
const t = function(e, t, ...n) {
const o = [ "readystatechange", "DOMContentLoaded", "load", "pageshow" ];
if (l && !i && o.includes(e)) {
s && console.log("Adding: ", e, t, n);
const a = {
event: e,
cb: t,
context: this,
args: n
};
c.push(a);
return;
}
if (d[this.constructor.name]) {
d[this.constructor.name].call(this, e, t, ...n);
}
};
document.addEventListener = t.bind(document);
window.addEventListener = t.bind(window);
Object.defineProperty(window, "onload", {
set(e) {
window.addEventListener("load", e);
}
});
}
function L(e) {
try {
e.cb.call(e.context, n[e.event], ...e.args);
} catch (e) {
console.error(e);
}
}
function j() {
if (u) {
return;
}
s && console.log("Firing Load Events", c);
u = true;
const e = c.filter(e => e.event === "readystatechange");
document.readyState = "interactive";
e.forEach(e => L(e));
for (const t of c) {
t.event === "DOMContentLoaded" && L(t);
}
for (const t of c) {
t.event === "load" && L(t);
}
c = [];
u = false;
i = true;
l = false;
D(() => {
document.readyState = "complete";
setTimeout(() => {
e.forEach(e => L(e));
}, 2);
});
}
function D(e) {
const t = a || document.readyState;
t === "complete" ? e() : d.Window("load", () => e());
}
function O(e) {
document.readyState !== "loading" ? e() : d.Window("DOMContentLoaded", () => e());
}
m();
})();

View File

@ -0,0 +1 @@
"use strict";(()=>{const s=window.debloatConfig||{},a=!0;let d=[];const c={HTMLDocument:document.addEventListener.bind(document),Window:window.addEventListener.bind(window)},n={};let r,o=!1,i=!1,l=!1,u=!1,e=!1,m=[],t=[];function f(e){var t;m=[...document.querySelectorAll("script[data-debloat-delay]")],m.length&&(o||(o=!0,(t=(t,e)=>{e.addEventListener(t,e=>n[t]=e)})("DOMContentLoaded",document),t("load",window),t("readystatechange",document),t("pageshow",window),t=function(e,t,...n){var o;l&&!i&&["readystatechange","DOMContentLoaded","load","pageshow"].includes(e)?(a,o={event:e,cb:t,context:this,args:n},d.push(o)):c[this.constructor.name]&&c[this.constructor.name].call(this,e,t,...n)},document.addEventListener=t.bind(document),window.addEventListener=t.bind(window),Object.defineProperty(window,"onload",{set(e){window.addEventListener("load",e)}})),w("js",e))}function h(e){t=[...document.querySelectorAll("link[data-debloat-delay]")],t.length&&w("css",e)}function w(t,n){t=t||"js";var o=!n&&s[t+"DelayType"]||"onload";const a="js"===t?v:y;switch("js"===t&&(n||"onload"===o?p():L(p)),o){case"onload":L(()=>a(n));break;case"interact":let e=!1;const d=["mousemove","mousedown","keydown","touchstart","wheel"],c=()=>{e||(e=!0,"js"===t?j(()=>setTimeout(a,2)):a())};d.forEach(e=>{document.addEventListener(e,c,{passive:!0,once:!0})}),"js"===t&&s.jsDelayMax&&j(()=>setTimeout(c,1e3*s.jsDelayMax));break;case"custom-delay":L(()=>{var e=1e3*parseInt(element.dataset.customDelay);setTimeout(a,e)})}}function y(){t.forEach(e=>g(e))}function v(e){if(p(),!e){l=!0,r=document.readyState;let t="loading";Object.defineProperty(document,"readyState",{configurable:!0,get(){return t},set(e){return t=e}})}let t;const n=new Promise(e=>t=e),o=()=>{if(m.length){const e=g(m.shift());e.then(o)}else t()};o(),n.then(E).catch(e=>{E()}),setTimeout(()=>!d.length||E(),45e3)}function p(o){e||(e=!0,m.forEach(e=>{var t,n=e.src||e.dataset.src;n&&(t=document.createElement("link"),Object.assign(t,{rel:o||"preload",as:"script",href:n,...e.crossOrigin&&{crossOrigin:e.crossOrigin}}),document.head.append(t))}))}function g(t){let e;var n=t.dataset.src,o=t=>new Promise(e=>{t.addEventListener("load",e),t.addEventListener("error",e)});if(n){const a=document.createElement("script");e=o(a),t.getAttributeNames().forEach(e=>{"src"===e||(a[e]=t[e])}),a.async=!1,a.src=n,t.parentNode.replaceChild(a,t)}else t.type&&"text/debloat-script"===t.type&&(t.type=t.dataset.type||"text/javascript",t.text+=" ");n=t.dataset.href;return n&&(e=o(t),t.href=n),["debloatDelay","src"].forEach(e=>{t.dataset[e]="",delete t.dataset[e]}),e||Promise.resolve()}function b(e){try{e.cb.call(e.context,n[e.event],...e.args)}catch(e){}}function E(){if(!u){a,u=!0;const e=d.filter(e=>"readystatechange"===e.event);document.readyState="interactive",e.forEach(e=>b(e));for(const t of d)"DOMContentLoaded"===t.event&&b(t);for(const n of d)"load"===n.event&&b(n);d=[],u=!1,i=!0,l=!1,L(()=>{document.readyState="complete",setTimeout(()=>{e.forEach(e=>b(e))},2)})}}function L(e){"complete"===(r||document.readyState)?e():c.Window("load",()=>e())}function j(e){"loading"!==document.readyState?e():c.Window("DOMContentLoaded",()=>e())}f(),h(),document.addEventListener("debloat-load-css",()=>h(!0)),document.addEventListener("debloat-load-js",()=>f(!0))})();

View File

@ -0,0 +1,173 @@
<?php
namespace Sphere\Debloat;
/**
* Filesystem based cache for assets.
*
* @author asadkn
* @since 1.0.0
*/
class FileCache
{
/**
* @var Filesystem
*/
protected $fs;
public $cache_path;
public $cache_url;
public function __construct()
{
$this->fs = Plugin::file_system();
$this->cache_path = WP_CONTENT_DIR . '/cache/debloat/';
$this->cache_url = WP_CONTENT_URL . '/cache/debloat/';
}
/**
* Get a cached file path.
*
* @param string $id
* @return bool
*/
public function get($id)
{
$file = $this->get_file_name($id);
if (!$file) {
return false;
}
$file = $this->get_cache_path($id) . $file;
if ($this->fs->is_file($file)) {
return $file;
}
return false;
}
/**
* Get a cached file URL.
*
* @param string $id
* @return bool|string
*/
public function get_url($id)
{
if (!$this->get($id)) {
return false;
}
$file = $this->get_file_name($id);
if (!$file) {
return false;
}
return $this->cache_url . $this->get_cache_type($file) . '/' . $file;
}
/**
* Get file name for cache storage/retrieval provided a URL or path.
*
* @param string $id URL or path of a file.
* @return string
*/
protected function get_file_name($id)
{
// Get a local path if a valid URL was provided. Remote URLs also work.
$file = $this->fs->url_to_local($id);
if (!$file) {
// Remote here.
}
// MD5 on the id instead of file, as it's likely to be a URL that can has a changing
// query string reuqiring a new cached entry.
$hash = md5($id);
return $hash . '.' . $this->get_cache_type($file ?: $id);
}
/**
* Save a file in the cache.
*
* @param string $id Path, a local asset URL, or a unqiue name.
* @param string $content
* @return bool|string File path on success or false on failure.
*/
public function set($id, string $content = '')
{
if (!$content) {
return false;
}
// Ensure path exists.
$path = $this->get_cache_path($id);
$this->fs->mkdir_p($path);
$file = $path . $this->get_file_name($id);
if ($this->fs->put_contents($file, $content)) {
return $file;
}
return false;
}
/**
* Get cache path based on provided file path or id.
*
* @param string $asset A local path or a URL.
* @return string
*/
public function get_cache_path($asset = '')
{
$path = $this->cache_path;
$asset = $asset ? $this->fs->url_to_local($asset) : '';
// Path with asset type directory.
return $path . $this->get_cache_type($asset) . '/';
}
/**
* Get the type of cache group based on file extension.
*
* @param string $id File path or name. URLs fine too.
* @return string
*/
protected function get_cache_type(string $file)
{
$extension = pathinfo($file, PATHINFO_EXTENSION);
$type = $extension === 'js' ? 'js' : 'css';
return $type;
}
/**
* Delete all the cached files for specified cache.
*
* @param string $type
* @return void
*/
public function delete_cache($type = 'js')
{
$dir = $this->cache_path . $type . '/';
$files = (array) $this->fs->dirlist($dir);
foreach ($files as $file) {
$this->fs->delete($dir . $file['name']);
}
}
/**
* Get number of files in cache for a specific type.
*
* @param string $type
* @return integer
*/
public function get_stats($type = 'js')
{
$dir = $this->cache_path . $type;
$files = $this->fs->dirlist($dir);
return $files ? count($files) : 0;
}
}

View File

@ -0,0 +1,231 @@
<?php
namespace Sphere\Debloat;
/**
* Filesystem that mainly wraps the WP_File_System
*
* @author asadkn
* @since 1.0.0
*
* @mixin \WP_Filesystem_Base
*/
class FileSystem
{
/**
* Pattern to remove protocol and www
*/
const PROTO_REMOVE_PATTERN = '#^(https?)?:?//(|www\.)#i';
/**
* @var WP_Filesystem_Base
*/
public $filesystem;
protected $valid_hosts;
protected $paths_urls;
/**
* Setup file system
*/
public function __construct()
{
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once wp_normalize_path(ABSPATH . '/wp-admin/includes/file.php');
// At shutdown is usually a ob_start callback which doesn't permit calling ob_*
if (did_action('shutdown') && ob_get_level() > 0) {
$creds = request_filesystem_credentials('');
}
else {
ob_start();
$creds = request_filesystem_credentials('');
ob_end_clean();
}
if (!$creds) {
$creds = array();
}
$filesystem = WP_Filesystem($creds);
if (!$filesystem) {
// Fallback to lax permissions
$upload = wp_upload_dir();
WP_Filesystem(false, $upload['basedir'], true);
}
}
$this->filesystem = $wp_filesystem;
}
/**
* Recursively make parent directories for a path.
*
* Note: Adapted a bit from wp_mkdir_p().
*
* @param string $path
* @return boolean true on success, false or failure.
*/
public function mkdir_p($path)
{
if ($this->is_dir($path)) {
return;
}
$path = str_replace('//', '/', $path);
// Safe mode safety.
$path = rtrim($path, '/');
$path = $path ?: '/';
$created = $this->mkdir($path, FS_CHMOD_DIR);
if ($created) {
return true;
}
// If it failed above, try again by creating the parent first, recursively.
$parent = dirname($path);
if ($parent !== '/' && $this->mkdir_p($parent)) {
return $this->mkdir($path, FS_CHMOD_DIR);
}
return true;
}
/**
* Get a local file by the provided URL, if possible.
*
* @see wp_normalize_path()
*
* @return bool|string Either the path or false on failure.
*/
public function url_to_local($url)
{
$url = trim($url);
// Not a URL, just return the path.
if (substr($url, 0, 4) !== 'http' && substr($url, 0, 2) !== '//') {
return $url;
}
$url = explode('?', trim($url));
$url = trim($url[0]);
// We're not working with encoded URLs.
if (strpos($url, '%') !== false) {
$url = urldecode($url);
}
// Add https:// for parse_url() or it fails.
$url_no_proto = preg_replace(self::PROTO_REMOVE_PATTERN, '', $url);
$url_host = parse_url('https://' . $url_no_proto, PHP_URL_HOST);
// Not a known host / URL.
if (!in_array($url_host, $this->get_valid_hosts())) {
return false;
}
/**
* Go through each known path url map and stop at first matched.
*/
$valid_urls = $this->get_paths_urls();
$url_dirname = dirname($url_no_proto);
$matched = [];
foreach ($valid_urls as $path_url) {
if (strpos($url_dirname, untrailingslashit($path_url['url'])) !== false) {
$matched = $path_url;
break;
}
}
// We have a matched path.
if (!empty($matched['path'])) {
$path = wp_normalize_path(
$matched['path'] . str_replace($matched['url'], '', $url_dirname)
);
$file = trailingslashit($path) . wp_basename($url_no_proto);
if (file_exists($file) && is_file($file) && is_readable($file)) {
return $file;
}
}
return false;
}
/**
* Get recognized hostnames for stylesheet URLs.
*
* @return array
*/
public function get_valid_hosts()
{
if (!$this->valid_hosts) {
$this->valid_hosts = wp_list_pluck(
$this->get_paths_urls(),
'host'
);
}
return apply_filters('debloat/file_system/valid_hosts', $this->valid_hosts);
}
/**
* Get a map of known path URLs, associated local path, and host.
*
* @return array
*/
public function get_paths_urls()
{
if (!$this->paths_urls) {
// We add https:// back for parse_url() to prevent it from failing
$site_url = preg_replace(self::PROTO_REMOVE_PATTERN, '', site_url());
$site_host = parse_url('https://' . $site_url, PHP_URL_HOST);
$content_url = preg_replace(self::PROTO_REMOVE_PATTERN, '', content_url());
$content_host = parse_url('https://' . $content_url, PHP_URL_HOST);
/**
* This array will be processed in order it's defined to find the matching host and URL.
*/
$hosts = [
// First priority to use content_host and content_url()
'content' => [
'url' => $content_url,
'path' => WP_CONTENT_DIR,
'host' => $content_host
],
// Fallback to using site URL with ABSPATH
'site' => [
'url' => $site_url,
'path' => ABSPATH,
'host' => $site_host
],
];
$this->paths_urls = apply_filters('debloat/file_system/paths_urls', $hosts);
}
return $this->paths_urls;
}
/**
* Proxies to WP_Filesystem_Base
*/
public function __call($name, $arguments)
{
return call_user_func_array([$this->filesystem, $name], $arguments);
}
}

View File

@ -0,0 +1,156 @@
<?php
namespace Sphere\Debloat\Integrations;
use Sphere\Debloat\Plugin;
/**
* Rules specific to Elementor plugin.
*/
class Elementor
{
public $allow_selectors;
public function __construct()
{
$this->register_hooks();
}
public function register_hooks()
{
add_action('wp', [$this, 'setup']);
}
public function setup()
{
if (in_array('elementor', Plugin::options()->integrations_css)) {
$this->setup_remove_css();
}
if (in_array('elementor', Plugin::options()->integrations_js)) {
$this->setup_delay_js();
}
}
/**
* Special rules related to remove css when Elementor is active.
*
* @return void
*/
public function setup_remove_css()
{
// Add all Elementor frontend files for CSS processing.
add_filter('debloat/remove_css_includes', function($include) {
$include[] = 'id:elementor-frontend-css';
$include[] = 'id:elementor-pro-css';
// $include[] = 'elementor/*font-awesome';
return $include;
});
add_filter('debloat/remove_css_excludes', function($exclude, \Sphere\Debloat\RemoveCss $remove_css) {
// Don't bother with animations CSS file as it won't remove much.
if (!empty($remove_css->used_markup['classes']['elementor-invisible'])) {
$exclude[] = 'id:elementor-animations';
}
return $exclude;
}, 10, 2);
/**
* Elementor selectors extras.
*/
$this->allow_selectors = [
[
'type' => 'any',
'sheet' => 'id:elementor-',
'search' => [
'*.e--ua-*',
'.elementor-loading',
'.elementor-invisible',
'.elementor-background-video-embed',
]
],
[
'type' => 'class',
'class' => 'elementor-invisible',
'sheet' => 'id:elementor-',
'search' => [
'.animated'
]
],
[
'type' => 'class',
'class' => 'elementor-invisible',
'sheet' => 'id:elementor-',
'search' => [
'.animated'
]
],
];
if (is_user_logged_in()) {
$this->allow_selectors[] = [
'type' => 'any',
'sheet' => 'id:elementor-',
'search' => [
'#wp-admin-bar*',
'*#wpadminbar*',
]
];
}
if (defined('ELEMENTOR_PRO_VERSION')) {
$this->allow_selectors = array_merge($this->allow_selectors, [
[
'type' => 'class',
'class' => 'elementor-posts-container',
// 'sheet' => 'id:elementor-',
'search' => [
'.elementor-posts-container',
'.elementor-has-item-ratio'
]
],
]);
}
add_filter('debloat/allow_css_selectors', function($allow, \Sphere\Debloat\RemoveCss $remove_css) {
$html = $remove_css->html;
if (strpos($html, 'background_slideshow_gallery') !== false) {
array_push($this->allow_selectors, ...[
[
'type' => 'any',
'sheet' => 'id:elementor-',
'search' => [
'*.swiper-*',
'*.elementor-background-slideshow*',
'.elementor-ken-burns*',
]
],
]);
}
return array_merge($allow, $this->allow_selectors);
}, 10, 2);
}
/**
* Special rules related to Delay JS when Elementor is active.
*
* @return void
*/
public function setup_delay_js()
{
add_filter('debloat/delay_js_includes', function($include) {
$include[] = 'elementor/*';
// Admin bar should also be delayed as elementor creates admin bar items later
// and the events won't register.
$include[] = 'wp-includes/js/admin-bar*.js';
return $include;
});
}
}

View File

@ -0,0 +1,177 @@
<?php
namespace Sphere\Debloat\Integrations;
use Sphere\Debloat\Plugin;
/**
* Rules specific to WPBakery Page Builder plugin.
*/
class Wpbakery
{
public $allow_selectors;
public function __construct()
{
$this->register_hooks();
}
public function register_hooks()
{
add_action('wp', [$this, 'setup']);
}
public function setup()
{
if (in_array('wpbakery', Plugin::options()->integrations_css)) {
$this->setup_remove_css();
}
if (in_array('wpbakery', Plugin::options()->integrations_js)) {
$this->setup_delay_js();
}
}
/**
* Special rules related to remove css when WPBakery is active.
*
* @return void
*/
public function setup_remove_css()
{
// Add all WPBakery frontend files for CSS processing.
add_filter('debloat/remove_css_includes', function($include) {
// Only need to remove unused on this. All other CSS files are modular and included only if needed.
$include[] = 'id:js_composer_front';
// FontAwesome can also go through this.
$include[] = 'js_composer/*font-awesome';
return $include;
});
add_filter('debloat/remove_css_excludes', function($exclude, \Sphere\Debloat\RemoveCss $remove_css) {
// // Don't bother with animations CSS file as it won't remove much.
// $exclude[] = 'id:vc_animate-css';
// // Pageable owl carousel.
// $exclude[] = 'id:vc_pageable_owl-carousel';
// // prettyPhoto would need all the CSS.
// $exclude[] = 'js_composer/*prettyphoto';
// // owlcarousel is added only if needed.
// $exclude[] = 'js_composer/*owl';
return $exclude;
}, 10, 2);
/**
* WPbakery selectors extras.
*/
$this->allow_selectors = [
[
'type' => 'any',
'search' => [
'.vc_mobile',
'.vc_desktop',
]
],
[
'type' => 'class',
'class' => 'vc_parallax',
'sheet' => 'id:js_composer_front',
'search' => [
'*.vc_parallax*',
'.vc_hidden'
]
],
[
'type' => 'class',
'class' => 'vc_pie_chart',
'sheet' => 'id:js_composer_front',
'search' => [
'.vc_ready'
]
],
[
'type' => 'class',
'class' => 'wpb_gmaps_widget',
'sheet' => 'id:js_composer_front',
'search' => [
'.map_ready',
]
],
[
'type' => 'class',
'class' => 'wpb_animate_when_almost_visible',
'sheet' => 'id:js_composer_front',
'search' => [
'.wpb_start_animation',
'.animated'
]
],
[
'type' => 'class',
'class' => 'vc_toggle',
'sheet' => 'id:js_composer_front',
'search' => [
'.vc_toggle_active',
]
],
[
'type' => 'class',
'class' => 'vc_grid',
'sheet' => 'id:js_composer_front',
'search' => [
'.vc_grid-loading',
'.vc_visible-item',
'.vc_is-hover',
// -complete and -failed etc. too
'*.vc-spinner*',
'*.vc-grid*.vc_active*',
// Other dependencies maybe: prettyPhoto, owlcarousel for pagination of specific type.
]
],
];
add_filter('debloat/allow_css_selectors', function($allow, \Sphere\Debloat\RemoveCss $remove_css) {
// $html = $remove_css->html;
// if (strpos($html, 'background_slideshow_gallery') !== false) {
// array_push($this->allow_selectors, ...[
// [
// 'type' => 'any',
// 'sheet' => 'id:elementor-',
// 'search' => [
// '*.swiper-*',
// '*.elementor-background-slideshow*',
// '.elementor-ken-burns*',
// ]
// ],
// ]);
// }
return array_merge($allow, $this->allow_selectors);
}, 10, 2);
}
/**
* Special rules related to Delay JS when WPBakery is active.
*
* @return void
*/
public function setup_delay_js()
{
add_filter('debloat/delay_js_includes', function($include) {
$include[] = 'js_composer/*';
return $include;
});
}
}

View File

@ -0,0 +1,201 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\Base\Asset;
use Sphere\Debloat\OptimizeCss\Stylesheet;
use MatthiasMullie\Minify;
/**
* Minifer for assets with cache integration.
*
* @author asadkn
* @since 1.0.0
*/
class Minifier
{
protected $asset_type = 'js';
/**
* @var Sphere\Debloat\OptimizeCss\Stylesheet|Sphere\Debloat\OptimizeJs\Script
*/
protected $asset;
/**
* @param \Sphere\Debloat\Base\Asset $asset
*/
public function __construct(Asset $asset)
{
if ($asset instanceof Stylesheet) {
$this->asset_type = 'css';
}
$this->asset = $asset;
}
/**
* Minify the asset, cache it, and replace its URL in the asset object.
*
* @uses Plugin::file_cache()
* @uses Plugin::file_system()
*
* @return string URL of the minified file.
*/
public function process()
{
// Not for inline assets.
if (!$this->asset->url) {
return;
}
// Debugging scripts. Do not minify.
if (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) {
return;
}
$file = Plugin::file_cache()->get_url($this->asset->url);
if (!$file) {
$minify = $this->minify();
if (!$minify) {
return;
}
// For CSS assets, convert URLs to fully qualified.
$this->maybe_convert_urls();
if (!Plugin::file_cache()->set($this->asset->url, $this->asset->content)) {
return;
}
$file = Plugin::file_cache()->get_url($this->asset->url);
}
$this->asset->minified_url = $file;
return $file;
}
/**
* Minify the file using the URL in the asset object.
*
* @return string|boolean
*/
public function minify()
{
// We support google fonts remote fetch.
if (
Plugin::options()->optimize_gfonts_inline &&
$this->asset instanceof Stylesheet &&
$this->asset->is_google_fonts()
) {
$this->fetch_remote_content();
} else {
// We minify and cache local source only for now.
$source_file = Plugin::file_system()->url_to_local($this->asset->url);
if (!$source_file) {
return false;
}
$source = Plugin::file_system()->get_contents($source_file);
$this->asset->content = $source;
}
/**
* We want a cached file with source data whether existing is minified or not - as
* post-processing is needed for URLs when inlining the CSS.
*
* For JS, caching the already min files also serves the purpose of not testing them
* again, unnecessarily.
*/
if ($this->is_content_minified()) {
return $this->asset->content;
}
Util\debug_log('Minifying: ' . $this->asset->url);
// JS minifier.
if ($this->asset_type === 'js') {
// Improper handling for older webpack: https://github.com/matthiasmullie/minify/issues/375
if (strpos($source, 'var __webpack_require__ = function (moduleId)') !== false) {
return false;
}
$minifier = new Minify\JS($this->asset->content);
$this->asset->content = $minifier->minify();
} else {
// CSS minifier. Set content and convert urls.
$minifier = new Minify\CSS($this->asset->content);
$this->asset->content = $minifier->minify();
}
return $this->asset->content;
}
/**
* Convert URLs for CSS assets.
*
* @return void
*/
public function maybe_convert_urls()
{
if ($this->asset_type !== 'css') {
return;
}
// Check if any non-data URLs exist.
if (!preg_match('/url\((?![\'"\s]*data:)/', $this->asset->content)) {
return;
}
$this->asset->convert_urls();
}
/**
* Get remote asset content and add to asset content.
*
* @return void
*/
public function fetch_remote_content()
{
$request = wp_remote_get($this->asset->url, [
'timeout' => 5,
// For google fonts mainly.
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36',
]);
if (is_wp_error($request) || empty($request['body'])) {
return;
}
$this->asset->content = $request['body'];
}
/**
* Check if provided content is already minified.
*
* @return boolean
*/
public function is_content_minified($content = '')
{
$content = $content ?: $this->asset->content;
// Already minified asset.
if (preg_match('/[\-\.]min\.(js|css)/', $this->asset->url)) {
return true;
}
$content = trim($content);
if (!$content) {
return true;
}
// Hacky, but will do.
$new_lines = substr_count($content, "\n", 0, min(strlen($content), 2000));
if ($new_lines < 5) {
return true;
}
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Sphere\Debloat\OptimizeCss;
use Sphere\Debloat\Plugin;
/**
* Add a few Google Font optimizations.
*
* @author asadkn
* @since 1.0.0
*/
class GoogleFonts
{
public $active = false;
protected $renders = [];
public function enable()
{
$this->active = true;
}
/**
* Add resource hints for preconnect and so on.
*
* @param string $html Full DOM HTML.
* @return void
*/
public function add_hints($html)
{
if (!$this->active || !Plugin::options()->optimize_gfonts) {
return $html;
}
// preconnect with dns-prefetch fallback for Firefox. Due to safari bug, can't be in same rel.
$hint = '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />';
$hint .= '<link rel="dns-prefetch" href="https://fonts.gstatic.com" />';
// Only needed if not inlined.
if (!Plugin::options()->optimize_css || !Plugin::options()->optimize_gfonts_inline) {
$hint .= '<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />';
$hint .= '<link rel="dns-prefetch" href="https://fonts.googleapis.com" />';
}
// Add in to head.
$html = str_replace('<head>', '<head>' . $hint, $html);
// No longer needed as preconnect's better and we always want to use https.
$html = str_replace("<link rel='dns-prefetch' href='//fonts.googleapis.com' />", '', $html);
return $html;
}
/**
* Late processing and injecting data in HTML DOM.
*
* @param string $html
* @return string
*/
public function render_in_dom($html)
{
if ($this->renders) {
$html = str_replace('<head>', '<head>' . implode('', $this->renders), $html);
}
$html = $this->add_hints($html);
return $html;
}
public function optimize(Stylesheet $sheet)
{
if (!$sheet->is_google_fonts()) {
return;
}
// Set normal render if optimizations are disabled.
if (!Plugin::options()->optimize_gfonts) {
$sheet->render_type = 'normal';
return;
}
$sheet->delay_type = 'preload';
if (strpos($sheet->url, 'display=') === false) {
$sheet->url .= '&display=swap';
}
}
public function do_render(Stylesheet $sheet)
{
return $sheet->render();
// $orig_media = $sheet->media;
// // If no display= or if display=auto.
// if (!preg_match('/display=(?!auto)/', $sheet->url)) {
// $sheet->media = 'x';
// }
// // Add the JS to render with display: swap on mobile.
// $extra = "<script>const e = document.currentScript.previousElementSibling; window.innerWidth > 1024 || (e.href+='&display=swap'); e.media='" . esc_js($orig_media) ."'</script>";
// $this->renders[] = $sheet->render() . $extra;
// // Return empty space to strip out the tag.
// return ' ';
}
}

View File

@ -0,0 +1,251 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\OptimizeCss\Stylesheet;
/**
* Process stylesheets to debloat and optimize CSS.
*
* @author asadkn
* @since 1.0.0
*/
class OptimizeCss
{
/**
* @var \DOMDocument
*/
protected $dom;
protected $html;
/**
* @var array
*/
protected $stylesheets;
/**
* @var array
*/
protected $exclude_sheets;
public function __construct(\DOMDocument $dom, string $raw_html)
{
$this->dom = $dom;
$this->html = $raw_html;
}
public function process()
{
$this->find_stylesheets();
// Setup optimization excludes.
$exclude = Util\option_to_array(Plugin::options()->optimize_css_excludes);
$this->exclude_sheets = apply_filters('debloat/optimize_css_excludes', $exclude);
// Remove CSS first, if any.
if ($this->should_remove_css()) {
$remove_css = new RemoveCss($this->stylesheets, $this->dom, $this->html);
$this->html = $remove_css->process();
}
/**
* Process and replace stylesheets with CSS cleaned.
*/
$has_delayed = false;
$has_gfonts = false;
$replacements = [];
/** @var Stylesheet $sheet */
foreach ($this->stylesheets as $sheet) {
$replacement = '';
// Optimizations such as min and inline.
$this->optimize($sheet);
if (Plugin::options()->optimize_gfonts && $sheet->is_google_fonts()) {
$replacement = Plugin::google_fonts()->do_render($sheet);
$has_gfonts = true;
}
if (!$replacement) {
// Get the rendered stylesheet to replace original with.
$replacement = $sheet->render();
}
if ($replacement) {
// Util\debug_log('Replacing: ' . print_r($sheet, true));
$replacements[$sheet->orig_url] = $replacement;
if ($sheet->has_delay()) {
$has_delayed = true;
// onload and preload type doesn't need prefetch; both use a non-JS method.
if (!in_array($sheet->delay_type, ['onload', 'preload'])) {
Plugin::delay_load()->add_preload($sheet);
}
}
}
// Found Google Fonts.
if ($sheet->is_google_fonts()) {
$has_gfonts = true;
}
// Free up memory.
$sheet->content = null;
$sheet->parsed_data = null;
}
/**
* Make stylesheet replacements, if any. Slightly more efficient in one go.
*/
if ($replacements) {
$urls = array_map('preg_quote', array_keys($replacements));
// Using callback to prevent issues with backreferences such as $1 or \0 in replacement string.
$this->html = preg_replace_callback(
'#<link[^>]*href=(?:"|\'|)('. implode('|', $urls) .')(?:"|\'|\s)[^>]*>#Usi',
function ($match) use ($replacements) {
if (!empty($replacements[ $match[1] ])) {
return $replacements[ $match[1] ];
}
return $match[0];
},
$this->html
);
}
if ($has_delayed) {
Plugin::delay_load()->enable(
Plugin::options()->delay_css_type === 'onload' ? true : null
);
}
if ($has_gfonts) {
Plugin::google_fonts()->enable();
$this->html = Plugin::google_fonts()->render_in_dom($this->html);
}
return $this->html;
}
/**
* Apply CSS optimizes such as minify and inline, if enabled.
*
* @param Stylesheet $sheet
* @return void
*/
public function optimize(Stylesheet $sheet)
{
if (!$this->should_optimize($sheet)) {
return;
}
// We're going to use onload delay method (non-JS) fixing render blocking.
$sheet->delay_type = 'onload';
$sheet->set_render('delay');
// Should the sheet be minified.
$minify = Plugin::options()->optimize_css_minify;
// For inline CSS, minification is enforced.
if (Plugin::options()->optimize_css_to_inline) {
$minify = true;
}
// Will optimize if it's a google font. Note: Has to be done before minify.
Plugin::google_fonts()->optimize($sheet);
// Google Fonts inline also relies on minification.
if ($sheet->is_google_fonts() && Plugin::options()->optimize_gfonts_inline) {
$minify = true;
}
if ($minify) {
$minifier = new Minifier($sheet);
$minifier->process();
}
if (Plugin::options()->optimize_css_to_inline) {
if (!$sheet->content) {
$file = Plugin::file_system()->url_to_local($sheet->get_content_url());
if ($file) {
$sheet->content = Plugin::file_system()->get_contents($file);
}
}
// If we have content by now.
if ($sheet->content) {
$sheet->set_render('inline');
}
}
}
/**
* Determine if stylesheet should be optimized, based on exclusion and inclusion
* rules and settings.
*
* @param Stylesheet $sheet
* @return boolean
*/
public function should_optimize(Stylesheet $sheet)
{
// Only go ahead if optimizations are enabled and remove css hasn't happened.
if (!Plugin::options()->optimize_css || $sheet->render_type === 'remove_css') {
return false;
}
// Debugging scripts. Can't be minified, so can't be inline either.
if (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) {
return;
}
// Handle manual excludes first.
if ($this->exclude_sheets) {
foreach ($this->exclude_sheets as $exclude) {
if (Util\asset_match($exclude, $sheet)) {
return false;
}
}
}
return true;
}
/**
* Find all the stylesheet links.
*
* @return Stylesheet[]
*/
public function find_stylesheets()
{
$stylesheets = [];
// Note: Can't use DOM parser as html entities in the URLs will be removed and
// replacing won't be possible later.
preg_match_all('#<link[^>]*stylesheet[^>]*>#Usi', $this->html, $matches);
foreach ($matches[0] as $sheet) {
$sheet = Stylesheet::from_tag($sheet);
if ($sheet) {
$stylesheets[] = $sheet;
}
}
$this->stylesheets = $stylesheets;
return $this->stylesheets;
}
/**
* Should unused CSS be removed.
*
* @return boolean
*/
public function should_remove_css()
{
$valid = Plugin::options()->remove_css && Plugin::process()->check_enabled(Plugin::options()->remove_css_on);
return apply_filters('debloat/should_remove_css', $valid);
}
}

View File

@ -0,0 +1,296 @@
<?php
namespace Sphere\Debloat\OptimizeCss;
use Sphere\Debloat\Base\Asset;
use Sphere\Debloat\Util;
/**
* Value object for stylehseets.
*
* @author asadkn
* @since 1.0.0
*/
class Stylesheet extends Asset
{
public $has_cache = false;
/**
* Specify a different id to use while rendering the script.
*
* @var string
*/
public $render_id = '';
/**
* @var string
*/
public $file;
/**
* CSS content.
*
* @var string
*/
public $content = '';
/**
* @var array
*/
public $parsed_data;
/**
* @var string
*/
public $render_type = '';
public $delay_type = 'onload';
/**
* Whether delay load exists. Some sheets may have a different render_type but
* may still need to be delayed in addition.
*
* @var boolean
*/
public $has_delay = false;
/**
* Media type for this stylesheet.
*
* @var string
*/
public $media;
/**
* Pre-rendered content
*
* @var string
*/
public $render_string;
public $original_size = 0;
public $new_size = 0;
/**
* Whether the stylesheet is a google fonts link.
*
* @var boolean
*/
protected $is_google_fonts = false;
public function __construct(string $id, string $url)
{
$this->id = $id;
$this->orig_url = $url;
// Cleaned URL.
$this->url = html_entity_decode($url, ENT_COMPAT | ENT_SUBSTITUTE);
if (!$this->id) {
$this->id = md5($this->url);
}
$this->is_google_fonts = stripos($this->url, 'fonts.googleapis.com/css') !== false;
}
/**
* Factory method: Create an instance provided an HTML tag.
*
* @param string $tag
* @return boolean|self
*/
public static function from_tag(string $tag)
{
$attrs = Util\parse_attrs($tag);
if (!isset($attrs['href'])) {
return false;
}
$sheet = new self($attrs['id'] ?? '', $attrs['href']);
$sheet->media = $attrs['media'] ?? '';
return $sheet;
}
public function render()
{
if ($this->render_string) {
return $this->render_string;
}
$attrs = [
'rel' => 'stylesheet',
'id' => $this->render_id ?: $this->id,
];
if ($this->media) {
$attrs['media'] = $this->media;
}
/**
* 1. Render type 'delay' at onload or delay via JS for later.
*/
if ($this->render_type === 'delay') {
if ($this->media === 'print') {
return '';
}
if ($this->delay_type === 'onload') {
$media = $this->media ?? 'all';
$attrs = array_replace($attrs, [
'media' => 'print',
'href' => $this->get_content_url(),
'onload' => "this.media='{$media}'"
]);
} else if ($this->delay_type === 'preload') {
$attrs = array_replace($attrs, [
'rel' => 'preload',
'as' => 'style',
'href' => $this->get_content_url(),
'onload' => "this.rel='stylesheet'"
]);
} else {
/**
* Other types of delayed loads are handled via JS, so add the relevant data.
*/
$attrs += [
'data-debloat-delay' => true,
'data-href' => $this->get_content_url()
];
}
return sprintf(
'<link %1$s/>',
implode(' ', $this->render_attrs($attrs, ['onload']))
);
}
/**
* 2. Render type 'inline' when the CSS has to be inlined.
*/
if ($this->render_type === 'inline') {
if (!$this->content) {
return '';
}
unset($attrs['rel']);
if ($attrs['media'] === 'all') {
unset($attrs['media']);
}
return sprintf(
'<style %1$s>%2$s</style>',
implode(' ', $this->render_attrs($attrs)),
$this->content
);
}
/**
* 3. Normal render. Usually for stylesheets that just have to be minified.
*/
if ($this->render_type === 'normal') {
$attrs += [
'href' => $this->get_content_url()
];
return sprintf(
'<link %1$s/>',
implode(' ', $this->render_attrs($attrs))
);
}
// Default to nothing.
return '';
}
public function set_render($type, $render_string = '')
{
$this->render_type = $type;
if ($render_string) {
$this->render_string = $render_string;
}
}
/**
* Whether or not stylesheet is delayed or has a delayed factor to it (such as
* remove_css + delay combo).
*
* @return boolean
*/
public function has_delay()
{
return $this->has_delay || $this->render_type === 'delay';
}
public function convert_urls()
{
// Original base URL.
$base_url = preg_replace('#[^/]+\?.*$#', '', $this->url);
// Borrowed and modified from MatthiasMullie\Minify\CSS.
$regex = '/
# open url()
url\(
\s*
# open path enclosure
(?P<quotes>["\'])?
# fetch path
(?P<path>.+?)
# close path enclosure, conditional
(?(quotes)(?P=quotes))
\s*
# close url()
\)
/ix';
preg_match_all($regex, $this->content, $matches, PREG_SET_ORDER);
if (!$matches) {
return;
}
foreach ($matches as $match) {
$url = trim($match['path']);
if (substr($url, 0, 5) === 'data:') {
continue;
}
$parsed_url = parse_url($url);
// Skip known host and protocol-relative paths.
if (!empty($parsed_url['host']) || empty($parsed_url['path']) || $parsed_url['path'][0] === '/') {
continue;
}
$new_url = $base_url . $url;
// URLs with quotes, #, brackets or characters above 0x7e should be quoted.
// Restore original quotes.
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/url()#syntax
if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $new_url)) {
$new_url = $match['quotes'] . $new_url . $match['quotes'];
}
$new_url = 'url(' . $new_url. ')';
$this->content = str_replace($match[0], $new_url, $this->content);
}
}
/**
* Whether the style is a google fonts URL.
*
* @return boolean
*/
public function is_google_fonts()
{
return $this->is_google_fonts;
}
}

View File

@ -0,0 +1,358 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\OptimizeJs\Script;
/**
* JS Optimizations such as defer and delay.
*
* @author asadkn
* @since 1.0.0
*/
class OptimizeJs
{
protected $html;
/**
* @var array
*/
protected $scripts = [];
/**
* Include and exclude scripts for "delay".
*
* @var array
*/
protected $include_scripts = [];
protected $exclude_scripts = [];
/**
* Exclude scripts from defer.
*/
protected $exclude_defer = [];
/**
* Scripts already delayed or deffered.
*/
protected $done_delay = [];
protected $done_defer = [];
/**
* Scripts registered data from core.
*/
protected $enqueues = [];
public function __construct(string $html)
{
$this->html = $html;
}
public function process()
{
$this->find_scripts();
// Figure out valid scripts.
$this->setup_valid_scripts();
do_action('debloat/optimize_js_begin', $this);
// Early First pass to setup valid parents for child dependency checks.
array_map(function($script) {
$this->should_defer_script($script);
}, $this->scripts);
/**
* Setup scripts defer and delays and render the replacements.
*/
$has_delayed = false;
foreach ($this->scripts as $script) {
// Has to be done for all scripts.
if (Plugin::options()->minify_js) {
$this->minify_js($script);
$script->render = true;
}
// Defer script if not already deferred.
if ($this->should_defer_script($script)) {
$script->defer = true;
$script->render = true;
$has_delayed = true;
}
// Should this script be delayed.
if ($this->should_delay_script($script)) {
$script->delay = true;
$script->render = true;
$has_delayed = true;
}
else {
Util\debug_log('Skipping: ' . print_r($script, true));
}
if ($script->render) {
$this->html = str_replace($script->orig_html, $script->render(), $this->html);
}
}
if ($has_delayed) {
Plugin::delay_load()->enable(true);
}
return $this->html;
}
public function find_scripts()
{
/**
* Collect all valid enqueues.
*/
$all_scripts = [];
foreach (wp_scripts()->registered as $handle => $script) {
// Not an enqueued script, ignore.
if (!in_array($handle, wp_scripts()->done)) {
continue;
}
// Don't mutate original.
$script = clone $script;
$script->deps = array_map(function($id) {
// jQuery JS should be mapped to core. Add -js suffix for others.
return $id === 'jquery' ? 'jquery-core-js' : $id . '-js';
}, $script->deps);
// Add -js prefix to match the IDs that will retrieved from attrs.
$handle .= '-js';
$all_scripts[$handle] = $script;
// Pseudo entry for extras, adding a dependency.
if (isset($script->extra['after'])) {
$all_scripts[$handle . '-after'] = (object) [
'deps' => [$handle]
];
}
}
$this->enqueues = $all_scripts;
/**
* Find all scripts.
*/
if (!preg_match_all('#<script.*?</script>#si', $this->html, $matches)) {
return;
}
foreach ($matches[0] as $script) {
$script = Script::from_tag($script);
if ($script) {
$script->deps = $this->enqueues[$script->id]->deps ?? [];
// Note: There can be multiple scripts with same URL or id. So we don't use $script->id.
$this->scripts[] = $script;
// Inline script has jQuery content? Ensure dependency.
if (
!$script->url &&
!in_array('jquery-core-js', $script->deps) &&
strpos($script->content, 'jQuery') !== false
) {
$script->deps[] = 'jquery-core-js';
}
}
}
}
/**
* Check if the script has a parent dependency that is delayed/deferred.
*
* @param Script $script
* @param array $valid
* @return boolean
*/
public function check_dependency(Script $script, array $valid)
{
// Check if one of parent dependencies is delayed/deferred.
foreach ((array) $script->deps as $dep) {
if (isset($valid[$dep])) {
return true;
}
}
// For translations, if wp-i18n-js is valid, so should be translations.
if (preg_match('/-js-translations/', $script->id, $matches)) {
if (isset($valid['wp-i18n-js'])) {
return true;
}
}
// Special case: Inline script with a parent dep. If parent is true, so is child.
// Note: 'before' and 'extra' isn't accounted for and is not usually needed. Since 'extra' is
// usually localization and 'before' has to happen before anyways.
if (preg_match('/(.+?-js)-(before|after)/', $script->id, $matches)) {
$handle = $matches[1];
// Parent was valid, so is the current child.
if (isset($valid[$handle])) {
return true;
}
}
return false;
}
/**
* Should the script be delayed using one of the JS delay methods.
*
* @param Script $script
* @return boolean
*/
public function should_delay_script(Script $script)
{
if (!Plugin::options()->delay_js) {
return false;
}
// Excludes should be handled before includes and parents check.
foreach ($this->exclude_scripts as $exclude) {
if (Util\asset_match($exclude, $script, 'orig_html')) {
return false;
}
}
// Delay all.
if (Plugin::options()->delay_js_all) {
return true;
}
if ($this->check_dependency($script, $this->done_delay)) {
$this->done_delay[$script->id] = true;
return true;
}
foreach ($this->include_scripts as $include) {
if (Util\asset_match($include, $script, 'orig_html')) {
$this->done_delay[$script->id] = true;
return true;
}
}
return false;
}
/**
* Should the script be deferred.
*
* @param Script $script
* @return boolean
*/
public function should_defer_script(Script $script)
{
// Defer not enabled.
if (!Plugin::options()->defer_js) {
return false;
}
foreach ($this->exclude_defer as $exclude) {
if (Util\asset_match($exclude, $script, 'orig_html')) {
return false;
}
}
// For inline scripts: By default not deferred, unless child of a deferred.
if (!$script->url) {
if (Plugin::options()->defer_js_inline) {
return true;
}
if ($this->check_dependency($script, $this->done_defer)) {
$this->done_defer[$script->id] = true;
return true;
}
return false;
}
// If defer or async attr already exists on original script.
if ($script->defer || $script->async) {
return false;
}
$this->done_defer[$script->id] = true;
return true;
}
/**
* Setup includes and excludes based on options.
*
* @return void
*/
public function setup_valid_scripts()
{
// Used by both delay and defer.
$shared_excludes = [
// Lazyloads should generally be loaded as early as possible and not deferred.
'lazysizes.js',
'lazyload.js',
'lazysizes.min.js',
'lazyLoadOptions',
'lazyLoadThumb',
];
/**
* Defer scripts.
*/
$defer_excludes = array_merge(
Util\option_to_array(Plugin::options()->defer_js_excludes),
$shared_excludes
);
$defer_excludes[] = 'id:-js-extra';
$this->exclude_defer = apply_filters('debloat/defer_js_excludes', $defer_excludes);
/**
* Delayed load scripts.
*/
$excludes = Util\option_to_array(Plugin::options()->delay_js_excludes);
$excludes = array_merge($excludes, $shared_excludes, [
// Jetpack stats.
'url://stats.wp.com',
'_stq.push',
// WPML browser redirect.
'browser-redirect/app.js',
// Skip -js-extra as it's global variables and localization that shouldn't be delayed.
'id:-js-extra'
]);
$this->exclude_scripts = apply_filters('debloat/delay_js_excludes', $excludes);
$this->include_scripts = array_merge(
$this->include_scripts,
Util\option_to_array(Plugin::options()->delay_js_includes)
);
// Enable delay adsense.
if (Plugin::options()->delay_js_adsense) {
$this->include_scripts[] = 'adsbygoogle.js';
}
$this->include_scripts = apply_filters('debloat/delay_js_includes', $this->include_scripts);
}
/**
* Minify the JS file, if possible.
*
* @param Script $script
* @return void
*/
public function minify_js(Script $script)
{
$minifier = new Minifier($script);
$minifier->process();
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace Sphere\Debloat\OptimizeJs;
use Sphere\Debloat\Util;
use Sphere\Debloat\Base\Asset;
/**
* Class for handling scripts.
*
* @author asadkn
* @since 1.0.0
*/
class Script extends Asset
{
public $defer = false;
public $async = false;
public $delay = false;
/**
* Dependencies to mirror from core script.
*
* @var array
*/
public $deps = [];
/**
* Inner content for inline scripts
*
* @var string
*/
public $content = '';
/**
* @var array
*/
public $attrs = [];
/**
* Original HTML tag.
*/
public $orig_html = '';
/**
* Whether the script is dirty and needs rendering.
*
* @var boolean
*/
public $render = false;
/**
* @var callable|null
*/
protected $minifier;
public function __construct(string $id, string $url = '', string $orig_html = '')
{
$this->id = $id;
$this->url = $url;
$this->orig_url = $url;
$this->orig_html = $orig_html;
if (!$this->id) {
$this->id = md5($this->url ?: uniqid('debloat_script', true));
}
// Has to be processed early as it might have to be searched for things like defer.
$this->process_content();
}
/**
* Factory method: Create an instance provided an HTML tag.
*
* @param string $tag
* @return boolean|self
*/
public static function from_tag(string $tag)
{
$attrs = Util\parse_attrs($tag);
// Only process scripts of type javascript or missing type (assumed JS by browser).
if (isset($attrs['type']) && strpos($attrs['type'], 'javascript') === false) {
return false;
}
$script = new self($attrs['id'] ?? '', $attrs['src'] ?? '', $tag);
// Note: We keep 'id' just in case if there was an original id.
$script->attrs = \array_diff_key($attrs, array_flip(['src', 'defer', 'async']));
if (isset($attrs['defer'])) {
$script->defer = true;
}
if (isset($attrs['async'])) {
$script->async = true;
}
return $script;
}
/**
* Render the HTML.
*
* @return string
*/
public function render()
{
// Add src if available.
if ($this->url) {
$this->attrs += [
'src' => $this->get_content_url(),
'id' => $this->id,
];
}
$this->process_delay();
// Setting local to avoid mutating.
$content = $this->content;
if (!$this->url && $content && is_callable($this->minifier)) {
$content = call_user_func($this->minifier, $content);
}
// Add defer for external scripts. For inline scripts, use data uri in src.
if ($this->defer) {
$this->attrs['defer'] = true;
if (!$this->url) {
$this->attrs['src'] = 'data:text/javascript;base64,' . base64_encode($content);
$content = '';
}
}
if ($this->async) {
$this->attrs['async'] = true;
}
$render_atts = implode(' ', $this->render_attrs($this->attrs));
$new_tag = sprintf(
'<script%1$s>%2$s</script>',
$render_atts ? " $render_atts" : '',
!$this->url ? $content : ''
);
return $new_tag;
}
/**
* Set the inner content, if any.
*
* @return void
*/
public function process_content()
{
if (!$this->url) {
$this->content = preg_replace('#<script[^>]*>(.+?)</script>\s*$#is', '\\1', $this->orig_html);
}
}
/**
* Set a callable minifier function.
*
* @param callable $minifier
* @return void
*/
public function set_minifier($minifier)
{
$this->minifier = $minifier;
}
/**
* Process and add delayed script attributes if needed.
*
* @return void
*/
public function process_delay()
{
if (!$this->delay) {
return;
}
// Defer / async shouldn't be enabled anymore. Delay load logic implies deferred anyways.
$this->defer = false;
$this->async = false;
// Add delay.
$this->attrs['data-debloat-delay'] = 1;
// Normal script with a URL.
if (!empty($this->attrs['src'])) {
$this->attrs['data-src'] = $this->attrs['src'];
}
// Inline script with content.
elseif ($this->content) {
if (!empty($this->attrs['type'])) {
$this->attrs['data-type'] = $this->attrs['type'];
}
$this->attrs['type'] = 'text/debloat-script';
}
unset($this->attrs['src']);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\Admin\OptionsData;
/**
* A very basic options class.
*
* @author asadkn
* @since 1.0.0
*/
class Options
{
/**
* @var string|array Option key to use for get_options().
*/
public $option_key;
public $_options;
public $defaults = [];
/**
* @param string|array $option_key
*/
public function __construct($option_key)
{
$this->option_key = $option_key;
}
/**
* Initialize
*/
public function init()
{
$this->load_defaults();
if (is_array($this->option_key)) {
$this->_options = [];
foreach ($this->option_key as $key) {
$this->_options = array_merge($this->_options, (array) get_option($key));
}
} else {
$this->_options = (array) get_option($this->option_key);
}
$this->_options = apply_filters('debloat/init_options', $this->_options);
}
public function load_defaults()
{
if (!class_exists('Sphere\Debloat\Admin\OptionsData')) {
return;
}
$this->defaults = array_reduce(
OptionsData::get_all(),
function($acc, $option) {
$acc[$option['id']] = isset($option['default']) ? $option['default'] : '';
return $acc;
},
[]
);
}
/**
* Get an option
*/
public function get($key, $fallback = '')
{
if (array_key_exists($key, $this->_options)) {
return $this->_options[$key];
}
if (array_key_exists($key, $this->defaults)) {
return $this->defaults[$key];
}
return $fallback;
}
public function __get($key)
{
return $this->get($key);
}
public function __set($key, $value)
{
$this->_options[$key] = $value;
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace Sphere\Debloat;
/**
* The Plugin Bootstrap and Setup.
*
* Dual acts as a container and a facade.
*
* @author asadkn
* @since 1.0.0
*
* @method static FileSystem file_system() Extends \WP_Filesystem_Base
* @method static Process process()
* @method static Options options()
* @method static DelayLoad delay_load()
* @method static FileCache file_cache()
* @method static OptimizeCss\GoogleFonts google_fonts()
*/
class Plugin
{
/**
* Plugin version
*/
const VERSION = '1.2.3';
public static $instance;
/**
* @var string Can be 'dev' for debgging.
*/
public $env = 'production';
/**
* Path to plugin folder, trailing slashed.
*/
public $dir_path;
public $dir_url;
public $plugin_file;
/**
* A pseudo service container
*/
public $container;
/**
* Set it hooks on init.
*/
public function init()
{
$this->dir_path = plugin_dir_path($this->plugin_file);
$this->dir_url = plugin_dir_url($this->plugin_file);
// Fire up the main autoloader.
require_once $this->dir_path . 'inc/autoloader.php';
new Autoloader([
'Sphere\Debloat\\' => $this->dir_path . 'inc',
]);
// Composer autoloader.
require_once $this->dir_path . 'vendor/autoload.php';
/**
* Setup and init common requires.
*/
// Options object with bound constructor args.
$this->container['options'] = $this->shared(
__NAMESPACE__ . '\Options',
[
['debloat_options', 'debloat_options_js', 'debloat_options_general']
]
);
// The process handler.
$this->container['process'] = $this->shared(__NAMESPACE__ . '\Process');
// File system with lazy init singleton.
$this->container['file_system'] = $this->shared(__NAMESPACE__ . '\FileSystem');
// Delay Load assets.
$this->container['delay_load'] = $this->shared(__NAMESPACE__ . '\DelayLoad');
// File cache handler.
$this->container['file_cache'] = $this->shared(__NAMESPACE__ . '\FileCache');
// Google Fonts is a singleton.
$this->container['google_fonts'] = $this->shared(__NAMESPACE__ . '\OptimizeCss\GoogleFonts');
/**
* Admin only requires
*
* We load these only on Admin side to keep things lean and performant.
*
* Note on CMB2:
* It's used ONLY as an admin side dependency and never even
* loaded on the frontend. Use native WP options API on front.
*/
if (is_admin() || defined('WP_CLI')) {
$admin = new Admin;
$admin->init();
// We don't want CMB2 backend loaded at all in AJAX requests (admin-ajax.php passes is_admin() test)
if (!wp_doing_ajax()) {
require_once $this->dir_path . 'vendor/cmb2/init.php';
}
// Path bug fix for cmb2 in composer
add_filter('cmb2_meta_box_url', function() {
return self::get_instance()->dir_url . 'vendor/cmb2';
});
// WP-CLI commands.
if (class_exists('\WP_CLI')) {
\WP_CLI::add_command('debloat', '\Sphere\Debloat\WpCli\Commands');
}
}
// Utility functions.
require_once $this->dir_path . 'inc/util.php';
$this->register_hooks();
// Load the options.
self::options()->init();
// Init process to setup hooks.
self::process()->init();
}
/**
* Creates a single instance class for container.
*
* @param string $class Fully-qualifed class name
* @param array|null $args Bound args to pass to constructor
*/
public function shared($class, $args = null)
{
return function($fresh = false) use ($class, $args) {
static $object;
if (!$object || $fresh) {
if (!$args) {
$object = new $class;
}
else {
$ref = new \ReflectionClass($class);
$object = $ref->newInstanceArgs($args);
}
}
return $object;
};
}
/**
* Setup hooks actions.
*/
public function register_hooks()
{
// Translations
add_action('plugins_loaded', array($this, 'load_textdomain'));
}
/**
* Setup translations.
*/
public function load_textdomain()
{
load_plugin_textdomain(
'debloat',
false,
basename($this->dir_path) . '/languages'
);
}
/**
* Gets an object from container.
*/
public function get($name, $args = array())
{
if (!isset($this->container[$name])) {
throw new \Exception("No container exists with key '{$name}'");
}
$object = $this->container[$name];
if (is_callable($object)) {
return call_user_func_array($object, $args);
}
else if (is_string($object)) {
$object = new $object;
}
return $object;
}
/**
* @uses self::get()
*/
public static function __callStatic($name, $args = array())
{
return self::get_instance()->get($name, $args);
}
/**
* @return $this
*/
public static function get_instance()
{
if (self::$instance == null) {
self::$instance = new static();
}
return self::$instance;
}
}

View File

@ -0,0 +1,259 @@
<?php
namespace Sphere\Debloat;
/**
* Debloat processing setup for JS and CSS optimizations.
*
* @author asadkn
* @since 1.0.0
*/
class Process
{
/**
* Init happens too early around plugins_loaded
*/
public function init()
{
// Setup a few extra options.
Plugin::options()->delay_css_type = 'interact';
Plugin::options()->delay_js_type = 'interact';
// Setup at init but before template_redirect.
add_action('init', [$this, 'setup']);
}
/**
* Setup filters for processing
*
* @since 1.1.0
*/
public function setup()
{
if ($this->should_process()) {
// Load integrations.
$integrations = array_unique(array_merge(
(array) Plugin::options()->integrations_js,
(array) Plugin::options()->integrations_css
));
if ($integrations) {
if (in_array('elementor', $integrations) && class_exists('\Elementor\Plugin', false)) {
new Integrations\Elementor;
}
if (in_array('wpbakery', $integrations) && class_exists('Vc_Manager')) {
new Integrations\Wpbakery;
}
}
/**
* Process HTML for inline and local stylesheets.
*
* wp_ob_end_flush_all() will take care of flushing it.
*
* Note: Autoptimize starts at priority 2 so we use 3 to process BEFORE AO.
*/
add_action('template_redirect', function() {
if (!apply_filters('debloat/should_process', true)) {
return false;
}
// Can't go in should_process() as that's too early.
if (function_exists('\amp_is_request') && \amp_is_request()) {
return false;
}
// Shouldn't process feeds, embeds (iframe), or robots.txt request.
if (\is_feed() || \is_embed() || \is_robots()) {
return false;
}
ob_start([$this, 'process_markup']);
}, -999);
// DEBUG: Devs if your output is disappearing - which you need for debugging,
// uncomment below and comment the init action above.
// add_action('template_redirect', function() { ob_start(); }, -999);
// add_action('shutdown', function() {
// $content = ob_get_clean();
// echo $this->process_markup($content);
// }, -10);
}
}
/**
* Process DOM Markup provided with the html.
*
* @param string $html
* @return string
*/
public function process_markup($html)
{
do_action('debloat/process_markup', $this);
if (!$this->is_valid_markup($html)) {
return $html;
}
$dom = null;
if ($this->should_optimize_css()) {
$dom = $this->get_dom($html);
$optimize = new OptimizeCss($dom, $html);
$html = $optimize->process();
}
if ($this->should_optimize_js()) {
$optimize_js = new OptimizeJs($html);
$html = $optimize_js->process();
}
// Add delay load JS and extras as needed.
$html = Plugin::delay_load()->render($html);
// Failed at processing DOM, return original.
if (!$dom) {
return $html;
}
return $html;
}
public function is_valid_markup($html)
{
if (stripos($html, '<html') === false) {
return false;
}
return true;
}
/**
* Get DOM object for the provided HTML.
*
* @param string $html
* @return boolean|\DOMDocument
*/
protected function get_dom($html)
{
if (!$html) {
return false;
}
$libxml_previous = libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$result = $dom->loadHTML($html);
libxml_clear_errors();
libxml_use_internal_errors($libxml_previous);
// Deprecating xpath querying.
// if ($result) {
// $dom->xpath = new \DOMXPath($dom);
// }
return $result ? $dom : false;
}
/**
* Should any processing be done at all.
*
* @return boolean
*/
public function should_process()
{
if (is_admin()) {
return false;
}
if (function_exists('is_customize_preview') && is_customize_preview()) {
return false;
}
if (isset($_GET['nodebloat'])) {
return false;
}
if (Util\is_elementor()) {
return false;
}
// WPBakery Page Builder. vc_is_page_editable() isn't reliable at all hooks.
if (!empty($_GET['vc_editable'])) {
return false;
}
if (Plugin::options()->disable_for_admins && current_user_can('manage_options')) {
return false;
}
return true;
}
/**
* Should the JS be optimized.
*
* @return boolean
*/
public function should_optimize_js()
{
$valid = true;
return apply_filters('debloat/should_optimize_js', $valid);
}
/**
* Should the CSS be optimized.
*
* @return boolean
*/
public function should_optimize_css()
{
$valid = Plugin::options()->remove_css || Plugin::options()->optimize_css;
return apply_filters('debloat/should_optimize_css', $valid);
}
/**
* Conditions test to see if current page matches in the provided valid conditions.
*
* @param array $enable_on
* @return boolean
*/
public function check_enabled(array $enable_on)
{
if (in_array('all', $enable_on)) {
return true;
}
$conditions = [
'pages' => 'is_page',
'posts' => 'is_single',
'archives' => 'is_archive',
'archive' => 'is_archive', // Alias
'categories' => 'is_category',
'tags' => 'is_tag',
'search' => 'is_search',
'404' => 'is_404',
'home' => function() {
return is_home() || is_front_page();
},
];
$satisfy = false;
foreach ($enable_on as $key) {
if (!isset($conditions[$key]) || !is_callable($conditions[$key])) {
continue;
}
$satisfy = call_user_func($conditions[$key]);
// Stop going further in loop once satisfied.
if ($satisfy) {
break;
}
}
return $satisfy;
}
}

View File

@ -0,0 +1,388 @@
<?php
namespace Sphere\Debloat;
use Sphere\Debloat\RemoveCss\Sanitizer;
use Sphere\Debloat\OptimizeCss\Stylesheet;
/**
* Process stylesheets to debloat and optimize CSS.
*
* @author asadkn
* @since 1.0.0
*/
class RemoveCss
{
/**
* @var \DOMDocument
*/
public $dom;
public $html;
/**
* @var array
*/
public $used_markup = [];
/**
* @var array
*/
protected $stylesheets;
protected $include_sheets = [];
protected $exclude_sheets = [];
/**
* Undocumented function
*
* @param Stylesheet[] $stylesheets
* @param \DOMDocument $dom
* @param string $raw_html
*/
public function __construct($stylesheets, \DOMDocument $dom, string $raw_html)
{
$this->stylesheets = $stylesheets;
$this->dom = $dom;
$this->html = $raw_html;
}
public function process()
{
// Collect all the classes, ids, tags used in DOM.
$this->find_used_selectors();
// Figure out valid sheets.
$this->setup_valid_sheets();
/**
* Process and replace stylesheets with CSS cleaned.
*/
do_action('debloat/remove_css_begin', $this);
$allow_selectors = $this->get_allowed_selectors();
foreach ($this->stylesheets as $sheet) {
if (!$this->should_process_stylesheet($sheet)) {
// Util\debug_log('Skipping: ' . print_r($sheet, true));
continue;
}
// Perhaps not a local file or unreadable.
$file_data = $this->process_file_by_url($sheet->url);
if (!$file_data) {
continue;
}
$sheet->content = $file_data['content'];
$sheet->file = $file_data['file'];
// Parsed sheet will be cached instead of being parsed by Sabberworm again.
$this->setup_sheet_cache($sheet);
/**
* Fire up sanitizer to process and remove unused CSS.
*/
$sanitizer = new Sanitizer($sheet, $this->used_markup, $allow_selectors);
$sanitized_css = $sanitizer->sanitize();
// Store sizes for debug info.
$sheet->original_size = strlen($sheet->content);
$sheet->new_size = $sanitized_css ? strlen($sanitized_css) : $sheet->original_size;
if ($sanitized_css) {
// Pre-render as we'll have to add a delayed css tag as well, if enabled.
$sheet->content = $sanitized_css;
$sheet->render_id = 'debloat-' . $sheet->id;
$sheet->set_render('inline');
$replacement = $sheet->render();
// Add tags for delayed CSS files.
if (Plugin::delay_load()->should_delay_css()) {
$sheet->delay_type = Plugin::options()->delay_css_type;
$sheet->set_render('delay');
$sheet->has_delay = true;
// Add the delay load CSS tag in addition to inlined sanitized CSS above.
$replacement .= $sheet->render();
}
$sheet->set_render('remove_css', $replacement);
// Save in parsed css cache if not already saved.
$this->save_sheet_cache($sheet);
}
// Free up memory.
$sheet->content = '';
$sheet->parsed_data = '';
}
// $this->stylesheets = array_map(function($sheet) {
// if (isset($sheet->original_size)) {
// $sheet->saved = $sheet->original_size - $sheet->new_size;
// }
// return $sheet;
// }, $this->stylesheets);
$total = array_reduce($this->stylesheets, function($acc, $item) {
if (!empty($item->original_size)) {
$acc += ($item->original_size - $item->new_size);
}
return $acc;
}, 0);
$this->html .= "\n<!-- Debloat Remove CSS Saved: {$total} bytes. -->";
return $this->html;
}
/**
* Add parsed data cache to stylesheet object. Will be used by save_sheet_cache later.
*
* @param Stylesheet $sheet
* @return void
*/
public function setup_sheet_cache(Stylesheet $sheet)
{
if (!isset($sheet->file)) {
return;
}
$cache = get_transient($this->get_transient_id($sheet));
if ($cache && $cache['mtime'] < Plugin::file_system()->mtime($sheet->file)) {
return;
}
if ($cache && !empty($cache['data'])) {
$sheet->parsed_data = $cache['data'];
$sheet->has_cache = true;
return;
}
}
protected function get_transient_id($sheet)
{
return substr('debloat_sheet_cache_' . $sheet->id, 0, 190);
}
/**
* Cache the parsed data.
*
* Note: This doesn't cache whole CSS as that would vary based on found selectors.
*
* @param Stylesheet $sheet
* @return void
*/
public function save_sheet_cache(Stylesheet $sheet)
{
if ($sheet->has_cache) {
return;
}
$cache_data = [
'data' => $sheet->parsed_data,
'mtime' => Plugin::file_system()->mtime($sheet->file)
];
// With expiry; won't be auto-loaded.
set_transient($this->get_transient_id($sheet), $cache_data, MONTH_IN_SECONDS);
}
/**
* Setup includes and excludes based on options.
*
* @return void
*/
public function setup_valid_sheets()
{
$default_excludes = [
'wp-includes/css/dashicons.css',
'admin-bar.css',
'wp-mediaelement'
];
$excludes = array_merge(
$default_excludes,
Util\option_to_array(Plugin::options()->remove_css_excludes)
);
$this->exclude_sheets = apply_filters('debloat/remove_css_excludes', $excludes, $this);
if (Plugin::options()->remove_css_all) {
return;
}
if (Plugin::options()->remove_css_theme) {
$this->include_sheets[] = content_url('themes') . '/*';
}
if (Plugin::options()->remove_css_plugins) {
$this->include_sheets[] = content_url('plugins') . '/*';
}
$this->include_sheets = array_merge(
$this->include_sheets,
Util\option_to_array(Plugin::options()->remove_css_includes)
);
$this->include_sheets = apply_filters('debloat/remove_css_includes', $this->include_sheets, $this);
}
/**
* Determine if stylesheet should be processed, based on exclusion and inclusion
* rules and settings.
*
* @param Stylesheet $sheet
* @return boolean
*/
public function should_process_stylesheet(Stylesheet $sheet)
{
// Handle manual excludes first.
if ($this->exclude_sheets) {
foreach ($this->exclude_sheets as $exclude) {
if (Util\asset_match($exclude, $sheet)) {
return false;
}
}
}
// All stylesheets are valid.
if (Plugin::options()->remove_css_all) {
return true;
}
foreach ($this->include_sheets as $include) {
if (Util\asset_match($include, $sheet)) {
return true;
}
}
return false;
}
/**
* Get all the allowed selectors in correct data format to be used by sanitizer.
*
* @return array
*/
public function get_allowed_selectors()
{
// Allowed selectors of type 'any': simple match in selector string.
$allowed_any = array_map(
function($value) {
if (!$value) {
return '';
}
return [
'type' => 'any',
'search' => [$value]
];
},
Util\option_to_array((string) Plugin::options()->allow_css_selectors)
);
// Conditional selectors.
$allowed_conditionals = [];
$conditionals = Plugin::options()->allow_css_conditionals
? (array) Plugin::options()->allow_conditionals_data
: [];
if ($conditionals) {
$allowed_conditionals = array_map(
function($value) {
if (!isset($value['match'])) {
return '';
}
$value['class'] = preg_replace('/^\./', '', trim($value['match']));
if ($value['type'] !== 'prefix' && isset($value['selectors'])) {
$value['search'] = Util\option_to_array($value['selectors']);
}
return $value;
},
$conditionals
);
}
$allowed = apply_filters(
'debloat/allow_css_selectors',
array_filter(array_merge($allowed_any, $allowed_conditionals)),
$this
);
return $allowed;
}
/**
* Find all the classes, ids, and tags used in the document.
*
* @return void
*/
protected function find_used_selectors()
{
$this->used_markup = [
'tags' => [],
'classes' => [],
'ids' => [],
];
/**
* @var DOMElement $node
*/
$classes = [];
foreach ($this->dom->getElementsByTagName('*') as $node) {
$this->used_markup['tags'][ $node->tagName ] = 1;
// Collect tag classes.
if ($node->hasAttribute('class')) {
$class = $node->getAttribute('class');
$ele_classes = preg_split('/\s+/', $class);
array_push($classes, ...$ele_classes);
}
if ($node->hasAttribute('id')) {
$this->used_markup['ids'][ $node->getAttribute('id') ] = 1;
}
}
// Add the classes.
$classes = array_filter(array_unique($classes));
if ($classes) {
$this->used_markup['classes'] = array_fill_keys($classes, 1);
}
}
/**
* Process a stylesheet via URL
*
* @uses Plugin::file_system()
* @uses Plugin::file_system()->url_to_local()
* @return boolean|array With 'content' and 'file'.
*/
public function process_file_by_url($url)
{
// Try to get local path for this stylesheet
$file = Plugin::file_system()->url_to_local($url);
if (!$file) {
return false;
}
// We can only support .css files yet
if (substr($file, -4) !== '.css') {
return false;
}
$content = Plugin::file_system()->get_contents($file);
if (!$content) {
return false;
}
return [
'content' => $content,
'file' => $file
];
}
}

View File

@ -0,0 +1,529 @@
<?php
namespace Sphere\Debloat\RemoveCss;
use Sabberworm\CSS\CSSList\AtRuleBlockList;
use Sabberworm\CSS\CSSList\CSSBlockList;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parser as CSSParser;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\Settings;
use Sabberworm\CSS\Value\URL;
use Sphere\Debloat\OptimizeCss\Stylesheet;
use Sphere\Debloat\Util;
/**
* Sanitizer removes the unnecessary CSS, provided a stylesheet.
*
* @author asadkn
* @since 1.0.0
*/
class Sanitizer
{
/**
* @var Stylesheet
*/
protected $sheet;
protected $css;
protected $used_markup = [
'classes' => [],
'tags' => [],
'ids' => []
];
protected $allow_selectors = [];
/**
* @param Stylesheet $sheet
* @param array $used_markup {
* @type array $classes
* @type array $tags
* @type array $ids
* }
*/
public function __construct(Stylesheet $sheet, array $used_markup, $allow = [])
{
$this->sheet = $sheet;
$this->css = $sheet->content;
$this->used_markup = array_replace($this->used_markup, $used_markup);
$this->allow_selectors = $allow;
}
public function set_cache($data)
{
$this->cache = $data;
}
public function sanitize()
{
$data = $this->sheet->parsed_data ?: [];
if (!$data) {
// Strip the dreaded UTF-8 byte order mark (BOM, \uFEFF). Ref: https://github.com/sabberworm/PHP-CSS-Parser/issues/150
$this->css = preg_replace('/^\xEF\xBB\xBF/', '', $this->css);
$config = Settings::create()->withMultibyteSupport(false);
$parser = new CSSParser($this->css, $config);
$parsed = $parser->parse();
// Fix relative URLs.
$this->convert_urls($parsed);
$data = $this->transform_data($parsed);
$this->sheet->parsed_data = $data;
}
$this->process_allowed_selectors();
return $this->render_css($data);
}
/**
* Convert relative URLs to full URLs for inline inclusion or changed paths.
*
* @param Document $data
* @return void
*/
public function convert_urls(Document $data)
{
$base_url = preg_replace('#[^/]+\?.*$#', '', $this->sheet->url);
$values = $data->getAllValues();
foreach ($values as $value) {
if (!($value instanceof URL)) {
continue;
}
$url = $value->getURL()->getString();
// if (substr($url, 0, 5) === 'data:') {
// continue;
// }
if (preg_match('/^(https?|data):/', $url)) {
continue;
}
$parsed_url = parse_url($url);
// Skip known host and protocol-relative paths.
if (!empty($parsed_url['host']) || empty($parsed_url['path']) || $parsed_url['path'][0] === '/') {
continue;
}
$new_url = $base_url . $url;
$value->getUrl()->setString($new_url);
}
}
/**
* Transform data structure to store in our format. This data will be used without
* loading CSS Parser on further requests.
*
* @param CSSBlockList $data
* @return array
*/
public function transform_data(CSSBlockList $data)
{
$items = [];
foreach ($data->getContents() as $content) {
if ($content instanceof AtRuleBlockList) {
$items[] = [
'rulesets' => $this->transform_data($content),
'at_rule' => "@{$content->atRuleName()} {$content->atRuleArgs()}",
];
}
else {
$item = [
//'css' => $content->render(OutputFormat::createPretty())
'css' => $content->render(OutputFormat::createCompact())
];
if ($content instanceof DeclarationBlock) {
$item['selectors'] = $this->parse_selectors($content->getSelectors());
}
$items[] = $item;
}
}
return $items;
}
/**
* Parse selectors to get classes, id, tags and attrs.
*
* @param array $selectors
* @return array
*/
protected function parse_selectors($selectors)
{
$selectors = array_map(
function($sel) {
return $sel->__toString();
},
$selectors
);
$selectors_data = [];
foreach ($selectors as $selector) {
$data = [
'classes' => [],
'ids' => [],
'tags' => [],
// 'pseudo' => [],
'attrs' => [],
'selector' => trim($selector),
];
// if (strpos($selector, ':root') !== false) {
// $data['pseudo'][':root'] = 1;
// }
// Based on AMP plugin.
// Handle :not() and pseudo selectors to eliminate false negatives.
$selector = preg_replace('/(?<!\\\\)::?[a-zA-Z0-9_-]+(\(.+?\))?/', '', $selector);
// Get attributes but remove them from the selector to prevent false positives
// from within attribute selector.
$selector = preg_replace_callback(
'/\[([A-Za-z0-9_:-]+)(\W?=[^\]]+)?\]/',
function($matches) use (&$data) {
$data['attrs'][] = $matches[1];
return '';
},
$selector
);
// Extract class names.
$selector = preg_replace_callback(
// The `\\\\.` will allow any char via escaping, like the colon in `.lg\:w-full`.
'/\.((?:[a-zA-Z0-9_-]+|\\\\.)+)/',
function($matches) use (&$data) {
$data['classes'][] = stripslashes($matches[1]);
return '';
},
$selector
);
// Extract IDs.
$selector = preg_replace_callback(
'/#([a-zA-Z0-9_-]+)/',
function( $matches ) use (&$data) {
$data['ids'][] = $matches[1];
return '';
},
$selector
);
// Extract tag names.
$selector = preg_replace_callback(
'/[a-zA-Z0-9_-]+/',
function($matches) use (&$data) {
$data['tags'][] = $matches[0];
return '';
},
$selector
);
$selectors_data[] = array_filter($data);
}
return array_filter($selectors_data);
}
public function render_css($data)
{
$rendered = [];
foreach ($data as $item) {
// Has CSS.
if (isset($item['css'])) {
$css = $item['css'];
// Render only if at least one selector meets the should_include criteria.
$should_render = !isset($item['selectors']) ||
0 !== count(
array_filter(
$item['selectors'],
function($selector) {
return $this->should_include($selector);
}
)
);
if ($should_render) {
$rendered[] = $css;
}
continue;
}
// Nested ruleset.
if (!empty($item['rulesets'])) {
$child_rulesets = $this->render_css($item['rulesets']);
if ($child_rulesets) {
$rendered[] = sprintf(
'%s { %s }',
$item['at_rule'],
$child_rulesets
);
}
}
}
return implode("", $rendered);
}
/**
* Pre-process allowed selectors.
*
* Convert data structures in proper format, mainly for performance.
*
* @return void
*/
protected function process_allowed_selectors()
{
foreach ($this->allow_selectors as $key => $value) {
// Check if selector rule valid for current sheet.
if (isset($value['sheet']) && !Util\asset_match($value['sheet'], $this->sheet)) {
unset($this->allow_selectors[$key]);
continue;
}
$value = $this->add_search_regex($value);
$regex = $value['search_regex'] ?? '';
// Pre-compute the matching regex for performance.
if (isset($value['search'])) {
$value['search'] = array_filter((array) $value['search']);
// If we still have something.
if ($value['search']) {
$loose_regex = '(' . implode('|', array_map('preg_quote', $value['search'])) . ')(?=\s|\.|\:|,|\[|$)';
// Combine with search_regex if available.
$regex = $regex ? "($loose_regex|$regex)" : $loose_regex;
}
}
if ($regex) {
$value['computed_search_regex'] = $regex;
}
$this->allow_selectors[$key] = $value;
}
}
/**
* Add search regex to array by converting astrisks to proper regex search.
*
* @param array $value
* @return array
*/
protected function add_search_regex(array $value)
{
if (isset($value['search_regex'])) {
return $value;
}
if (isset($value['search'])) {
$value['search'] = (array) $value['search'];
$regex = [];
foreach ($value['search'] as $key => $search) {
if (strpos($search, '*') !== false) {
$search = trim($search);
// Optimize regex for starting.
// Note: Ending asterisk removal isn't necessary. PCRE engine is optimized for that.
$has_first_asterisk = 0;
$search = preg_replace('/^\*(.+?)/', '\\1', $search, 1, $has_first_asterisk);
// 1. Space and asterisk matches a class itself, followed by space (child), or comma separator.
// 2. Only asterisk is considered more of a prefix/suffix and .class* will match .classname too.
$search = preg_quote($search);
$search = str_replace(' \*', '(\s|$|,|\:).*?', $search);
$search = str_replace('\*', '.*?', $search);
// Note: To prevent ^(.*?) which is slow, we add starting position match only
// if the search doesn't start with asterisk match.
$regex[] = ($has_first_asterisk ? '' : '^') . $search;
unset($value['search'][$key]);
}
}
if ($regex) {
$value['search_regex'] = '(' . implode('|', $regex) . ')';
}
}
return $value;
}
/**
* Whether to include a selector in the output.
*
* @param array $selector {
* @type string[]|null $classes
* @type string[]|null $ids
* @type string[]|null $tags
* }
* @return boolean
*/
public function should_include($selector)
{
// :root is always valid.
// Note: Selectors of type `:root .class` will not match this but will be validated below
// if .class is used, as intended.
if ($selector['selector'] === ':root') {
return true;
}
// If it's an attribute selector with nothing else, it should be kept. Perhaps *[attr] or [attr].
if (!empty($selector['attrs'])
&& (empty($selector['classes']) && empty($selector['ids']) && empty($selector['tags']))
) {
return true;
}
// Check allow list.
// @todo move to cached pre-processed. Clear on settings change.
// $this->allow_selectors = [
// [
// 'type' => 'any',
// 'search' => '.auth-modal',
// ],
// [
// 'type' => 'prefix',
// 'class' => 's-dark'
// ],
// [
// 'type' => 'class',
// 'class' => 'has-lb',
// 'search' => ['.mfp-']
// ],
// [
// 'type' => 'any',
// 'class' => 'has-lb',
// 'search' => ['.mfp-']
// ],
// ];
if ($this->allow_selectors) {
foreach ($this->allow_selectors as $include) {
/**
* Prefix-based + all other classes/tags/etc. in selector exist in doc.
*
* Note: It's basically to ignore the first class and include the sub-classes based
* on their existence in doc. Example: .scheme-dark or .scheme-light.
*/
if ($include['type'] === 'prefix') {
// Check if exact match.
if (('.' . $include['class']) === $selector['selector']) {
return true;
}
// Check if first class matches.
$has_prefix = $include['class'] === substr($selector['selector'], 1, strlen($include['class']));
if ($has_prefix) {
// Will check for validity later below. Remove first class as it's allowed.
if (isset($selector['classes'])) {
$selector['classes'] = array_diff($selector['classes'], [$include['class']]);
}
// WARNING: Due to this break, if there's a rule to allow all selectors of this prefix
// that appear later, it won't be validated.
// @todo Sort prefixes to be at the end or run them later.
break;
}
continue;
}
// Check if a class exists in document.
if ($include['type'] === 'class') {
if (!$this->is_used($include['class'], 'classes')) {
continue;
}
}
// Simple search selector string.
// $search = !empty($include['search']) ? (array) $include['search'] : [];
// Any type, normal selector string match.
// Note: The regex is equal at n=1 and faster at n>1, surprisingly.
// if ($search) {
// foreach ($search as $to_match) {
// if (strpos($selector['selector'], $to_match) !== false) {
// return true;
// }
// }
// }
// Pre-computed regex - combined 'search' and 'search_regex'.
if (!empty($include['computed_search_regex'])) {
if (preg_match('#' . $include['computed_search_regex'] . '#', $selector['selector'])) {
return true;
}
}
}
}
$valid = true;
if (
// Check if all classes are used.
(!empty($selector['classes']) && !$this->is_used($selector['classes'], 'classes'))
// Check if all the ids are used.
|| (!empty($selector['ids']) && !$this->is_used($selector['ids'], 'ids'))
// Check for the target tags in used.
|| (!empty($selector['tags']) && !$this->is_used($selector['tags'], 'tags'))
) {
$valid = false;
}
return $valid;
}
/**
* Test if a selector classes, ids, or tags are used in the doc (provided in $this->used_markup).
*
* @param string|array $targets
* @param string $type 'classes', 'tags', or 'ids'.
* @return boolean
*/
public function is_used($targets, $type = '')
{
if (!$type) {
return false;
}
if (!is_array($targets)) {
$targets = (array) $targets;
}
foreach ($targets as $target) {
// All targets must exist.
if (!isset($this->used_markup[$type][$target])) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,126 @@
<?php
namespace Sphere\Debloat\Util;
use Sphere\Debloat\Plugin;
/**
* Convert an option value to array format, and clean it.
*
* @param string|array $value
* @return array
*/
function option_to_array($value, $separator = "\n")
{
$value = $value ?: [];
if (is_string($value)) {
$value = array_map('trim', explode($separator, $value));
}
if (!is_array($value)) {
throw new \Exception('A string or array is expected.');
}
$value = array_filter($value);
return $value;
}
/**
* Get attributes given an HTML tag.
*
* @param string $tag
* @return array
*/
function parse_attrs($tag)
{
// Should be a valid html tag with attributes.
preg_match('#<\w+\s*([^>]*)/?>#', $tag, $match);
if (!$match) {
return [];
}
$attr_string = $match[1];
$regex = '(?P<keys>[_a-zA-Z][-\w:.]*)'
. '(?:' // Attribute value.
. '\s*=\s*' // All values begin with '='.
. '(?:'
. '"([^"]*)"' // Double-quoted.
. '|'
. "'([^']*)'" // Single-quoted.
. '|'
. '([^\s"\']+)' // Non-quoted.
. '(?:\s|$)' // Must have a space.
. ')'
. '|'
. '(?:\s|$)' // If attribute has no value, space is required.
. ')';
preg_match_all('#' . $regex . '#i', $attr_string, $matches);
$attrs = [];
foreach ((array) $matches['keys'] as $i => $key) {
// Any of the capturing groups for double-quoted, single-quoted or non-quoted.
$value = $matches[2][$i] ?: $matches[3][$i] ?: $matches[4][$i];
$attrs[$key] = $value;
}
return $attrs;
}
/**
* Match an asset against a string based search.
*
* @param string $match Search string.
* @param object $asset Asset object.
* @param string $search_prop Property to search in.
* @return boolean
*/
function asset_match($match, $asset, $search_prop = 'url')
{
$search_prop = $search_prop ?: 'url';
$search = trim($match);
// Starts with id: or url: - search in specific properties.
if (preg_match('/^(id|url):\s*(.+?)$/', $search, $matches)) {
$search_prop = $matches[1];
$search = $matches[2];
}
$search = preg_quote(trim($search));
$search = str_replace('\*', '(.+?)', $search);
return preg_match('#' . $search . '#i', $asset->{$search_prop});
}
/**
* Check if it's a call from Elementor Page Builder preview.
*
* @return boolean
*/
function is_elementor($preview_check = true)
{
// Elmentor may use AJAX to get widget configs etc.
if (isset($_REQUEST['action']) && $_REQUEST['action'] === 'elementor_ajax') {
return true;
}
if (!class_exists('\Elementor\Plugin', false)) {
return false;
}
if ($preview_check && isset($_REQUEST['elementor-preview'])) {
return true;
}
return \Elementor\Plugin::$instance->editor->is_edit_mode();
}
function debug_log($message)
{
if (Plugin::get_instance()->env === 'dev') {
echo wp_kses_post($message); // phpcs:ignore WordPress.Security.EscapeOutput - Hardcoded debug output only enabled when environment is set to 'dev'.
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Sphere\Debloat\WpCli;
use Sphere\Debloat\Admin\Cache;
/**
* Commands for WPCLI
*/
class Commands extends \WP_CLI_Command {
/**
* Flush the Debloat cache.
*
* [--network]
* Flush CSS Cache for all the sites in the network.
*
* ## EXAMPLES
*
* 1. wp debloat empty-cache
* - Delete all the cached content.
*
* 2. wp debloat empty-cache --network
* - Delete all the cached content including all transients for sites in a network.
*
* @since 2.1.0
* @access public
* @alias empty-cache
*/
public function empty_cache($args, $assoc_args)
{
$network = !empty($assoc_args['network']) && is_multisite();
$cache = new Cache;
if ($network) {
/** @var \WP_Site[] $blogs */
$blogs = get_sites();
foreach ($blogs as $key => $blog) {
$blog_id = $blog->blog_id;
switch_to_blog($blog_id);
$cache->empty();
\WP_CLI::success('Emptied debloat cache for site - ' . get_option('home'));
restore_current_blog();
}
return;
}
$cache->empty();
\WP_CLI::success('Emptied all debloat cache.');
}
}