Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 175 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
| PhpRedisPinger | |
0.00% |
0 / 175 |
|
0.00% |
0 / 9 |
4970 | |
0.00% |
0 / 1 |
| ping | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
90 | |||
| ping_shards | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| ping_cluster | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
110 | |||
| ping_single | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
182 | |||
| build_cluster_seeds | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| normalize_ping_result | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
72 | |||
| build_parameters_from_constants | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
90 | |||
| phpredis_auth | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
| redis_auth_secret_string | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
72 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace NewfoldLabs\WP\Module\Performance\Cache\Types; |
| 4 | |
| 5 | use NewfoldLabs\WP\Module\Performance\Helpers\RedisEnv; |
| 6 | |
| 7 | /** |
| 8 | * Performs a lightweight Redis connectivity check using phpredis. |
| 9 | * |
| 10 | * Mirrors the common connection modes from the Newfold Redis drop-in, but intentionally stays |
| 11 | * defensive: if we cannot confidently connect, we return failure with a message. |
| 12 | */ |
| 13 | final class PhpRedisPinger { |
| 14 | |
| 15 | /** |
| 16 | * Run Redis PING using the active connection mode (single, cluster, or shards). |
| 17 | * |
| 18 | * @return array{ok:bool, message?:string} |
| 19 | */ |
| 20 | public static function ping(): array { |
| 21 | if ( ! extension_loaded( 'redis' ) || ! class_exists( 'Redis', false ) ) { |
| 22 | return array( |
| 23 | 'ok' => false, |
| 24 | 'message' => __( 'phpredis is not available.', 'wp-module-performance' ), |
| 25 | ); |
| 26 | } |
| 27 | |
| 28 | try { |
| 29 | if ( defined( 'WP_REDIS_SHARDS' ) && is_array( WP_REDIS_SHARDS ) ) { |
| 30 | if ( ! class_exists( 'RedisArray', false ) ) { |
| 31 | return array( |
| 32 | 'ok' => false, |
| 33 | 'message' => __( 'RedisArray is not available in this PHP Redis build.', 'wp-module-performance' ), |
| 34 | ); |
| 35 | } |
| 36 | return self::ping_shards(); |
| 37 | } |
| 38 | |
| 39 | if ( defined( 'WP_REDIS_CLUSTER' ) ) { |
| 40 | if ( ! class_exists( 'RedisCluster', false ) ) { |
| 41 | return array( |
| 42 | 'ok' => false, |
| 43 | 'message' => __( 'RedisCluster is not available in this PHP Redis build.', 'wp-module-performance' ), |
| 44 | ); |
| 45 | } |
| 46 | return self::ping_cluster(); |
| 47 | } |
| 48 | |
| 49 | return self::ping_single(); |
| 50 | } catch ( \Throwable $e ) { |
| 51 | return array( |
| 52 | 'ok' => false, |
| 53 | 'message' => $e->getMessage(), |
| 54 | ); |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * Ping Redis in RedisArray (sharded) mode. |
| 60 | * |
| 61 | * @return array{ok:bool, message?:string} |
| 62 | */ |
| 63 | private static function ping_shards(): array { |
| 64 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Drop-in constant. |
| 65 | $redis = new \RedisArray( array_values( WP_REDIS_SHARDS ) ); |
| 66 | $pong = $redis->ping(); |
| 67 | |
| 68 | return self::normalize_ping_result( $pong ); |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Ping Redis in cluster mode. |
| 73 | * |
| 74 | * @return array{ok:bool, message?:string} |
| 75 | */ |
| 76 | private static function ping_cluster(): array { |
| 77 | $version = phpversion( 'redis' ); |
| 78 | $version = is_string( $version ) ? $version : '0.0.0'; |
| 79 | |
| 80 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Drop-in constant. |
| 81 | $cluster = WP_REDIS_CLUSTER; |
| 82 | |
| 83 | if ( is_string( $cluster ) ) { |
| 84 | $redis = new \RedisCluster( $cluster ); |
| 85 | $pong = $redis->ping(); |
| 86 | return self::normalize_ping_result( $pong ); |
| 87 | } |
| 88 | |
| 89 | if ( ! is_array( $cluster ) ) { |
| 90 | return array( |
| 91 | 'ok' => false, |
| 92 | 'message' => __( 'Unsupported WP_REDIS_CLUSTER configuration.', 'wp-module-performance' ), |
| 93 | ); |
| 94 | } |
| 95 | |
| 96 | $parameters = self::build_parameters_from_constants(); |
| 97 | |
| 98 | $args = array( |
| 99 | 'cluster' => self::build_cluster_seeds( $cluster ), |
| 100 | 'timeout' => $parameters['timeout'], |
| 101 | 'read_timeout' => $parameters['read_timeout'], |
| 102 | 'persistent' => (bool) $parameters['persistent'], |
| 103 | ); |
| 104 | |
| 105 | if ( isset( $parameters['password'] ) && version_compare( $version, '4.3.0', '>=' ) ) { |
| 106 | $args['password'] = $parameters['password']; |
| 107 | } |
| 108 | |
| 109 | if ( version_compare( $version, '5.3.0', '>=' ) && defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) { |
| 110 | if ( ! array_key_exists( 'password', $args ) ) { |
| 111 | $args['password'] = null; |
| 112 | } |
| 113 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Drop-in constant. |
| 114 | $args['ssl'] = WP_REDIS_SSL_CONTEXT; |
| 115 | } |
| 116 | |
| 117 | $redis = new \RedisCluster( null, ...array_values( $args ) ); |
| 118 | $pong = $redis->ping(); |
| 119 | |
| 120 | return self::normalize_ping_result( $pong ); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Ping a single Redis instance. |
| 125 | * |
| 126 | * @return array{ok:bool, message?:string} |
| 127 | */ |
| 128 | private static function ping_single(): array { |
| 129 | $parameters = self::build_parameters_from_constants(); |
| 130 | $version = phpversion( 'redis' ); |
| 131 | $version = is_string( $version ) ? $version : '0.0.0'; |
| 132 | |
| 133 | $redis = new \Redis(); |
| 134 | |
| 135 | $retry_interval = $parameters['retry_interval']; |
| 136 | $args = array( |
| 137 | 'host' => $parameters['host'], |
| 138 | 'port' => (int) $parameters['port'], |
| 139 | 'timeout' => $parameters['timeout'], |
| 140 | 'reserved' => '', |
| 141 | 'retry_interval' => null === $retry_interval ? 0 : (int) $retry_interval, |
| 142 | ); |
| 143 | |
| 144 | if ( version_compare( $version, '3.1.3', '>=' ) ) { |
| 145 | $args['read_timeout'] = $parameters['read_timeout']; |
| 146 | } |
| 147 | |
| 148 | if ( strcasecmp( 'tls', (string) $parameters['scheme'] ) === 0 ) { |
| 149 | $args['host'] = sprintf( |
| 150 | '%s://%s', |
| 151 | $parameters['scheme'], |
| 152 | str_replace( 'tls://', '', (string) $parameters['host'] ) |
| 153 | ); |
| 154 | |
| 155 | if ( version_compare( $version, '5.3.0', '>=' ) && defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) { |
| 156 | $args['others'] = array( |
| 157 | 'stream' => WP_REDIS_SSL_CONTEXT, |
| 158 | ); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | if ( strcasecmp( 'unix', (string) $parameters['scheme'] ) === 0 ) { |
| 163 | $args['host'] = (string) $parameters['path']; |
| 164 | $args['port'] = -1; |
| 165 | } |
| 166 | |
| 167 | call_user_func_array( array( $redis, 'connect' ), array_values( $args ) ); |
| 168 | |
| 169 | self::phpredis_auth( $redis, $parameters, $version ); |
| 170 | |
| 171 | if ( isset( $parameters['database'] ) && $parameters['database'] ) { |
| 172 | $db = $parameters['database']; |
| 173 | if ( ctype_digit( (string) $db ) ) { |
| 174 | $db = (int) $db; |
| 175 | } |
| 176 | if ( $db ) { |
| 177 | $redis->select( $db ); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | $pong = $redis->ping(); |
| 182 | return self::normalize_ping_result( $pong ); |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * Build host:port seed strings from a WP_REDIS_CLUSTER array-style definition. |
| 187 | * |
| 188 | * @param mixed $cluster_def Cluster definition from WP_REDIS_CLUSTER. |
| 189 | * @return array<int, string> |
| 190 | */ |
| 191 | private static function build_cluster_seeds( $cluster_def ): array { |
| 192 | $seeds = array(); |
| 193 | foreach ( (array) $cluster_def as $key => $define ) { |
| 194 | if ( is_array( $define ) ) { |
| 195 | $seeds[] = implode( ':', array_map( 'strval', $define ) ); |
| 196 | continue; |
| 197 | } |
| 198 | |
| 199 | $host = is_int( $key ) ? strval( $define ) : strval( $key ); |
| 200 | $port = strval( $define ); |
| 201 | $seeds[] = "{$host}:{$port}"; |
| 202 | } |
| 203 | |
| 204 | return $seeds; |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Interpret a PING response from phpredis as success or failure. |
| 209 | * |
| 210 | * @param mixed $pong Return value from Redis::ping(). |
| 211 | * @return array{ok:bool, message?:string} |
| 212 | */ |
| 213 | private static function normalize_ping_result( $pong ): array { |
| 214 | if ( true === $pong ) { |
| 215 | return array( 'ok' => true ); |
| 216 | } |
| 217 | |
| 218 | if ( is_string( $pong ) && stripos( $pong, 'PONG' ) !== false ) { |
| 219 | return array( 'ok' => true ); |
| 220 | } |
| 221 | |
| 222 | if ( is_array( $pong ) ) { |
| 223 | foreach ( $pong as $v ) { |
| 224 | if ( is_string( $v ) && stripos( $v, 'PONG' ) !== false ) { |
| 225 | return array( 'ok' => true ); |
| 226 | } |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | return array( |
| 231 | 'ok' => false, |
| 232 | 'message' => __( 'Redis did not respond to PING as expected.', 'wp-module-performance' ), |
| 233 | ); |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Build connection parameters from WP_REDIS_* constants plus optional environment fallbacks. |
| 238 | * |
| 239 | * @return array{scheme:string,host:string,port:int,path:string,password?:string,database:int|float|string,timeout:float|int,read_timeout:float|int,retry_interval:?int,persistent:bool} |
| 240 | */ |
| 241 | private static function build_parameters_from_constants(): array { |
| 242 | $parameters = array( |
| 243 | 'scheme' => 'tcp', |
| 244 | 'host' => '127.0.0.1', |
| 245 | 'port' => 6379, |
| 246 | 'path' => '', |
| 247 | 'database' => 0, |
| 248 | 'timeout' => 1, |
| 249 | 'read_timeout' => 1, |
| 250 | 'retry_interval' => null, |
| 251 | 'persistent' => false, |
| 252 | ); |
| 253 | |
| 254 | $settings = array( |
| 255 | 'scheme', |
| 256 | 'host', |
| 257 | 'port', |
| 258 | 'path', |
| 259 | 'password', |
| 260 | 'username', |
| 261 | 'database', |
| 262 | 'timeout', |
| 263 | 'read_timeout', |
| 264 | 'retry_interval', |
| 265 | ); |
| 266 | |
| 267 | foreach ( $settings as $setting ) { |
| 268 | $constant = sprintf( 'WP_REDIS_%s', strtoupper( $setting ) ); |
| 269 | if ( defined( $constant ) ) { |
| 270 | $parameters[ $setting ] = constant( $constant ); |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | if ( isset( $parameters['password'] ) && '' === $parameters['password'] ) { |
| 275 | unset( $parameters['password'] ); |
| 276 | } |
| 277 | |
| 278 | if ( ! isset( $parameters['password'] ) ) { |
| 279 | $from_env = RedisEnv::string_value( 'WP_REDIS_PASSWORD' ); |
| 280 | if ( '' !== $from_env ) { |
| 281 | $parameters['password'] = $from_env; |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | if ( ! isset( $parameters['username'] ) ) { |
| 286 | $user_env = RedisEnv::string_value( 'WP_REDIS_USERNAME' ); |
| 287 | if ( '' !== $user_env ) { |
| 288 | $parameters['username'] = $user_env; |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | return $parameters; |
| 293 | } |
| 294 | |
| 295 | /** |
| 296 | * Authenticate using the PHP Redis extension: legacy password, ACL array (Redis 6+), or username and password. |
| 297 | * |
| 298 | * @param \Redis $redis Connected client. |
| 299 | * @param array<string, mixed> $parameters From build_parameters_from_constants(). |
| 300 | * @param string $phpredis_ver Extension version string. |
| 301 | */ |
| 302 | private static function phpredis_auth( \Redis $redis, array $parameters, $phpredis_ver ): void { |
| 303 | if ( ! isset( $parameters['password'] ) ) { |
| 304 | return; |
| 305 | } |
| 306 | |
| 307 | $pw = $parameters['password']; |
| 308 | |
| 309 | if ( is_array( $pw ) ) { |
| 310 | $redis->auth( $pw ); |
| 311 | return; |
| 312 | } |
| 313 | |
| 314 | $pw_string = self::redis_auth_secret_string( $pw ); |
| 315 | if ( null === $pw_string ) { |
| 316 | return; |
| 317 | } |
| 318 | |
| 319 | if ( isset( $parameters['username'] ) && is_string( $parameters['username'] ) && '' !== $parameters['username'] ) { |
| 320 | if ( version_compare( (string) $phpredis_ver, '5.3.0', '>=' ) ) { |
| 321 | $redis->auth( $parameters['username'], $pw_string ); |
| 322 | return; |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | $redis->auth( $pw_string ); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Coerce password to string for phpredis without triggering PHP 8+ conversion fatals. |
| 331 | * |
| 332 | * @param mixed $pw Password value from constants or env. |
| 333 | * @return string|null String to pass to Redis::auth(), or null if the value cannot be used safely. |
| 334 | */ |
| 335 | private static function redis_auth_secret_string( $pw ): ?string { |
| 336 | if ( is_string( $pw ) ) { |
| 337 | return $pw; |
| 338 | } |
| 339 | if ( is_int( $pw ) || is_float( $pw ) ) { |
| 340 | return (string) $pw; |
| 341 | } |
| 342 | if ( is_bool( $pw ) ) { |
| 343 | return $pw ? '1' : ''; |
| 344 | } |
| 345 | if ( is_object( $pw ) && method_exists( $pw, '__toString' ) ) { |
| 346 | return (string) $pw; |
| 347 | } |
| 348 | |
| 349 | return null; |
| 350 | } |
| 351 | } |