# xspeed/1.0.7/includes/class-browser-cache.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.0.7. 166 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.0.7/code/includes/class-browser-cache.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.7/raw/includes/class-browser-cache.php
- Modified: 2026-06-14T07:01:26+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.0.7/code/includes/class-browser-cache.php#L10-L20`.

```php
<?php
/**
 * Browser_Cache — writes/removes browser-cache directives in the site
 * root .htaccess so static assets get long Cache-Control + Expires
 * headers, and serves an nginx snippet for non-Apache hosts.
 *
 * Same shape as Gzip: marker block, insert_with_markers, snippet
 * fallback. Independent toggle so users can enable browser caching
 * without compression and vice versa.
 *
 * The default TTLs follow LiteSpeed/WP Rocket conventions:
 *   - Static assets (CSS/JS/fonts/images): 1 year + immutable.
 *   - HTML: 1 hour (so post edits go live the same day even if a CDN
 *     has cached the document).
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

final class Browser_Cache {

	public const MARKER = 'xSpeed Browser Cache';

	public const DEFAULT_ASSET_TTL = 31536000; // 1 year
	public const DEFAULT_HTML_TTL  = 3600;     // 1 hour

	public static function apply( bool $enabled, array $opts = array() ): bool {
		if ( ! function_exists( 'XSpeed\\Server::supports_htaccess' ) && class_exists( '\\XSpeed\\Server' ) ) {
			if ( ! Server::supports_htaccess() ) {
				return false;
			}
		}
		$htaccess = ABSPATH . '.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			return false;
		}
		if ( ! function_exists( 'insert_with_markers' ) ) {
			require_once ABSPATH . 'wp-admin/includes/misc.php';
		}
		$rules = $enabled ? self::apache_rules( $opts ) : array();
		return (bool) insert_with_markers( $htaccess, self::MARKER, $rules );
	}

	/**
	 * Apache rule lines. mod_expires handles the Expires header; an
	 * inline mod_headers Cache-Control mirror lets us include
	 * `immutable` (mod_expires doesn't emit it).
	 *
	 * @return string[]
	 */
	public static function apache_rules( array $opts = array() ): array {
		$asset = (int) ( $opts['asset_ttl'] ?? self::DEFAULT_ASSET_TTL );
		$html  = (int) ( $opts['html_ttl'] ?? self::DEFAULT_HTML_TTL );
		if ( $asset < 0 ) {
			$asset = self::DEFAULT_ASSET_TTL;
		}
		if ( $html < 0 ) {
			$html = self::DEFAULT_HTML_TTL;
		}
		return array(
			'<IfModule mod_expires.c>',
			'  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"',
			'</IfModule>',
			'<IfModule mod_headers.c>',
			'  <FilesMatch "\.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2|woff|ttf|otf|eot|mp4|webm|mp3|ogg)$">',
			'    Header set Cache-Control "public, max-age=' . $asset . ', immutable"',
			'  </FilesMatch>',
			'  <FilesMatch "\.html$">',
			'    Header set Cache-Control "public, max-age=' . $html . '"',
			'  </FilesMatch>',
			'</IfModule>',
		);
	}

	/**
	 * 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 . '";',
				'}',
			)
		);
	}
}

```
