', ' ExpiresActive On', ' ExpiresByType text/html "access plus ' . $html . ' seconds"', ' ExpiresByType text/css "access plus ' . $asset . ' seconds"', ' ExpiresByType text/javascript "access plus ' . $asset . ' seconds"', ' ExpiresByType application/javascript "access plus ' . $asset . ' seconds"', ' ExpiresByType application/json "access plus ' . $html . ' seconds"', ' ExpiresByType image/jpeg "access plus ' . $asset . ' seconds"', ' ExpiresByType image/png "access plus ' . $asset . ' seconds"', ' ExpiresByType image/webp "access plus ' . $asset . ' seconds"', ' ExpiresByType image/avif "access plus ' . $asset . ' seconds"', ' ExpiresByType image/gif "access plus ' . $asset . ' seconds"', ' ExpiresByType image/svg+xml "access plus ' . $asset . ' seconds"', ' ExpiresByType image/x-icon "access plus ' . $asset . ' seconds"', ' ExpiresByType font/woff2 "access plus ' . $asset . ' seconds"', ' ExpiresByType font/woff "access plus ' . $asset . ' seconds"', ' ExpiresByType font/ttf "access plus ' . $asset . ' seconds"', ' ExpiresByType font/otf "access plus ' . $asset . ' seconds"', '', '', ' ', ' Header set Cache-Control "public, max-age=' . $asset . ', immutable"', ' ', ' ', ' Header set Cache-Control "public, max-age=' . $html . '"', ' ', '', ); } /** * nginx snippet — uses `expires` directive (the canonical nginx way) * plus an `add_header` line for the immutable flag. */ /** * Probe whether the server is actually emitting the Cache-Control * headers our nginx snippet is supposed to add. Picks a recognisable * static asset (anything in `wp-includes/css/` is always served on * a WP install) and HEADs it, looking for `immutable` in the response * — the unique fingerprint our snippet adds that WP core's defaults * never set. Cached in a 5-minute transient so this never adds * latency to the dashboard. * * Returns: * true → headers present, no notice needed * false → headers absent, snippet hasn't been pasted yet (or hasn't * been reloaded into nginx), keep the notice up */ public static function probe_headers_present(): bool { $cached = get_transient( 'xspeed_browser_cache_probe' ); if ( null !== $cached && false !== $cached ) { return (bool) $cached; } $asset_url = includes_url( 'css/dashicons.min.css' ); $resp = wp_remote_head( $asset_url, array( 'timeout' => 3, 'sslverify' => false, 'redirection' => 0, 'headers' => array( 'Cache-Control' => 'no-cache' ), ) ); if ( is_wp_error( $resp ) ) { set_transient( 'xspeed_browser_cache_probe', 0, MINUTE_IN_SECONDS ); return false; } // wp_remote_retrieve_header() returns a STRING for a single header // but an ARRAY when the header appears more than once (common behind // CDNs / proxies, or nginx with multiple add_header lines). Casting // an array with (string) emits an "Array to string conversion" // warning AND flattens to the literal "Array", so the immutable // check below silently false-negatives. Normalize array → string // first. (FBS-82141) $raw = wp_remote_retrieve_header( $resp, 'cache-control' ); $cache_control = is_array( $raw ) ? implode( ', ', $raw ) : (string) $raw; $active = false !== stripos( $cache_control, 'immutable' ); set_transient( 'xspeed_browser_cache_probe', $active ? 1 : 0, 5 * MINUTE_IN_SECONDS ); return $active; } public static function nginx_snippet( array $opts = array() ): string { $asset = (int) ( $opts['asset_ttl'] ?? self::DEFAULT_ASSET_TTL ); $html = (int) ( $opts['html_ttl'] ?? self::DEFAULT_HTML_TTL ); return implode( "\n", array( 'location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2|woff|ttf|otf|eot|mp4|webm|mp3|ogg)$ {', ' expires ' . $asset . 's;', ' add_header Cache-Control "public, max-age=' . $asset . ', immutable";', '}', 'location ~* \.html$ {', ' expires ' . $html . 's;', ' add_header Cache-Control "public, max-age=' . $html . '";', '}', ) ); } }