| 1 |
<?php |
| 2 |
/** |
| 3 |
* Object_Cache — read-only detector + flusher + wp-config snippet |
| 4 |
* generator for the persistent object cache. |
| 5 |
* |
| 6 |
* We deliberately do NOT install our own object-cache.php drop-in |
| 7 |
* from this Free release — that's invasive, has many failure modes |
| 8 |
* (auth, TLS, cluster vs single, Redis vs Predis vs phpredis, |
| 9 |
* Memcache vs Memcached), and changes how every site reads/writes |
| 10 |
* persistent state. The Free plugin's role is: |
| 11 |
* |
| 12 |
* 1. Tell the user whether a drop-in is currently active and |
| 13 |
* which backend it appears to be. |
| 14 |
* 2. Provide a Flush button that calls wp_cache_flush() — which |
| 15 |
* works regardless of which drop-in is installed. |
| 16 |
* 3. Save backend-config values (host, port, password, etc.) and |
| 17 |
* render a wp-config.php snippet the user can paste, so the |
| 18 |
* flow is "configure here → copy snippet → install drop-in" |
| 19 |
* without us writing to wp-config ourselves. |
| 20 |
* |
| 21 |
* The Pro plugin (or a later Free release once well tested) can |
| 22 |
* ship a drop-in that consumes these saved values automatically. |
| 23 |
* |
| 24 |
* @package XSpeed |
| 25 |
*/ |
| 26 |
|
| 27 |
declare(strict_types=1); |
| 28 |
|
| 29 |
namespace XSpeed; |
| 30 |
|
| 31 |
defined( 'ABSPATH' ) || exit; |
| 32 |
|
| 33 |
final class Object_Cache { |
| 34 |
|
| 35 |
/** |
| 36 |
* Inspect the runtime + filesystem for a persistent object cache. |
| 37 |
* |
| 38 |
* @return array{ |
| 39 |
* drop_in_installed: bool, |
| 40 |
* drop_in_path: string, |
| 41 |
* drop_in_label: string, |
| 42 |
* backend: string, // redis|memcached|apcu|wp_default|unknown |
| 43 |
* wp_cache_active: bool, // wp_using_ext_object_cache |
| 44 |
* class_available: array<string,bool> |
| 45 |
* } |
| 46 |
*/ |
| 47 |
public static function detect(): array { |
| 48 |
$dropin = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR . '/object-cache.php' : ''; |
| 49 |
$has_drop_in = '' !== $dropin && file_exists( $dropin ); |
| 50 |
$label = $has_drop_in ? self::sniff_drop_in_label( $dropin ) : ''; |
| 51 |
$ext_in_use = function_exists( 'wp_using_ext_object_cache' ) ? (bool) wp_using_ext_object_cache() : false; |
| 52 |
|
| 53 |
// Class sniffer — independent of any plugin. Tells us what's |
| 54 |
// available to actually use, separate from what's wired up. |
| 55 |
$class_available = array( |
| 56 |
'Redis' => class_exists( '\\Redis' ), |
| 57 |
'Memcached' => class_exists( '\\Memcached' ), |
| 58 |
'Memcache' => class_exists( '\\Memcache' ), |
| 59 |
'APCu' => function_exists( 'apcu_enabled' ) && @apcu_enabled(), |
| 60 |
); |
| 61 |
|
| 62 |
$backend = 'unknown'; |
| 63 |
if ( ! $ext_in_use ) { |
| 64 |
$backend = 'wp_default'; |
| 65 |
} elseif ( $has_drop_in ) { |
| 66 |
// Best-effort: the label usually contains the backend name. |
| 67 |
$lc = strtolower( $label ); |
| 68 |
if ( false !== strpos( $lc, 'redis' ) ) { |
| 69 |
$backend = 'redis'; |
| 70 |
} elseif ( false !== strpos( $lc, 'memcached' ) || false !== strpos( $lc, 'memcache' ) ) { |
| 71 |
$backend = 'memcached'; |
| 72 |
} elseif ( false !== strpos( $lc, 'apcu' ) ) { |
| 73 |
$backend = 'apcu'; |
| 74 |
} |
| 75 |
} |
| 76 |
|
| 77 |
return array( |
| 78 |
'drop_in_installed' => $has_drop_in, |
| 79 |
'drop_in_path' => $dropin, |
| 80 |
'drop_in_label' => $label, |
| 81 |
'backend' => $backend, |
| 82 |
'wp_cache_active' => $ext_in_use, |
| 83 |
'class_available' => $class_available, |
| 84 |
); |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Flush whatever cache backend is wired up. Works against any |
| 89 |
* compliant drop-in OR the WP default in-memory cache. |
| 90 |
*/ |
| 91 |
public static function flush(): bool { |
| 92 |
if ( ! function_exists( 'wp_cache_flush' ) ) { |
| 93 |
return false; |
| 94 |
} |
| 95 |
return (bool) wp_cache_flush(); |
| 96 |
} |
| 97 |
|
| 98 |
/** |
| 99 |
* Render a paste-into-wp-config.php snippet for the chosen backend |
| 100 |
* using the supplied settings. The constant names match the |
| 101 |
* conventions of the widely-used Redis Object Cache + W3TC drop-ins |
| 102 |
* so users with those installed get a working configuration |
| 103 |
* without any further translation. |
| 104 |
*/ |
| 105 |
public static function render_config_snippet( array $opts ): string { |
| 106 |
$backend = (string) ( $opts['backend'] ?? 'redis' ); |
| 107 |
$lines = array( "/* xSpeed object cache config — paste above the \"That's all, stop editing!\" comment in wp-config.php. */" ); |
| 108 |
|
| 109 |
if ( 'redis' === $backend ) { |
| 110 |
$host = self::str( $opts, 'redis_host', '127.0.0.1' ); |
| 111 |
$port = self::int( $opts, 'redis_port', 6379 ); |
| 112 |
$pass = self::str( $opts, 'redis_password', '' ); |
| 113 |
$db = self::int( $opts, 'redis_database', 0 ); |
| 114 |
$prefix = self::str( $opts, 'key_prefix', '' ); |
| 115 |
$timeout = self::int( $opts, 'connection_timeout', 1 ); |
| 116 |
$persist = ! empty( $opts['persistent'] ); |
| 117 |
|
| 118 |
$lines[] = "define( 'WP_REDIS_HOST', '" . self::esc( $host ) . "' );"; |
| 119 |
$lines[] = "define( 'WP_REDIS_PORT', " . $port . ' );'; |
| 120 |
if ( '' !== $pass ) { |
| 121 |
$lines[] = "define( 'WP_REDIS_PASSWORD', '" . self::esc( $pass ) . "' );"; |
| 122 |
} |
| 123 |
$lines[] = "define( 'WP_REDIS_DATABASE', " . $db . ' );'; |
| 124 |
if ( '' !== $prefix ) { |
| 125 |
$lines[] = "define( 'WP_CACHE_KEY_SALT', '" . self::esc( $prefix ) . "' );"; |
| 126 |
} |
| 127 |
$lines[] = "define( 'WP_REDIS_TIMEOUT', " . $timeout . ' );'; |
| 128 |
$lines[] = "define( 'WP_REDIS_PERSISTENT', " . ( $persist ? 'true' : 'false' ) . ' );'; |
| 129 |
} elseif ( 'memcached' === $backend ) { |
| 130 |
$host = self::str( $opts, 'memcached_host', '127.0.0.1' ); |
| 131 |
$port = self::int( $opts, 'memcached_port', 11211 ); |
| 132 |
$prefix = self::str( $opts, 'key_prefix', '' ); |
| 133 |
$lines[] = "global \$memcached_servers;"; |
| 134 |
$lines[] = "\$memcached_servers = array( array( '" . self::esc( $host ) . "', " . $port . ' ) );'; |
| 135 |
if ( '' !== $prefix ) { |
| 136 |
$lines[] = "define( 'WP_CACHE_KEY_SALT', '" . self::esc( $prefix ) . "' );"; |
| 137 |
} |
| 138 |
} else { |
| 139 |
$lines[] = '// No snippet for backend: ' . $backend; |
| 140 |
} |
| 141 |
|
| 142 |
return implode( "\n", $lines ) . "\n"; |
| 143 |
} |
| 144 |
|
| 145 |
private static function sniff_drop_in_label( string $path ): string { |
| 146 |
$head = @file_get_contents( $path, false, null, 0, 2048 ); |
| 147 |
if ( ! is_string( $head ) || '' === $head ) { |
| 148 |
return ''; |
| 149 |
} |
| 150 |
// PluginName / Plugin Name in standard WP file header form. |
| 151 |
if ( preg_match( '#Plugin Name:\s*([^\r\n]+)#i', $head, $m ) ) { |
| 152 |
return trim( $m[1] ); |
| 153 |
} |
| 154 |
// Many drop-ins just put their identity in a comment. |
| 155 |
if ( preg_match( '#\*\s*([A-Za-z][A-Za-z0-9 _\-]{2,40}(?:Cache|Redis|Memcached)[^\r\n]*)#i', $head, $m ) ) { |
| 156 |
return trim( $m[1] ); |
| 157 |
} |
| 158 |
return basename( $path ); |
| 159 |
} |
| 160 |
|
| 161 |
private static function str( array $opts, string $key, string $default ): string { |
| 162 |
return isset( $opts[ $key ] ) && '' !== $opts[ $key ] ? (string) $opts[ $key ] : $default; |
| 163 |
} |
| 164 |
|
| 165 |
private static function int( array $opts, string $key, int $default ): int { |
| 166 |
return isset( $opts[ $key ] ) && '' !== $opts[ $key ] ? (int) $opts[ $key ] : $default; |
| 167 |
} |
| 168 |
|
| 169 |
private static function esc( string $s ): string { |
| 170 |
return str_replace( array( '\\', "'" ), array( '\\\\', "\\'" ), $s ); |
| 171 |
} |
| 172 |
} |
| 173 |
|