PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / trunk
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score vtrunk
trunk 1.0 1.0.1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1 2.1.1 2.1.2 2.2 2.2.1 All 69 releases
powered-cache / includes / dropins / redis-object-cache.php

redis-object-cache.php in Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score trunk, at includes/dropins/redis-object-cache.php

1,592 lines 47.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // WP Redis
4 // This file needs to be symlinked or copied to wp-content/object-cache.php
5
6 // Users with setups where multiple installs share a common wp-config.php or $table_prefix
7 // can use this to guarantee uniqueness for the keys generated by this object cache.
8 if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
9 define( 'WP_CACHE_KEY_SALT', '' );
10 }
11
12 if ( ! defined( 'WP_REDIS_OBJECT_CACHE' ) ) {
13 define( 'WP_REDIS_OBJECT_CACHE', true );
14 }
15
16 if ( ! defined( 'WP_REDIS_USE_CACHE_GROUPS' ) ) {
17 define( 'WP_REDIS_USE_CACHE_GROUPS', false );
18 }
19
20 if ( ! defined( 'WP_REDIS_DEFAULT_EXPIRE_SECONDS' ) ) {
21 define( 'WP_REDIS_DEFAULT_EXPIRE_SECONDS', 0 );
22 }
23
24 if ( ! defined( 'WP_REDIS_IGNORE_GLOBAL_GROUPS' ) ) {
25 define( 'WP_REDIS_IGNORE_GLOBAL_GROUPS', false );
26 }
27
28 /**
29 * Adds data to the cache, if the cache key doesn't already exist.
30 *
31 * @uses $wp_object_cache Object Cache Class
32 * @see WP_Object_Cache::add()
33 *
34 * @param int|string $key The cache key to use for retrieval later
35 * @param mixed $data The data to add to the cache store
36 * @param string $group The group to add the cache to
37 * @param int $expire When the cache data should be expired
38 * @return bool False if cache key and group already exist, true on success
39 */
40 function wp_cache_add( $key, $data, $group = '', $expire = WP_REDIS_DEFAULT_EXPIRE_SECONDS ) {
41 global $wp_object_cache;
42
43 return $wp_object_cache->add( $key, $data, $group, (int) $expire );
44 }
45
46 /**
47 * Closes the cache.
48 *
49 * This function has ceased to do anything since WordPress 2.5. The
50 * functionality was removed along with the rest of the persistent cache. This
51 * does not mean that plugins can't implement this function when they need to
52 * make sure that the cache is cleaned up after WordPress no longer needs it.
53 *
54 * @return bool Always returns True
55 */
56 function wp_cache_close() {
57 return true;
58 }
59
60 /**
61 * Decrement numeric cache item's value
62 *
63 * @uses $wp_object_cache Object Cache Class
64 * @see WP_Object_Cache::decr()
65 *
66 * @param int|string $key The cache key to increment
67 * @param int $offset The amount by which to decrement the item's value. Default is 1.
68 * @param string $group The group the key is in.
69 * @return false|int False on failure, the item's new value on success.
70 */
71 function wp_cache_decr( $key, $offset = 1, $group = '' ) {
72 global $wp_object_cache;
73
74 return $wp_object_cache->decr( $key, $offset, $group );
75 }
76
77 /**
78 * Removes the cache contents matching key and group.
79 *
80 * @uses $wp_object_cache Object Cache Class
81 * @see WP_Object_Cache::delete()
82 *
83 * @param int|string $key What the contents in the cache are called
84 * @param string $group Where the cache contents are grouped
85 * @return bool True on successful removal, false on failure
86 */
87 function wp_cache_delete( $key, $group = '' ) {
88 global $wp_object_cache;
89
90 return $wp_object_cache->delete( $key, $group );
91 }
92
93 /**
94 * Removes cache contents for a given group.
95 *
96 * @uses $wp_object_cache Object Cache Class
97 * @see WP_Object_Cache::delete_group()
98 *
99 * @param string $group Where the cache contents are grouped
100 * @return bool True on successful removal, false on failure
101 */
102 function wp_cache_delete_group( $group ) {
103 global $wp_object_cache;
104 return $wp_object_cache->delete_group( $group );
105 }
106
107
108 /**
109 * Removes all cache items.
110 *
111 * @uses $wp_object_cache Object Cache Class
112 * @see WP_Object_Cache::flush()
113 *
114 * @return bool False on failure, true on success
115 */
116 function wp_cache_flush() {
117 global $wp_object_cache;
118
119 return $wp_object_cache->flush();
120 }
121
122 /**
123 * Retrieves the cache contents from the cache by key and group.
124 *
125 * @uses $wp_object_cache Object Cache Class
126 * @see WP_Object_Cache::get()
127 *
128 * @param int|string $key What the contents in the cache are called
129 * @param string $group Where the cache contents are grouped
130 * @param bool $force Whether to force an update of the local cache from the persistent cache (default is false)
131 * @param &bool $found Whether key was found in the cache. Disambiguates a return of false, a storable value.
132 * @return bool|mixed False on failure to retrieve contents or the cache contents on success
133 */
134 function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
135 global $wp_object_cache;
136
137 return $wp_object_cache->get( $key, $group, $force, $found );
138 }
139
140 /**
141 * Retrieves multiple values from the cache in one call.
142 *
143 * @see WP_Object_Cache::get_multiple()
144 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
145 *
146 * @param array $keys Array of keys under which the cache contents are stored.
147 * @param string $group Optional. Where the cache contents are grouped. Default empty.
148 * @param bool $force Optional. Whether to force an update of the local cache
149 * from the persistent cache. Default false.
150 * @return array Array of values organized into groups.
151 */
152 function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
153 global $wp_object_cache;
154
155 return $wp_object_cache->get_multiple( $keys, $group, $force );
156 }
157
158 /**
159 * Removes all cache items from the in-memory runtime cache.
160 *
161 * @see WP_Object_Cache::flush()
162 *
163 * @return bool True on success, false on failure.
164 */
165 function wp_cache_flush_runtime() {
166 global $wp_object_cache;
167
168 return $wp_object_cache->flush( false );
169 }
170
171 /**
172 * Removes all cache items in a group, if the object cache implementation supports it.
173 *
174 * Before calling this function, always check for group flushing support using the
175 * `wp_cache_supports( 'flush_group' )` function.
176 *
177 * @see WP_Object_Cache::flush_group()
178 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
179 *
180 * @param string $group Name of group to remove from cache.
181 * @return bool True if group was flushed, false otherwise.
182 */
183 function wp_cache_flush_group( $group ) {
184 global $wp_object_cache;
185
186 return $wp_object_cache->flush_group( $group );
187 }
188
189
190 /**
191 * Increment numeric cache item's value
192 *
193 * @uses $wp_object_cache Object Cache Class
194 * @see WP_Object_Cache::incr()
195 *
196 * @param int|string $key The cache key to increment
197 * @param int $offset The amount by which to increment the item's value. Default is 1.
198 * @param string $group The group the key is in.
199 * @return false|int False on failure, the item's new value on success.
200 */
201 function wp_cache_incr( $key, $offset = 1, $group = '' ) {
202 global $wp_object_cache;
203
204 return $wp_object_cache->incr( $key, $offset, $group );
205 }
206
207 /**
208 * Sets up Object Cache Global and assigns it.
209 *
210 * @global WP_Object_Cache $wp_object_cache WordPress Object Cache
211 */
212 function wp_cache_init() {
213 global $wp_object_cache;
214
215 if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
216 $wp_object_cache = new WP_Object_Cache;
217 }
218 }
219
220 /**
221 * Replaces the contents of the cache with new data.
222 *
223 * @uses $wp_object_cache Object Cache Class
224 * @see WP_Object_Cache::replace()
225 *
226 * @param int|string $key What to call the contents in the cache
227 * @param mixed $data The contents to store in the cache
228 * @param string $group Where to group the cache contents
229 * @param int $expire When to expire the cache contents
230 * @return bool False if not exists, true if contents were replaced
231 */
232 function wp_cache_replace( $key, $data, $group = '', $expire = WP_REDIS_DEFAULT_EXPIRE_SECONDS ) {
233 global $wp_object_cache;
234
235 return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
236 }
237
238 /**
239 * Saves the data to the cache.
240 *
241 * @uses $wp_object_cache Object Cache Class
242 * @see WP_Object_Cache::set()
243 *
244 * @param int|string $key What to call the contents in the cache
245 * @param mixed $data The contents to store in the cache
246 * @param string $group Where to group the cache contents
247 * @param int $expire When to expire the cache contents
248 * @return bool False on failure, true on success
249 */
250 function wp_cache_set( $key, $data, $group = '', $expire = WP_REDIS_DEFAULT_EXPIRE_SECONDS ) {
251 global $wp_object_cache;
252
253 return $wp_object_cache->set( $key, $data, $group, (int) $expire );
254 }
255
256 /**
257 * Switch the internal blog id.
258 *
259 * This changes the blog id used to create keys in blog specific groups.
260 *
261 * @param int $blog_id Blog ID
262 */
263 function wp_cache_switch_to_blog( $blog_id ) {
264 global $wp_object_cache;
265
266 return $wp_object_cache->switch_to_blog( $blog_id );
267 }
268
269 /**
270 * Adds a group or set of groups to the list of global groups.
271 *
272 * @param string|array $groups A group or an array of groups to add
273 */
274 function wp_cache_add_global_groups( $groups ) {
275 global $wp_object_cache;
276
277 return $wp_object_cache->add_global_groups( $groups );
278 }
279
280 /**
281 * Adds a group or set of groups to the list of non-persistent groups.
282 *
283 * @param string|array $groups A group or an array of groups to add
284 */
285 function wp_cache_add_non_persistent_groups( $groups ) {
286 global $wp_object_cache;
287
288 $wp_object_cache->add_non_persistent_groups( $groups );
289 }
290
291 /**
292 * Adds a group or set of groups to the list of groups that use Redis hashes.
293 *
294 * @param string|array $groups A group or an array of groups to add.
295 */
296 function wp_cache_add_redis_hash_groups( $groups ) {
297 global $wp_object_cache;
298
299 $wp_object_cache->add_redis_hash_groups( $groups );
300 }
301
302 /**
303 * Reset internal cache keys and structures. If the cache backend uses global
304 * blog or site IDs as part of its cache keys, this function instructs the
305 * backend to reset those keys and perform any cleanup since blog or site IDs
306 * have changed since cache init.
307 *
308 * This function is deprecated. Use wp_cache_switch_to_blog() instead of this
309 * function when preparing the cache for a blog switch. For clearing the cache
310 * during unit tests, consider using wp_cache_init(). wp_cache_init() is not
311 * recommended outside of unit tests as the performance penalty for using it is
312 * high.
313 *
314 * @deprecated 3.5.0
315 */
316 function wp_cache_reset() {
317 _deprecated_function( __FUNCTION__, '3.5' );
318
319 global $wp_object_cache;
320
321 return $wp_object_cache->reset();
322 }
323
324 /**
325 * Determines whether the object cache implementation supports a particular feature.
326 *
327 * @since 6.1.0
328 *
329 * @param string $feature Name of the feature to check for. Possible values include:
330 * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
331 * 'flush_runtime', 'flush_group'.
332 * @return bool True if the feature is supported, false otherwise.
333 */
334 function wp_cache_supports( $feature ) {
335 switch ( $feature ) {
336 case 'get_multiple':
337 case 'flush_runtime':
338 case 'flush_group':
339 return true;
340
341 case 'add_multiple':
342 case 'set_multiple':
343 case 'delete_multiple':
344 default:
345 return false;
346 }
347 }
348
349 /**
350 * WordPress Object Cache
351 *
352 * The WordPress Object Cache is used to save on trips to the database. The
353 * Object Cache stores all of the cache data to memory and makes the cache
354 * contents available by using a key, which is used to name and later retrieve
355 * the cache contents.
356 *
357 * The Object Cache can be replaced by other caching mechanisms by placing files
358 * in the wp-content folder which is looked at in wp-settings. If that file
359 * exists, then this file will not be included.
360 */
361 #[AllowDynamicProperties]
362 class WP_Object_Cache {
363
364 /**
365 * Holds the cached objects
366 *
367 * @var array
368 */
369 public $cache = [];
370
371 /**
372 * The amount of times the cache data was already stored in the cache.
373 *
374 * @var int
375 */
376 public $cache_hits = 0;
377
378 /**
379 * Amount of times the cache did not have the request in cache
380 *
381 * @var int
382 */
383 public $cache_misses = 0;
384
385 /**
386 * The amount of times a request was made to Redis
387 *
388 * @var int
389 */
390 public $redis_calls = [];
391
392 /**
393 * List of global groups
394 *
395 * @var array
396 */
397 public $global_groups = [];
398
399 /**
400 * List of non-persistent groups
401 *
402 * @var array
403 */
404 public $non_persistent_groups = [];
405
406 /**
407 * List of groups which use Redis hashes.
408 *
409 * @var array
410 */
411 public $redis_hash_groups = [];
412
413 /**
414 * The blog prefix to prepend to keys in non-global groups.
415 *
416 * @var int
417 */
418 public $blog_prefix;
419
420 /**
421 * Whether or not Redis is connected
422 *
423 * @var bool
424 */
425 public $is_redis_connected = false;
426
427 /**
428 * Whether or not the object cache thinks Redis needs a flush
429 *
430 * @var bool
431 */
432 public $do_redis_failback_flush = false;
433
434 /**
435 * The last triggered error
436 *
437 * @var string
438 */
439 public $last_triggered_error = '';
440
441 /**
442 * The missing redis message.
443 *
444 * @var string
445 */
446 public $missing_redis_message = '';
447
448 /**
449 * Whether or not to use true cache groups, instead of flattening.
450 *
451 * @var bool
452 */
453 const USE_GROUPS = WP_REDIS_USE_CACHE_GROUPS;
454
455 /**
456 * Adds data to the cache if it doesn't already exist.
457 *
458 * @uses WP_Object_Cache::_exists Checks to see if the cache already has data.
459 * @uses WP_Object_Cache::set Sets the data after the checking the cache
460 * contents existence.
461 *
462 * @param int|string $key What to call the contents in the cache
463 * @param mixed $data The contents to store in the cache
464 * @param string $group Where to group the cache contents
465 * @param int $expire When to expire the cache contents
466 * @return bool False if cache key and group already exist, true on success
467 */
468 public function add( $key, $data, $group = 'default', $expire = WP_REDIS_DEFAULT_EXPIRE_SECONDS ) {
469
470 if ( empty( $group ) ) {
471 $group = 'default';
472 }
473
474 if ( function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition() ) {
475 return false;
476 }
477
478 if ( $this->_exists( $key, $group ) ) {
479 return false;
480 }
481
482 return $this->set( $key, $data, $group, (int) $expire );
483 }
484
485 /**
486 * Sets the list of global groups.
487 *
488 * @param array $groups List of groups that are global.
489 */
490 public function add_global_groups( $groups ) {
491 $groups = (array) $groups;
492
493 // Allow force ignoring of global groups.
494 if ( is_array( WP_REDIS_IGNORE_GLOBAL_GROUPS ) ) {
495 $groups = array_diff( $groups, WP_REDIS_IGNORE_GLOBAL_GROUPS );
496 }
497
498 $groups = array_fill_keys( $groups, true );
499 $this->global_groups = array_merge( $this->global_groups, $groups );
500 }
501
502 /**
503 * Sets the list of non-persistent groups.
504 *
505 * @param array $groups List of groups that are non-persistent.
506 */
507 public function add_non_persistent_groups( $groups ) {
508 $groups = (array) $groups;
509
510 $groups = array_fill_keys( $groups, true );
511 $this->non_persistent_groups = array_merge( $this->non_persistent_groups, $groups );
512 }
513
514 /**
515 * Sets the list of groups that use Redis hashes.
516 *
517 * @param array $groups List of groups that use Redis hashes.
518 */
519 public function add_redis_hash_groups( $groups ) {
520 $groups = (array) $groups;
521
522 $groups = array_fill_keys( $groups, true );
523 $this->redis_hash_groups = array_merge( $this->redis_hash_groups, $groups );
524 }
525
526 /**
527 * Decrement numeric cache item's value
528 *
529 * @param int|string $key The cache key to increment
530 * @param int $offset The amount by which to decrement the item's value. Default is 1.
531 * @param string $group The group the key is in.
532 * @return false|int False on failure, the item's new value on success.
533 */
534 public function decr( $key, $offset = 1, $group = 'default' ) {
535
536 if ( empty( $group ) ) {
537 $group = 'default';
538 }
539
540 // The key needs to exist in order to be decremented
541 if ( ! $this->_exists( $key, $group ) ) {
542 return false;
543 }
544
545 $offset = (int) $offset;
546
547 // If this isn't a persistent group, we have to sort this out ourselves, grumble grumble.
548 if ( ! $this->_should_persist( $group ) ) {
549 $existing = $this->_get_internal( $key, $group );
550 if ( empty( $existing ) || ! is_numeric( $existing ) ) {
551 $existing = 0;
552 } else {
553 $existing -= $offset;
554 }
555 if ( $existing < 0 ) {
556 $existing = 0;
557 }
558 $this->_set_internal( $key, $group, $existing );
559 return $existing;
560 }
561
562 if ( $this->_should_use_redis_hashes( $group ) ) {
563 $redis_safe_group = $this->_key( '', $group );
564 $result = $this->_call_redis( 'hIncrBy', $redis_safe_group, $key, -$offset, $group );
565 if ( $result < 0 ) {
566 $result = 0;
567 $this->_call_redis( 'hSet', $redis_safe_group, $key, $result );
568 }
569 } else {
570 $id = $this->_key( $key, $group );
571 $result = $this->_call_redis( 'decrBy', $id, $offset );
572 if ( $result < 0 ) {
573 $result = 0;
574 $this->_call_redis( 'set', $id, $result );
575 }
576 }
577
578 if ( is_int( $result ) ) {
579 $this->_set_internal( $key, $group, $result );
580 }
581 return $result;
582 }
583
584 /**
585 * Remove the contents of the cache key in the group
586 *
587 * If the cache key does not exist in the group and $force parameter is set
588 * to false, then nothing will happen. The $force parameter is set to false
589 * by default.
590 *
591 * @param int|string $key What the contents in the cache are called
592 * @param string $group Where the cache contents are grouped
593 * @param bool $force Optional. Whether to force the unsetting of the cache
594 * key in the group
595 * @return bool False if the contents weren't deleted and true on success
596 */
597 public function delete( $key, $group = 'default', $force = false ) {
598
599 if ( empty( $group ) ) {
600 $group = 'default';
601 }
602
603 if ( ! $force && ! $this->_exists( $key, $group ) ) {
604 return false;
605 }
606
607 if ( $this->_should_persist( $group ) ) {
608 if ( $this->_should_use_redis_hashes( $group ) ) {
609 $redis_safe_group = $this->_key( '', $group );
610 $result = $this->_call_redis( 'hDel', $redis_safe_group, $key );
611 } else {
612 $id = $this->_key( $key, $group );
613 $result = $this->_call_redis( 'del', $id );
614 }
615 if ( 1 !== $result ) {
616 return false;
617 }
618 }
619
620 $this->_unset_internal( $key, $group );
621 return true;
622 }
623
624 /**
625 * Remove the contents of all cache keys in the group.
626 *
627 * @param string $group Where the cache contents are grouped.
628 * @return boolean True on success, false on failure.
629 */
630 public function delete_group( $group ) {
631 if ( ! $this->_should_use_redis_hashes( $group ) ) {
632 return false;
633 }
634
635 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
636 $redis_safe_group = $this->_key( '', $group );
637 if ( $this->_should_persist( $group ) ) {
638 $result = $this->_call_redis( 'del', $redis_safe_group );
639 if ( 1 !== $result ) {
640 return false;
641 }
642 } elseif ( ! $this->_should_persist( $group ) && ! isset( $this->cache[ $multisite_safe_group ] ) ) {
643 return false;
644 }
645 unset( $this->cache[ $multisite_safe_group ] );
646 return true;
647 }
648
649 /**
650 * Clears the object cache of all data.
651 *
652 * By default, this will flush the session cache as well as Redis, but we
653 * can leave the redis cache intact if we want. This is helpful when, for
654 * instance, you're running a batch process and want to clear the session
655 * store to reduce the memory footprint, but you don't want to have to
656 * re-fetch all the values from the database.
657 *
658 * @param bool $redis Should we flush redis as well as the session cache?
659 * @return bool Always returns true
660 */
661 public function flush( $redis = true ) {
662 $this->cache = array();
663 if ( $redis ) {
664 $this->_call_redis( 'flushdb' );
665 }
666
667 return true;
668 }
669
670 /**
671 * Removes all cache items in a group.
672 *
673 * @param string $group Name of group to remove from cache.
674 * @return true Always returns true.
675 */
676 public function flush_group( $group ) {
677 if ( ! $this->_should_use_redis_hashes( $group ) ) {
678 return false;
679 }
680
681 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
682 $redis_safe_group = $this->_key( '', $group );
683 if ( $this->_should_persist( $group ) ) {
684 $result = $this->_call_redis( 'del', $redis_safe_group );
685 if ( 1 !== $result ) {
686 return false;
687 }
688 } elseif ( ! $this->_should_persist( $group ) && ! isset( $this->cache[ $multisite_safe_group ] ) ) {
689 return false;
690 }
691 unset( $this->cache[ $multisite_safe_group ] );
692 return true;
693 }
694
695 /**
696 * Retrieves the cache contents, if it exists
697 *
698 * The contents will be first attempted to be retrieved by searching by the
699 * key in the cache group. If the cache is hit (success) then the contents
700 * are returned.
701 *
702 * On failure, the number of cache misses will be incremented.
703 *
704 * @param int|string $key What the contents in the cache are called
705 * @param string $group Where the cache contents are grouped
706 * @param string $force Whether to force a refetch rather than relying on the local cache (default is false)
707 * @param bool $found Optional. Whether the key was found in the cache. Disambiguates a return of false, a storable value. Passed by reference. Default null.
708 * @return bool|mixed False on failure to retrieve contents or the cache contents on success
709 */
710 public function get( $key, $group = 'default', $force = false, &$found = null ) {
711
712 if ( empty( $group ) ) {
713 $group = 'default';
714 }
715
716 // Key is set internally, so we can use this value
717 if ( $this->_isset_internal( $key, $group ) && ! $force ) {
718 $this->cache_hits += 1;
719 $found = true;
720 return $this->_get_internal( $key, $group );
721 }
722
723 // Not a persistent group, so don't try Redis if the value doesn't exist
724 // internally
725 if ( ! $this->_should_persist( $group ) ) {
726 $this->cache_misses += 1;
727 $found = false;
728 return false;
729 }
730
731 if ( $this->_should_use_redis_hashes( $group ) ) {
732 $redis_safe_group = $this->_key( '', $group );
733 $value = $this->_call_redis( 'hGet', $redis_safe_group, $key );
734 } else {
735 $id = $this->_key( $key, $group );
736 $value = $this->_call_redis( 'get', $id );
737 }
738
739 // PhpRedis returns `false` when the key doesn't exist
740 if ( false === $value ) {
741 $this->cache_misses += 1;
742 $found = false;
743 return false;
744 }
745
746 // All non-numeric values are serialized
747 $value = is_numeric( $value ) ? intval( $value ) : unserialize( $value );
748
749 $this->_set_internal( $key, $group, $value );
750 $this->cache_hits += 1;
751 $found = true;
752 return $value;
753 }
754
755 /**
756 * Retrieves multiple values from the cache in one call.
757 *
758 * @param array $keys Array of keys under which the cache contents are stored.
759 * @param string $group Optional. Where the cache contents are grouped. Default empty.
760 * @param bool $force Optional. Whether to force an update of the local cache
761 * from the persistent cache. Default false.
762 * @return array Array of values organized into groups.
763 */
764 public function get_multiple( $keys, $group = 'default', $force = false ) {
765 if ( empty( $group ) ) {
766 $group = 'default';
767 }
768
769 $cache = array();
770 if ( ! $this->_should_persist( $group ) ) {
771 foreach ( $keys as $key ) {
772 $cache[ $key ] = $this->_isset_internal( $key, $group ) ? $this->_get_internal( $key, $group ) : false;
773 false !== $cache[ $key ] ? $this->cache_hits++ : $this->cache_misses++;
774 }
775 return $cache;
776 }
777
778 // Attempt to fetch values from the internal cache.
779 if ( ! $force ) {
780 foreach ( $keys as $key ) {
781 if ( $this->_isset_internal( $key, $group ) ) {
782 $cache[ $key ] = $this->_get_internal( $key, $group );
783 $this->cache_hits++;
784 }
785 }
786 }
787 $remaining_keys = array_values( array_diff( $keys, array_keys( $cache ) ) );
788 // If all keys were satisfied by the internal cache, we're sorted.
789 if ( empty( $remaining_keys ) ) {
790 return $cache;
791 }
792 if ( $this->_should_use_redis_hashes( $group ) ) {
793 $redis_safe_group = $this->_key( '', $group );
794 $results = $this->_call_redis( 'hmGet', $redis_safe_group, $remaining_keys );
795 $results = is_array( $results ) ? array_values( $results ) : $results;
796 } else {
797 $ids = array();
798 foreach ( $remaining_keys as $key ) {
799 $ids[] = $this->_key( $key, $group );
800 }
801 $results = $this->_call_redis( 'mget', $ids );
802 }
803 // Process the results from the Redis call.
804 foreach ( $remaining_keys as $i => $key ) {
805 $value = isset( $results[ $i ] ) ? $results[ $i ] : false;
806 if ( false !== $value ) {
807 // All non-numeric values are serialized
808 $value = is_numeric( $value ) ? intval( $value ) : unserialize( $value );
809 $this->_set_internal( $key, $group, $value );
810 $this->cache_hits++;
811 } else {
812 $this->cache_misses++;
813 }
814 $cache[ $key ] = $value;
815 }
816 // Make sure return values are returned in the order of the passed keys.
817 $return_cache = array();
818 foreach ( $keys as $key ) {
819 $return_cache[ $key ] = isset( $cache[ $key ] ) ? $cache[ $key ] : false;
820 }
821 return $return_cache;
822 }
823
824 /**
825 * Increment numeric cache item's value
826 *
827 * @param int|string $key The cache key to increment
828 * @param int $offset The amount by which to increment the item's value. Default is 1.
829 * @param string $group The group the key is in.
830 * @return false|int False on failure, the item's new value on success.
831 */
832 public function incr( $key, $offset = 1, $group = 'default' ) {
833
834 if ( empty( $group ) ) {
835 $group = 'default';
836 }
837
838 // The key needs to exist in order to be incremented
839 if ( ! $this->_exists( $key, $group ) ) {
840 return false;
841 }
842
843 $offset = (int) $offset;
844
845 // If this isn't a persistent group, we have to sort this out ourselves, grumble grumble.
846 if ( ! $this->_should_persist( $group ) ) {
847 $existing = $this->_get_internal( $key, $group );
848 if ( empty( $existing ) || ! is_numeric( $existing ) ) {
849 $existing = 1;
850 } else {
851 $existing += $offset;
852 }
853 if ( $existing < 0 ) {
854 $existing = 0;
855 }
856 $this->_set_internal( $key, $group, $existing );
857 return $existing;
858 }
859
860 if ( $this->_should_use_redis_hashes( $group ) ) {
861 $redis_safe_group = $this->_key( '', $group );
862 $result = $this->_call_redis( 'hIncrBy', $redis_safe_group, $key, $offset, $group );
863 if ( $result < 0 ) {
864 $result = 0;
865 $this->_call_redis( 'hSet', $redis_safe_group, $key, $result );
866 }
867 } else {
868 $id = $this->_key( $key, $group );
869 $result = $this->_call_redis( 'incrBy', $id, $offset );
870 if ( $result < 0 ) {
871 $result = 0;
872 $this->_call_redis( 'set', $id, $result );
873 }
874 }
875
876 if ( is_int( $result ) ) {
877 $this->_set_internal( $key, $group, $result );
878 }
879 return $result;
880 }
881
882 /**
883 * Replace the contents in the cache, if contents already exist
884 * @see WP_Object_Cache::set()
885 *
886 * @param int|string $key What to call the contents in the cache
887 * @param mixed $data The contents to store in the cache
888 * @param string $group Where to group the cache contents
889 * @param int $expire When to expire the cache contents
890 * @return bool False if not exists, true if contents were replaced
891 */
892 public function replace( $key, $data, $group = 'default', $expire = WP_REDIS_DEFAULT_EXPIRE_SECONDS ) {
893
894 if ( empty( $group ) ) {
895 $group = 'default';
896 }
897
898 if ( ! $this->_exists( $key, $group ) ) {
899 return false;
900 }
901
902 return $this->set( $key, $data, $group, (int) $expire );
903 }
904
905 /**
906 * Reset keys
907 *
908 * @deprecated 3.5.0
909 */
910 public function reset() {
911 _deprecated_function( __FUNCTION__, '3.5', 'switch_to_blog()' );
912 }
913
914 /**
915 * Sets the data contents into the cache
916 *
917 * The cache contents is grouped by the $group parameter followed by the
918 * $key. This allows for duplicate ids in unique groups. Therefore, naming of
919 * the group should be used with care and should follow normal function
920 * naming guidelines outside of core WordPress usage.
921 *
922 * The $expire parameter is not used, because the cache will automatically
923 * expire for each time a page is accessed and PHP finishes. The method is
924 * more for cache plugins which use files.
925 *
926 * @param int|string $key What to call the contents in the cache
927 * @param mixed $data The contents to store in the cache
928 * @param string $group Where to group the cache contents
929 * @param int $expire TTL for the data, in seconds
930 * @return bool Always returns true
931 */
932 public function set( $key, $data, $group = 'default', $expire = WP_REDIS_DEFAULT_EXPIRE_SECONDS ) {
933
934 if ( empty( $group ) ) {
935 $group = 'default';
936 }
937
938 if ( is_object( $data ) ) {
939 $data = clone $data;
940 }
941
942 $this->_set_internal( $key, $group, $data );
943
944 if ( ! $this->_should_persist( $group ) ) {
945 return true;
946 }
947
948 // If this is an integer, store it as such. Otherwise, serialize it.
949 if ( ! is_numeric( $data ) || intval( $data ) !== $data ) {
950 $data = serialize( $data );
951 }
952
953 // Redis doesn't support expire on hash group keys
954 if ( $this->_should_use_redis_hashes( $group ) ) {
955 $redis_safe_group = $this->_key( '', $group );
956 $this->_call_redis( 'hSet', $redis_safe_group, $key, $data );
957 return true;
958 }
959
960 $id = $this->_key( $key, $group );
961 if ( empty( $expire ) ) {
962 $this->_call_redis( 'set', $id, $data );
963 } else {
964 $this->_call_redis( 'setex', $id, $expire, $data );
965 }
966 return true;
967 }
968
969 /**
970 * Echoes the stats of the caching.
971 *
972 * Gives the cache hits, and cache misses. Also prints every cached group,
973 * key and the data.
974 */
975 public function stats() {
976 $total_redis_calls = 0;
977 foreach ( $this->redis_calls as $method => $calls ) {
978 $total_redis_calls += $calls;
979 }
980 $out = array();
981 $out[] = '<p>';
982 $out[] = '<strong>Cache Hits:</strong>' . (int) $this->cache_hits . '<br />';
983 $out[] = '<strong>Cache Misses:</strong>' . (int) $this->cache_misses . '<br />';
984 $out[] = '<strong>Redis Client:</strong>' . get_class( $this->redis ) . '<br />';
985 $out[] = '<strong>Redis Calls:</strong>' . (int) $total_redis_calls . ':<br />';
986 foreach ( $this->redis_calls as $method => $calls ) {
987 $out[] = ' - ' . esc_html( $method ) . ': ' . (int) $calls . '<br />';
988 }
989 $out[] = '</p>';
990 $out[] = '<ul>';
991 foreach ( $this->cache as $group => $cache ) {
992 $out[] = '<li><strong>Group:</strong> ' . esc_html( $group ) . ' - ( ' . number_format( strlen( serialize( $cache ) ) / 1024, 2 ) . 'k )</li>';
993 }
994 $out[] = '</ul>';
995 echo implode( PHP_EOL, $out ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,WordPressDotOrg.sniffs.OutputEscaping.UnescapedOutputParameter
996 }
997
998 /**
999 * Switch the internal blog id.
1000 *
1001 * This changes the blog id used to create keys in blog specific groups.
1002 *
1003 * @param int $blog_id Blog ID
1004 */
1005 public function switch_to_blog( $blog_id ) {
1006 $blog_id = (int) $blog_id;
1007 $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
1008 }
1009
1010 /**
1011 * Utility function to determine whether a key exists in the cache.
1012 *
1013 * @access protected
1014 */
1015 protected function _exists( $key, $group ) {
1016 if ( $this->_isset_internal( $key, $group ) ) {
1017 return true;
1018 }
1019
1020 if ( ! $this->_should_persist( $group ) ) {
1021 return false;
1022 }
1023
1024 if ( $this->_should_use_redis_hashes( $group ) ) {
1025 $redis_safe_group = $this->_key( '', $group );
1026 return $this->_call_redis( 'hExists', $redis_safe_group, $key );
1027 }
1028 $id = $this->_key( $key, $group );
1029 return $this->_call_redis( 'exists', $id );
1030 }
1031
1032 /**
1033 * Check whether there's a value in the internal object cache.
1034 *
1035 * @param string $key
1036 * @param string $group
1037 * @return boolean
1038 */
1039 protected function _isset_internal( $key, $group ) {
1040 if ( $this->_should_use_redis_hashes( $group ) ) {
1041 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
1042 return isset( $this->cache[ $multisite_safe_group ] ) && array_key_exists( $key, $this->cache[ $multisite_safe_group ] );
1043 } else {
1044 $key = $this->_key( $key, $group );
1045 return array_key_exists( $key, $this->cache );
1046 }
1047 }
1048
1049 /**
1050 * Get a value from the internal object cache
1051 *
1052 * @param string $key
1053 * @param string $group
1054 * @return mixed
1055 */
1056 protected function _get_internal( $key, $group ) {
1057 $value = null;
1058 if ( $this->_should_use_redis_hashes( $group ) ) {
1059 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
1060 if ( isset( $this->cache[ $multisite_safe_group ] ) && array_key_exists( $key, $this->cache[ $multisite_safe_group ] ) ) {
1061 $value = $this->cache[ $multisite_safe_group ][ $key ];
1062 }
1063 } else {
1064 $key = $this->_key( $key, $group );
1065 if ( array_key_exists( $key, $this->cache ) ) {
1066 $value = $this->cache[ $key ];
1067 }
1068 }
1069 if ( is_object( $value ) ) {
1070 return clone $value;
1071 }
1072 return $value;
1073 }
1074
1075 /**
1076 * Set a value to the internal object cache
1077 *
1078 * @param string $key
1079 * @param string $group
1080 * @param mixed $value
1081 */
1082 protected function _set_internal( $key, $group, $value ) {
1083 if ( $this->_should_use_redis_hashes( $group ) ) {
1084 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
1085 if ( ! isset( $this->cache[ $multisite_safe_group ] ) ) {
1086 $this->cache[ $multisite_safe_group ] = array();
1087 }
1088 $this->cache[ $multisite_safe_group ][ $key ] = $value;
1089 } else {
1090 $key = $this->_key( $key, $group );
1091 $this->cache[ $key ] = $value;
1092 }
1093 }
1094
1095 /**
1096 * Unset a value from the internal object cache
1097 *
1098 * @param string $key
1099 * @param string $group
1100 */
1101 protected function _unset_internal( $key, $group ) {
1102 if ( $this->_should_use_redis_hashes( $group ) ) {
1103 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
1104 if ( isset( $this->cache[ $multisite_safe_group ] ) && array_key_exists( $key, $this->cache[ $multisite_safe_group ] ) ) {
1105 unset( $this->cache[ $multisite_safe_group ][ $key ] );
1106 }
1107 } else {
1108 $key = $this->_key( $key, $group );
1109 if ( array_key_exists( $key, $this->cache ) ) {
1110 unset( $this->cache[ $key ] );
1111 }
1112 }
1113 }
1114
1115 /**
1116 * Utility function to generate the redis key for a given key and group.
1117 *
1118 * @param string $key The cache key.
1119 * @param string $group The cache group.
1120 * @return string A properly prefixed redis cache key.
1121 */
1122 protected function _key( $key = '', $group = 'default' ) {
1123 if ( empty( $group ) ) {
1124 $group = 'default';
1125 }
1126
1127 if ( ! empty( $this->global_groups[ $group ] ) ) {
1128 $prefix = $this->global_prefix;
1129 } else {
1130 $prefix = $this->blog_prefix;
1131 }
1132
1133 return preg_replace( '/\s+/', '', WP_CACHE_KEY_SALT . "$prefix$group:$key" );
1134 }
1135
1136 /**
1137 * Does this group use persistent storage?
1138 *
1139 * @param string $group Cache group.
1140 * @return bool true if the group is persistent, false if not.
1141 */
1142 protected function _should_persist( $group ) {
1143 return empty( $this->non_persistent_groups[ $group ] );
1144 }
1145
1146 /**
1147 * Should this group use Redis hashes?
1148 *
1149 * @param string $group Cache group.
1150 * @return bool True if the group should use Redis hashes, false if not.
1151 */
1152 protected function _should_use_redis_hashes( $group ) {
1153 if ( self::USE_GROUPS || ! empty( $this->redis_hash_groups[ $group ] ) ) {
1154 return true;
1155 }
1156 return false;
1157 }
1158
1159 /**
1160 * Wrapper method for connecting to Redis, which lets us retry the connection
1161 */
1162 protected function _connect_redis() {
1163 global $redis_server;
1164
1165 $check_dependencies = array( $this, 'check_client_dependencies' );
1166 /**
1167 * Permits alternate dependency check mechanism to be used.
1168 *
1169 * @param callable $check_dependencies Callback to execute.
1170 */
1171 $check_dependencies = apply_filters( 'wp_redis_check_client_dependencies_callback', $check_dependencies );
1172 $dependencies_ok = call_user_func( $check_dependencies );
1173 if ( true !== $dependencies_ok ) {
1174 $this->is_redis_connected = false;
1175 $this->missing_redis_message = $dependencies_ok;
1176 return $this->is_redis_connected;
1177 }
1178 $client_parameters = $this->build_client_parameters( $redis_server );
1179
1180 try {
1181 $client_connection = array( $this, 'prepare_client_connection' );
1182 /**
1183 * Permits alternate initial client connection mechanism to be used.
1184 *
1185 * @param callable $client_connection Callback to execute.
1186 */
1187 $client_connection = apply_filters( 'wp_redis_prepare_client_connection_callback', $client_connection );
1188 $this->redis = call_user_func_array( $client_connection, array( $client_parameters ) );
1189 } catch ( Exception $e ) {
1190 $this->_exception_handler( $e );
1191 $this->is_redis_connected = false;
1192 return $this->is_redis_connected;
1193 }
1194
1195 $keys_methods = array(
1196 'auth' => 'auth',
1197 'database' => 'select',
1198 );
1199
1200 try {
1201 $setup_connection = array( $this, 'perform_client_connection' );
1202 /**
1203 * Permits alternate setup client connection mechanism to be used.
1204 *
1205 * @param callable $setup_connection Callback to execute.
1206 */
1207 $setup_connection = apply_filters( 'wp_redis_perform_client_connection_callback', $setup_connection );
1208 call_user_func_array( $setup_connection, array( $this->redis, $client_parameters, $keys_methods ) );
1209 } catch ( Exception $e ) {
1210 $this->_exception_handler( $e );
1211 $this->is_redis_connected = false;
1212 return $this->is_redis_connected;
1213 }
1214
1215 $this->is_redis_connected = $this->redis->isConnected();
1216 if ( ! $this->is_redis_connected ) {
1217 $this->missing_redis_message = 'Warning! WP Redis object cache cannot connect to Redis server.';
1218 }
1219 return $this->is_redis_connected;
1220 }
1221
1222 /**
1223 * Are the required dependencies for connecting to Redis available?
1224 *
1225 * @return mixed True if the required dependencies are present, string if
1226 * not with a message describing the issue.
1227 */
1228 public function check_client_dependencies() {
1229 $class_to_check = 'Redis';
1230 if ( defined( 'WP_REDIS_USE_RELAY' ) && WP_REDIS_USE_RELAY ) {
1231 $class_to_check = 'Relay\Relay';
1232 }
1233
1234 if ( ! class_exists( $class_to_check ) ) {
1235 return 'Warning! PHPRedis extension is unavailable, which is required by WP Redis object cache.';
1236 }
1237
1238 return true;
1239 }
1240
1241 /**
1242 * Builds an array to be passed to a function that will set up the Redis
1243 * client.
1244 *
1245 * @param array $redis_server Parameters used to construct a Redis client.
1246 * @return array Final parameters to use to construct a Redis client with
1247 * with defaults applied.
1248 */
1249 public function build_client_parameters( $redis_server ) {
1250 // Default Redis port.
1251 $port = 6379;
1252 // Default Redis database number.
1253 $database = 0;
1254
1255 if ( empty( $redis_server ) ) {
1256 // Attempt to automatically load Pantheon's Redis config from the env.
1257 if ( isset( $_SERVER['CACHE_HOST'] ) ) {
1258 $redis_server = [
1259 // Don't use WP methods to sanitize the host due to plugin loading issues with other caching methods.
1260 // @phpcs:ignore WordPressVIPMinimum.Functions.StripTags.StripTagsOneParameter,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1261 'host' => strip_tags( $_SERVER['CACHE_HOST'] ),
1262 'port' => ! empty( $_SERVER['CACHE_PORT'] ) ? intval( $_SERVER['CACHE_PORT'] ) : $port,
1263 // Don't attempt to sanitize passwords as this can break authentication.
1264 // @phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1265 'auth' => ! empty( $_SERVER['CACHE_PASSWORD'] ) ? $_SERVER['CACHE_PASSWORD'] : null,
1266 'database' => ! empty( $_SERVER['CACHE_DB'] ) ? intval( $_SERVER['CACHE_DB'] ) : $database,
1267 ];
1268 } else {
1269 $redis_server = [
1270 'host' => '127.0.0.1',
1271 'port' => $port,
1272 'database' => $database,
1273 ];
1274 }
1275 }
1276
1277 if ( file_exists( $redis_server['host'] ) && 'socket' === filetype( $redis_server['host'] ) ) { // unix socket connection.
1278 // port must be null or socket won't connect.
1279 unset( $redis_server['port'] );
1280 $port = -1;
1281 }
1282
1283 $defaults = [
1284 'host' => $redis_server['host'],
1285 'port' => $port,
1286 'timeout' => 1000, // I multiplied this by 1000 so we'd have a common measure of ms instead of s and ms, need to make sure this gets divided by 1000.
1287 'retry_interval' => 100,
1288 ];
1289 // 1s timeout, 100ms delay between reconnections.
1290
1291 // merging the defaults with the original $redis_server enables any custom parameters to get sent downstream to the redis client.
1292 return array_replace_recursive( $defaults, $redis_server );
1293 }
1294
1295 /**
1296 * Constructs a PHPRedis Redis client.
1297 *
1298 * @param array $client_parameters Parameters used to construct a Redis client.
1299 * @return Redis Redis client.
1300 */
1301 public function prepare_client_connection( $client_parameters ) {
1302 if ( defined( 'WP_REDIS_USE_RELAY' ) && WP_REDIS_USE_RELAY ) {
1303 $redis = new Relay\Relay;
1304 } else {
1305 $redis = new Redis;
1306 }
1307
1308 $redis->connect(
1309 $client_parameters['host'],
1310 $client_parameters['port'],
1311 // $client_parameters['timeout'] is sent in milliseconds,
1312 // connect() takes seconds, so divide by 1000
1313 $client_parameters['timeout'] / 1000,
1314 null,
1315 $client_parameters['retry_interval']
1316 );
1317
1318 return $redis;
1319 }
1320
1321 /**
1322 * Sets up the Redis connection (ie authentication and specific database).
1323 *
1324 * @param Redis $redis Redis client.
1325 * @param array $client_parameters Parameters used to configure Redis.
1326 * @param array $keys_methods Associative array of keys from
1327 * $client_parameters to use as method arguments for $redis.
1328 * @return bool True if successful.
1329 */
1330 public function perform_client_connection( $redis, $client_parameters, $keys_methods ) {
1331 foreach ( $keys_methods as $key => $method ) {
1332 if ( ! isset( $client_parameters[ $key ] ) ) {
1333 continue;
1334 }
1335 try {
1336 $redis->$method( $client_parameters[ $key ] );
1337 } catch ( RedisException $e ) {
1338
1339 /**
1340 * PhpRedis throws an Exception when it fails a server call.
1341 * To prevent WordPress from fataling, we catch the Exception.
1342 */
1343 throw new Exception( $e->getMessage(), $e->getCode(), $e );
1344 }
1345 }
1346 return true;
1347 }
1348
1349 /**
1350 * Wrapper method for calls to Redis, which fails gracefully when Redis is unavailable
1351 *
1352 * @param string $method
1353 * @param mixed $args
1354 * @return mixed
1355 */
1356 protected function _call_redis( $method ) {
1357 global $wpdb;
1358
1359 $arguments = func_get_args();
1360 array_shift( $arguments ); // ignore $method
1361
1362 // $group is intended for the failback, and isn't passed to the Redis callback
1363 if ( 'hIncrBy' === $method ) {
1364 $group = array_pop( $arguments );
1365 }
1366
1367 if ( $this->is_redis_connected ) {
1368 try {
1369 if ( ! isset( $this->redis_calls[ $method ] ) ) {
1370 $this->redis_calls[ $method ] = 0;
1371 }
1372 $this->redis_calls[ $method ]++;
1373 $retval = call_user_func_array( array( $this->redis, $method ), $arguments );
1374 return $retval;
1375 } catch ( Exception $e ) {
1376 $retry_exception_messages = $this->retry_exception_messages();
1377 /**
1378 * PhpRedis throws an Exception when it fails a server call.
1379 * To prevent WordPress from fataling, we catch the Exception.
1380 */
1381 if ( $this->exception_message_matches( $e->getMessage(), $retry_exception_messages ) ) {
1382
1383 $this->_exception_handler( $e );
1384
1385 // Attempt to refresh the connection if it was successfully established once
1386 // $this->is_redis_connected will be set inside _connect_redis()
1387 if ( $this->_connect_redis() ) {
1388 return call_user_func_array( array( $this, '_call_redis' ), array_merge( array( $method ), $arguments ) );
1389 }
1390 // Fall through to fallback below
1391 } else {
1392 throw $e;
1393 }
1394 }
1395 } // End if().
1396
1397 if ( $this->is_redis_failback_flush_enabled() && ! $this->do_redis_failback_flush && ! empty( $wpdb ) ) {
1398 if ( $this->multisite ) {
1399 $table = $wpdb->sitemeta;
1400 $col1 = 'meta_key';
1401 $col2 = 'meta_value';
1402 } else {
1403 $table = $wpdb->options;
1404 $col1 = 'option_name';
1405 $col2 = 'option_value';
1406 }
1407 // @codingStandardsIgnoreStart
1408 $wpdb->query( "INSERT IGNORE INTO {$table} ({$col1},{$col2}) VALUES ('wp_redis_do_redis_failback_flush',1)" );
1409 // @codingStandardsIgnoreEnd
1410 $this->do_redis_failback_flush = true;
1411 }
1412
1413 // Mock expected behavior from Redis for these methods
1414 switch ( $method ) {
1415 case 'incr':
1416 case 'incrBy':
1417 $val = $this->cache[ $arguments[0] ];
1418 $offset = isset( $arguments[1] ) && 'incrBy' === $method ? $arguments[1] : 1;
1419 $val = $val + $offset;
1420 return $val;
1421 case 'hIncrBy':
1422 $val = $this->_get_internal( $arguments[1], $group );
1423 return $val + $arguments[2];
1424 case 'decrBy':
1425 case 'decr':
1426 $val = $this->cache[ $arguments[0] ];
1427 $offset = isset( $arguments[1] ) && 'decrBy' === $method ? $arguments[1] : 1;
1428 $val = $val - $offset;
1429 return $val;
1430 case 'del':
1431 case 'hDel':
1432 return 1;
1433 case 'flushAll':
1434 case 'flushdb':
1435 case 'IsConnected':
1436 case 'exists':
1437 case 'get':
1438 case 'mget':
1439 case 'hGet':
1440 case 'hmGet':
1441 return false;
1442 }
1443
1444 }
1445
1446 /**
1447 * Returns a filterable array of expected Exception messages that may be thrown
1448 *
1449 * @return array Array of expected exception messages
1450 */
1451 public function retry_exception_messages() {
1452 $retry_exception_messages = array( 'socket error on read socket', 'Connection closed', 'Redis server went away' );
1453 return apply_filters( 'wp_redis_retry_exception_messages', $retry_exception_messages );
1454 }
1455
1456 /**
1457 * Compares individual message to list of messages.
1458 *
1459 * @param string $error Message to compare
1460 * @param array $errors Array of messages to compare to
1461 * @return bool whether $error matches any items in $errors
1462 */
1463 public function exception_message_matches( $error, $errors ) {
1464 foreach ( $errors as $message ) {
1465 $pattern = $this->_format_message_for_pattern( $message );
1466 $matches = (bool) preg_match( $pattern, $error );
1467 if ( $matches ) {
1468 return true;
1469 }
1470 }
1471 return false;
1472 }
1473
1474 /**
1475 * Prepends and appends '/' if not present in a string
1476 *
1477 * @param string $message Potential regex string that may need '/'
1478 * @return string Regex pattern
1479 */
1480 protected function _format_message_for_pattern( $message ) {
1481 $var = $message;
1482 $var = '/' === $var[0] ? $var : '/' . $var;
1483 $var = '/' === $var[ strlen( $var ) - 1 ] ? $var : $var . '/';
1484 return $var;
1485 }
1486
1487 /**
1488 * Handles exceptions by triggering a php error.
1489 *
1490 * @param Exception $exception
1491 * @return null
1492 */
1493 protected function _exception_handler( $exception ) {
1494 try {
1495 $this->last_triggered_error = 'WP Redis: ' . $exception->getMessage();
1496 // pc:fix
1497 $this->missing_redis_message = 'Redis Connection error, please check the credentials!';
1498 // Be friendly to developers debugging production servers by triggering an error
1499 // @codingStandardsIgnoreStart
1500 trigger_error( $this->last_triggered_error, E_USER_WARNING );
1501 // @codingStandardsIgnoreEnd
1502 } catch ( PHPUnit_Framework_Error_Warning $e ) {
1503 // PHPUnit throws an Exception when `trigger_error()` is called.
1504 // To ensure our tests (which expect Exceptions to be caught) continue to run,
1505 // we catch the PHPUnit exception and inspect the RedisException message
1506 }
1507 }
1508
1509 /**
1510 * Admin UI to let the end user know something about the Redis connection isn't working.
1511 */
1512 public function wp_action_admin_notices_warn_missing_redis() {
1513 if ( ! current_user_can( 'manage_options' ) || empty( $this->missing_redis_message ) ) {
1514 return;
1515 }
1516 echo '<div class="message error"><p>' . esc_html( $this->missing_redis_message ) . '</p></div>';
1517 }
1518
1519 /**
1520 * Whether or not wakeup flush is enabled
1521 *
1522 * @return bool
1523 */
1524 private function is_redis_failback_flush_enabled() {
1525 if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) {
1526 return false;
1527 } elseif ( defined( 'WP_REDIS_DISABLE_FAILBACK_FLUSH' ) && WP_REDIS_DISABLE_FAILBACK_FLUSH ) {
1528 return false;
1529 }
1530 return true;
1531 }
1532
1533 /**
1534 * Sets up object properties; PHP 5 style constructor
1535 *
1536 * @return null|WP_Object_Cache If cache is disabled, returns null.
1537 */
1538 public function __construct() {
1539 global $blog_id, $table_prefix, $wpdb;
1540
1541 $this->multisite = is_multisite();
1542 $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
1543
1544 if ( ! $this->_connect_redis() && function_exists( 'add_action' ) ) {
1545 add_action( 'admin_notices', array( $this, 'wp_action_admin_notices_warn_missing_redis' ) );
1546 }
1547
1548 if ( $this->is_redis_failback_flush_enabled() && ! empty( $wpdb ) ) {
1549 if ( $this->multisite ) {
1550 $table = $wpdb->sitemeta;
1551 $col1 = 'meta_key';
1552 $col2 = 'meta_value';
1553 } else {
1554 $table = $wpdb->options;
1555 $col1 = 'option_name';
1556 $col2 = 'option_value';
1557 }
1558 // @codingStandardsIgnoreStart
1559 $this->do_redis_failback_flush = (bool) $wpdb->get_results( "SELECT {$col2} FROM {$table} WHERE {$col1}='wp_redis_do_redis_failback_flush'" );
1560 // @codingStandardsIgnoreEnd
1561 if ( $this->is_redis_connected && $this->do_redis_failback_flush ) {
1562 $ret = $this->_call_redis( 'flushdb' );
1563 if ( $ret ) {
1564 // @codingStandardsIgnoreStart
1565 $wpdb->query( "DELETE FROM {$table} WHERE {$col1}='wp_redis_do_redis_failback_flush'" );
1566 // @codingStandardsIgnoreEnd
1567 $this->do_redis_failback_flush = false;
1568 }
1569 }
1570 }
1571
1572 $this->global_prefix = ( $this->multisite || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
1573
1574 /**
1575 * @todo This should be moved to the PHP4 style constructor, PHP5
1576 * already calls __destruct()
1577 */
1578 register_shutdown_function( array( $this, '__destruct' ) );
1579 }
1580
1581 /**
1582 * Will save the object cache before object is completely destroyed.
1583 *
1584 * Called upon object destruction, which should be when PHP ends.
1585 *
1586 * @return bool True value. Won't be used by PHP
1587 */
1588 public function __destruct() {
1589 return true;
1590 }
1591 }
1592