cluster

Infrastructure files for Nordgedanken and Midnightthoughts.
git clone git://archive.git.mtrnord.blog/MTRNord/cluster.git
Log | Files | Refs | README

object-cache.php (105926B)


      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