PluginProbe ʕ •ᴥ•ʔ
Redis Object Cache / 2.7.0
Redis Object Cache v2.7.0
trunk 1.0 1.0.1 1.0.2 1.1 1.1.1 1.2 1.2.1 1.2.2 1.2.3 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.17 2.0.18 2.0.19 2.0.2 2.0.20 2.0.21 2.0.22 2.0.23 2.0.25 2.0.26 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.6.0 2.6.1 2.6.2 2.6.3 2.6.5 2.7.0 2.8.0
redis-cache / includes / object-cache.php
redis-cache / includes Last commit date
cli 10 months ago ui 10 months ago class-autoloader.php 5 years ago class-metrics.php 10 months ago class-plugin.php 10 months ago class-predis.php 10 months ago class-qm-collector.php 3 years ago class-qm-output.php 3 years ago class-ui.php 5 years ago diagnostics.php 8 months ago object-cache.php 8 months ago
object-cache.php
3065 lines
1 <?php
2 /**
3 * Plugin Name: Redis Object Cache Drop-In
4 * Plugin URI: https://wordpress.org/plugins/redis-cache/
5 * Description: A persistent object cache backend powered by Redis. Supports Predis, PhpRedis, Relay, replication, sentinels, clustering and WP-CLI.
6 * Version: 2.7.0
7 * Author: Till Krüss
8 * Author URI: https://objectcache.pro
9 * License: GPLv3
10 * License URI: http://www.gnu.org/licenses/gpl-3.0.html
11 * Requires PHP: 7.2
12 *
13 * @package Rhubarb\RedisCache
14 */
15
16 defined( '\\ABSPATH' ) || exit;
17
18 // phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact, Generic.WhiteSpace.ScopeIndent.Incorrect
19 if ( ! defined( 'WP_REDIS_DISABLED' ) || ! WP_REDIS_DISABLED ) :
20
21 /**
22 * Determines whether the object cache implementation supports a particular feature.
23 *
24 * Possible values include:
25 * - `add_multiple`, `set_multiple`, `get_multiple` and `delete_multiple`
26 * - `flush_runtime` and `flush_group`
27 *
28 * @param string $feature Name of the feature to check for.
29 * @return bool True if the feature is supported, false otherwise.
30 */
31 function wp_cache_supports( $feature ) {
32 switch ( $feature ) {
33 case 'add_multiple':
34 case 'set_multiple':
35 case 'get_multiple':
36 case 'delete_multiple':
37 case 'flush_runtime':
38 case 'flush_group':
39 return true;
40
41 default:
42 return false;
43 }
44 }
45
46
47 /**
48 * Adds a value to cache.
49 *
50 * If the specified key already exists, the value is not stored and the function
51 * returns false.
52 *
53 * @param string $key The key under which to store the value.
54 * @param mixed $data The value to store.
55 * @param string $group The group value appended to the $key.
56 * @param int $expire The expiration time, defaults to 0.
57 *
58 * @return bool Returns TRUE on success or FALSE on failure.
59 */
60 function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
61 global $wp_object_cache;
62
63 return $wp_object_cache->add( $key, $data, $group, $expire );
64 }
65
66 /**
67 * Adds multiple values to the cache in one call.
68 *
69 * @param array $data Array of keys and values to be set.
70 * @param string $group Optional. Where the cache contents are grouped. Default empty.
71 * @param int $expire Optional. When to expire the cache contents, in seconds.
72 * Default 0 (no expiration).
73 * @return bool[] Array of return values, grouped by key. Each value is either
74 * true on success, or false if cache key and group already exist.
75 */
76 function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
77 global $wp_object_cache;
78
79 return $wp_object_cache->add_multiple( $data, $group, $expire );
80 }
81
82 /**
83 * Closes the cache.
84 *
85 * This function has ceased to do anything since WordPress 2.5. The
86 * functionality was removed along with the rest of the persistent cache. This
87 * does not mean that plugins can't implement this function when they need to
88 * make sure that the cache is cleaned up after WordPress no longer needs it.
89 *
90 * @return bool Always returns True
91 */
92 function wp_cache_close() {
93 return true;
94 }
95
96 /**
97 * Decrement a numeric item's value.
98 *
99 * @param string $key The key under which to store the value.
100 * @param int $offset The amount by which to decrement the item's value.
101 * @param string $group The group value appended to the $key.
102 *
103 * @return int|bool Returns item's new value on success or FALSE on failure.
104 */
105 function wp_cache_decr( $key, $offset = 1, $group = '' ) {
106 global $wp_object_cache;
107
108 return $wp_object_cache->decrement( $key, $offset, $group );
109 }
110
111 /**
112 * Remove the item from the cache.
113 *
114 * @param string $key The key under which to store the value.
115 * @param string $group The group value appended to the $key.
116 * @param int $time The amount of time the server will wait to delete the item in seconds.
117 *
118 * @return bool Returns TRUE on success or FALSE on failure.
119 */
120 function wp_cache_delete( $key, $group = '', $time = 0 ) {
121 global $wp_object_cache;
122
123 return $wp_object_cache->delete( $key, $group, $time );
124 }
125
126 /**
127 * Deletes multiple values from the cache in one call.
128 *
129 * @param array $keys Array of keys under which the cache to deleted.
130 * @param string $group Optional. Where the cache contents are grouped. Default empty.
131 * @return bool[] Array of return values, grouped by key. Each value is either
132 * true on success, or false if the contents were not deleted.
133 */
134 function wp_cache_delete_multiple( array $keys, $group = '' ) {
135 global $wp_object_cache;
136
137 return $wp_object_cache->delete_multiple( $keys, $group );
138 }
139
140 /**
141 * Invalidate all items in the cache. If `WP_REDIS_SELECTIVE_FLUSH` is `true`,
142 * only keys prefixed with the `WP_REDIS_PREFIX` are flushed.
143 *
144 * @return bool Returns TRUE on success or FALSE on failure.
145 */
146 function wp_cache_flush() {
147 global $wp_object_cache;
148
149 return $wp_object_cache->flush();
150 }
151
152 /**
153 * Removes all cache items in a group.
154 *
155 * @param string $group Name of group to remove from cache.
156 * @return true Returns TRUE on success or FALSE on failure.
157 */
158 function wp_cache_flush_group( $group )
159 {
160 global $wp_object_cache;
161
162 return $wp_object_cache->flush_group( $group );
163 }
164
165 /**
166 * Removes all cache items from the in-memory runtime cache.
167 *
168 * @return bool True on success, false on failure.
169 */
170 function wp_cache_flush_runtime() {
171 global $wp_object_cache;
172
173 return $wp_object_cache->flush_runtime();
174 }
175
176 /**
177 * Retrieve object from cache.
178 *
179 * Gets an object from cache based on $key and $group.
180 *
181 * @param string $key The key under which to store the value.
182 * @param string $group The group value appended to the $key.
183 * @param bool $force Optional. Whether to force an update of the local cache from the persistent
184 * cache. Default false.
185 * @param bool $found Optional. Whether the key was found in the cache. Disambiguates a return of false,
186 * a storable value. Passed by reference. Default null.
187 *
188 * @return bool|mixed Cached object value.
189 */
190 function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
191 global $wp_object_cache;
192
193 return $wp_object_cache->get( $key, $group, $force, $found );
194 }
195
196 /**
197 * Retrieves multiple values from the cache in one call.
198 *
199 * @param array $keys Array of keys under which the cache contents are stored.
200 * @param string $group Optional. Where the cache contents are grouped. Default empty.
201 * @param bool $force Optional. Whether to force an update of the local cache
202 * from the persistent cache. Default false.
203 * @return array Array of values organized into groups.
204 */
205 function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
206 global $wp_object_cache;
207
208 return $wp_object_cache->get_multiple( $keys, $group, $force );
209 }
210
211 /**
212 * Increment a numeric item's value.
213 *
214 * @param string $key The key under which to store the value.
215 * @param int $offset The amount by which to increment the item's value.
216 * @param string $group The group value appended to the $key.
217 *
218 * @return int|bool Returns item's new value on success or FALSE on failure.
219 */
220 function wp_cache_incr( $key, $offset = 1, $group = '' ) {
221 global $wp_object_cache;
222
223 return $wp_object_cache->increment( $key, $offset, $group );
224 }
225
226 /**
227 * Sets up Object Cache Global and assigns it.
228 *
229 * @return void
230 */
231 function wp_cache_init() {
232 global $wp_object_cache;
233
234 if ( ! defined( 'WP_REDIS_PREFIX' ) && getenv( 'WP_REDIS_PREFIX' ) ) {
235 define( 'WP_REDIS_PREFIX', getenv( 'WP_REDIS_PREFIX' ) );
236 }
237
238 if ( ! defined( 'WP_REDIS_SELECTIVE_FLUSH' ) && getenv( 'WP_REDIS_SELECTIVE_FLUSH' ) ) {
239 define( 'WP_REDIS_SELECTIVE_FLUSH', (bool) getenv( 'WP_REDIS_SELECTIVE_FLUSH' ) );
240 }
241
242 // Backwards compatibility: map `WP_CACHE_KEY_SALT` constant to `WP_REDIS_PREFIX`.
243 if ( defined( 'WP_CACHE_KEY_SALT' ) && ! defined( 'WP_REDIS_PREFIX' ) ) {
244 define( 'WP_REDIS_PREFIX', WP_CACHE_KEY_SALT );
245 }
246
247 // Set unique prefix for sites hosted on Cloudways
248 if ( ! defined( 'WP_REDIS_PREFIX' ) && isset( $_SERVER['cw_allowed_ip'] ) ) {
249 define( 'WP_REDIS_PREFIX', getenv( 'HTTP_X_APP_USER' ) );
250 }
251
252 if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
253 $fail_gracefully = defined( 'WP_REDIS_GRACEFUL' ) && WP_REDIS_GRACEFUL;
254
255 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
256 $wp_object_cache = new WP_Object_Cache( $fail_gracefully );
257 }
258 }
259
260 /**
261 * Replaces a value in cache.
262 *
263 * This method is similar to "add"; however, is does not successfully set a value if
264 * the object's key is not already set in cache.
265 *
266 * @param string $key The key under which to store the value.
267 * @param mixed $data The value to store.
268 * @param string $group The group value appended to the $key.
269 * @param int $expire The expiration time, defaults to 0.
270 *
271 * @return bool Returns TRUE on success or FALSE on failure.
272 */
273 function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
274 global $wp_object_cache;
275
276 return $wp_object_cache->replace( $key, $data, $group, $expire );
277 }
278
279 /**
280 * Sets a value in cache.
281 *
282 * The value is set whether or not this key already exists in Redis.
283 *
284 * @param string $key The key under which to store the value.
285 * @param mixed $data The value to store.
286 * @param string $group The group value appended to the $key.
287 * @param int $expire The expiration time, defaults to 0.
288 *
289 * @return bool Returns TRUE on success or FALSE on failure.
290 */
291 function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
292 global $wp_object_cache;
293
294 return $wp_object_cache->set( $key, $data, $group, $expire );
295 }
296
297 /**
298 * Sets multiple values to the cache in one call.
299 *
300 * @param array $data Array of keys and values to be set.
301 * @param string $group Optional. Where the cache contents are grouped. Default empty.
302 * @param int $expire Optional. When to expire the cache contents, in seconds.
303 * Default 0 (no expiration).
304 * @return bool[] Array of return values, grouped by key. Each value is either
305 * true on success, or false on failure.
306 */
307 function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
308 global $wp_object_cache;
309
310 return $wp_object_cache->set_multiple( $data, $group, $expire );
311 }
312
313 /**
314 * Switch the internal blog id.
315 *
316 * This changes the blog id used to create keys in blog specific groups.
317 *
318 * @param int $blog_id The blog ID.
319 *
320 * @return bool
321 */
322 function wp_cache_switch_to_blog( $blog_id ) {
323 global $wp_object_cache;
324
325 return $wp_object_cache->switch_to_blog( $blog_id );
326 }
327
328 /**
329 * Adds a group or set of groups to the list of Redis groups.
330 *
331 * @param string|array $groups A group or an array of groups to add.
332 *
333 * @return void
334 */
335 function wp_cache_add_global_groups( $groups ) {
336 global $wp_object_cache;
337
338 $wp_object_cache->add_global_groups( $groups );
339 }
340
341 /**
342 * Adds a group or set of groups to the list of non-Redis groups.
343 *
344 * @param string|array $groups A group or an array of groups to add.
345 *
346 * @return void
347 */
348 function wp_cache_add_non_persistent_groups( $groups ) {
349 global $wp_object_cache;
350
351 $wp_object_cache->add_non_persistent_groups( $groups );
352 }
353
354 /**
355 * Object cache class definition
356 */
357 #[AllowDynamicProperties]
358 class WP_Object_Cache {
359 /**
360 * The Redis client.
361 *
362 * @var mixed
363 */
364 private $redis;
365
366 /**
367 * The Redis server version.
368 *
369 * @var null|string
370 */
371 private $redis_version = null;
372
373 /**
374 * Track if Redis is available.
375 *
376 * @var bool
377 */
378 private $redis_connected = false;
379
380 /**
381 * Check to fail gracefully or throw an exception.
382 *
383 * @var bool
384 */
385 private $fail_gracefully = true;
386
387 /**
388 * Whether to use igbinary serialization.
389 *
390 * @var bool
391 */
392 private $use_igbinary = false;
393
394 /**
395 * Holds the non-Redis objects.
396 *
397 * @var array
398 */
399 public $cache = [];
400
401 /**
402 * Holds the diagnostics values.
403 *
404 * @var array
405 */
406 public $diagnostics = null;
407
408 /**
409 * Holds the error messages.
410 *
411 * @var array
412 */
413 public $errors = [];
414
415 /**
416 * List of global groups.
417 *
418 * @var array<string>
419 */
420 public $global_groups = [
421 'blog-details',
422 'blog-id-cache',
423 'blog-lookup',
424 'global-posts',
425 'networks',
426 'rss',
427 'sites',
428 'site-details',
429 'site-lookup',
430 'site-options',
431 'site-transient',
432 'users',
433 'useremail',
434 'userlogins',
435 'usermeta',
436 'user_meta',
437 'userslugs',
438 ];
439
440 /**
441 * List of groups that will not be flushed.
442 *
443 * @var array
444 */
445 public $unflushable_groups = [];
446
447 /**
448 * List of groups not saved to Redis.
449 *
450 * @var array
451 */
452 public $ignored_groups = [];
453
454 /**
455 * List of groups and their types.
456 *
457 * @var array
458 */
459 public $group_type = [];
460
461 /**
462 * Prefix used for global groups.
463 *
464 * @var string
465 */
466 public $global_prefix = '';
467
468 /**
469 * Prefix used for non-global groups.
470 *
471 * @var int
472 */
473 public $blog_prefix = 0;
474
475 /**
476 * Track how many requests were found in cache.
477 *
478 * @var int
479 */
480 public $cache_hits = 0;
481
482 /**
483 * Track how may requests were not cached.
484 *
485 * @var int
486 */
487 public $cache_misses = 0;
488
489 /**
490 * The amount of Redis commands made.
491 *
492 * @var int
493 */
494 public $cache_calls = 0;
495
496 /**
497 * The amount of microseconds (μs) waited for Redis commands.
498 *
499 * @var float
500 */
501 public $cache_time = 0;
502
503 /**
504 * Instantiate the Redis class.
505 *
506 * @param bool $fail_gracefully Handles and logs errors if true throws exceptions otherwise.
507 */
508 public function __construct( $fail_gracefully = false ) {
509 global $blog_id, $table_prefix;
510
511 $this->fail_gracefully = $fail_gracefully;
512
513 if ( defined( 'WP_REDIS_GLOBAL_GROUPS' ) && is_array( WP_REDIS_GLOBAL_GROUPS ) ) {
514 $this->global_groups = array_map( [ $this, 'sanitize_key_part' ], WP_REDIS_GLOBAL_GROUPS );
515 }
516
517 $this->global_groups[] = 'redis-cache';
518
519 if ( defined( 'WP_REDIS_IGNORED_GROUPS' ) && is_array( WP_REDIS_IGNORED_GROUPS ) ) {
520 $this->ignored_groups = array_map( [ $this, 'sanitize_key_part' ], WP_REDIS_IGNORED_GROUPS );
521 }
522
523 if ( defined( 'WP_REDIS_UNFLUSHABLE_GROUPS' ) && is_array( WP_REDIS_UNFLUSHABLE_GROUPS ) ) {
524 $this->unflushable_groups = array_map( [ $this, 'sanitize_key_part' ], WP_REDIS_UNFLUSHABLE_GROUPS );
525 }
526
527 $this->cache_group_types();
528
529 $this->use_igbinary = defined( 'WP_REDIS_IGBINARY' ) && WP_REDIS_IGBINARY && extension_loaded( 'igbinary' );
530
531 $client = $this->determine_client();
532 $parameters = $this->build_parameters();
533
534 try {
535 switch ( $client ) {
536 case 'phpredis':
537 $this->connect_using_phpredis( $parameters );
538 break;
539 case 'relay':
540 $this->connect_using_relay( $parameters );
541 break;
542 case 'credis':
543 $this->connect_using_credis( $parameters );
544 break;
545 case 'predis':
546 default:
547 $this->connect_using_predis( $parameters );
548 break;
549 }
550
551 if ( defined( 'WP_REDIS_CLUSTER' ) ) {
552 $connectionId = is_string( WP_REDIS_CLUSTER )
553 ? WP_REDIS_CLUSTER
554 : current( $this->build_cluster_connection_array() );
555
556 $this->diagnostics[ 'ping' ] = $client === 'predis'
557 ? $this->redis->getClientBy( 'id', $connectionId )->ping()
558 : $this->redis->ping( $connectionId );
559 } else {
560 $this->diagnostics[ 'ping' ] = $this->redis->ping();
561 }
562
563 $this->fetch_info();
564
565 $this->redis_connected = true;
566 } catch ( Exception $exception ) {
567 $this->handle_exception( $exception );
568 }
569
570 // Assign global and blog prefixes for use with keys.
571 if ( function_exists( 'is_multisite' ) ) {
572 $this->global_prefix = is_multisite() ? '' : $table_prefix;
573 $this->blog_prefix = is_multisite() ? $blog_id : $table_prefix;
574 }
575 }
576
577 /**
578 * Set group type array
579 *
580 * @return void
581 */
582 protected function cache_group_types() {
583 foreach ( $this->global_groups as $group ) {
584 $this->group_type[ $group ] = 'global';
585 }
586
587 foreach ( $this->unflushable_groups as $group ) {
588 $this->group_type[ $group ] = 'unflushable';
589 }
590
591 foreach ( $this->ignored_groups as $group ) {
592 $this->group_type[ $group ] = 'ignored';
593 }
594 }
595
596 /**
597 * Determine the Redis client.
598 *
599 * @return string
600 */
601 protected function determine_client() {
602 $client = 'predis';
603
604 if ( class_exists( 'Redis' ) ) {
605 $client = 'phpredis';
606 }
607
608 if ( defined( 'WP_REDIS_CLIENT' ) ) {
609 $client = (string) WP_REDIS_CLIENT;
610 $client = str_replace( 'pecl', 'phpredis', $client );
611 }
612
613 return trim( strtolower( $client ) );
614 }
615
616 /**
617 * Build the connection parameters from config constants.
618 *
619 * @return array
620 */
621 protected function build_parameters() {
622 $parameters = [
623 'scheme' => 'tcp',
624 'host' => '127.0.0.1',
625 'port' => 6379,
626 'database' => 0,
627 'timeout' => 1,
628 'read_timeout' => 1,
629 'retry_interval' => null,
630 'persistent' => false,
631 ];
632
633 $settings = [
634 'scheme',
635 'host',
636 'port',
637 'path',
638 'password',
639 'database',
640 'timeout',
641 'read_timeout',
642 'retry_interval',
643 ];
644
645 foreach ( $settings as $setting ) {
646 $constant = sprintf( 'WP_REDIS_%s', strtoupper( $setting ) );
647
648 if ( defined( $constant ) ) {
649 $parameters[ $setting ] = constant( $constant );
650 }
651 }
652
653 if ( isset( $parameters[ 'password' ] ) && $parameters[ 'password' ] === '' ) {
654 unset( $parameters[ 'password' ] );
655 }
656
657 $this->diagnostics[ 'timeout' ] = $parameters[ 'timeout' ];
658 $this->diagnostics[ 'read_timeout' ] = $parameters[ 'read_timeout' ];
659 $this->diagnostics[ 'retry_interval' ] = $parameters[ 'retry_interval' ];
660
661 return $parameters;
662 }
663
664 /**
665 * Connect to Redis using the PhpRedis (PECL) extension.
666 *
667 * @param array $parameters Connection parameters built by the `build_parameters` method.
668 * @return void
669 */
670 protected function connect_using_phpredis( $parameters ) {
671 $version = phpversion( 'redis' );
672
673 $this->diagnostics[ 'client' ] = sprintf( 'PhpRedis (v%s)', $version );
674
675 if ( defined( 'WP_REDIS_SHARDS' ) ) {
676 $this->redis = new RedisArray( array_values( WP_REDIS_SHARDS ) );
677
678 $this->diagnostics[ 'shards' ] = WP_REDIS_SHARDS;
679 } elseif ( defined( 'WP_REDIS_CLUSTER' ) ) {
680 if ( is_string( WP_REDIS_CLUSTER ) ) {
681 $this->redis = new RedisCluster( WP_REDIS_CLUSTER );
682 } else {
683 $args = [
684 'cluster' => $this->build_cluster_connection_array(),
685 'timeout' => $parameters['timeout'],
686 'read_timeout' => $parameters['read_timeout'],
687 'persistent' => $parameters['persistent'],
688 ];
689
690 if ( isset( $parameters['password'] ) && version_compare( $version, '4.3.0', '>=' ) ) {
691 $args['password'] = $parameters['password'];
692 }
693
694 if ( version_compare( $version, '5.3.0', '>=' ) && defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) {
695 if ( ! array_key_exists( 'password', $args ) ) {
696 $args['password'] = null;
697 }
698
699 $args['ssl'] = WP_REDIS_SSL_CONTEXT;
700 }
701
702 $this->redis = new RedisCluster( null, ...array_values( $args ) );
703 $this->diagnostics += $args;
704 }
705 } else {
706 $this->redis = new Redis();
707
708 $args = [
709 'host' => $parameters['host'],
710 'port' => $parameters['port'],
711 'timeout' => $parameters['timeout'],
712 '',
713 'retry_interval' => (int) $parameters['retry_interval'],
714 ];
715
716 if ( version_compare( $version, '3.1.3', '>=' ) ) {
717 $args['read_timeout'] = $parameters['read_timeout'];
718 }
719
720 if ( strcasecmp( 'tls', $parameters['scheme'] ) === 0 ) {
721 $args['host'] = sprintf(
722 '%s://%s',
723 $parameters['scheme'],
724 str_replace( 'tls://', '', $parameters['host'] )
725 );
726
727 if ( version_compare( $version, '5.3.0', '>=' ) && defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) {
728 $args['others']['stream'] = WP_REDIS_SSL_CONTEXT;
729 }
730 }
731
732 if ( strcasecmp( 'unix', $parameters['scheme'] ) === 0 ) {
733 $args['host'] = $parameters['path'];
734 $args['port'] = -1;
735 }
736
737 call_user_func_array( [ $this->redis, 'connect' ], array_values( $args ) );
738
739 if ( isset( $parameters['password'] ) ) {
740 $args['password'] = $parameters['password'];
741 $this->redis->auth( $parameters['password'] );
742 }
743
744 if ( isset( $parameters['database'] ) ) {
745 if ( ctype_digit( (string) $parameters['database'] ) ) {
746 $parameters['database'] = (int) $parameters['database'];
747 }
748
749 $args['database'] = $parameters['database'];
750
751 if ( $parameters['database'] ) {
752 $this->redis->select( $parameters['database'] );
753 }
754 }
755
756 $this->diagnostics += $args;
757 }
758 }
759
760 /**
761 * Connect to Redis using the Relay extension.
762 *
763 * @param array $parameters Connection parameters built by the `build_parameters` method.
764 * @return void
765 */
766 protected function connect_using_relay( $parameters ) {
767 $version = phpversion( 'relay' );
768
769 $this->diagnostics[ 'client' ] = sprintf( 'Relay (v%s)', $version );
770
771 if ( defined( 'WP_REDIS_SHARDS' ) ) {
772 throw new Exception('Relay does not support sharding.');
773 } elseif ( defined( 'WP_REDIS_CLUSTER' ) ) {
774 throw new Exception('Relay does not cluster connections.');
775 } else {
776 $this->redis = new Relay\Relay;
777
778 $args = [
779 'host' => $parameters['host'],
780 'port' => $parameters['port'],
781 'timeout' => $parameters['timeout'],
782 '',
783 'retry_interval' => (int) $parameters['retry_interval'],
784 ];
785
786 $args['read_timeout'] = $parameters['read_timeout'];
787
788 if ( strcasecmp( 'tls', $parameters['scheme'] ) === 0 ) {
789 $args['host'] = sprintf(
790 '%s://%s',
791 $parameters['scheme'],
792 str_replace( 'tls://', '', $parameters['host'] )
793 );
794
795 if ( defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) {
796 $args['others']['stream'] = WP_REDIS_SSL_CONTEXT;
797 }
798 }
799
800 if ( strcasecmp( 'unix', $parameters['scheme'] ) === 0 ) {
801 $args['host'] = $parameters['path'];
802 $args['port'] = -1;
803 }
804
805 call_user_func_array( [ $this->redis, 'connect' ], array_values( $args ) );
806
807 if ( isset( $parameters['password'] ) ) {
808 $args['password'] = $parameters['password'];
809 $this->redis->auth( $parameters['password'] );
810 }
811
812 if ( isset( $parameters['database'] ) ) {
813 if ( ctype_digit( (string) $parameters['database'] ) ) {
814 $parameters['database'] = (int) $parameters['database'];
815 }
816
817 $args['database'] = $parameters['database'];
818
819 if ( $parameters['database'] ) {
820 $this->redis->select( $parameters['database'] );
821 }
822 }
823
824 $this->diagnostics += $args;
825 }
826 }
827
828 /**
829 * Connect to Redis using the Predis library.
830 *
831 * @param array $parameters Connection parameters built by the `build_parameters` method.
832 * @throws \Exception If the Predis library was not found or is unreadable.
833 * @return void
834 */
835 protected function connect_using_predis( $parameters ) {
836 $client = 'Predis';
837
838 // Load bundled Predis library.
839 if ( ! class_exists( 'Predis\Client' ) ) {
840 $predis = '/dependencies/predis/predis/autoload.php';
841
842 $pluginDir = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR . '/redis-cache' : null;
843 $contentDir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR . '/plugins/redis-cache' : null;
844 $pluginPath = defined( 'WP_REDIS_PLUGIN_PATH' ) ? WP_REDIS_PLUGIN_PATH : null;
845
846 if ( $pluginDir && is_readable( $pluginDir . $predis ) ) {
847 require_once $pluginDir . $predis;
848 } elseif ( $contentDir && is_readable( $contentDir . $predis ) ) {
849 require_once $contentDir . $predis;
850 } elseif ( $pluginPath && is_readable( $pluginPath . $predis ) ) {
851 require_once $pluginPath . $predis;
852 } else {
853 throw new Exception(
854 'Predis library not found. Re-install Redis Cache plugin or delete the object-cache.php.'
855 );
856 }
857 }
858
859 $servers = false;
860 $options = [];
861
862 if ( defined( 'WP_REDIS_SHARDS' ) ) {
863 $servers = WP_REDIS_SHARDS;
864 $parameters['shards'] = $servers;
865 } elseif ( defined( 'WP_REDIS_SENTINEL' ) ) {
866 $servers = WP_REDIS_SERVERS;
867 $parameters['servers'] = $servers;
868 $options['replication'] = 'sentinel';
869 $options['service'] = WP_REDIS_SENTINEL;
870 } elseif ( defined( 'WP_REDIS_SERVERS' ) ) {
871 $servers = WP_REDIS_SERVERS;
872 $parameters['servers'] = $servers;
873 $options['replication'] = 'predis';
874 } elseif ( defined( 'WP_REDIS_CLUSTER' ) ) {
875 $servers = $this->build_cluster_connection_array();
876 $parameters['cluster'] = $servers;
877 $options['cluster'] = 'redis';
878 }
879
880 if ( strcasecmp( 'unix', $parameters['scheme'] ) === 0 ) {
881 unset($parameters['host'], $parameters['port']);
882 }
883
884 if ( isset( $parameters['read_timeout'] ) && $parameters['read_timeout'] ) {
885 $parameters['read_write_timeout'] = $parameters['read_timeout'];
886 }
887
888 foreach ( [ 'WP_REDIS_SERVERS', 'WP_REDIS_SHARDS', 'WP_REDIS_CLUSTER' ] as $constant ) {
889 if ( defined( $constant ) ) {
890 if ( $parameters['database'] ) {
891 $options['parameters']['database'] = $parameters['database'];
892 }
893
894 if ( isset( $parameters['password'] ) ) {
895 if ( is_array( $parameters['password'] ) ) {
896 $options['parameters']['username'] = WP_REDIS_PASSWORD[0];
897 $options['parameters']['password'] = WP_REDIS_PASSWORD[1];
898 } else {
899 $options['parameters']['password'] = WP_REDIS_PASSWORD;
900 }
901 }
902 }
903 }
904
905 if ( isset( $parameters['password'] ) ) {
906 if ( is_array( $parameters['password'] ) ) {
907 $parameters['username'] = array_shift( $parameters['password'] );
908 $parameters['password'] = implode( '', $parameters['password'] );
909 }
910
911 if ( defined( 'WP_REDIS_USERNAME' ) ) {
912 $parameters['username'] = WP_REDIS_USERNAME;
913 }
914 }
915
916 if ( defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) {
917 $parameters['ssl'] = WP_REDIS_SSL_CONTEXT;
918 }
919
920 $this->redis = new Predis\Client( $servers ?: $parameters, $options );
921 $this->redis->connect();
922
923 $this->diagnostics = array_merge(
924 [ 'client' => sprintf( '%s (v%s)', $client, Predis\Client::VERSION ) ],
925 $parameters,
926 $options
927 );
928 }
929
930 /**
931 * Connect to Redis using the Credis library.
932 *
933 * @param array $parameters Connection parameters built by the `build_parameters` method.
934 * @throws \Exception If the Credis library was not found or is unreadable.
935 * @throws \Exception If redis sharding should be configured as Credis does not support sharding.
936 * @throws \Exception If more than one sentinel is configured as Credis does not support multiple sentinel servers.
937 * @return void
938 */
939 protected function connect_using_credis( $parameters ) {
940 trigger_error( 'Credis support is deprecated and will be removed in the future', E_USER_DEPRECATED );
941
942 $client = 'Credis';
943
944 $creds_path = sprintf(
945 '%s/redis-cache/dependencies/colinmollenhour/credis/',
946 defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins'
947 );
948
949 $to_load = [];
950
951 if ( ! class_exists( 'Credis_Client' ) ) {
952 $to_load[] = 'Client.php';
953 }
954
955 $has_shards = defined( 'WP_REDIS_SHARDS' );
956 $has_sentinel = defined( 'WP_REDIS_SENTINEL' );
957 $has_servers = defined( 'WP_REDIS_SERVERS' );
958 $has_cluster = defined( 'WP_REDIS_CLUSTER' );
959
960 if ( ( $has_shards || $has_sentinel || $has_servers || $has_cluster ) && ! class_exists( 'Credis_Cluster' ) ) {
961 $to_load[] = 'Cluster.php';
962
963 if ( defined( 'WP_REDIS_SENTINEL' ) && ! class_exists( 'Credis_Sentinel' ) ) {
964 $to_load[] = 'Sentinel.php';
965 }
966 }
967
968 foreach ( $to_load as $sub_path ) {
969 $path = $creds_path . $sub_path;
970
971 if ( file_exists( $path ) ) {
972 require_once $path;
973 } else {
974 throw new Exception(
975 'Credis library not found. Re-install Redis Cache plugin or delete object-cache.php.'
976 );
977 }
978 }
979
980 if ( defined( 'WP_REDIS_SHARDS' ) ) {
981 throw new Exception(
982 'Sharding not supported by bundled Credis library. Please review your Redis Cache configuration.'
983 );
984 }
985
986 if ( defined( 'WP_REDIS_SENTINEL' ) ) {
987 if ( is_array( WP_REDIS_SERVERS ) && count( WP_REDIS_SERVERS ) > 1 ) {
988 throw new Exception(
989 'Multiple sentinel servers are not supported by the bundled Credis library. Please review your Redis Cache configuration.'
990 );
991 }
992
993 $connection_string = array_values( WP_REDIS_SERVERS )[0];
994 $sentinel = new Credis_Sentinel( new Credis_Client( $connection_string ) );
995 $this->redis = $sentinel->getCluster( WP_REDIS_SENTINEL );
996 $args['servers'] = WP_REDIS_SERVERS;
997 } elseif ( defined( 'WP_REDIS_CLUSTER' ) || defined( 'WP_REDIS_SERVERS' ) ) {
998 $parameters['db'] = $parameters['database'];
999
1000 $is_cluster = defined( 'WP_REDIS_CLUSTER' );
1001 $clients = $is_cluster ? WP_REDIS_CLUSTER : WP_REDIS_SERVERS;
1002
1003 foreach ( $clients as $index => $connection_string ) {
1004 // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
1005 $url_components = parse_url( $connection_string );
1006
1007 if ( isset( $url_components['query'] ) ) {
1008 parse_str( $url_components['query'], $add_params );
1009 }
1010
1011 if ( ! $is_cluster && isset( $add_params['alias'] ) ) {
1012 $add_params['master'] = 'master' === $add_params['alias'];
1013 }
1014
1015 $add_params['host'] = $url_components['host'];
1016 $add_params['port'] = $url_components['port'];
1017
1018 if ( ! isset( $add_params['alias'] ) ) {
1019 $add_params['alias'] = "redis-$index";
1020 }
1021
1022 $clients[ $index ] = array_merge( $parameters, $add_params );
1023
1024 unset($add_params);
1025 }
1026
1027 $this->redis = new Credis_Cluster( $clients );
1028
1029 foreach ( $clients as $index => $_client ) {
1030 $connection_string = "{$_client['scheme']}://{$_client['host']}:{$_client['port']}";
1031 unset( $_client['scheme'], $_client['host'], $_client['port'] );
1032
1033 $params = array_filter( $_client );
1034
1035 if ( $params ) {
1036 $connection_string .= '?' . http_build_query( $params, '', '&' );
1037 }
1038
1039 $clients[ $index ] = $connection_string;
1040 }
1041
1042 $args['servers'] = $clients;
1043 } else {
1044 $args = [
1045 'host' => $parameters['scheme'] === 'unix' ? $parameters['path'] : $parameters['host'],
1046 'port' => $parameters['port'],
1047 'timeout' => $parameters['timeout'],
1048 'persistent' => '',
1049 'database' => $parameters['database'],
1050 'password' => isset( $parameters['password'] ) ? $parameters['password'] : null,
1051 ];
1052
1053 $this->redis = new Credis_Client( ...array_values( $args ) );
1054 }
1055
1056 // Don't use PhpRedis if it is available.
1057 $this->redis->forceStandalone();
1058
1059 $this->redis->connect();
1060
1061 if ( $parameters['read_timeout'] ) {
1062 $args['read_timeout'] = $parameters['read_timeout'];
1063 $this->redis->setReadTimeout( $parameters['read_timeout'] );
1064 }
1065
1066 $this->diagnostics = array_merge(
1067 [ 'client' => sprintf( '%s (%s)', $client, 'bundled' ) ],
1068 $args
1069 );
1070 }
1071
1072 /**
1073 * Fetches Redis `INFO` mostly for server version.
1074 *
1075 * @return void
1076 */
1077 public function fetch_info() {
1078 if ( defined( 'WP_REDIS_CLUSTER' ) ) {
1079 $connectionId = is_string( WP_REDIS_CLUSTER )
1080 ? 'SERVER'
1081 : current( $this->build_cluster_connection_array() );
1082
1083 $info = $this->is_predis()
1084 ? $this->redis->getClientBy( 'id', $connectionId )->info()
1085 : $this->redis->info( $connectionId );
1086 } else if ($this->is_predis() && $this->redis->getConnection() instanceof Predis\Connection\Replication\MasterSlaveReplication) {
1087 $info = $this->redis->getClientBy( 'role' , 'master' )->info();
1088 } else {
1089 if ( $this->is_predis() ) {
1090 $connection = $this->redis->getConnection();
1091 if ( $connection instanceof Predis\Connection\Replication\ReplicationInterface ) {
1092 $node = $connection->getCurrent();
1093 $connection->switchToMaster();
1094 }
1095 }
1096
1097 $info = $this->redis->info();
1098
1099 if ( isset( $connection, $node ) ) {
1100 $connection->switchTo($node);
1101 }
1102 }
1103
1104 if ( isset( $info['redis_version'] ) ) {
1105 $this->redis_version = $info['redis_version'];
1106 } elseif ( isset( $info['Server']['redis_version'] ) ) {
1107 $this->redis_version = $info['Server']['redis_version'];
1108 }
1109 }
1110
1111 /**
1112 * Is Redis available?
1113 *
1114 * @return bool
1115 */
1116 public function redis_status() {
1117 return (bool) $this->redis_connected;
1118 }
1119
1120 /**
1121 * Returns the Redis instance.
1122 *
1123 * @return mixed
1124 */
1125 public function redis_instance() {
1126 return $this->redis;
1127 }
1128
1129 /**
1130 * Returns the Redis server version.
1131 *
1132 * @return null|string
1133 */
1134 public function redis_version() {
1135 return $this->redis_version;
1136 }
1137
1138 /**
1139 * Adds a value to cache.
1140 *
1141 * If the specified key already exists, the value is not stored and the function
1142 * returns false.
1143 *
1144 * @param string $key The key under which to store the value.
1145 * @param mixed $value The value to store.
1146 * @param string $group The group value appended to the $key.
1147 * @param int $expiration The expiration time, defaults to 0.
1148 * @return bool Returns TRUE on success or FALSE on failure.
1149 */
1150 public function add( $key, $value, $group = 'default', $expiration = 0 ) {
1151 return $this->add_or_replace( true, $key, $value, $group, $expiration );
1152 }
1153
1154 /**
1155 * Adds multiple values to the cache in one call.
1156 *
1157 * @param array $data Array of keys and values to be added.
1158 * @param string $group Optional. Where the cache contents are grouped.
1159 * @param int $expire Optional. When to expire the cache contents, in seconds.
1160 * Default 0 (no expiration).
1161 * @return bool[] Array of return values, grouped by key. Each value is either
1162 * true on success, or false if cache key and group already exist.
1163 */
1164 public function add_multiple( array $data, $group = 'default', $expire = 0 ) {
1165 if ( function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition() ) {
1166 return array_combine( array_keys( $data ), array_fill( 0, count( $data ), false ) );
1167 }
1168
1169 if (
1170 $this->redis_status() &&
1171 method_exists( $this->redis, 'pipeline' ) &&
1172 ! $this->is_ignored_group( $group )
1173 ) {
1174 return $this->add_multiple_at_once( $data, $group, $expire );
1175 }
1176
1177 $values = [];
1178
1179 foreach ( $data as $key => $value ) {
1180 $values[ $key ] = $this->add( $key, $value, $group, $expire );
1181 }
1182
1183 return $values;
1184 }
1185
1186 /**
1187 * Adds multiple values to the cache in one call.
1188 *
1189 * @param array $data Array of keys and values to be added.
1190 * @param string $group Optional. Where the cache contents are grouped.
1191 * @param int $expire Optional. When to expire the cache contents, in seconds.
1192 * Default 0 (no expiration).
1193 * @return bool[] Array of return values, grouped by key. Each value is either
1194 * true on success, or false if cache key and group already exist.
1195 */
1196 protected function add_multiple_at_once( array $data, $group = 'default', $expire = 0 ) {
1197 $keys = array_keys( $data );
1198
1199 $san_group = $this->sanitize_key_part( $group );
1200
1201 $tx = $this->redis->pipeline();
1202
1203 $orig_exp = $expire;
1204 $expire = $this->validate_expiration( $expire );
1205 $derived_keys = [];
1206
1207 foreach ( $data as $key => $value ) {
1208 /**
1209 * Filters the cache expiration time
1210 *
1211 * @param int $expiration The time in seconds the entry expires. 0 for no expiry.
1212 * @param string $key The cache key.
1213 * @param string $group The cache group.
1214 * @param mixed $orig_exp The original expiration value before validation.
1215 */
1216 $expire = apply_filters( 'redis_cache_expiration', $expire, $key, $group, $orig_exp );
1217
1218 $san_key = $this->sanitize_key_part( $key );
1219 $derived_key = $derived_keys[ $key ] = $this->fast_build_key( $san_key, $san_group );
1220
1221 $args = [ $derived_key, $this->maybe_serialize( $value ) ];
1222
1223 if ( $this->is_predis() ) {
1224 $args[] = 'nx';
1225
1226 if ( $expire ) {
1227 $args[] = 'ex';
1228 $args[] = $expire;
1229 }
1230 } else {
1231 if ( $expire ) {
1232 $args[] = [ 'nx', 'ex' => $expire ];
1233 } else {
1234 $args[] = [ 'nx' ];
1235 }
1236 }
1237
1238 $tx->set( ...$args );
1239 }
1240
1241 try {
1242 $start_time = microtime( true );
1243
1244 $method = $this->is_predis() ? 'execute' : 'exec';
1245
1246 $results = array_map( function ( $response ) {
1247 return (bool) $this->parse_redis_response( $response );
1248 }, $tx->{$method}() ?: [] );
1249
1250 if ( count( $results ) !== count( $keys ) ) {
1251 $tx->discard();
1252
1253 return array_fill_keys( $keys, false );
1254 }
1255
1256 $results = array_combine( $keys, $results );
1257
1258 foreach ( $results as $key => $result ) {
1259 if ( $result ) {
1260 $this->add_to_internal_cache( $derived_keys[ $key ], $data[ $key ] );
1261 }
1262 }
1263
1264 $execute_time = microtime( true ) - $start_time;
1265
1266 $this->cache_calls++;
1267 $this->cache_time += $execute_time;
1268 } catch ( Exception $exception ) {
1269 $this->handle_exception( $exception );
1270
1271 return array_combine( $keys, array_fill( 0, count( $keys ), false ) );
1272 }
1273
1274 return $results;
1275 }
1276
1277 /**
1278 * Replace a value in the cache.
1279 *
1280 * If the specified key doesn't exist, the value is not stored and the function
1281 * returns false.
1282 *
1283 * @param string $key The key under which to store the value.
1284 * @param mixed $value The value to store.
1285 * @param string $group The group value appended to the $key.
1286 * @param int $expiration The expiration time, defaults to 0.
1287 * @return bool Returns TRUE on success or FALSE on failure.
1288 */
1289 public function replace( $key, $value, $group = 'default', $expiration = 0 ) {
1290 return $this->add_or_replace( false, $key, $value, $group, $expiration );
1291 }
1292
1293 /**
1294 * Add or replace a value in the cache.
1295 *
1296 * Add does not set the value if the key exists; replace does not replace if the value doesn't exist.
1297 *
1298 * @param bool $add True if should only add if value doesn't exist, false to only add when value already exists.
1299 * @param string $key The key under which to store the value.
1300 * @param mixed $value The value to store.
1301 * @param string $group The group value appended to the $key.
1302 * @param int $expiration The expiration time, defaults to 0.
1303 * @return bool Returns TRUE on success or FALSE on failure.
1304 */
1305 protected function add_or_replace( $add, $key, $value, $group = 'default', $expiration = 0 ) {
1306 $cache_addition_suspended = function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition();
1307
1308 if ( $add && $cache_addition_suspended ) {
1309 return false;
1310 }
1311
1312 $result = true;
1313
1314 $san_key = $this->sanitize_key_part( $key );
1315 $san_group = $this->sanitize_key_part( $group );
1316
1317 $derived_key = $this->fast_build_key( $san_key, $san_group );
1318
1319 // Save if group not excluded and redis is up.
1320 if ( ! $this->is_ignored_group( $san_group ) && $this->redis_status() ) {
1321 try {
1322 $orig_exp = $expiration;
1323 $expiration = $this->validate_expiration( $expiration );
1324
1325 /**
1326 * Filters the cache expiration time
1327 *
1328 * @since 1.4.2
1329 * @param int $expiration The time in seconds the entry expires. 0 for no expiry.
1330 * @param string $key The cache key.
1331 * @param string $group The cache group.
1332 * @param mixed $orig_exp The original expiration value before validation.
1333 */
1334 $expiration = apply_filters( 'redis_cache_expiration', $expiration, $key, $group, $orig_exp );
1335 $start_time = microtime( true );
1336
1337 if ( $add ) {
1338 $args = [ $derived_key, $this->maybe_serialize( $value ) ];
1339
1340 if ( $this->is_predis() ) {
1341 $args[] = 'nx';
1342
1343 if ( $expiration ) {
1344 $args[] = 'ex';
1345 $args[] = $expiration;
1346 }
1347 } else {
1348 if ( $expiration ) {
1349 $args[] = [
1350 'nx',
1351 'ex' => $expiration,
1352 ];
1353 } else {
1354 $args[] = [ 'nx' ];
1355 }
1356 }
1357
1358 $result = $this->parse_redis_response(
1359 $this->redis->set( ...$args )
1360 );
1361
1362 if ( ! $result ) {
1363 return false;
1364 }
1365 } elseif ( $expiration ) {
1366 $result = $this->parse_redis_response( $this->redis->setex( $derived_key, $expiration, $this->maybe_serialize( $value ) ) );
1367 } else {
1368 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $this->maybe_serialize( $value ) ) );
1369 }
1370
1371 $execute_time = microtime( true ) - $start_time;
1372
1373 $this->cache_calls++;
1374 $this->cache_time += $execute_time;
1375 } catch ( Exception $exception ) {
1376 $this->handle_exception( $exception );
1377
1378 return false;
1379 }
1380 }
1381
1382 $exists = array_key_exists( $derived_key, $this->cache );
1383
1384 if ( (bool) $add === $exists ) {
1385 return false;
1386 }
1387
1388 if ( $result ) {
1389 $this->add_to_internal_cache( $derived_key, $value );
1390 }
1391
1392 return $result;
1393 }
1394
1395 /**
1396 * Remove the item from the cache.
1397 *
1398 * @param string $key The key under which to store the value.
1399 * @param string $group The group value appended to the $key.
1400 * @return bool Returns TRUE on success or FALSE on failure.
1401 */
1402 public function delete( $key, $group = 'default', $deprecated = false ) {
1403 $result = false;
1404
1405 $san_key = $this->sanitize_key_part( $key );
1406 $san_group = $this->sanitize_key_part( $group );
1407
1408 $derived_key = $this->fast_build_key( $san_key, $san_group );
1409
1410 if ( array_key_exists( $derived_key, $this->cache ) ) {
1411 unset( $this->cache[ $derived_key ] );
1412 $result = true;
1413 }
1414
1415 $start_time = microtime( true );
1416
1417 if ( $this->redis_status() && ! $this->is_ignored_group( $san_group ) ) {
1418 try {
1419 $result = $this->parse_redis_response( $this->redis->del( $derived_key ) );
1420 } catch ( Exception $exception ) {
1421 $this->handle_exception( $exception );
1422
1423 return false;
1424 }
1425 }
1426
1427 $execute_time = microtime( true ) - $start_time;
1428
1429 $this->cache_calls++;
1430 $this->cache_time += $execute_time;
1431
1432 if ( function_exists( 'do_action' ) ) {
1433 /**
1434 * Fires on every cache key deletion
1435 *
1436 * @since 1.3.3
1437 * @param string $key The cache key.
1438 * @param string $group The group value appended to the $key.
1439 * @param float $execute_time Execution time for the request in seconds.
1440 */
1441 do_action( 'redis_object_cache_delete', $key, $group, $execute_time );
1442 }
1443
1444 return (bool) $result;
1445 }
1446
1447 /**
1448 * Deletes multiple values from the cache in one call.
1449 *
1450 * @param array $keys Array of keys to be deleted.
1451 * @param string $group Optional. Where the cache contents are grouped.
1452 * @return bool[] Array of return values, grouped by key. Each value is either
1453 * true on success, or false if the contents were not deleted.
1454 */
1455 public function delete_multiple( array $keys, $group = 'default' ) {
1456 if (
1457 $this->redis_status() &&
1458 method_exists( $this->redis, 'pipeline' ) &&
1459 ! $this->is_ignored_group( $group )
1460 ) {
1461 return $this->delete_multiple_at_once( $keys, $group );
1462 }
1463
1464 $values = [];
1465
1466 foreach ( $keys as $key ) {
1467 $values[ $key ] = $this->delete( $key, $group );
1468 }
1469
1470 return $values;
1471 }
1472
1473 /**
1474 * Deletes multiple values from the cache in one call.
1475 *
1476 * @param array $keys Array of keys to be deleted.
1477 * @param string $group Optional. Where the cache contents are grouped.
1478 * @return bool[] Array of return values, grouped by key. Each value is either
1479 * true on success, or false if the contents were not deleted.
1480 */
1481 protected function delete_multiple_at_once( array $keys, $group = 'default' ) {
1482 $start_time = microtime( true );
1483
1484 try {
1485 $tx = $this->redis->pipeline();
1486
1487 foreach ( $keys as $key ) {
1488 $derived_key = $this->build_key( (string) $key, $group );
1489
1490 $tx->del( $derived_key );
1491
1492 unset( $this->cache[ $derived_key ] );
1493 }
1494
1495 $method = $this->is_predis() ? 'execute' : 'exec';
1496
1497 $results = array_map( function ( $response ) {
1498 return (bool) $this->parse_redis_response( $response );
1499 }, $tx->{$method}() ?: [] );
1500
1501 if ( count( $results ) !== count( $keys ) ) {
1502 $tx->discard();
1503
1504 return array_fill_keys( $keys, false );
1505 }
1506
1507 $execute_time = microtime( true ) - $start_time;
1508 } catch ( Exception $exception ) {
1509 $this->handle_exception( $exception );
1510
1511 return array_combine( $keys, array_fill( 0, count( $keys ), false ) );
1512 }
1513
1514 if ( function_exists( 'do_action' ) ) {
1515 foreach ( $keys as $key ) {
1516 /**
1517 * Fires on every cache key deletion
1518 *
1519 * @since 1.3.3
1520 * @param string $key The cache key.
1521 * @param string $group The group value appended to the $key.
1522 * @param float $execute_time Execution time for the request in seconds.
1523 */
1524 do_action( 'redis_object_cache_delete', $key, $group, $execute_time );
1525 }
1526 }
1527
1528 return array_combine( $keys, $results );
1529 }
1530
1531 /**
1532 * Removes all cache items from the in-memory runtime cache.
1533 *
1534 * @return bool True on success, false on failure.
1535 */
1536 public function flush_runtime() {
1537 $this->cache = [];
1538
1539 return true;
1540 }
1541
1542 /**
1543 * Executes Lua flush script.
1544 *
1545 * @return array|false Returns array on success, false on failure
1546 */
1547 protected function execute_lua_script( $script ) {
1548 $results = [];
1549
1550 if ( defined( 'WP_REDIS_CLUSTER' ) ) {
1551 return $this->execute_lua_script_on_cluster( $script );
1552 }
1553
1554 $flushTimeout = defined( 'WP_REDIS_FLUSH_TIMEOUT' ) ? WP_REDIS_FLUSH_TIMEOUT : 5;
1555
1556 if ( $this->is_predis() ) {
1557 $connection = $this->redis->getConnection();
1558
1559 if ($connection instanceof Predis\Connection\Replication\ReplicationInterface) {
1560 $connection = $connection->getMaster();
1561 }
1562
1563 $timeout = $connection->getParameters()->read_write_timeout ?? ini_get( 'default_socket_timeout' );
1564 stream_set_timeout( $connection->getResource(), $flushTimeout );
1565 } else {
1566 $timeout = $this->redis->getOption( Redis::OPT_READ_TIMEOUT );
1567 $this->redis->setOption( Redis::OPT_READ_TIMEOUT, $flushTimeout );
1568 }
1569
1570 try {
1571 $results[] = $this->parse_redis_response( $script() );
1572 } catch ( Exception $exception ) {
1573 $this->handle_exception( $exception );
1574 $results = false;
1575 }
1576
1577 if ( $this->is_predis() ) {
1578 stream_set_timeout( $connection->getResource(), $timeout ); // @phpstan-ignore variable.undefined
1579 } else {
1580 $this->redis->setOption( Redis::OPT_READ_TIMEOUT, $timeout );
1581 }
1582
1583 return $results;
1584 }
1585
1586 /**
1587 * Executes Lua flush script on Redis cluster.
1588 *
1589 * @return array|false Returns array on success, false on failure
1590 */
1591 protected function execute_lua_script_on_cluster( $script ) {
1592 $results = [];
1593 $redis = $this->redis;
1594 $flushTimeout = defined( 'WP_REDIS_FLUSH_TIMEOUT' ) ? WP_REDIS_FLUSH_TIMEOUT : 5;
1595
1596 if ( $this->is_predis() ) {
1597 foreach ( $this->redis->getIterator() as $master ) {
1598 $timeout = $master->getConnection()->getParameters()->read_write_timeout ?? ini_get( 'default_socket_timeout' );
1599 stream_set_timeout( $master->getConnection()->getResource(), $flushTimeout );
1600
1601 $this->redis = $master;
1602 $results[] = $this->parse_redis_response( $script() );
1603
1604 stream_set_timeout($master->getConnection()->getResource(), $timeout);
1605 }
1606 } else {
1607 try {
1608 foreach ( $this->redis->_masters() as $master ) {
1609 $this->redis = new Redis();
1610 $this->redis->connect( $master[0], $master[1], 0, null, 0, $flushTimeout );
1611
1612 $results[] = $this->parse_redis_response( $script() );
1613 }
1614 } catch ( Exception $exception ) {
1615 $this->handle_exception( $exception );
1616 $this->redis = $redis;
1617
1618 return false;
1619 }
1620 }
1621
1622 $this->redis = $redis;
1623
1624 return $results;
1625 }
1626
1627 /**
1628 * Invalidate all items in the cache. If `WP_REDIS_SELECTIVE_FLUSH` is `true`,
1629 * only keys prefixed with the `WP_REDIS_PREFIX` are flushed.
1630 *
1631 * @return bool True on success, false on failure.
1632 */
1633 public function flush() {
1634 $results = [];
1635 $this->cache = [];
1636
1637 if ( $this->redis_status() ) {
1638 $salt = defined( 'WP_REDIS_PREFIX' ) ? trim( WP_REDIS_PREFIX ) : null;
1639 $selective = defined( 'WP_REDIS_SELECTIVE_FLUSH' ) ? WP_REDIS_SELECTIVE_FLUSH : null;
1640
1641 $start_time = microtime( true );
1642
1643 if ( $salt && $selective ) {
1644 $script = $this->get_flush_closure( $salt );
1645 $results = $this->execute_lua_script( $script );
1646
1647 if ( empty( $results ) ) {
1648 return false;
1649 }
1650 } else {
1651 if ( defined( 'WP_REDIS_CLUSTER' ) ) {
1652 try {
1653 if ( $this->is_predis() ) {
1654 foreach ( $this->redis->getIterator() as $master ) {
1655 $results[] = $this->parse_redis_response( $master->flushdb() );
1656 }
1657 } else {
1658 foreach ( $this->redis->_masters() as $master ) {
1659 $results[] = $this->parse_redis_response( $this->redis->flushdb( $master ) );
1660 }
1661 }
1662 } catch ( Exception $exception ) {
1663 $this->handle_exception( $exception );
1664
1665 return false;
1666 }
1667 } else {
1668 try {
1669 $results[] = $this->parse_redis_response( $this->redis->flushdb() );
1670 } catch ( Exception $exception ) {
1671 $this->handle_exception( $exception );
1672
1673 return false;
1674 }
1675 }
1676 }
1677
1678 if ( function_exists( 'do_action' ) ) {
1679 $execute_time = microtime( true ) - $start_time;
1680
1681 /**
1682 * Fires on every cache flush
1683 *
1684 * @since 1.3.5
1685 * @param null|array $results Array of flush results.
1686 * @param int $deprecated Unused. Default 0.
1687 * @param bool $seletive Whether a selective flush took place.
1688 * @param string $salt The defined key prefix.
1689 * @param float $execute_time Execution time for the request in seconds.
1690 */
1691 do_action( 'redis_object_cache_flush', $results, 0, $selective, $salt, $execute_time );
1692 }
1693 }
1694
1695 if ( empty( $results ) ) {
1696 return false;
1697 }
1698
1699 foreach ( $results as $result ) {
1700 if ( ! $result ) {
1701 return false;
1702 }
1703 }
1704
1705 return true;
1706 }
1707
1708 /**
1709 * Removes all cache items in a group.
1710 *
1711 * @param string $group Name of group to remove from cache.
1712 * @return bool Returns TRUE on success or FALSE on failure.
1713 */
1714 public function flush_group( $group ) {
1715 if ( defined( 'WP_REDIS_DISABLE_GROUP_FLUSH' ) && WP_REDIS_DISABLE_GROUP_FLUSH ) {
1716 return $this->flush();
1717 }
1718
1719 $san_group = $this->sanitize_key_part( $group );
1720
1721 if ( is_multisite() && ! $this->is_global_group( $san_group ) ) {
1722 $salt = str_replace( "{$this->blog_prefix}:{$san_group}", "*:{$san_group}", $this->fast_build_key( '*', $san_group ) );
1723 } else {
1724 $salt = $this->fast_build_key( '*', $san_group );
1725 }
1726
1727 foreach ( $this->cache as $key => $value ) {
1728 if ( strpos( $key, "{$san_group}:" ) === 0 || strpos( $key, ":{$san_group}:" ) !== false ) {
1729 unset( $this->cache[ $key ] );
1730 }
1731 }
1732
1733 if ( in_array( $san_group, $this->unflushable_groups ) ) {
1734 return false;
1735 }
1736
1737 if ( ! $this->redis_status() ) {
1738 return false;
1739 }
1740
1741 $start_time = microtime( true );
1742 $script = $this->lua_flush_closure( $salt, false );
1743 $results = $this->execute_lua_script( $script );
1744
1745 if ( empty( $results ) ) {
1746 return false;
1747 }
1748
1749 if ( function_exists( 'do_action' ) ) {
1750 $execute_time = microtime( true ) - $start_time;
1751
1752 /**
1753 * Fires on every group cache flush
1754 *
1755 * @param null|array $results Array of flush results.
1756 * @param string $salt The defined key prefix.
1757 * @param float $execute_time Execution time for the request in seconds.
1758 * @since 2.2.3
1759 */
1760 do_action( 'redis_object_cache_flush_group', $results, $salt, $execute_time );
1761 }
1762
1763 foreach ( $results as $result ) {
1764 if ( ! $result ) {
1765 return false;
1766 }
1767 }
1768
1769 return true;
1770 }
1771
1772 /**
1773 * Returns a closure to flush selectively.
1774 *
1775 * @param string $salt The salt to be used to differentiate.
1776 * @return callable Generated callable executing the lua script.
1777 */
1778 protected function get_flush_closure( $salt ) {
1779 if ( $this->unflushable_groups ) {
1780 return $this->lua_flush_extended_closure( $salt );
1781 } else {
1782 return $this->lua_flush_closure( $salt );
1783 }
1784 }
1785
1786 /**
1787 * Quotes a string for usage in the `glob` function
1788 *
1789 * @param string $string The string to quote.
1790 * @return string
1791 */
1792 protected function glob_quote( $string ) {
1793 $characters = [ '*', '+', '?', '!', '{', '}', '[', ']', '(', ')', '|', '@' ];
1794
1795 return str_replace(
1796 $characters,
1797 array_map(
1798 function ( $character ) {
1799 return "[{$character}]";
1800 },
1801 $characters
1802 ),
1803 $string
1804 );
1805 }
1806
1807 /**
1808 * Returns a closure ready to be called to flush selectively ignoring unflushable groups.
1809 *
1810 * @param string $salt The salt to be used to differentiate.
1811 * @param bool $escape ...
1812 * @return callable Generated callable executing the lua script.
1813 */
1814 protected function lua_flush_closure( $salt, $escape = true ) {
1815 $salt = $escape ? $this->glob_quote( $salt ) : $salt;
1816
1817 return function () use ( $salt ) {
1818 // phpcs:disable Squiz.PHP.Heredoc.NotAllowed
1819 $script = <<<LUA
1820 local cur = 0
1821 local i = 0
1822 local tmp
1823 repeat
1824 tmp = redis.call('SCAN', cur, 'MATCH', '{$salt}*')
1825 cur = tonumber(tmp[1])
1826 if tmp[2] then
1827 for _, v in pairs(tmp[2]) do
1828 redis.call('del', v)
1829 i = i + 1
1830 end
1831 end
1832 until 0 == cur
1833 return i
1834 LUA;
1835
1836 if ( isset($this->redis_version) && version_compare( $this->redis_version, '5', '<' ) && version_compare( $this->redis_version, '3.2', '>=' ) ) {
1837 $script = 'redis.replicate_commands()' . "\n" . $script;
1838 }
1839
1840 $args = $this->is_predis() ? [ $script, 0 ] : [ $script ];
1841
1842 return call_user_func_array( [ $this->redis, 'eval' ], $args );
1843 };
1844 }
1845
1846 /**
1847 * Returns a closure ready to be called to flush selectively.
1848 *
1849 * @param string $salt The salt to be used to differentiate.
1850 * @return callable Generated callable executing the lua script.
1851 */
1852 protected function lua_flush_extended_closure( $salt ) {
1853 $salt = $this->glob_quote( $salt );
1854
1855 return function () use ( $salt ) {
1856 $salt_length = strlen( $salt );
1857
1858 $unflushable = array_map(
1859 function ( $group ) {
1860 return ":{$group}:";
1861 },
1862 $this->unflushable_groups
1863 );
1864
1865 $script = <<<LUA
1866 local cur = 0
1867 local i = 0
1868 local d, tmp
1869 repeat
1870 tmp = redis.call('SCAN', cur, 'MATCH', '{$salt}*')
1871 cur = tonumber(tmp[1])
1872 if tmp[2] then
1873 for _, v in pairs(tmp[2]) do
1874 d = true
1875 for _, s in pairs(KEYS) do
1876 d = d and not v:find(s, {$salt_length})
1877 if not d then break end
1878 end
1879 if d then
1880 redis.call('del', v)
1881 i = i + 1
1882 end
1883 end
1884 end
1885 until 0 == cur
1886 return i
1887 LUA;
1888 if ( isset($this->redis_version) && version_compare( $this->redis_version, '5', '<' ) && version_compare( $this->redis_version, '3.2', '>=' ) ) {
1889 $script = 'redis.replicate_commands()' . "\n" . $script;
1890 }
1891
1892 $args = $this->is_predis()
1893 ? array_merge( [ $script, count( $unflushable ) ], $unflushable )
1894 : [ $script, $unflushable, count( $unflushable ) ];
1895
1896 return call_user_func_array( [ $this->redis, 'eval' ], $args );
1897 };
1898 }
1899
1900 /**
1901 * Retrieve object from cache.
1902 *
1903 * Gets an object from cache based on $key and $group.
1904 *
1905 * @param string $key The key under which to store the value.
1906 * @param string $group The group value appended to the $key.
1907 * @param bool $force Optional. Whether to force a refetch rather than relying on the local
1908 * cache. Default false.
1909 * @param bool $found Optional. Whether the key was found in the cache. Disambiguates a return of
1910 * false, a storable value. Passed by reference. Default null.
1911 * @return bool|mixed Cached object value.
1912 */
1913 public function get( $key, $group = 'default', $force = false, &$found = null ) {
1914 $san_key = $this->sanitize_key_part( $key );
1915 $san_group = $this->sanitize_key_part( $group );
1916 $derived_key = $this->fast_build_key( $san_key, $san_group );
1917
1918 if ( array_key_exists( $derived_key, $this->cache ) && ! $force ) {
1919 $found = true;
1920 $this->cache_hits++;
1921 $value = $this->get_from_internal_cache( $derived_key );
1922
1923 return $value;
1924 } elseif ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) {
1925 $found = false;
1926 $this->cache_misses++;
1927
1928 return false;
1929 }
1930
1931 $start_time = microtime( true );
1932
1933 try {
1934 $result = $this->redis->get( $derived_key );
1935 } catch ( Exception $exception ) {
1936 $this->handle_exception( $exception );
1937
1938 return false;
1939 }
1940
1941 $execute_time = microtime( true ) - $start_time;
1942
1943 $this->cache_calls++;
1944 $this->cache_time += $execute_time;
1945
1946 if ( $result === null || $result === false ) {
1947 $found = false;
1948 $this->cache_misses++;
1949
1950 return false;
1951 } else {
1952 $found = true;
1953 $this->cache_hits++;
1954 $value = $this->maybe_unserialize( $result );
1955 }
1956
1957 $this->add_to_internal_cache( $derived_key, $value );
1958
1959 if ( function_exists( 'do_action' ) ) {
1960 /**
1961 * Fires on every cache get request
1962 *
1963 * @since 1.2.2
1964 * @param mixed $value Value of the cache entry.
1965 * @param string $key The cache key.
1966 * @param string $group The group value appended to the $key.
1967 * @param bool $force Whether a forced refetch has taken place rather than relying on the local cache.
1968 * @param bool $found Whether the key was found in the cache.
1969 * @param float $execute_time Execution time for the request in seconds.
1970 */
1971 do_action( 'redis_object_cache_get', $key, $value, $group, $force, $found, $execute_time );
1972 }
1973
1974 if ( function_exists( 'apply_filters' ) && function_exists( 'has_filter' ) ) {
1975 if ( has_filter( 'redis_object_cache_get_value' ) ) {
1976 /**
1977 * Filters the return value
1978 *
1979 * @since 1.4.2
1980 * @param mixed $value Value of the cache entry.
1981 * @param string $key The cache key.
1982 * @param string $group The group value appended to the $key.
1983 * @param bool $force Whether a forced refetch has taken place rather than relying on the local cache.
1984 * @param bool $found Whether the key was found in the cache.
1985 */
1986 return apply_filters( 'redis_object_cache_get_value', $value, $key, $group, $force, $found );
1987 }
1988 }
1989
1990 return $value;
1991 }
1992
1993 /**
1994 * Retrieves multiple values from the cache in one call.
1995 *
1996 * @param array $keys Array of keys under which the cache contents are stored.
1997 * @param string $group Optional. Where the cache contents are grouped. Default empty.
1998 * @param bool $force Optional. Whether to force an update of the local cache
1999 * from the persistent cache. Default false.
2000 * @return array|false Array of values organized into groups.
2001 */
2002 public function get_multiple( $keys, $group = 'default', $force = false ) {
2003 if ( ! is_array( $keys ) ) {
2004 return false;
2005 }
2006
2007 $cache = [];
2008 $derived_keys = [];
2009 $start_time = microtime( true );
2010
2011 $san_group = $this->sanitize_key_part( $group );
2012
2013 foreach ( $keys as $key ) {
2014 $san_key = $this->sanitize_key_part( $key );
2015 $derived_keys[ $key ] = $this->fast_build_key( $san_key, $san_group );
2016 }
2017
2018 if ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) {
2019 foreach ( $keys as $key ) {
2020 $value = $this->get_from_internal_cache( $derived_keys[ $key ] );
2021 $cache[ $key ] = $value;
2022
2023 if ($value === false) {
2024 $this->cache_misses++;
2025 } else {
2026 $this->cache_hits++;
2027 }
2028 }
2029
2030 return $cache;
2031 }
2032
2033 if ( ! $force ) {
2034 foreach ( $keys as $key ) {
2035 $value = $this->get_from_internal_cache( $derived_keys[ $key ] );
2036
2037 if ( $value === false ) {
2038 $this->cache_misses++;
2039
2040 } else {
2041 $cache[ $key ] = $value;
2042 $this->cache_hits++;
2043 }
2044 }
2045 }
2046
2047 $remaining_keys = array_filter(
2048 $keys,
2049 function ( $key ) use ( $cache ) {
2050 return ! array_key_exists( $key, $cache );
2051 }
2052 );
2053
2054 if ( empty( $remaining_keys ) ) {
2055 return $cache;
2056 }
2057
2058 $start_time = microtime( true );
2059 $results = [];
2060
2061 $remaining_ids = array_map(
2062 function ( $key ) use ( $derived_keys ) {
2063 return $derived_keys[ $key ];
2064 },
2065 $remaining_keys
2066 );
2067
2068 try {
2069 $results = array_combine(
2070 $remaining_keys,
2071 $this->redis->mget( $remaining_ids )
2072 ?: array_fill( 0, count( $remaining_ids ), false )
2073 );
2074 } catch ( Exception $exception ) {
2075 $this->handle_exception( $exception );
2076
2077 $results = array_combine(
2078 $remaining_keys,
2079 array_fill( 0, count( $remaining_ids ), false )
2080 );
2081 }
2082
2083 $execute_time = microtime( true ) - $start_time;
2084
2085 $this->cache_calls++;
2086 $this->cache_time += $execute_time;
2087
2088 foreach ( $results as $key => $value ) {
2089 if ( $value === null || $value === false ) {
2090 $cache[ $key ] = false;
2091 $this->cache_misses++;
2092 } else {
2093 $cache[ $key ] = $this->maybe_unserialize( $value );
2094 $this->add_to_internal_cache( $derived_keys[ $key ], $cache[ $key ] );
2095 $this->cache_hits++;
2096 }
2097 }
2098
2099 if ( function_exists( 'do_action' ) ) {
2100 /**
2101 * Fires on every cache get multiple request
2102 *
2103 * @since 2.0.6
2104 * @param array $keys Array of keys under which the cache contents are stored.
2105 * @param array $cache Cache items.
2106 * @param string $group The group value appended to the $key.
2107 * @param bool $force Whether a forced refetch has taken place rather than relying on the local cache.
2108 * @param float $execute_time Execution time for the request in seconds.
2109 */
2110 do_action( 'redis_object_cache_get_multiple', $keys, $cache, $group, $force, $execute_time );
2111 }
2112
2113 if ( function_exists( 'apply_filters' ) && function_exists( 'has_filter' ) ) {
2114 if ( has_filter( 'redis_object_cache_get_value' ) ) {
2115 foreach ( $cache as $key => $value ) {
2116 /**
2117 * Filters the return value
2118 *
2119 * @since 1.4.2
2120 * @param mixed $value Value of the cache entry.
2121 * @param string $key The cache key.
2122 * @param string $group The group value appended to the $key.
2123 * @param bool $force Whether a forced refetch has taken place rather than relying on the local cache.
2124 */
2125 $cache[ $key ] = apply_filters( 'redis_object_cache_get_value', $value, $key, $group, $force );
2126 }
2127 }
2128 }
2129
2130 return $cache;
2131 }
2132
2133 /**
2134 * Sets a value in cache.
2135 *
2136 * The value is set whether or not this key already exists in Redis.
2137 *
2138 * @param string $key The key under which to store the value.
2139 * @param mixed $value The value to store.
2140 * @param string $group The group value appended to the $key.
2141 * @param int $expiration The expiration time, defaults to 0.
2142 * @return bool Returns TRUE on success or FALSE on failure.
2143 */
2144 public function set( $key, $value, $group = 'default', $expiration = 0 ) {
2145 $result = true;
2146 $start_time = microtime( true );
2147
2148 $san_key = $this->sanitize_key_part( $key );
2149 $san_group = $this->sanitize_key_part( $group );
2150
2151 $derived_key = $this->fast_build_key( $san_key, $san_group );
2152
2153 // Save if group not excluded from redis and redis is up.
2154 if ( ! $this->is_ignored_group( $group ) && $this->redis_status() ) {
2155 $orig_exp = $expiration;
2156 $expiration = $this->validate_expiration( $expiration );
2157
2158 /**
2159 * Filters the cache expiration time
2160 *
2161 * @since 1.4.2
2162 * @param int $expiration The time in seconds the entry expires. 0 for no expiry.
2163 * @param string $key The cache key.
2164 * @param string $group The cache group.
2165 * @param mixed $orig_exp The original expiration value before validation.
2166 */
2167 $expiration = apply_filters( 'redis_cache_expiration', $expiration, $key, $group, $orig_exp );
2168
2169 try {
2170 if ( $expiration ) {
2171 $result = $this->parse_redis_response( $this->redis->setex( $derived_key, $expiration, $this->maybe_serialize( $value ) ) );
2172 } else {
2173 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $this->maybe_serialize( $value ) ) );
2174 }
2175 } catch ( Exception $exception ) {
2176 $this->handle_exception( $exception );
2177
2178 return false;
2179 }
2180
2181 $execute_time = microtime( true ) - $start_time;
2182 $this->cache_calls++;
2183 $this->cache_time += $execute_time;
2184 }
2185
2186 // If the set was successful, or we didn't go to redis.
2187 if ( $result ) {
2188 $this->add_to_internal_cache( $derived_key, $value );
2189 }
2190
2191 if ( function_exists( 'do_action' ) ) {
2192 $execute_time = microtime( true ) - $start_time;
2193
2194 /**
2195 * Fires on every cache set
2196 *
2197 * @since 1.2.2
2198 * @param string $key The cache key.
2199 * @param mixed $value Value of the cache entry.
2200 * @param string $group The group value appended to the $key.
2201 * @param int $expiration The time in seconds the entry expires. 0 for no expiry.
2202 * @param float $execute_time Execution time for the request in seconds.
2203 */
2204 do_action( 'redis_object_cache_set', $key, $value, $group, $expiration, $execute_time );
2205 }
2206
2207 return $result;
2208 }
2209
2210 /**
2211 * Sets multiple values to the cache in one call.
2212 *
2213 * @param array $data Array of key and value to be set.
2214 * @param string $group Optional. Where the cache contents are grouped.
2215 * @param int $expire Optional. When to expire the cache contents, in seconds.
2216 * Default 0 (no expiration).
2217 * @return bool[] Array of return values, grouped by key. Each value is always true.
2218 */
2219 public function set_multiple( array $data, $group = 'default', $expire = 0 ) {
2220 if (
2221 $this->redis_status() &&
2222 method_exists( $this->redis, 'pipeline' ) &&
2223 ! $this->is_ignored_group( $group )
2224 ) {
2225 return $this->set_multiple_at_once( $data, $group, $expire );
2226 }
2227
2228 $values = [];
2229
2230 foreach ( $data as $key => $value ) {
2231 $values[ $key ] = $this->set( $key, $value, $group, $expire );
2232 }
2233
2234 return $values;
2235 }
2236
2237 /**
2238 * Sets multiple values to the cache in one call.
2239 *
2240 * @param array $data Array of key and value to be set.
2241 * @param string $group Optional. Where the cache contents are grouped.
2242 * @param int $expiration Optional. When to expire the cache contents, in seconds.
2243 * Default 0 (no expiration).
2244 * @return bool[] Array of return values, grouped by key. Each value is always true.
2245 */
2246 protected function set_multiple_at_once( array $data, $group = 'default', $expiration = 0 ) {
2247 $start_time = microtime( true );
2248
2249 $san_group = $this->sanitize_key_part( $group );
2250 $derived_keys = [];
2251
2252 $orig_exp = $expiration;
2253 $expiration = $this->validate_expiration( $expiration );
2254 $expirations = [];
2255
2256 $tx = $this->redis->pipeline();
2257 $keys = array_keys( $data );
2258
2259 foreach ( $data as $key => $value ) {
2260 $san_key = $this->sanitize_key_part( $key );
2261 $derived_key = $derived_keys[ $key ] = $this->fast_build_key( $san_key, $san_group );
2262
2263 /**
2264 * Filters the cache expiration time
2265 *
2266 * @param int $expiration The time in seconds the entry expires. 0 for no expiry.
2267 * @param string $key The cache key.
2268 * @param string $group The cache group.
2269 * @param mixed $orig_exp The original expiration value before validation.
2270 */
2271 $expiration = $expirations[ $key ] = apply_filters( 'redis_cache_expiration', $expiration, $key, $group, $orig_exp );
2272
2273 if ( $expiration ) {
2274 $tx->setex( $derived_key, $expiration, $this->maybe_serialize( $value ) );
2275 } else {
2276 $tx->set( $derived_key, $this->maybe_serialize( $value ) );
2277 }
2278 }
2279
2280 try {
2281 $method = $this->is_predis() ? 'execute' : 'exec';
2282
2283 $results = array_map( function ( $response ) {
2284 return (bool) $this->parse_redis_response( $response );
2285 }, $tx->{$method}() ?: [] );
2286
2287 if ( count( $results ) !== count( $keys ) ) {
2288 $tx->discard();
2289
2290 return array_fill_keys( $keys, false );
2291 }
2292
2293 $results = array_combine( $keys, $results );
2294
2295 foreach ( $results as $key => $result ) {
2296 if ( $result ) {
2297 $this->add_to_internal_cache( $derived_keys[ $key ], $data[ $key ] );
2298 }
2299 }
2300 } catch ( Exception $exception ) {
2301 $this->handle_exception( $exception );
2302
2303 return array_combine( $keys, array_fill( 0, count( $keys ), false ) );
2304 }
2305
2306 $execute_time = microtime( true ) - $start_time;
2307
2308 $this->cache_calls++;
2309 $this->cache_time += $execute_time;
2310
2311 if ( function_exists( 'do_action' ) ) {
2312 foreach ( $data as $key => $value ) {
2313 /**
2314 * Fires on every cache set
2315 *
2316 * @param string $key The cache key.
2317 * @param mixed $value Value of the cache entry.
2318 * @param string $group The group value appended to the $key.
2319 * @param int $expiration The time in seconds the entry expires. 0 for no expiry.
2320 * @param float $execute_time Execution time for the request in seconds.
2321 */
2322 do_action( 'redis_object_cache_set', $key, $value, $group, $expirations[ $key ], $execute_time );
2323 }
2324 }
2325
2326 return $results;
2327 }
2328
2329 /**
2330 * Increment a Redis counter by the amount specified
2331 *
2332 * @param string $key The key name.
2333 * @param int $offset Optional. The increment. Defaults to 1.
2334 * @param string $group Optional. The key group. Default is 'default'.
2335 * @return int|bool
2336 */
2337 public function increment( $key, $offset = 1, $group = 'default' ) {
2338 $offset = (int) $offset;
2339 $start_time = microtime( true );
2340
2341 $san_key = $this->sanitize_key_part( $key );
2342 $san_group = $this->sanitize_key_part( $group );
2343
2344 $derived_key = $this->fast_build_key( $san_key, $san_group );
2345
2346 // If group is a non-Redis group, save to internal cache, not Redis.
2347 if ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) {
2348 $value = $this->get_from_internal_cache( $derived_key );
2349 $value += $offset;
2350 $this->add_to_internal_cache( $derived_key, $value );
2351
2352 return $value;
2353 }
2354
2355 try {
2356 if ( $this->use_igbinary ) {
2357 $value = (int) $this->parse_redis_response( $this->maybe_unserialize( $this->redis->get( $derived_key ) ) );
2358 $value += $offset;
2359 $serialized = $this->maybe_serialize( $value );
2360
2361 if ( ($pttl = $this->redis->pttl( $derived_key )) > 0 ) {
2362 if ( $this->is_predis() ) {
2363 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $serialized, 'px', $pttl ) );
2364 } else {
2365 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $serialized, [ 'px' => $pttl ] ) );
2366 }
2367 } else {
2368 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $serialized ) );
2369 }
2370
2371 if ( $result ) {
2372 $this->add_to_internal_cache( $derived_key, $value );
2373 $result = $value;
2374 }
2375 } else {
2376 $result = $this->parse_redis_response( $this->redis->incrBy( $derived_key, $offset ) );
2377 $this->add_to_internal_cache( $derived_key, (int) $this->redis->get( $derived_key ) );
2378 }
2379 } catch ( Exception $exception ) {
2380 $this->handle_exception( $exception );
2381
2382 return false;
2383 }
2384
2385 $execute_time = microtime( true ) - $start_time;
2386
2387 $this->cache_calls += 2;
2388 $this->cache_time += $execute_time;
2389
2390 return $result;
2391 }
2392
2393 /**
2394 * Alias of `increment()`.
2395 *
2396 * @see self::increment()
2397 * @param string $key The key name.
2398 * @param int $offset Optional. The increment. Defaults to 1.
2399 * @param string $group Optional. The key group. Default is 'default'.
2400 * @return int|bool
2401 */
2402 public function incr( $key, $offset = 1, $group = 'default' ) {
2403 return $this->increment( $key, $offset, $group );
2404 }
2405
2406 /**
2407 * Decrement a Redis counter by the amount specified
2408 *
2409 * @param string $key The key name.
2410 * @param int $offset Optional. The decrement. Defaults to 1.
2411 * @param string $group Optional. The key group. Default is 'default'.
2412 * @return int|bool
2413 */
2414 public function decrement( $key, $offset = 1, $group = 'default' ) {
2415 $offset = (int) $offset;
2416 $start_time = microtime( true );
2417
2418 $san_key = $this->sanitize_key_part( $key );
2419 $san_group = $this->sanitize_key_part( $group );
2420
2421 $derived_key = $this->fast_build_key( $san_key, $san_group );
2422
2423 // If group is a non-Redis group, save to internal cache, not Redis.
2424 if ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) {
2425 $value = $this->get_from_internal_cache( $derived_key );
2426 $value -= $offset;
2427 $this->add_to_internal_cache( $derived_key, $value );
2428
2429 return $value;
2430 }
2431
2432 try {
2433 if ( $this->use_igbinary ) {
2434 $value = (int) $this->parse_redis_response( $this->maybe_unserialize( $this->redis->get( $derived_key ) ) );
2435 $value -= $offset;
2436 $serialized = $this->maybe_serialize( $value );
2437
2438 if ( ($pttl = $this->redis->pttl( $derived_key )) > 0 ) {
2439 if ( $this->is_predis() ) {
2440 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $serialized, 'px', $pttl ) );
2441 } else {
2442 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $serialized, [ 'px' => $pttl ] ) );
2443 }
2444 } else {
2445 $result = $this->parse_redis_response( $this->redis->set( $derived_key, $serialized ) );
2446 }
2447
2448 if ( $result ) {
2449 $this->add_to_internal_cache( $derived_key, $value );
2450 $result = $value;
2451 }
2452 } else {
2453 $result = $this->parse_redis_response( $this->redis->decrBy( $derived_key, $offset ) );
2454 $this->add_to_internal_cache( $derived_key, (int) $this->redis->get( $derived_key ) );
2455 }
2456 } catch ( Exception $exception ) {
2457 $this->handle_exception( $exception );
2458
2459 return false;
2460 }
2461
2462 $execute_time = microtime( true ) - $start_time;
2463
2464 $this->cache_calls += 2;
2465 $this->cache_time += $execute_time;
2466
2467 return $result;
2468 }
2469
2470 /**
2471 * Alias of `decrement()`.
2472 *
2473 * @see self::decrement()
2474 * @param string $key The key name.
2475 * @param int $offset Optional. The decrement. Defaults to 1.
2476 * @param string $group Optional. The key group. Default is 'default'.
2477 * @return int|bool
2478 */
2479 public function decr( $key, $offset = 1, $group = 'default' ) {
2480 return $this->decrement( $key, $offset, $group );
2481 }
2482
2483 /**
2484 * Render data about current cache requests
2485 * Used by the Debug bar plugin
2486 *
2487 * @return void
2488 */
2489 public function stats() {
2490 // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
2491 ?>
2492 <p>
2493 <strong>Redis Status:</strong>
2494 <?php echo $this->redis_status() ? 'Connected' : 'Not connected'; ?>
2495 <br />
2496 <strong>Redis Client:</strong>
2497 <?php echo $this->diagnostics['client'] ?: 'Unknown'; ?>
2498 <br />
2499 <strong>Cache Hits:</strong>
2500 <?php echo (int) $this->cache_hits; ?>
2501 <br />
2502 <strong>Cache Misses:</strong>
2503 <?php echo (int) $this->cache_misses; ?>
2504 <br />
2505 <strong>Cache Size:</strong>
2506 <?php echo number_format_i18n( strlen( serialize( $this->cache ) ) / 1024, 2 ); ?> KB
2507 </p>
2508 <?php
2509 // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
2510 }
2511
2512 /**
2513 * Returns various information about the object cache.
2514 *
2515 * @return object
2516 */
2517 public function info() {
2518 $total = $this->cache_hits + $this->cache_misses;
2519
2520 $bytes = array_map(
2521 function ( $keys ) {
2522 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2523 return strlen( serialize( $keys ) );
2524 },
2525 $this->cache
2526 );
2527
2528 return (object) [
2529 'hits' => $this->cache_hits,
2530 'misses' => $this->cache_misses,
2531 'ratio' => $total > 0 ? round( $this->cache_hits / ( $total / 100 ), 1 ) : 100,
2532 'bytes' => array_sum( $bytes ),
2533 'time' => $this->cache_time,
2534 'calls' => $this->cache_calls,
2535 'groups' => (object) [
2536 'global' => $this->global_groups,
2537 'non_persistent' => $this->ignored_groups,
2538 'unflushable' => $this->unflushable_groups,
2539 ],
2540 'errors' => empty( $this->errors ) ? null : $this->errors,
2541 'meta' => [
2542 'Client' => $this->diagnostics['client'] ?? 'Unknown',
2543 'Redis Version' => $this->redis_version,
2544 ],
2545 ];
2546 }
2547
2548 /**
2549 * Builds a key for the cached object using the prefix, group and key.
2550 *
2551 * @param string $key The key under which to store the value, pre-sanitized.
2552 * @param string $group The group value appended to the $key, pre-sanitized.
2553 * @return string
2554 */
2555 public function build_key( $key, $group = 'default' ) {
2556 if ( empty( $group ) ) {
2557 $group = 'default';
2558 }
2559
2560 $san_key = $this->sanitize_key_part( $key );
2561 $san_group = $this->sanitize_key_part( $group );
2562
2563 return $this->fast_build_key($san_key, $san_group);
2564 }
2565
2566 /**
2567 * Builds a key for the cached object using the prefix, group and key.
2568 *
2569 * @param string $key The key under which to store the value, pre-sanitized.
2570 * @param string $group The group value appended to the $key, pre-sanitized.
2571 * @return string
2572 */
2573 public function fast_build_key( $key, $group = 'default' ) {
2574 if ( empty( $group ) ) {
2575 $group = 'default';
2576 }
2577
2578 $salt = defined( 'WP_REDIS_PREFIX' ) ? trim( WP_REDIS_PREFIX ) : '';
2579
2580 $prefix = $this->is_global_group( $group ) ? $this->global_prefix : $this->blog_prefix;
2581 $prefix = trim( (string) $prefix, '_-:$' );
2582
2583 return "{$salt}{$prefix}:{$group}:{$key}";
2584 }
2585
2586 /**
2587 * Replaces the set group separator by another one
2588 *
2589 * @param string $part The string to sanitize.
2590 * @return string Sanitized string.
2591 */
2592 protected function sanitize_key_part( $part ) {
2593 return is_string( $part ) ? str_replace( ':', '-', $part ) : $part;
2594 }
2595
2596 /**
2597 * Checks if the given group is part the ignored group array
2598 *
2599 * @param string $group Name of the group to check, pre-sanitized.
2600 * @return bool
2601 */
2602 protected function is_ignored_group( $group ) {
2603 return $this->is_group_of_type( $group, 'ignored' );
2604 }
2605
2606 /**
2607 * Checks if the given group is part the global group array
2608 *
2609 * @param string $group Name of the group to check, pre-sanitized.
2610 * @return bool
2611 */
2612 protected function is_global_group( $group ) {
2613 return $this->is_group_of_type( $group, 'global' );
2614 }
2615
2616 /**
2617 * Checks if the given group is part the unflushable group array
2618 *
2619 * @param string $group Name of the group to check, pre-sanitized.
2620 * @return bool
2621 */
2622 protected function is_unflushable_group( $group ) {
2623 return $this->is_group_of_type( $group, 'unflushable' );
2624 }
2625
2626 /**
2627 * Checks the type of the given group
2628 *
2629 * @param string $group Name of the group to check, pre-sanitized.
2630 * @param string $type Type of the group to check.
2631 * @return bool
2632 */
2633 private function is_group_of_type( $group, $type ) {
2634 return isset( $this->group_type[ $group ] )
2635 && $this->group_type[ $group ] == $type;
2636 }
2637
2638 /**
2639 * Convert Redis responses into something meaningful
2640 *
2641 * @param mixed $response Response sent from the redis instance.
2642 * @return mixed
2643 */
2644 protected function parse_redis_response( $response ) {
2645 if ( is_bool( $response ) ) {
2646 return $response;
2647 }
2648
2649 if ( is_numeric( $response ) ) {
2650 return $response;
2651 }
2652
2653 if ( is_object( $response ) && method_exists( $response, 'getPayload' ) ) {
2654 return $response->getPayload() === 'OK';
2655 }
2656
2657 return false;
2658 }
2659
2660 /**
2661 * Simple wrapper for saving object to the internal cache.
2662 *
2663 * @param string $derived_key Key to save value under.
2664 * @param mixed $value Object value.
2665 */
2666 public function add_to_internal_cache( $derived_key, $value ) {
2667 if ( is_object( $value ) ) {
2668 $value = clone $value;
2669 }
2670
2671 $this->cache[ $derived_key ] = $value;
2672 }
2673
2674 /**
2675 * Get a value specifically from the internal, run-time cache, not Redis.
2676 *
2677 * @param int|string $derived_key Key value.
2678 *
2679 * @return bool|mixed Value on success; false on failure.
2680 */
2681 public function get_from_internal_cache( $derived_key ) {
2682 if ( ! array_key_exists( $derived_key, $this->cache ) ) {
2683 return false;
2684 }
2685
2686 if ( is_object( $this->cache[ $derived_key ] ) ) {
2687 return clone $this->cache[ $derived_key ];
2688 }
2689
2690 return $this->cache[ $derived_key ];
2691 }
2692
2693 /**
2694 * In multisite, switch blog prefix when switching blogs
2695 *
2696 * @param int $_blog_id Blog ID.
2697 * @return bool
2698 */
2699 public function switch_to_blog( $_blog_id ) {
2700 if ( ! function_exists( 'is_multisite' ) || ! is_multisite() ) {
2701 return false;
2702 }
2703
2704 $this->blog_prefix = (int) $_blog_id;
2705
2706 return true;
2707 }
2708
2709 /**
2710 * Sets the list of global groups.
2711 *
2712 * @param array $groups List of groups that are global.
2713 */
2714 public function add_global_groups( $groups ) {
2715 $groups = (array) $groups;
2716
2717 if ( $this->redis_status() ) {
2718 $this->global_groups = array_unique( array_merge( $this->global_groups, $groups ) );
2719 } else {
2720 $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
2721 }
2722
2723 $this->cache_group_types();
2724 }
2725
2726 /**
2727 * Sets the list of groups not to be cached by Redis.
2728 *
2729 * @param array $groups List of groups that are to be ignored.
2730 */
2731 public function add_non_persistent_groups( $groups ) {
2732 /**
2733 * Filters list of groups to be added to {@see self::$ignored_groups}
2734 *
2735 * @since 2.1.7
2736 * @param string[] $groups List of groups to be ignored.
2737 */
2738 $groups = apply_filters( 'redis_cache_add_non_persistent_groups', (array) $groups );
2739
2740 $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
2741 $this->cache_group_types();
2742 }
2743
2744 /**
2745 * Sets the list of groups not to flushed cached.
2746 *
2747 * @param array $groups List of groups that are unflushable.
2748 */
2749 public function add_unflushable_groups( $groups ) {
2750 $groups = (array) $groups;
2751
2752 $this->unflushable_groups = array_unique( array_merge( $this->unflushable_groups, $groups ) );
2753 $this->cache_group_types();
2754 }
2755
2756 /**
2757 * Wrapper to validate the cache keys expiration value
2758 *
2759 * @param mixed $expiration Incoming expiration value (whatever it is).
2760 */
2761 protected function validate_expiration( $expiration ) {
2762 $expiration = is_int( $expiration ) || ctype_digit( (string) $expiration ) ? (int) $expiration : 0;
2763
2764 if ( defined( 'WP_REDIS_MAXTTL' ) ) {
2765 $max = (int) WP_REDIS_MAXTTL;
2766
2767 if ( $expiration === 0 || $expiration > $max ) {
2768 $expiration = $max;
2769 }
2770 }
2771
2772 return $expiration;
2773 }
2774
2775 /**
2776 * Unserialize value only if it was serialized.
2777 *
2778 * @param string $original Maybe unserialized original, if is needed.
2779 * @return mixed Unserialized data can be any type.
2780 */
2781 protected function maybe_unserialize( $original ) {
2782 if ( $this->use_igbinary ) {
2783 return igbinary_unserialize( $original );
2784 }
2785
2786 // Don't attempt to unserialize data that wasn't serialized going in.
2787 if ( $this->is_serialized( $original ) ) {
2788 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
2789 $value = @unserialize( $original );
2790
2791 return is_object( $value ) ? clone $value : $value;
2792 }
2793
2794 return $original;
2795 }
2796
2797 /**
2798 * Serialize data, if needed.
2799 *
2800 * @param mixed $data Data that might be serialized.
2801 * @return mixed A scalar data
2802 */
2803 protected function maybe_serialize( $data ) {
2804 if ( is_object( $data ) ) {
2805 $data = clone $data;
2806 }
2807
2808 if ( $this->use_igbinary ) {
2809 return igbinary_serialize( $data );
2810 }
2811
2812 if ( is_array( $data ) || is_object( $data ) ) {
2813 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2814 return serialize( $data );
2815 }
2816
2817 if ( $this->is_serialized( $data, false ) ) {
2818 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2819 return serialize( $data );
2820 }
2821
2822 return $data;
2823 }
2824
2825 /**
2826 * Check value to find if it was serialized.
2827 *
2828 * If $data is not an string, then returned value will always be false.
2829 * Serialized data is always a string.
2830 *
2831 * @param string $data Value to check to see if was serialized.
2832 * @param bool $strict Optional. Whether to be strict about the end of the string. Default true.
2833 * @return bool False if not serialized and true if it was.
2834 */
2835 protected function is_serialized( $data, $strict = true ) {
2836 // if it isn't a string, it isn't serialized.
2837 if ( ! is_string( $data ) ) {
2838 return false;
2839 }
2840
2841 $data = trim( $data );
2842
2843 if ( 'N;' === $data ) {
2844 return true;
2845 }
2846
2847 if ( strlen( $data ) < 4 ) {
2848 return false;
2849 }
2850
2851 if ( ':' !== $data[1] ) {
2852 return false;
2853 }
2854
2855 if ( $strict ) {
2856 $lastc = substr( $data, -1 );
2857
2858 if ( ';' !== $lastc && '}' !== $lastc ) {
2859 return false;
2860 }
2861 } else {
2862 $semicolon = strpos( $data, ';' );
2863 $brace = strpos( $data, '}' );
2864
2865 // Either ; or } must exist.
2866 if ( false === $semicolon && false === $brace ) {
2867 return false;
2868 }
2869
2870 // But neither must be in the first X characters.
2871 if ( false !== $semicolon && $semicolon < 3 ) {
2872 return false;
2873 }
2874
2875 if ( false !== $brace && $brace < 4 ) {
2876 return false;
2877 }
2878 }
2879 $token = $data[0];
2880
2881 switch ( $token ) {
2882 case 's':
2883 if ( $strict ) {
2884 if ( '"' !== substr( $data, -2, 1 ) ) {
2885 return false;
2886 }
2887 } elseif ( false === strpos( $data, '"' ) ) {
2888 return false;
2889 }
2890 // Or else fall through.
2891 // No break!
2892 case 'a':
2893 case 'O':
2894 return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
2895 case 'b':
2896 case 'i':
2897 case 'd':
2898 $end = $strict ? '$' : '';
2899
2900 return (bool) preg_match( "/^{$token}:[0-9.E-]+;$end/", $data );
2901 }
2902
2903 return false;
2904 }
2905
2906 /**
2907 * Handle the redis failure gracefully or throw an exception.
2908 *
2909 * @param \Exception $exception Exception thrown.
2910 * @throws \Exception If `fail_gracefully` flag is set to a falsy value.
2911 * @return void
2912 */
2913 protected function handle_exception( $exception ) {
2914 $this->redis_connected = false;
2915
2916 // When Redis is unavailable, fall back to the internal cache by forcing all groups to be "no redis" groups.
2917 $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $this->global_groups ) );
2918
2919 error_log( $exception ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
2920
2921 if ( function_exists( 'do_action' ) ) {
2922 /**
2923 * Fires when an object cache related error occurs.
2924 *
2925 * @since 1.5.0
2926 * @param \Exception $exception The exception.
2927 * @param string $message The exception message.
2928 */
2929 do_action( 'redis_object_cache_error', $exception, $exception->getMessage() );
2930 }
2931
2932 if ( ! $this->fail_gracefully ) {
2933 $this->show_error_and_die( $exception );
2934 }
2935
2936 $this->errors[] = $exception->getMessage();
2937 }
2938
2939 /**
2940 * Show Redis connection error screen, or load custom `/redis-error.php`.
2941 *
2942 * @return void
2943 */
2944 protected function show_error_and_die( Exception $exception ) {
2945 wp_load_translations_early();
2946
2947 $domain = 'redis-cache';
2948 $locale = defined( 'WPLANG' ) ? WPLANG : 'en_US';
2949 $mofile = WP_LANG_DIR . "/plugins/{$domain}-{$locale}.mo";
2950
2951 if ( load_textdomain( $domain, $mofile, $locale ) === false ) {
2952 add_filter( 'pre_determine_locale', function () {
2953 return defined( 'WPLANG' ) ? WPLANG : 'en_US';
2954 } );
2955
2956 add_filter( 'pre_get_language_files_from_path', '__return_empty_array' );
2957 }
2958
2959 // Load custom Redis error template, if present.
2960 if ( file_exists( WP_CONTENT_DIR . '/redis-error.php' ) ) {
2961 require_once WP_CONTENT_DIR . '/redis-error.php';
2962 die();
2963 }
2964
2965 $verbose = wp_installing()
2966 || defined( 'WP_ADMIN' )
2967 || ( defined( 'WP_DEBUG' ) && WP_DEBUG );
2968
2969 $message = '<h1>' . __( 'Error establishing a Redis connection', 'redis-cache' ) . "</h1>\n";
2970
2971 if ( $verbose ) {
2972 $message .= "<p><code>" . $exception->getMessage() . "</code></p>\n";
2973
2974 $message .= '<p>' . sprintf(
2975 // translators: %s = Formatted wp-config.php file name.
2976 __( 'WordPress is unable to establish a connection to Redis. This means that the connection information in your %s file are incorrect, or that the Redis server is not reachable.', 'redis-cache' ),
2977 '<code>wp-config.php</code>'
2978 ) . "</p>\n";
2979
2980 $message .= "<ul>\n";
2981 $message .= '<li>' . __( 'Is the correct Redis host and port set?', 'redis-cache' ) . "</li>\n";
2982 $message .= '<li>' . __( 'Is the Redis server running?', 'redis-cache' ) . "</li>\n";
2983 $message .= "</ul>\n";
2984
2985 $message .= '<p>' . sprintf(
2986 // translators: %s = Link to installation instructions.
2987 __( 'If you need help, please read the <a href="%s">installation instructions</a>.', 'redis-cache' ),
2988 'https://github.com/rhubarbgroup/redis-cache/blob/develop/INSTALL.md'
2989 ) . "</p>\n";
2990 }
2991
2992 $message .= '<p>' . sprintf(
2993 // translators: %1$s = Formatted object-cache.php file name, %2$s = Formatted wp-content directory name.
2994 __( 'To disable Redis, delete the %1$s file in the %2$s directory.', 'redis-cache' ),
2995 '<code>object-cache.php</code>',
2996 '<code>/wp-content/</code>'
2997 ) . "</p>\n";
2998
2999 // phpcs:disable WordPress.Security.EscapeOutput
3000 wp_die( $message );
3001 // phpcs:enable
3002 }
3003
3004 /**
3005 * Builds a clean connection array out of redis clusters array.
3006 *
3007 * @return array
3008 */
3009 protected function build_cluster_connection_array() {
3010 $cluster = array_values( WP_REDIS_CLUSTER );
3011
3012 foreach ( $cluster as $key => $server ) {
3013 // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
3014 $components = parse_url( $server );
3015
3016 if ( ! empty( $components['scheme'] ) ) {
3017 $scheme = $components['scheme'];
3018 } elseif ( defined( 'WP_REDIS_SCHEME' ) ) {
3019 $scheme = WP_REDIS_SCHEME;
3020 } else {
3021 $scheme = null;
3022 }
3023
3024 if ( isset( $scheme ) ) {
3025 $cluster[ $key ] = sprintf(
3026 '%s://%s:%d',
3027 $scheme,
3028 $components['host'],
3029 $components['port']
3030 );
3031 } else {
3032 $cluster[ $key ] = sprintf(
3033 '%s:%d',
3034 $components['host'],
3035 $components['port']
3036 );
3037 }
3038 }
3039
3040 return $cluster;
3041 }
3042
3043 /**
3044 * Check whether Predis client is in use.
3045 *
3046 * @return bool
3047 */
3048 protected function is_predis() {
3049 return $this->redis instanceof Predis\Client;
3050 }
3051
3052 /**
3053 * Allows access to private properties for backwards compatibility.
3054 *
3055 * @param string $name Name of the property.
3056 * @return mixed
3057 */
3058 public function __get( $name ) {
3059 return isset( $this->{$name} ) ? $this->{$name} : null;
3060 }
3061 }
3062
3063 endif;
3064 // phpcs:enable Generic.WhiteSpace.ScopeIndent.IncorrectExact, Generic.WhiteSpace.ScopeIndent.Incorrect
3065