| 1 |
<?php |
| 2 |
/** |
| 3 |
* Bloat — disable WordPress features site owners rarely use but every |
| 4 |
* frontend pays for in bytes / requests / attack surface. |
| 5 |
* |
| 6 |
* Each setting is a single toggle that adds (or doesn't add) one or |
| 7 |
* two filters. Per the SETTINGS.md standard, every toggle ships with a |
| 8 |
* label + description that names the actual ergonomic value. |
| 9 |
* |
| 10 |
* Every toggle is opt-in (default false). The defaults are |
| 11 |
* conservative because every site has at least one plugin that quietly |
| 12 |
* depends on the surface this module strips — better to make the user |
| 13 |
* choose than to break themes on activation. |
| 14 |
* |
| 15 |
* Tier: Free (FEATURES.md "Others" §10-§15 — declared in commit |
| 16 |
* `4e36051` before this implementation). |
| 17 |
* |
| 18 |
* @package XSpeed |
| 19 |
*/ |
| 20 |
|
| 21 |
declare(strict_types=1); |
| 22 |
|
| 23 |
namespace XSpeed\Modules\Bloat; |
| 24 |
|
| 25 |
defined( 'ABSPATH' ) || exit; |
| 26 |
|
| 27 |
use XSpeed\Module; |
| 28 |
use XSpeed\Settings_Manager; |
| 29 |
|
| 30 |
final class BloatModule extends Module { |
| 31 |
|
| 32 |
public const SLUG = 'bloat'; |
| 33 |
public const TIER = self::TIER_FREE; |
| 34 |
public const VERSION = '1.0.0'; |
| 35 |
|
| 36 |
public function ui_metadata(): array { |
| 37 |
return array( |
| 38 |
'label' => __( 'Bloat Control', 'xspeed' ), |
| 39 |
'icon' => 'Sliders', |
| 40 |
'description' => __( 'Turn off WordPress defaults you do not use — saves bytes, requests, and attack surface.', 'xspeed' ), |
| 41 |
); |
| 42 |
} |
| 43 |
|
| 44 |
public function settings_schema(): array { |
| 45 |
return array( |
| 46 |
'disable_emojis' => array( |
| 47 |
'type' => 'bool', |
| 48 |
'default' => false, |
| 49 |
'label' => __( 'Disable Emojis', 'xspeed' ), |
| 50 |
'description' => __( 'Remove the emoji detection script and its inline styles from every page. Modern browsers draw emojis natively, so visitors still see them. Saves a script and an inline stylesheet per page.', 'xspeed' ), |
| 51 |
), |
| 52 |
'disable_dashicons_frontend' => array( |
| 53 |
'type' => 'bool', |
| 54 |
'default' => false, |
| 55 |
'label' => __( 'Disable Dashicons on Frontend', 'xspeed' ), |
| 56 |
'description' => __( 'Drop the dashicons stylesheet from non-admin pages. Most themes do not need it. Saves ~45 KB per visitor.', 'xspeed' ), |
| 57 |
), |
| 58 |
'disable_oembed' => array( |
| 59 |
'type' => 'bool', |
| 60 |
'default' => false, |
| 61 |
'label' => __( 'Disable oEmbed Discovery + wp-embed.min.js', 'xspeed' ), |
| 62 |
'description' => __( 'Strip the auto-embed handlers + the embed script. Posts that paste a YouTube URL will no longer auto-render the player — embed it via a block instead. Saves a request per page.', 'xspeed' ), |
| 63 |
), |
| 64 |
'disable_rss_feeds' => array( |
| 65 |
'type' => 'bool', |
| 66 |
'default' => false, |
| 67 |
'label' => __( 'Disable RSS Feeds', 'xspeed' ), |
| 68 |
'description' => __( 'Return a 404 on /feed/ and similar endpoints. Useful for sites that do not publish feeds and want to cut feed-fetcher traffic.', 'xspeed' ), |
| 69 |
), |
| 70 |
'disable_xmlrpc' => array( |
| 71 |
'type' => 'bool', |
| 72 |
'default' => false, |
| 73 |
'label' => __( 'Disable XML-RPC', 'xspeed' ), |
| 74 |
'description' => __( 'Disable the legacy xmlrpc.php endpoint. Cuts pingback brute-force noise; safe to disable unless you use a remote WP client (Jetpack, WordPress mobile app).', 'xspeed' ), |
| 75 |
), |
| 76 |
'strip_jquery_migrate' => array( |
| 77 |
'type' => 'bool', |
| 78 |
'default' => false, |
| 79 |
'label' => __( 'Strip jQuery Migrate on Frontend', 'xspeed' ), |
| 80 |
'description' => __( 'Remove the jquery-migrate compatibility shim from non-admin pages. Saves ~10 KB; safe on modern themes / plugins.', 'xspeed' ), |
| 81 |
), |
| 82 |
'strip_editor_styles' => array( |
| 83 |
'type' => 'bool', |
| 84 |
'default' => false, |
| 85 |
'label' => __( 'Strip Block-Editor Styles on Frontend', 'xspeed' ), |
| 86 |
'description' => __( 'Drop editor-only stylesheets (wp-editor, wp-components, and friends) from anonymous pages. A plugin that enqueues them on the frontend usually does so by accident — they can add hundreds of KB of render-blocking CSS. Frontend block styles (wp-block-library) are never touched.', 'xspeed' ), |
| 87 |
), |
| 88 |
'remove_rsd_link' => array( |
| 89 |
'type' => 'bool', |
| 90 |
'default' => false, |
| 91 |
'label' => __( 'Remove RSD Link', 'xspeed' ), |
| 92 |
'description' => __( 'Drop the Really Simple Discovery link from the page head. Only old desktop blogging clients read it.', 'xspeed' ), |
| 93 |
), |
| 94 |
'remove_shortlink' => array( |
| 95 |
'type' => 'bool', |
| 96 |
'default' => false, |
| 97 |
'label' => __( 'Remove Shortlink', 'xspeed' ), |
| 98 |
'description' => __( 'Drop the ?p=123 shortlink tag and header from posts and pages. The shortlinks keep working; they are just no longer advertised.', 'xspeed' ), |
| 99 |
), |
| 100 |
'remove_rest_api_links' => array( |
| 101 |
'type' => 'bool', |
| 102 |
'default' => false, |
| 103 |
'label' => __( 'Remove REST API Links', 'xspeed' ), |
| 104 |
'description' => __( 'Drop the /wp-json/ discovery link tag and Link header. The REST API itself stays on; to block it, use the setting below.', 'xspeed' ), |
| 105 |
), |
| 106 |
'hide_wp_version' => array( |
| 107 |
'type' => 'bool', |
| 108 |
'default' => false, |
| 109 |
'label' => __( 'Hide WordPress Version', 'xspeed' ), |
| 110 |
'description' => __( 'Remove the WordPress generator tag from pages and feeds, so it no longer states the WordPress version. Other plugins print their own tags; those stay. Script and style URLs still carry ?ver= numbers.', 'xspeed' ), |
| 111 |
), |
| 112 |
'disable_self_pingbacks' => array( |
| 113 |
'type' => 'bool', |
| 114 |
'default' => false, |
| 115 |
'label' => __( 'Disable Self-Pingbacks', 'xspeed' ), |
| 116 |
'description' => __( 'Stop WordPress from sending a pingback to your own site when a post links to another of your posts. Pingbacks to other sites are not affected.', 'xspeed' ), |
| 117 |
), |
| 118 |
'restrict_rest_to_authed' => array( |
| 119 |
'type' => 'bool', |
| 120 |
'default' => false, |
| 121 |
'label' => __( 'Restrict REST API to Logged-In Users', 'xspeed' ), |
| 122 |
'description' => __( 'Block /wp-json/ for anonymous requests. WooCommerce checkout, contact-form submissions, and many block-editor previews need anonymous REST — keep this off unless you know your site does not depend on it.', 'xspeed' ), |
| 123 |
), |
| 124 |
); |
| 125 |
} |
| 126 |
|
| 127 |
public function boot(): void { |
| 128 |
/* |
| 129 |
* Deferred to `init` priority 0. This reads the module's settings, |
| 130 |
* which builds settings_schema(), whose labels go through __(), and |
| 131 |
* boot() runs on `plugins_loaded` — before `after_setup_theme`, the |
| 132 |
* earliest point WordPress 6.7+ treats as safe to translate. |
| 133 |
* |
| 134 |
* Priority 0 (not the default 10) because the body itself registers |
| 135 |
* an `init` callback at priority 9: adding a hook to the action that |
| 136 |
* is currently running only takes effect if the new priority is still |
| 137 |
* ahead of the running position, so we have to be first. Every other |
| 138 |
* hook it registers fires later than `init`. |
| 139 |
*/ |
| 140 |
add_action( 'init', array( $this, 'boot_on_init' ), 0 ); |
| 141 |
} |
| 142 |
|
| 143 |
/** |
| 144 |
* The real boot body — see boot() for why it runs on `init`. |
| 145 |
*/ |
| 146 |
public function boot_on_init(): void { |
| 147 |
$opts = Settings_Manager::get( self::SLUG ); |
| 148 |
|
| 149 |
if ( ! empty( $opts['disable_emojis'] ) ) { |
| 150 |
self::disable_emojis(); |
| 151 |
} |
| 152 |
|
| 153 |
if ( ! empty( $opts['disable_dashicons_frontend'] ) ) { |
| 154 |
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_dashicons' ), 100 ); |
| 155 |
} |
| 156 |
|
| 157 |
if ( ! empty( $opts['disable_oembed'] ) ) { |
| 158 |
add_action( 'init', array( __CLASS__, 'disable_oembed' ), 9 ); |
| 159 |
} |
| 160 |
|
| 161 |
if ( ! empty( $opts['disable_rss_feeds'] ) ) { |
| 162 |
add_action( 'do_feed', array( __CLASS__, 'block_feed' ), 1 ); |
| 163 |
add_action( 'do_feed_rdf', array( __CLASS__, 'block_feed' ), 1 ); |
| 164 |
add_action( 'do_feed_rss', array( __CLASS__, 'block_feed' ), 1 ); |
| 165 |
add_action( 'do_feed_rss2', array( __CLASS__, 'block_feed' ), 1 ); |
| 166 |
add_action( 'do_feed_atom', array( __CLASS__, 'block_feed' ), 1 ); |
| 167 |
add_action( 'do_feed_rss2_comments', array( __CLASS__, 'block_feed' ), 1 ); |
| 168 |
add_action( 'do_feed_atom_comments', array( __CLASS__, 'block_feed' ), 1 ); |
| 169 |
} |
| 170 |
|
| 171 |
if ( ! empty( $opts['disable_xmlrpc'] ) ) { |
| 172 |
add_filter( 'xmlrpc_enabled', '__return_false' ); |
| 173 |
add_filter( 'wp_headers', array( __CLASS__, 'strip_xmlrpc_header' ) ); |
| 174 |
add_filter( 'pings_open', '__return_false' ); |
| 175 |
} |
| 176 |
|
| 177 |
if ( ! empty( $opts['strip_jquery_migrate'] ) ) { |
| 178 |
add_action( 'wp_default_scripts', array( __CLASS__, 'strip_jquery_migrate' ) ); |
| 179 |
} |
| 180 |
|
| 181 |
if ( ! empty( $opts['strip_editor_styles'] ) ) { |
| 182 |
// Late, so anything enqueued at normal priority is already queued. |
| 183 |
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_editor_styles' ), PHP_INT_MAX ); |
| 184 |
} |
| 185 |
|
| 186 |
if ( ! empty( $opts['remove_rsd_link'] ) ) { |
| 187 |
remove_action( 'wp_head', 'rsd_link' ); |
| 188 |
} |
| 189 |
|
| 190 |
if ( ! empty( $opts['remove_shortlink'] ) ) { |
| 191 |
remove_action( 'wp_head', 'wp_shortlink_wp_head' ); |
| 192 |
remove_action( 'template_redirect', 'wp_shortlink_header', 11 ); |
| 193 |
} |
| 194 |
|
| 195 |
if ( ! empty( $opts['remove_rest_api_links'] ) ) { |
| 196 |
remove_action( 'wp_head', 'rest_output_link_wp_head' ); |
| 197 |
remove_action( 'template_redirect', 'rest_output_link_header', 11 ); |
| 198 |
remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' ); |
| 199 |
} |
| 200 |
|
| 201 |
if ( ! empty( $opts['hide_wp_version'] ) ) { |
| 202 |
remove_action( 'wp_head', 'wp_generator' ); |
| 203 |
add_filter( 'the_generator', '__return_empty_string' ); |
| 204 |
} |
| 205 |
|
| 206 |
if ( ! empty( $opts['disable_self_pingbacks'] ) ) { |
| 207 |
add_action( 'pre_ping', array( __CLASS__, 'strip_self_pings' ) ); |
| 208 |
} |
| 209 |
|
| 210 |
if ( ! empty( $opts['restrict_rest_to_authed'] ) ) { |
| 211 |
add_filter( 'rest_authentication_errors', array( __CLASS__, 'restrict_rest' ) ); |
| 212 |
} |
| 213 |
} |
| 214 |
|
| 215 |
public static function dequeue_dashicons(): void { |
| 216 |
if ( is_admin_bar_showing() || is_user_logged_in() ) { |
| 217 |
return; // the admin bar uses dashicons; only strip on truly anonymous pages. |
| 218 |
} |
| 219 |
wp_dequeue_style( 'dashicons' ); |
| 220 |
wp_deregister_style( 'dashicons' ); |
| 221 |
} |
| 222 |
|
| 223 |
/** |
| 224 |
* The front-end hooks live in default-filters.php, which loads before |
| 225 |
* `init`, so removing them here works. The admin ones are added by |
| 226 |
* wp-admin/includes/admin-filters.php, which loads after `init`; they |
| 227 |
* are removed on `admin_init` instead. wp_enqueue_emoji_styles is the |
| 228 |
* WP 6.4+ path; print_emoji_styles is kept for older cores. |
| 229 |
*/ |
| 230 |
public static function disable_emojis(): void { |
| 231 |
remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); |
| 232 |
remove_action( 'embed_head', 'print_emoji_detection_script' ); |
| 233 |
remove_action( 'wp_enqueue_scripts', 'wp_enqueue_emoji_styles' ); |
| 234 |
remove_action( 'enqueue_embed_scripts', 'wp_enqueue_emoji_styles' ); |
| 235 |
remove_action( 'wp_print_styles', 'print_emoji_styles' ); |
| 236 |
remove_filter( 'the_content_feed', 'wp_staticize_emoji' ); |
| 237 |
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' ); |
| 238 |
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ); |
| 239 |
add_filter( 'tiny_mce_plugins', array( __CLASS__, 'strip_tinymce_emoji' ) ); |
| 240 |
add_action( 'admin_init', array( __CLASS__, 'disable_admin_emojis' ) ); |
| 241 |
} |
| 242 |
|
| 243 |
public static function disable_admin_emojis(): void { |
| 244 |
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); |
| 245 |
remove_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' ); |
| 246 |
remove_action( 'admin_print_styles', 'print_emoji_styles' ); |
| 247 |
} |
| 248 |
|
| 249 |
/** |
| 250 |
* @param mixed $plugins |
| 251 |
* @return mixed |
| 252 |
*/ |
| 253 |
public static function strip_tinymce_emoji( $plugins ) { |
| 254 |
return is_array( $plugins ) ? array_values( array_diff( $plugins, array( 'wpemoji' ) ) ) : $plugins; |
| 255 |
} |
| 256 |
|
| 257 |
public static function disable_oembed(): void { |
| 258 |
// Strip discovery <link> from <head>. |
| 259 |
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); |
| 260 |
remove_action( 'wp_head', 'wp_oembed_add_host_js' ); |
| 261 |
// Drop the auto-embed filter (paste-a-URL-becomes-embed). |
| 262 |
remove_filter( 'the_content', array( $GLOBALS['wp_embed'] ?? null, 'autoembed' ), 8 ); |
| 263 |
// Drop wp-embed.min.js + the rewrite rule. |
| 264 |
add_action( |
| 265 |
'wp_footer', |
| 266 |
static function () { |
| 267 |
wp_dequeue_script( 'wp-embed' ); |
| 268 |
}, |
| 269 |
1 |
| 270 |
); |
| 271 |
add_filter( |
| 272 |
'rewrite_rules_array', |
| 273 |
static function ( $rules ) { |
| 274 |
if ( ! is_array( $rules ) ) { |
| 275 |
return $rules; |
| 276 |
} |
| 277 |
foreach ( $rules as $rule => $rewrite ) { |
| 278 |
if ( false !== strpos( (string) $rewrite, 'embed=true' ) ) { |
| 279 |
unset( $rules[ $rule ] ); |
| 280 |
} |
| 281 |
} |
| 282 |
return $rules; |
| 283 |
} |
| 284 |
); |
| 285 |
} |
| 286 |
|
| 287 |
/** |
| 288 |
* Editor-only style handles that have no business on an anonymous |
| 289 |
* frontend page. Deliberately NOT wp-block-library / |
| 290 |
* wp-block-library-theme / global-styles — those style the blocks |
| 291 |
* visitors actually see. Observed live: a plugin pulled wp-editor + |
| 292 |
* wp-components (and their deps) onto a marketing homepage, several |
| 293 |
* hundred KB of render-blocking CSS nothing on the page used. |
| 294 |
*/ |
| 295 |
private const EDITOR_STYLE_HANDLES = array( |
| 296 |
'wp-editor', |
| 297 |
'wp-block-editor', |
| 298 |
'wp-block-directory', |
| 299 |
'wp-components', |
| 300 |
'wp-preferences', |
| 301 |
'wp-media-utils', |
| 302 |
'wp-reusable-blocks', |
| 303 |
'wp-patterns', |
| 304 |
'wp-edit-blocks', |
| 305 |
'wp-edit-post', |
| 306 |
'wp-edit-site', |
| 307 |
'wp-edit-widgets', |
| 308 |
'wp-format-library', |
| 309 |
'wp-list-reusable-blocks', |
| 310 |
'wp-nux', |
| 311 |
); |
| 312 |
|
| 313 |
public static function dequeue_editor_styles(): void { |
| 314 |
// Logged-in views legitimately reach editor surfaces (front-end |
| 315 |
// editing, admin bar flows), and a builder editing screen is a |
| 316 |
// front-end URL — same guard set as the other frontend strips. |
| 317 |
if ( is_user_logged_in() || is_admin() || \XSpeed\Builder_Editor::is_active() ) { |
| 318 |
return; |
| 319 |
} |
| 320 |
$styles = wp_styles(); |
| 321 |
foreach ( self::EDITOR_STYLE_HANDLES as $handle ) { |
| 322 |
wp_dequeue_style( $handle ); |
| 323 |
} |
| 324 |
// Dequeue alone is not enough: dependencies are resolved again at |
| 325 |
// print time, so any queued sheet that lists one of these as a dep |
| 326 |
// pulls it straight back. Strip the handles from every registered |
| 327 |
// sheet's deps too — same technique strip_jquery_migrate() uses. |
| 328 |
foreach ( $styles->registered as $dependency ) { |
| 329 |
if ( is_array( $dependency->deps ?? null ) && array_intersect( $dependency->deps, self::EDITOR_STYLE_HANDLES ) ) { |
| 330 |
$dependency->deps = array_values( array_diff( $dependency->deps, self::EDITOR_STYLE_HANDLES ) ); |
| 331 |
} |
| 332 |
} |
| 333 |
} |
| 334 |
|
| 335 |
/** |
| 336 |
* `pre_ping` passes the link list by reference. |
| 337 |
* |
| 338 |
* @param array $links |
| 339 |
*/ |
| 340 |
public static function strip_self_pings( &$links ): void { |
| 341 |
if ( ! is_array( $links ) ) { |
| 342 |
return; |
| 343 |
} |
| 344 |
$links = array_values( |
| 345 |
array_filter( |
| 346 |
$links, |
| 347 |
static function ( $link ): bool { |
| 348 |
return ! self::is_own_url( (string) $link ); |
| 349 |
} |
| 350 |
) |
| 351 |
); |
| 352 |
} |
| 353 |
|
| 354 |
/** |
| 355 |
* Same host (any scheme, any case, with or without www.) and a path |
| 356 |
* inside the home path. A plain prefix check missed http:// links on |
| 357 |
* an https site, which migrated sites still carry, and matched |
| 358 |
* example.test.evil.test as home. |
| 359 |
*/ |
| 360 |
public static function is_own_url( string $url ): bool { |
| 361 |
$home_parts = wp_parse_url( (string) home_url() ); |
| 362 |
$parts = wp_parse_url( $url ); |
| 363 |
if ( ! is_array( $home_parts ) || ! is_array( $parts ) || empty( $parts['host'] ) || empty( $home_parts['host'] ) ) { |
| 364 |
return false; |
| 365 |
} |
| 366 |
$strip = static function ( string $host ): string { |
| 367 |
$host = strtolower( $host ); |
| 368 |
return 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : $host; |
| 369 |
}; |
| 370 |
if ( $strip( $parts['host'] ) !== $strip( $home_parts['host'] ) ) { |
| 371 |
return false; |
| 372 |
} |
| 373 |
$home_path = rtrim( (string) ( $home_parts['path'] ?? '' ), '/' ); |
| 374 |
$path = (string) ( $parts['path'] ?? '' ); |
| 375 |
return '' === $home_path || $path === $home_path || 0 === strpos( $path, $home_path . '/' ); |
| 376 |
} |
| 377 |
|
| 378 |
public static function block_feed(): void { |
| 379 |
wp_die( |
| 380 |
esc_html__( 'Feeds are disabled.', 'xspeed' ), |
| 381 |
'', |
| 382 |
array( 'response' => 404 ) |
| 383 |
); |
| 384 |
} |
| 385 |
|
| 386 |
/** |
| 387 |
* @param array $headers |
| 388 |
* @return array |
| 389 |
*/ |
| 390 |
public static function strip_xmlrpc_header( $headers ) { |
| 391 |
if ( is_array( $headers ) ) { |
| 392 |
unset( $headers['X-Pingback'] ); |
| 393 |
} |
| 394 |
return $headers; |
| 395 |
} |
| 396 |
|
| 397 |
/** |
| 398 |
* @param \WP_Scripts $scripts |
| 399 |
*/ |
| 400 |
public static function strip_jquery_migrate( $scripts ): void { |
| 401 |
// Builders and their add-ons still rely on jQuery Migrate shims; a |
| 402 |
// builder editing screen is a front-end URL, so is_admin() misses it |
| 403 |
// and the editor loses methods it calls. (#281) |
| 404 |
if ( is_admin() || \XSpeed\Builder_Editor::is_active() || ! isset( $scripts->registered['jquery'] ) ) { |
| 405 |
return; |
| 406 |
} |
| 407 |
$jquery = $scripts->registered['jquery']; |
| 408 |
if ( is_array( $jquery->deps ?? null ) ) { |
| 409 |
$jquery->deps = array_values( array_diff( $jquery->deps, array( 'jquery-migrate' ) ) ); |
| 410 |
} |
| 411 |
} |
| 412 |
|
| 413 |
/** |
| 414 |
* Block anonymous /wp-json/ access. Logged-in users + already-errored |
| 415 |
* requests pass through untouched. |
| 416 |
* |
| 417 |
* @param \WP_Error|null|true $result |
| 418 |
* @return \WP_Error|null|true |
| 419 |
*/ |
| 420 |
public static function restrict_rest( $result ) { |
| 421 |
if ( ! empty( $result ) ) { |
| 422 |
return $result; // upstream auth already decided. |
| 423 |
} |
| 424 |
if ( is_user_logged_in() ) { |
| 425 |
return $result; |
| 426 |
} |
| 427 |
return new \WP_Error( |
| 428 |
'rest_forbidden_anonymous', |
| 429 |
__( 'Anonymous REST access is disabled on this site.', 'xspeed' ), |
| 430 |
array( 'status' => 401 ) |
| 431 |
); |
| 432 |
} |
| 433 |
|
| 434 |
public function cli_commands(): array { |
| 435 |
return array( |
| 436 |
array( |
| 437 |
'name' => 'xspeed bloat', |
| 438 |
'callback' => array( $this, 'cli_handler' ), |
| 439 |
'shortdesc' => 'Show which bloat-removal toggles are active.', |
| 440 |
'ai_hint' => 'What unnecessary WordPress output is being stripped (emojis, embeds, jQuery Migrate, dashicons)? Use when asked why extra scripts still load on the frontend, or before recommending bloat removal.', |
| 441 |
'synopsis' => array(), |
| 442 |
), |
| 443 |
); |
| 444 |
} |
| 445 |
|
| 446 |
public function cli_handler( array $args, array $assoc ): void { |
| 447 |
$opts = Settings_Manager::get( self::SLUG ); |
| 448 |
foreach ( $opts as $key => $value ) { |
| 449 |
\WP_CLI::log( sprintf( '%-30s %s', $key, $value ? 'on' : 'off' ) ); |
| 450 |
} |
| 451 |
} |
| 452 |
|
| 453 |
/** |
| 454 |
* Bloat has no master switch -- it is on when any of its boolean |
| 455 |
* flags is set. (#363) |
| 456 |
*/ |
| 457 |
public function is_active(): ?bool { |
| 458 |
return $this->any_bool_flag_on(); |
| 459 |
} |
| 460 |
} |
| 461 |
|