PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.2
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
xspeed / includes / modules / Bloat / BloatModule.php

BloatModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.2, at includes/modules/Bloat/BloatModule.php

261 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 * Six toggles, all 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_dashicons_frontend' => array(
47 'type' => 'bool',
48 'default' => false,
49 'label' => __( 'Disable Dashicons on Frontend', 'xspeed' ),
50 'description' => __( 'Drop the dashicons stylesheet from non-admin pages. Most themes do not need it. Saves ~45 KB per visitor.', 'xspeed' ),
51 ),
52 'disable_oembed' => array(
53 'type' => 'bool',
54 'default' => false,
55 'label' => __( 'Disable oEmbed Discovery + wp-embed.min.js', 'xspeed' ),
56 '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' ),
57 ),
58 'disable_rss_feeds' => array(
59 'type' => 'bool',
60 'default' => false,
61 'label' => __( 'Disable RSS Feeds', 'xspeed' ),
62 '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' ),
63 ),
64 'disable_xmlrpc' => array(
65 'type' => 'bool',
66 'default' => false,
67 'label' => __( 'Disable XML-RPC', 'xspeed' ),
68 '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' ),
69 ),
70 'strip_jquery_migrate' => array(
71 'type' => 'bool',
72 'default' => false,
73 'label' => __( 'Strip jQuery Migrate on Frontend', 'xspeed' ),
74 'description' => __( 'Remove the jquery-migrate compatibility shim from non-admin pages. Saves ~10 KB; safe on modern themes / plugins.', 'xspeed' ),
75 ),
76 'restrict_rest_to_authed' => array(
77 'type' => 'bool',
78 'default' => false,
79 'label' => __( 'Restrict REST API to Logged-In Users', 'xspeed' ),
80 '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' ),
81 ),
82 );
83 }
84
85 public function boot(): void {
86 /*
87 * Deferred to `init` priority 0. This reads the module's settings,
88 * which builds settings_schema(), whose labels go through __(), and
89 * boot() runs on `plugins_loaded` — before `after_setup_theme`, the
90 * earliest point WordPress 6.7+ treats as safe to translate.
91 *
92 * Priority 0 (not the default 10) because the body itself registers
93 * an `init` callback at priority 9: adding a hook to the action that
94 * is currently running only takes effect if the new priority is still
95 * ahead of the running position, so we have to be first. Every other
96 * hook it registers fires later than `init`.
97 */
98 add_action( 'init', array( $this, 'boot_on_init' ), 0 );
99 }
100
101 /**
102 * The real boot body — see boot() for why it runs on `init`.
103 */
104 public function boot_on_init(): void {
105 $opts = Settings_Manager::get( self::SLUG );
106
107 if ( ! empty( $opts['disable_dashicons_frontend'] ) ) {
108 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_dashicons' ), 100 );
109 }
110
111 if ( ! empty( $opts['disable_oembed'] ) ) {
112 add_action( 'init', array( __CLASS__, 'disable_oembed' ), 9 );
113 }
114
115 if ( ! empty( $opts['disable_rss_feeds'] ) ) {
116 add_action( 'do_feed', array( __CLASS__, 'block_feed' ), 1 );
117 add_action( 'do_feed_rdf', array( __CLASS__, 'block_feed' ), 1 );
118 add_action( 'do_feed_rss', array( __CLASS__, 'block_feed' ), 1 );
119 add_action( 'do_feed_rss2', array( __CLASS__, 'block_feed' ), 1 );
120 add_action( 'do_feed_atom', array( __CLASS__, 'block_feed' ), 1 );
121 add_action( 'do_feed_rss2_comments', array( __CLASS__, 'block_feed' ), 1 );
122 add_action( 'do_feed_atom_comments', array( __CLASS__, 'block_feed' ), 1 );
123 }
124
125 if ( ! empty( $opts['disable_xmlrpc'] ) ) {
126 add_filter( 'xmlrpc_enabled', '__return_false' );
127 add_filter( 'wp_headers', array( __CLASS__, 'strip_xmlrpc_header' ) );
128 add_filter( 'pings_open', '__return_false' );
129 }
130
131 if ( ! empty( $opts['strip_jquery_migrate'] ) ) {
132 add_action( 'wp_default_scripts', array( __CLASS__, 'strip_jquery_migrate' ) );
133 }
134
135 if ( ! empty( $opts['restrict_rest_to_authed'] ) ) {
136 add_filter( 'rest_authentication_errors', array( __CLASS__, 'restrict_rest' ) );
137 }
138 }
139
140 public static function dequeue_dashicons(): void {
141 if ( is_admin_bar_showing() || is_user_logged_in() ) {
142 return; // the admin bar uses dashicons; only strip on truly anonymous pages.
143 }
144 wp_dequeue_style( 'dashicons' );
145 wp_deregister_style( 'dashicons' );
146 }
147
148 public static function disable_oembed(): void {
149 // Strip discovery <link> from <head>.
150 remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
151 remove_action( 'wp_head', 'wp_oembed_add_host_js' );
152 // Drop the auto-embed filter (paste-a-URL-becomes-embed).
153 remove_filter( 'the_content', array( $GLOBALS['wp_embed'] ?? null, 'autoembed' ), 8 );
154 // Drop wp-embed.min.js + the rewrite rule.
155 add_action(
156 'wp_footer',
157 static function () {
158 wp_dequeue_script( 'wp-embed' );
159 },
160 1
161 );
162 add_filter(
163 'rewrite_rules_array',
164 static function ( $rules ) {
165 if ( ! is_array( $rules ) ) {
166 return $rules;
167 }
168 foreach ( $rules as $rule => $rewrite ) {
169 if ( false !== strpos( (string) $rewrite, 'embed=true' ) ) {
170 unset( $rules[ $rule ] );
171 }
172 }
173 return $rules;
174 }
175 );
176 }
177
178 public static function block_feed(): void {
179 wp_die(
180 esc_html__( 'Feeds are disabled.', 'xspeed' ),
181 '',
182 array( 'response' => 404 )
183 );
184 }
185
186 /**
187 * @param array $headers
188 * @return array
189 */
190 public static function strip_xmlrpc_header( $headers ) {
191 if ( is_array( $headers ) ) {
192 unset( $headers['X-Pingback'] );
193 }
194 return $headers;
195 }
196
197 /**
198 * @param \WP_Scripts $scripts
199 */
200 public static function strip_jquery_migrate( $scripts ): void {
201 // Builders and their add-ons still rely on jQuery Migrate shims; a
202 // builder editing screen is a front-end URL, so is_admin() misses it
203 // and the editor loses methods it calls. (#281)
204 if ( is_admin() || \XSpeed\Builder_Editor::is_active() || ! isset( $scripts->registered['jquery'] ) ) {
205 return;
206 }
207 $jquery = $scripts->registered['jquery'];
208 if ( is_array( $jquery->deps ?? null ) ) {
209 $jquery->deps = array_values( array_diff( $jquery->deps, array( 'jquery-migrate' ) ) );
210 }
211 }
212
213 /**
214 * Block anonymous /wp-json/ access. Logged-in users + already-errored
215 * requests pass through untouched.
216 *
217 * @param \WP_Error|null|true $result
218 * @return \WP_Error|null|true
219 */
220 public static function restrict_rest( $result ) {
221 if ( ! empty( $result ) ) {
222 return $result; // upstream auth already decided.
223 }
224 if ( is_user_logged_in() ) {
225 return $result;
226 }
227 return new \WP_Error(
228 'rest_forbidden_anonymous',
229 __( 'Anonymous REST access is disabled on this site.', 'xspeed' ),
230 array( 'status' => 401 )
231 );
232 }
233
234 public function cli_commands(): array {
235 return array(
236 array(
237 'name' => 'xspeed bloat',
238 'callback' => array( $this, 'cli_handler' ),
239 'shortdesc' => 'Show which bloat-removal toggles are active.',
240 '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.',
241 'synopsis' => array(),
242 ),
243 );
244 }
245
246 public function cli_handler( array $args, array $assoc ): void {
247 $opts = Settings_Manager::get( self::SLUG );
248 foreach ( $opts as $key => $value ) {
249 \WP_CLI::log( sprintf( '%-30s %s', $key, $value ? 'on' : 'off' ) );
250 }
251 }
252
253 /**
254 * Bloat has no master switch -- it is on when any of its disable_* /
255 * strip_* / restrict_* flags is set. (#363)
256 */
257 public function is_active(): ?bool {
258 return $this->any_bool_flag_on();
259 }
260 }
261