| @@ -15,8 +15,11 @@ | ||
| 15 | 15 | declare(strict_types=1); |
| 16 | 16 | |
| 17 | 17 | namespace XSpeed\Modules\Gzip; |
| 18 | 18 | |
| 19 | +defined( 'ABSPATH' ) || exit; | |
| 20 | + | |
| 21 | +use XSpeed\Deep_Link; | |
| 19 | 22 | use XSpeed\Gzip as LegacyGzip; |
| 20 | 23 | use XSpeed\Module; |
| 21 | 24 | use XSpeed\Server; |
| 22 | 25 | use XSpeed\Settings_Manager; |
| @@ -28,11 +31,17 @@ | ||
| 28 | 31 | public const VERSION = '1.1.0'; |
| 29 | 32 | |
| 30 | 33 | public function ui_metadata(): array { |
| 31 | 34 | return array( |
| 32 | - 'label' => 'GZIP', | |
| 33 | - 'icon' => 'Layers', | |
| 34 | - 'description' => 'Compress responses to reduce transfer size.', | |
| 35 | + 'label' => __( 'Compression', 'xspeed' ), | |
| 36 | + 'tab_label' => __( 'GZIP', 'xspeed' ), // its own tab on the Compression page | |
| 37 | + 'icon' => 'Layers', | |
| 38 | + 'description' => __( 'Compress responses to reduce transfer size.', 'xspeed' ), | |
| 39 | + // Host panel merges GZIP (this module) + Brotli (Pro) into one | |
| 40 | + // page — they are one decision with a fallback chain, not two | |
| 41 | + // sidebar rows (FBS-83633). The panel renders this module's own | |
| 42 | + // schema form plus a Brotli Pro section. | |
| 43 | + 'custom_panel' => 'CompressionPanel', | |
| 35 | 44 | ); |
| 36 | 45 | } |
| 37 | 46 | |
| 38 | 47 | public function settings_schema(): array { |
| @@ -39,15 +48,39 @@ | ||
| 39 | 48 | return array( |
| 40 | 49 | 'gzip_enabled' => array( |
| 41 | 50 | 'type' => 'bool', |
| 42 | 51 | 'default' => false, |
| 43 | - 'label' => 'Enable GZIP Compression', | |
| 44 | - 'description' => 'On Apache / LiteSpeed we write the .htaccess rules automatically. On nginx the snippet below must be added to your server config.', | |
| 52 | + 'label' => __( 'Enable GZIP Compression', 'xspeed' ), | |
| 53 | + // Server-conditional. The old copy said "On nginx the snippet | |
| 54 | + // below must be added to your server config" — unconditionally, | |
| 55 | + // and there is no snippet below: the Compression page is a tab | |
| 56 | + // strip plus this toggle, and NginxServerBlock only mounts under | |
| 57 | + // the Cache panel. So Apache/LiteSpeed users read dead text | |
| 58 | + // about a server they aren't on, and nginx users got a promise | |
| 59 | + // the page couldn't keep. The nginx half now lives in | |
| 60 | + // ui_notices(), which can link to where the snippet actually is. | |
| 61 | + 'description' => self::gzip_description(), | |
| 45 | 62 | ), |
| 46 | 63 | ); |
| 47 | 64 | } |
| 48 | 65 | |
| 49 | 66 | /** |
| 67 | + * Copy for the GZIP toggle, matched to the server we're actually on. | |
| 68 | + * | |
| 69 | + * Schema descriptions have no server-conditional rendering, so the choice | |
| 70 | + * has to happen here rather than in the panel. | |
| 71 | + */ | |
| 72 | + private static function gzip_description(): string { | |
| 73 | + if ( class_exists( '\\XSpeed\\Server' ) && Server::supports_htaccess() ) { | |
| 74 | + return __( 'Compress responses before sending them. We write the .htaccess rules automatically on this server.', 'xspeed' ); | |
| 75 | + } | |
| 76 | + if ( class_exists( '\\XSpeed\\Server' ) && Server::NGINX === Server::type() ) { | |
| 77 | + return __( 'Compress responses before sending them. nginx cannot be configured from WordPress, so the directives ship in the unified server-block snippet — see the notice below.', 'xspeed' ); | |
| 78 | + } | |
| 79 | + return __( 'Compress responses before sending them. On this server the directives have to be added to your server config by hand — see the notice below.', 'xspeed' ); | |
| 80 | + } | |
| 81 | + | |
| 82 | + /** | |
| 50 | 83 | * 1.1.0: drain gzip_enabled from the legacy xspeed_options blob into |
| 51 | 84 | * this module's option. Idempotent — a re-run with the legacy key |
| 52 | 85 | * already gone is a no-op. |
| 53 | 86 | */ |
| @@ -71,8 +104,9 @@ | ||
| 71 | 104 | array( |
| 72 | 105 | 'name' => 'xspeed gzip', |
| 73 | 106 | 'callback' => array( $this, 'cli_handler' ), |
| 74 | 107 | 'shortdesc' => 'Show GZIP status (server type, active, mode).', |
| 108 | + 'ai_hint' => 'Is text compression (GZIP/Brotli) actually working on this site? Answers "why are my HTML/CSS/JS transfers so large" and whether the server is compressing at all. Reports the detected server, whether compression is active, and how it is applied.', | |
| 75 | 109 | 'synopsis' => array(), |
| 76 | 110 | ), |
| 77 | 111 | ); |
| 78 | 112 | } |
| @@ -87,24 +121,81 @@ | ||
| 87 | 121 | $opts = Settings_Manager::get( self::SLUG ); |
| 88 | 122 | if ( empty( $opts['gzip_enabled'] ) ) { |
| 89 | 123 | return array(); |
| 90 | 124 | } |
| 91 | - if ( 'auto' === Server::gzip_mode() && LegacyGzip::probe_active() ) { | |
| 125 | + | |
| 126 | + // Probe the live response — works regardless of server type. If gzip | |
| 127 | + // is actually being served, nothing is wrong. Mirrors BrowserCache's | |
| 128 | + // probe_headers_present() pattern. | |
| 129 | + // | |
| 130 | + // Tri-state: true = proven serving, false = proven not serving, | |
| 131 | + // null = the loopback never reached the origin, which is not evidence | |
| 132 | + // of anything. Telling someone their server is misconfigured because | |
| 133 | + // *we* couldn't call it is the bug behind issue #18. (#18) | |
| 134 | + $serving = LegacyGzip::probe_active(); | |
| 135 | + | |
| 136 | + // nginx gets a notice EITHER WAY, because on nginx this notice is the | |
| 137 | + // only route from the Compression page to the snippet — the panel body | |
| 138 | + // is a tab strip plus one toggle, and NginxServerBlock mounts under | |
| 139 | + // the Cache panel alone. Suppressing it on a correctly-configured box | |
| 140 | + // left that user with no way to reach the directives at all (say, to | |
| 141 | + // re-paste them after an nginx upgrade). Working sites get a calm | |
| 142 | + // "here's where it lives"; broken ones get the warning. (#87) | |
| 143 | + // | |
| 144 | + // An unreachable probe (null) takes the calm wording too: we cannot | |
| 145 | + // prove gzip is missing, so the notice points at the snippet without | |
| 146 | + // claiming the server is misconfigured. Only a proven-false probe | |
| 147 | + // warns. (#18) | |
| 148 | + if ( Server::NGINX === Server::type() ) { | |
| 149 | + $proven_missing = ( false === $serving ); | |
| 150 | + return array( | |
| 151 | + array( | |
| 152 | + 'tone' => $proven_missing ? 'warn' : 'info', | |
| 153 | + 'title' => $proven_missing | |
| 154 | + ? __( 'GZIP requires server config on nginx', 'xspeed' ) | |
| 155 | + : __( 'GZIP is being served by nginx', 'xspeed' ), | |
| 156 | + 'body' => $proven_missing | |
| 157 | + ? __( 'xSpeed can only auto-configure GZIP on Apache and LiteSpeed (via .htaccess). The directives are included in the unified server-block snippet on the Cache panel — copy + paste it once into your nginx vhost (or container nginx config) and reload nginx.', 'xspeed' ) | |
| 158 | + : __( 'Responses are compressed. xSpeed cannot configure nginx from WordPress, so these directives live in the unified server-block snippet on the Cache panel — that is where to re-copy them if your server config is ever rebuilt.', 'xspeed' ), | |
| 159 | + // Lands on the snippet itself and auto-expands it, rather | |
| 160 | + // than on the Cache page where it is one collapsed section | |
| 161 | + // among several. Same call BrowserCacheModule makes. | |
| 162 | + 'action' => Deep_Link::action( | |
| 163 | + __( 'Go to the snippet', 'xspeed' ), | |
| 164 | + 'cache', | |
| 165 | + 'nginx_snippet' | |
| 166 | + ), | |
| 167 | + ), | |
| 168 | + ); | |
| 169 | + } | |
| 170 | + | |
| 171 | + // Non-nginx: stay quiet unless the probe positively proved gzip is | |
| 172 | + // missing. `null` (unreachable) must not produce a notice. (#18) | |
| 173 | + if ( false !== $serving ) { | |
| 92 | 174 | return array(); |
| 93 | 175 | } |
| 176 | + // Probe says NOT active and gzip is enabled — surface the right | |
| 177 | + // notice per server topology. | |
| 94 | 178 | if ( 'auto' === Server::gzip_mode() ) { |
| 95 | - return array(); | |
| 179 | + // Apache/LiteSpeed but probe failed — .htaccess write must | |
| 180 | + // have been blocked, or another plugin is overriding. Tell | |
| 181 | + // the user something is wrong, no snippet (we can't fix it | |
| 182 | + // without their server access). | |
| 183 | + return array( | |
| 184 | + array( | |
| 185 | + 'tone' => 'warn', | |
| 186 | + 'title' => __( 'GZIP enabled but not active on the server', 'xspeed' ), | |
| 187 | + 'body' => __( 'xSpeed wrote the .htaccess rules but the response still isn\'t gzipped. Your server may have AllowOverride disabled, another caching plugin overriding, or mod_deflate missing. Ask your host to enable GZIP on Apache.', 'xspeed' ), | |
| 188 | + ), | |
| 189 | + ); | |
| 96 | 190 | } |
| 97 | - // Manual server (nginx, IIS, unknown) → render the snippet. | |
| 98 | - $type = Server::type(); | |
| 191 | + // IIS / unknown server fallback — keep the legacy "paste this" | |
| 192 | + // notice until a non-nginx unified-snippet surface ships. (nginx | |
| 193 | + // returned above, whether or not gzip is currently being served.) | |
| 99 | 194 | return array( |
| 100 | 195 | array( |
| 101 | 196 | 'tone' => 'warn', |
| 102 | - 'title' => sprintf( | |
| 103 | - /* translators: %s: server software label (e.g. "nginx") */ | |
| 104 | - __( 'GZIP requires server config on %s', 'xspeed' ), | |
| 105 | - 'nginx' === $type ? 'nginx' : __( 'this server', 'xspeed' ) | |
| 106 | - ), | |
| 197 | + 'title' => __( 'GZIP requires server config on this server', 'xspeed' ), | |
| 107 | 198 | 'body' => __( 'xSpeed can only auto-configure GZIP on Apache and LiteSpeed (via .htaccess). Paste the snippet below into your server config and reload.', 'xspeed' ), |
| 108 | 199 | 'snippet' => LegacyGzip::nginx_snippet(), |
| 109 | 200 | ), |
| 110 | 201 | ); |
| @@ -131,8 +222,12 @@ | ||
| 131 | 222 | $old_gzip = is_array( $old ) ? ! empty( $old['gzip_enabled'] ) : false; |
| 132 | 223 | $new_gzip = is_array( $new ) ? ! empty( $new['gzip_enabled'] ) : false; |
| 133 | 224 | if ( $old_gzip !== $new_gzip ) { |
| 134 | 225 | LegacyGzip::apply( $new_gzip ); |
| 226 | + // Settings just changed — current "probe says active" answer | |
| 227 | + // is stale. Drop the transient so the next dashboard load | |
| 228 | + // re-checks the live response. | |
| 229 | + delete_transient( 'xspeed_gzip_active' ); | |
| 135 | 230 | } |
| 136 | 231 | } |
| 137 | 232 | |
| 138 | 233 | /** |
| @@ -173,8 +268,29 @@ | ||
| 173 | 268 | $opts = Settings_Manager::get( self::SLUG ); |
| 174 | 269 | \WP_CLI::log( 'enabled ' . ( $opts['gzip_enabled'] ? 'true' : 'false' ) ); |
| 175 | 270 | \WP_CLI::log( 'server ' . Server::type() ); |
| 176 | 271 | \WP_CLI::log( 'mode ' . Server::gzip_mode() ); |
| 177 | - \WP_CLI::log( 'active ' . ( LegacyGzip::probe_active() ? 'true' : 'false' ) ); | |
| 272 | + $active = LegacyGzip::probe_active(); | |
| 273 | + \WP_CLI::log( 'active ' . ( null === $active ? 'unknown (loopback probe failed)' : ( $active ? 'true' : 'false' ) ) ); | |
| 178 | 274 | \WP_CLI::log( 'nginx_snippet ' . LegacyGzip::nginx_snippet() ); |
| 275 | + } | |
| 276 | + | |
| 277 | + /** | |
| 278 | + * GZIP directives for the unified nginx server-block snippet. Null | |
| 279 | + * when the module is disabled. | |
| 280 | + */ | |
| 281 | + public function nginx_directives(): ?string { | |
| 282 | + $opts = Settings_Manager::get( self::SLUG ); | |
| 283 | + if ( empty( $opts['gzip_enabled'] ) ) { | |
| 284 | + return null; | |
| 285 | + } | |
| 286 | + $snippet = LegacyGzip::nginx_snippet(); | |
| 287 | + return is_string( $snippet ) && '' !== $snippet ? $snippet : null; | |
| 288 | + } | |
| 289 | + | |
| 290 | + /** | |
| 291 | + * Gzip stores its switch as `gzip_enabled`, not `enabled`. (#363) | |
| 292 | + */ | |
| 293 | + public function is_active(): ?bool { | |
| 294 | + return $this->any_bool_flag_on(); | |
| 179 | 295 | } |
| 180 | 296 | } |