__( 'Bloat Control', 'xspeed' ), 'icon' => 'Sliders', 'description' => __( 'Turn off WordPress defaults you do not use — saves bytes, requests, and attack surface.', 'xspeed' ), ); } public function settings_schema(): array { return array( 'disable_dashicons_frontend' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Disable Dashicons on Frontend', 'xspeed' ), 'description' => __( 'Drop the dashicons stylesheet from non-admin pages. Most themes do not need it. Saves ~45 KB per visitor.', 'xspeed' ), ), 'disable_oembed' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Disable oEmbed Discovery + wp-embed.min.js', 'xspeed' ), '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' ), ), 'disable_rss_feeds' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Disable RSS Feeds', 'xspeed' ), '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' ), ), 'disable_xmlrpc' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Disable XML-RPC', 'xspeed' ), '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' ), ), 'strip_jquery_migrate' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Strip jQuery Migrate on Frontend', 'xspeed' ), 'description' => __( 'Remove the jquery-migrate compatibility shim from non-admin pages. Saves ~10 KB; safe on modern themes / plugins.', 'xspeed' ), ), 'strip_editor_styles' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Strip Block-Editor Styles on Frontend', 'xspeed' ), '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' ), ), 'restrict_rest_to_authed' => array( 'type' => 'bool', 'default' => false, 'label' => __( 'Restrict REST API to Logged-In Users', 'xspeed' ), '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' ), ), ); } public function boot(): void { /* * Deferred to `init` priority 0. This reads the module's settings, * which builds settings_schema(), whose labels go through __(), and * boot() runs on `plugins_loaded` — before `after_setup_theme`, the * earliest point WordPress 6.7+ treats as safe to translate. * * Priority 0 (not the default 10) because the body itself registers * an `init` callback at priority 9: adding a hook to the action that * is currently running only takes effect if the new priority is still * ahead of the running position, so we have to be first. Every other * hook it registers fires later than `init`. */ add_action( 'init', array( $this, 'boot_on_init' ), 0 ); } /** * The real boot body — see boot() for why it runs on `init`. */ public function boot_on_init(): void { $opts = Settings_Manager::get( self::SLUG ); if ( ! empty( $opts['disable_dashicons_frontend'] ) ) { add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_dashicons' ), 100 ); } if ( ! empty( $opts['disable_oembed'] ) ) { add_action( 'init', array( __CLASS__, 'disable_oembed' ), 9 ); } if ( ! empty( $opts['disable_rss_feeds'] ) ) { add_action( 'do_feed', array( __CLASS__, 'block_feed' ), 1 ); add_action( 'do_feed_rdf', array( __CLASS__, 'block_feed' ), 1 ); add_action( 'do_feed_rss', array( __CLASS__, 'block_feed' ), 1 ); add_action( 'do_feed_rss2', array( __CLASS__, 'block_feed' ), 1 ); add_action( 'do_feed_atom', array( __CLASS__, 'block_feed' ), 1 ); add_action( 'do_feed_rss2_comments', array( __CLASS__, 'block_feed' ), 1 ); add_action( 'do_feed_atom_comments', array( __CLASS__, 'block_feed' ), 1 ); } if ( ! empty( $opts['disable_xmlrpc'] ) ) { add_filter( 'xmlrpc_enabled', '__return_false' ); add_filter( 'wp_headers', array( __CLASS__, 'strip_xmlrpc_header' ) ); add_filter( 'pings_open', '__return_false' ); } if ( ! empty( $opts['strip_jquery_migrate'] ) ) { add_action( 'wp_default_scripts', array( __CLASS__, 'strip_jquery_migrate' ) ); } if ( ! empty( $opts['strip_editor_styles'] ) ) { // Late, so anything enqueued at normal priority is already queued. add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_editor_styles' ), PHP_INT_MAX ); } if ( ! empty( $opts['restrict_rest_to_authed'] ) ) { add_filter( 'rest_authentication_errors', array( __CLASS__, 'restrict_rest' ) ); } } public static function dequeue_dashicons(): void { if ( is_admin_bar_showing() || is_user_logged_in() ) { return; // the admin bar uses dashicons; only strip on truly anonymous pages. } wp_dequeue_style( 'dashicons' ); wp_deregister_style( 'dashicons' ); } public static function disable_oembed(): void { // Strip discovery from . remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); remove_action( 'wp_head', 'wp_oembed_add_host_js' ); // Drop the auto-embed filter (paste-a-URL-becomes-embed). remove_filter( 'the_content', array( $GLOBALS['wp_embed'] ?? null, 'autoembed' ), 8 ); // Drop wp-embed.min.js + the rewrite rule. add_action( 'wp_footer', static function () { wp_dequeue_script( 'wp-embed' ); }, 1 ); add_filter( 'rewrite_rules_array', static function ( $rules ) { if ( ! is_array( $rules ) ) { return $rules; } foreach ( $rules as $rule => $rewrite ) { if ( false !== strpos( (string) $rewrite, 'embed=true' ) ) { unset( $rules[ $rule ] ); } } return $rules; } ); } /** * Editor-only style handles that have no business on an anonymous * frontend page. Deliberately NOT wp-block-library / * wp-block-library-theme / global-styles — those style the blocks * visitors actually see. Observed live: a plugin pulled wp-editor + * wp-components (and their deps) onto a marketing homepage, several * hundred KB of render-blocking CSS nothing on the page used. */ private const EDITOR_STYLE_HANDLES = array( 'wp-editor', 'wp-block-editor', 'wp-block-directory', 'wp-components', 'wp-preferences', 'wp-media-utils', 'wp-reusable-blocks', 'wp-patterns', 'wp-edit-blocks', 'wp-edit-post', 'wp-edit-site', 'wp-edit-widgets', 'wp-format-library', 'wp-list-reusable-blocks', 'wp-nux', ); public static function dequeue_editor_styles(): void { // Logged-in views legitimately reach editor surfaces (front-end // editing, admin bar flows), and a builder editing screen is a // front-end URL — same guard set as the other frontend strips. if ( is_user_logged_in() || is_admin() || \XSpeed\Builder_Editor::is_active() ) { return; } $styles = wp_styles(); foreach ( self::EDITOR_STYLE_HANDLES as $handle ) { wp_dequeue_style( $handle ); } // Dequeue alone is not enough: dependencies are resolved again at // print time, so any queued sheet that lists one of these as a dep // pulls it straight back. Strip the handles from every registered // sheet's deps too — same technique strip_jquery_migrate() uses. foreach ( $styles->registered as $dependency ) { if ( is_array( $dependency->deps ?? null ) && array_intersect( $dependency->deps, self::EDITOR_STYLE_HANDLES ) ) { $dependency->deps = array_values( array_diff( $dependency->deps, self::EDITOR_STYLE_HANDLES ) ); } } } public static function block_feed(): void { wp_die( esc_html__( 'Feeds are disabled.', 'xspeed' ), '', array( 'response' => 404 ) ); } /** * @param array $headers * @return array */ public static function strip_xmlrpc_header( $headers ) { if ( is_array( $headers ) ) { unset( $headers['X-Pingback'] ); } return $headers; } /** * @param \WP_Scripts $scripts */ public static function strip_jquery_migrate( $scripts ): void { // Builders and their add-ons still rely on jQuery Migrate shims; a // builder editing screen is a front-end URL, so is_admin() misses it // and the editor loses methods it calls. (#281) if ( is_admin() || \XSpeed\Builder_Editor::is_active() || ! isset( $scripts->registered['jquery'] ) ) { return; } $jquery = $scripts->registered['jquery']; if ( is_array( $jquery->deps ?? null ) ) { $jquery->deps = array_values( array_diff( $jquery->deps, array( 'jquery-migrate' ) ) ); } } /** * Block anonymous /wp-json/ access. Logged-in users + already-errored * requests pass through untouched. * * @param \WP_Error|null|true $result * @return \WP_Error|null|true */ public static function restrict_rest( $result ) { if ( ! empty( $result ) ) { return $result; // upstream auth already decided. } if ( is_user_logged_in() ) { return $result; } return new \WP_Error( 'rest_forbidden_anonymous', __( 'Anonymous REST access is disabled on this site.', 'xspeed' ), array( 'status' => 401 ) ); } public function cli_commands(): array { return array( array( 'name' => 'xspeed bloat', 'callback' => array( $this, 'cli_handler' ), 'shortdesc' => 'Show which bloat-removal toggles are active.', '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.', 'synopsis' => array(), ), ); } public function cli_handler( array $args, array $assoc ): void { $opts = Settings_Manager::get( self::SLUG ); foreach ( $opts as $key => $value ) { \WP_CLI::log( sprintf( '%-30s %s', $key, $value ? 'on' : 'off' ) ); } } /** * Bloat has no master switch -- it is on when any of its disable_* / * strip_* / restrict_* flags is set. (#363) */ public function is_active(): ?bool { return $this->any_bool_flag_on(); } }