PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.4 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 All 33 releases
xspeed / includes / modules / Minify / MinifyModule.php

MinifyModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.7, at includes/modules/Minify/MinifyModule.php

369 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Minify module.
4 *
5 * Owns the minify_html / minify_css / minify_js settings. Engine work
6 * still happens in XSpeed\Minifier (filters style_loader_src and
7 * script_loader_src), but the Module is now the storage and editing
8 * authority. Settings live in xspeed_module_minify; the legacy
9 * xspeed_options blob is drained on first boot and on POST.
10 *
11 * Tier: Free. See SETTINGS.md for the contract this module satisfies.
12 *
13 * @package XSpeed
14 */
15
16 declare(strict_types=1);
17
18 namespace XSpeed\Modules\Minify;
19
20 defined( 'ABSPATH' ) || exit;
21
22 use XSpeed\Minifier as LegacyMinifier;
23 use XSpeed\Minify_Filters;
24 use XSpeed\Module;
25 use XSpeed\Settings_Manager;
26
27 final class MinifyModule extends Module {
28
29 public const SLUG = 'minify';
30 public const TIER = self::TIER_FREE;
31 public const VERSION = '1.2.0';
32
33 public function ui_metadata(): array {
34 return array(
35 'label' => __( 'CSS & JavaScript', 'xspeed' ),
36 'tab_label' => __( 'Minify', 'xspeed' ), // its own tab on the CSS & JavaScript page
37 'icon' => 'Wand2',
38 'description' => __( 'Makes HTML, CSS and JavaScript files smaller and loads scripts later.', 'xspeed' ),
39 'group' => 'performance',
40 // Host page: Minify (this module) / Critical CSS (Pro) / Unused
41 // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
42 // page instead of three sidebar rows (FBS-83633).
43 'custom_panel' => 'CssJsPanel',
44 );
45 }
46
47 public function settings_schema(): array {
48 return array(
49 'minify_html' => array(
50 'type' => 'bool',
51 'default' => false,
52 'label' => __( 'Minify HTML', 'xspeed' ),
53 // Names the logged-out caveat up front: minification runs on the
54 // request that WRITES a cache entry, and should_cache() refuses
55 // logged-in requests — so "view source while logged in" shows
56 // un-minified HTML and reads as the feature being broken. (#2)
57 'description' => __( 'Removes spaces and comments from your pages. Safe on most themes. Only logged-out visitors see it, so check in a private window.', 'xspeed' ),
58 ),
59 'minify_css' => array(
60 'type' => 'bool',
61 'default' => false,
62 'label' => __( 'Minify CSS', 'xspeed' ),
63 'description' => __( 'Makes your site\'s own CSS files smaller. CSS from other domains is left alone.', 'xspeed' ),
64 ),
65 'minify_js' => array(
66 'type' => 'bool',
67 'default' => false,
68 'label' => __( 'Minify JavaScript', 'xspeed' ),
69 'description' => __( 'Makes your site\'s own JavaScript files smaller. Turn off if a script on your site stops working.', 'xspeed' ),
70 ),
71 'defer_js' => array(
72 'type' => 'bool',
73 'default' => false,
74 'label' => __( 'Defer JavaScript', 'xspeed' ),
75 'description' => __( 'Runs scripts after the page has loaded, so content shows sooner. jQuery and scripts that need it are skipped.', 'xspeed' ),
76 ),
77 'delay_js' => array(
78 'type' => 'bool',
79 'default' => false,
80 'label' => __( 'Delay JavaScript', 'xspeed' ),
81 'description' => __( 'Scripts load only when the visitor scrolls, taps or types. Pages show much sooner, but test menus and sliders. Cookie consent banners still load first.', 'xspeed' ),
82 ),
83 'async_css' => array(
84 'type' => 'bool',
85 'default' => false,
86 'label' => __( 'Load CSS without blocking', 'xspeed' ),
87 'description' => __( 'On pages that have critical CSS, the page shows before the rest of its CSS finishes loading. Pages without critical CSS keep loading their CSS normally, because deferring it makes the page show unstyled and then jump.', 'xspeed' ),
88 ),
89 'remove_query_strings' => array(
90 'type' => 'bool',
91 'default' => false,
92 'label' => __( 'Remove version from file URLs', 'xspeed' ),
93 'description' => __( 'Removes ?ver= from CSS and JS links, which some CDNs cache better. After a plugin update, returning visitors may keep old files until their browser cache expires.', 'xspeed' ),
94 'advanced' => true,
95 ),
96 'defer_js_excluded' => array(
97 'type' => 'list',
98 'default' => array( 'jquery-core', 'jquery-migrate' ),
99 'item_type' => 'string',
100 'label' => __( 'Scripts to never defer or delay', 'xspeed' ),
101 'description' => __( 'Script handles or parts of script URLs, one per line. jQuery is listed because most themes need it early; cookie consent banners are skipped on their own.', 'xspeed' ),
102 // Only relevant once defer OR delay is on — the exclusion list
103 // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
104 'dependsOn' => array(
105 'any' => array(
106 array( 'field' => 'defer_js' ),
107 array( 'field' => 'delay_js' ),
108 ),
109 ),
110 ),
111 'delay_js_targets' => array(
112 'type' => 'list',
113 'default' => array(),
114 'item_type' => 'string',
115 'label' => __( 'Delay only these scripts', 'xspeed' ),
116 'description' => __( 'Script handles or parts of script URLs, one per line. Leave empty to delay all scripts; otherwise only these, plus known trackers and chat widgets, are delayed.', 'xspeed' ),
117 'info_title' => __( 'Consent banners and Delay JS', 'xspeed' ),
118 'info' => sprintf(
119 /* translators: %s: comma-separated list of consent plugins, each followed by the word to type in parentheses. */
120 __( 'These consent banners load straight away, even with Delay JS on: %s. To delay one on purpose, add the word in parentheses, or the script handle, to this list. An entry that only matches part of the address, such as /plugins/ or .js, never delays a banner, and an entry in Scripts to never defer or delay always wins.', 'xspeed' ),
121 implode( ', ', Minify_Filters::consent_manager_labels() )
122 ),
123 'dependsOn' => array( 'field' => 'delay_js' ),
124 ),
125 'delay_js_smart' => array(
126 'type' => 'bool',
127 'default' => false,
128 'label' => __( 'Smart delay', 'xspeed' ),
129 'description' => __( 'Also delays scripts that code on the page depends on, which helps page builder sites most. The riskiest setting here, so test menus and sliders.', 'xspeed' ),
130 'dependsOn' => array( 'field' => 'delay_js' ),
131 ),
132 'delay_js_timeout' => array(
133 'type' => 'int',
134 'default' => 8000,
135 'min' => 0,
136 'max' => 60000,
137 'label' => __( 'Delay timeout (ms)', 'xspeed' ),
138 'unit' => 'ms',
139 'description' => __( 'Load delayed scripts after this long if the visitor does nothing. Set 0 to wait for a scroll, tap or key press only.', 'xspeed' ),
140 'advanced' => true,
141 'dependsOn' => array( 'field' => 'delay_js' ),
142 ),
143 'combine_css' => array(
144 'type' => 'bool',
145 'default' => false,
146 'label' => __( 'Combine CSS files', 'xspeed' ),
147 'description' => __( 'Joins your site\'s own CSS files into one. This helps only on old HTTP/1.1 servers, so leave it off on most hosts.', 'xspeed' ),
148 'advanced' => true,
149 ),
150 'combine_js' => array(
151 'type' => 'bool',
152 'default' => false,
153 'label' => __( 'Combine JavaScript files', 'xspeed' ),
154 'description' => __( 'Joins your site\'s own scripts into one file. Turn off if a script stops working after you enable it.', 'xspeed' ),
155 'advanced' => true,
156 ),
157 );
158 }
159
160 /**
161 * 1.1.0: drain minify_html / minify_css / minify_js from the legacy
162 * `xspeed_options` blob into this module's per-module option, then
163 * delete the keys from the legacy blob so duplicate sources can't
164 * re-appear. Idempotent — re-running is a no-op once the keys are
165 * gone from xspeed_options.
166 */
167 public function migrations(): array {
168 return array(
169 '1.1.0' => static function ( array $opts ): array {
170 $legacy = get_option( 'xspeed_options', array() );
171 if ( ! is_array( $legacy ) ) {
172 return $opts;
173 }
174 $dirty = false;
175 foreach ( array( 'minify_html', 'minify_css', 'minify_js' ) as $key ) {
176 if ( array_key_exists( $key, $legacy ) ) {
177 $opts[ $key ] = (bool) $legacy[ $key ];
178 unset( $legacy[ $key ] );
179 $dirty = true;
180 }
181 }
182 if ( $dirty ) {
183 update_option( 'xspeed_options', $legacy );
184 }
185 return $opts;
186 },
187 );
188 }
189
190 /**
191 * Conflict declarations. Detected automatically by Conflict_Registry,
192 * but listing them here keeps the module self-documenting.
193 */
194 public function conflicts(): array {
195 return array(
196 array(
197 'plugin' => 'autoptimize/autoptimize.php',
198 'feature' => 'minify.html',
199 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
200 'reason' => 'Autoptimize is active and handles minification.',
201 ),
202 array(
203 'plugin' => 'wp-rocket/wp-rocket.php',
204 'feature' => 'minify.html',
205 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
206 'reason' => 'WP Rocket already handles minification.',
207 ),
208 );
209 }
210
211 public function cli_commands(): array {
212 return array(
213 array(
214 'name' => 'xspeed minify',
215 'callback' => array( $this, 'cli_handler' ),
216 'shortdesc' => 'Inspect or purge xSpeed minify cache.',
217 'ai_hint' => 'Which CSS/JS optimizations are active (minify, combine, defer, delay, async)? Use for questions about render-blocking resources, unminified assets in PageSpeed, or when JavaScript broke after enabling optimizations.',
218 'synopsis' => array(
219 array(
220 'type' => 'positional',
221 'name' => 'action',
222 'options' => array( 'status', 'purge' ),
223 'optional' => false,
224 ),
225 ),
226 ),
227 );
228 }
229
230 /**
231 * On boot:
232 * 1. Seed our per-module option from the legacy blob if neither
233 * our option nor the migration has run yet (covers the
234 * already-installed-before-this-module-shipped path).
235 * 2. Instantiate the v1 Minifier engine; it now reads from
236 * Settings_Manager::get('minify') via its updated read path.
237 */
238 public function boot(): void {
239 /*
240 * Deferred to `init`: both calls below read this module's settings,
241 * which builds settings_schema(), whose labels go through __().
242 * boot() runs on `plugins_loaded`, before `after_setup_theme` — the
243 * earliest point WordPress 6.7+ considers safe to translate — so doing
244 * it here fires _load_textdomain_just_in_time on every request and
245 * resolves those labels against an unloaded domain.
246 *
247 * Every filter LegacyMinifier registers fires after `init`, so running
248 * one hook later is equivalent.
249 */
250 add_action( 'init', array( $this, 'boot_on_init' ) );
251 }
252
253 /**
254 * The real boot body — see boot() for why it runs on `init`.
255 */
256 public function boot_on_init(): void {
257 $this->seed_from_legacy_if_needed();
258 new LegacyMinifier();
259 }
260
261 public function activate(): void {
262 // Plugin activation hits all modules. Same seed logic — safe to
263 // run more than once.
264 $this->seed_from_legacy_if_needed();
265 }
266
267 /**
268 * Say so when HTML minification is switched on but suppressed.
269 *
270 * `Minifier::skip_reason()` was consulted only by `wp xspeed minify status`
271 * — the dashboard read "on" while `minify_html()` returned its input
272 * untouched, so the feature looked broken rather than paused. A field
273 * report showed a live site with `minify_html: on` and 3,856 indented lines
274 * delivered, and nothing anywhere explaining the contradiction. (#2)
275 *
276 * @return array<int,array<string,mixed>>
277 */
278 public function ui_notices(): array {
279 $opts = $this->get_settings();
280 if ( empty( $opts['minify_html'] ) ) {
281 return array();
282 }
283
284 $reason = LegacyMinifier::skip_reason();
285 if ( '' === $reason ) {
286 return array();
287 }
288
289 // Two different causes, two different fixes — naming the wrong one
290 // sends the user hunting in the wrong file.
291 $body = 'wp_debug' === $reason
292 ? __( 'HTML minification is paused because WP_DEBUG is enabled in wp-config.php. Readable HTML is usually what you want while debugging, so xSpeed leaves the markup alone. Cached pages are served un-minified until WP_DEBUG is turned off.', 'xspeed' )
293 : __( 'HTML minification is paused because a plugin or theme is returning true from the xspeed_skip_minify filter. Cached pages are served un-minified until that filter stops suppressing it.', 'xspeed' );
294
295 return array(
296 array(
297 'tone' => 'info',
298 'title' => __( 'HTML minification is on but currently paused', 'xspeed' ),
299 'body' => $body,
300 ),
301 );
302 }
303
304 private function seed_from_legacy_if_needed(): void {
305 $existing = get_option( 'xspeed_module_minify', null );
306 if ( null !== $existing ) {
307 return;
308 }
309 $legacy = get_option( 'xspeed_options', array() );
310 if ( ! is_array( $legacy ) ) {
311 return;
312 }
313 $seed = array( '_version' => self::VERSION );
314 $dirty = false;
315 foreach ( array( 'minify_html', 'minify_css', 'minify_js' ) as $key ) {
316 if ( array_key_exists( $key, $legacy ) ) {
317 $seed[ $key ] = (bool) $legacy[ $key ];
318 unset( $legacy[ $key ] );
319 $dirty = true;
320 }
321 }
322 if ( $dirty ) {
323 update_option( 'xspeed_module_minify', $seed );
324 update_option( 'xspeed_options', $legacy );
325 }
326 }
327
328 public function cli_handler( array $args, array $assoc ): void {
329 $action = $args[0] ?? 'status';
330
331 if ( 'status' === $action ) {
332 $opts = Settings_Manager::get( self::SLUG );
333 // A bare "on" is a lie when the skip guard is active: the
334 // setting is stored, but Minifier::minify_html() returns its
335 // input untouched and the delivered HTML is unchanged. Say so
336 // on the same line, so the contradiction can never be read as
337 // "minify is broken".
338 $skip = \XSpeed\Minifier::skip_reason();
339 $html_state = $opts['minify_html'] ? 'on' : 'off';
340 if ( $opts['minify_html'] && '' !== $skip ) {
341 $html_state .= ( 'wp_debug' === $skip )
342 ? ' (NOT APPLIED — WP_DEBUG is enabled; set WP_DEBUG to false to minify HTML)'
343 : ' (NOT APPLIED — suppressed by the xspeed_skip_minify filter)';
344 }
345 \WP_CLI::log( sprintf( 'minify_html: %s', $html_state ) );
346 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
347 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
348 return;
349 }
350
351 if ( 'purge' === $action ) {
352 LegacyMinifier::purge_minified();
353 \WP_CLI::success( 'Minify cache purged.' );
354 return;
355 }
356
357 \WP_CLI::error( "Unknown action: $action" );
358 }
359
360 /**
361 * Minify has no master switch -- it is on when any of minify_html /
362 * minify_css / minify_js / defer_js / delay_js / async_css /
363 * remove_query_strings is set. (#363)
364 */
365 public function is_active(): ?bool {
366 return $this->any_bool_flag_on();
367 }
368 }
369