PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.2.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.2.0
1.3.3 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 All 29 releases
xspeed / includes / modules / Bloat / BloatModule.php

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

230 lines 7.4 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',
39 'icon' => 'Sliders',
40 'description' => 'Turn off WordPress defaults you do not use — saves bytes, requests, and attack surface.',
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',
50 'description' => 'Drop the dashicons stylesheet from non-admin pages. Most themes do not need it. Saves ~45 KB per visitor.',
51 ),
52 'disable_oembed' => array(
53 'type' => 'bool',
54 'default' => false,
55 'label' => 'Disable oEmbed Discovery + wp-embed.min.js',
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.',
57 ),
58 'disable_rss_feeds' => array(
59 'type' => 'bool',
60 'default' => false,
61 'label' => 'Disable RSS Feeds',
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.',
63 ),
64 'disable_xmlrpc' => array(
65 'type' => 'bool',
66 'default' => false,
67 'label' => 'Disable XML-RPC',
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).',
69 ),
70 'strip_jquery_migrate' => array(
71 'type' => 'bool',
72 'default' => false,
73 'label' => 'Strip jQuery Migrate on Frontend',
74 'description' => 'Remove the jquery-migrate compatibility shim from non-admin pages. Saves ~10 KB; safe on modern themes / plugins.',
75 ),
76 'restrict_rest_to_authed' => array(
77 'type' => 'bool',
78 'default' => false,
79 'label' => 'Restrict REST API to Logged-In Users',
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.',
81 ),
82 );
83 }
84
85 public function boot(): void {
86 $opts = Settings_Manager::get( self::SLUG );
87
88 if ( ! empty( $opts['disable_dashicons_frontend'] ) ) {
89 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_dashicons' ), 100 );
90 }
91
92 if ( ! empty( $opts['disable_oembed'] ) ) {
93 add_action( 'init', array( __CLASS__, 'disable_oembed' ), 9 );
94 }
95
96 if ( ! empty( $opts['disable_rss_feeds'] ) ) {
97 add_action( 'do_feed', array( __CLASS__, 'block_feed' ), 1 );
98 add_action( 'do_feed_rdf', array( __CLASS__, 'block_feed' ), 1 );
99 add_action( 'do_feed_rss', array( __CLASS__, 'block_feed' ), 1 );
100 add_action( 'do_feed_rss2', array( __CLASS__, 'block_feed' ), 1 );
101 add_action( 'do_feed_atom', array( __CLASS__, 'block_feed' ), 1 );
102 add_action( 'do_feed_rss2_comments', array( __CLASS__, 'block_feed' ), 1 );
103 add_action( 'do_feed_atom_comments', array( __CLASS__, 'block_feed' ), 1 );
104 }
105
106 if ( ! empty( $opts['disable_xmlrpc'] ) ) {
107 add_filter( 'xmlrpc_enabled', '__return_false' );
108 add_filter( 'wp_headers', array( __CLASS__, 'strip_xmlrpc_header' ) );
109 add_filter( 'pings_open', '__return_false' );
110 }
111
112 if ( ! empty( $opts['strip_jquery_migrate'] ) ) {
113 add_action( 'wp_default_scripts', array( __CLASS__, 'strip_jquery_migrate' ) );
114 }
115
116 if ( ! empty( $opts['restrict_rest_to_authed'] ) ) {
117 add_filter( 'rest_authentication_errors', array( __CLASS__, 'restrict_rest' ) );
118 }
119 }
120
121 public static function dequeue_dashicons(): void {
122 if ( is_admin_bar_showing() || is_user_logged_in() ) {
123 return; // the admin bar uses dashicons; only strip on truly anonymous pages.
124 }
125 wp_dequeue_style( 'dashicons' );
126 wp_deregister_style( 'dashicons' );
127 }
128
129 public static function disable_oembed(): void {
130 // Strip discovery <link> from <head>.
131 remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
132 remove_action( 'wp_head', 'wp_oembed_add_host_js' );
133 // Drop the auto-embed filter (paste-a-URL-becomes-embed).
134 remove_filter( 'the_content', array( $GLOBALS['wp_embed'] ?? null, 'autoembed' ), 8 );
135 // Drop wp-embed.min.js + the rewrite rule.
136 add_action(
137 'wp_footer',
138 static function () {
139 wp_dequeue_script( 'wp-embed' );
140 },
141 1
142 );
143 add_filter(
144 'rewrite_rules_array',
145 static function ( $rules ) {
146 if ( ! is_array( $rules ) ) {
147 return $rules;
148 }
149 foreach ( $rules as $rule => $rewrite ) {
150 if ( false !== strpos( (string) $rewrite, 'embed=true' ) ) {
151 unset( $rules[ $rule ] );
152 }
153 }
154 return $rules;
155 }
156 );
157 }
158
159 public static function block_feed(): void {
160 wp_die(
161 esc_html__( 'Feeds are disabled.', 'xspeed' ),
162 '',
163 array( 'response' => 404 )
164 );
165 }
166
167 /**
168 * @param array $headers
169 * @return array
170 */
171 public static function strip_xmlrpc_header( $headers ) {
172 if ( is_array( $headers ) ) {
173 unset( $headers['X-Pingback'] );
174 }
175 return $headers;
176 }
177
178 /**
179 * @param \WP_Scripts $scripts
180 */
181 public static function strip_jquery_migrate( $scripts ): void {
182 if ( is_admin() || ! isset( $scripts->registered['jquery'] ) ) {
183 return;
184 }
185 $jquery = $scripts->registered['jquery'];
186 if ( is_array( $jquery->deps ?? null ) ) {
187 $jquery->deps = array_values( array_diff( $jquery->deps, array( 'jquery-migrate' ) ) );
188 }
189 }
190
191 /**
192 * Block anonymous /wp-json/ access. Logged-in users + already-errored
193 * requests pass through untouched.
194 *
195 * @param \WP_Error|null|true $result
196 * @return \WP_Error|null|true
197 */
198 public static function restrict_rest( $result ) {
199 if ( ! empty( $result ) ) {
200 return $result; // upstream auth already decided.
201 }
202 if ( is_user_logged_in() ) {
203 return $result;
204 }
205 return new \WP_Error(
206 'rest_forbidden_anonymous',
207 __( 'Anonymous REST access is disabled on this site.', 'xspeed' ),
208 array( 'status' => 401 )
209 );
210 }
211
212 public function cli_commands(): array {
213 return array(
214 array(
215 'name' => 'xspeed bloat',
216 'callback' => array( $this, 'cli_handler' ),
217 'shortdesc' => 'Show which bloat-removal toggles are active.',
218 'synopsis' => array(),
219 ),
220 );
221 }
222
223 public function cli_handler( array $args, array $assoc ): void {
224 $opts = Settings_Manager::get( self::SLUG );
225 foreach ( $opts as $key => $value ) {
226 \WP_CLI::log( sprintf( '%-30s %s', $key, $value ? 'on' : 'off' ) );
227 }
228 }
229 }
230