File "object-cache.php"
Full Path: /home/digimqhe/flashdigi.uk/comment-content/cgi-bin/core/object-cache.php
File size: 37.11 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Hummingbird Redis Object Cache
*
* @link https://wpmudev.com/project/wp-hummingbird/
* @since 2.5.0
* @package Hummingbird
*
* @wordpress-plugin
* Plugin Name: Hummingbird Redis Object Cache
* Plugin URI: https://wpmudev.com/project/wp-hummingbird/
* Description: Hummingbird object cache powered by Redis.
* Author: WPMU DEV
* Author URI: https://profiles.wordpress.org/wpmudev/
*
* Based on Eric Mann's and Erick Hitter's Redis Object Cache: https://github.com/ericmann/Redis-Object-Cache
* Based on Till Krüss's Redis Object Cache: https://wordpress.org/plugins/redis-cache/
*/
if ( ! defined( 'WPHB_REDIS_HOST' ) && ! defined( 'WPHB_REDIS_PORT' ) ) {
return;
}
/**
* Adds data to the cache, if the cache key doesn't already exist.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to use for retrieval later.
* @param mixed $data The data to add to the cache.
* @param string $group Optional. The group to add the cache to. Enables the same key
* to be used across groups. Default empty.
* @param int $expire Optional. When the cache data should expire, in seconds.
* Default 0 (no expiration).
* @return bool False if cache key and group already exist, true on success.
* @throws Exception Exception.
*/
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->add( $key, $data, $group, (int) $expire );
}
/**
* Closes the cache.
*
* This function has ceased to do anything since WordPress 2.5. The
* functionality was removed along with the rest of the persistent cache.
*
* This does not mean that plugins can't implement this function when they need
* to make sure that the cache is cleaned up after WordPress no longer needs it.
*
* @return true Always returns true.
*/
function wp_cache_close() {
return true;
}
/**
* Decrements numeric cache item's value.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to decrement.
* @param int $offset Optional. The amount by which to decrement the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default empty.
* @return false|int False on failure, the item's new value on success.
*/
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->decr( $key, $offset, $group );
}
/**
* Removes the cache contents matching key and group.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key What the contents in the cache are called.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @return bool True on successful removal, false on failure.
*/
function wp_cache_delete( $key, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->delete( $key, $group );
}
/**
* Removes all cache items.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @return bool False on failure, true on success
*/
function wp_cache_flush() {
global $wp_object_cache;
return $wp_object_cache->flush();
}
/**
* Retrieves the cache contents from the cache by key and group.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key under which the cache contents are stored.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param bool $force Optional. Whether to force an update of the local cache from the persistent
* cache. Default false.
* @param bool $found Optional. Whether the key was found in the cache (passed by reference).
* Disambiguates a return of false, a storable value. Default null.
* @return bool|mixed False on failure to retrieve contents or the cache
* contents on success
*/
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
global $wp_object_cache;
return $wp_object_cache->get( $key, $group, $force, $found );
}
/**
* Retrieve multiple values from cache.
*
* Gets multiple values from cache, including across multiple groups. Mirrors the Memcached Object Cache
* plugin's argument and return-value formats.
* Usage: array( 'group0' => array( 'key0', 'key1', 'key2', ), 'group1' => array( 'key0' ) )
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param array $groups Array of groups and keys to retrieve.
* @return bool|mixed Array of cached values, keys in the format $group:$key. Non-existent keys false
*/
function wp_cache_get_multi( $groups ) {
global $wp_object_cache;
return $wp_object_cache->get_multi( $groups );
}
/**
* Increment numeric cache item's value
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key for the cache contents that should be incremented.
* @param int $offset Optional. The amount by which to increment the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default empty.
* @return false|int False on failure, the item's new value on success.
*/
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->incr( $key, $offset, $group );
}
/**
* Sets up Object Cache Global and assigns it.
*
* @global WP_Object_Cache $wp_object_cache WordPress Object Cache
*
* @throws Exception Exception.
*/
function wp_cache_init() {
global $wp_object_cache;
$wp_object_cache = new WP_Object_Cache();
}
/**
* Replaces the contents of the cache with new data.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The key for the cache data that should be replaced.
* @param mixed $data The new data to store in the cache.
* @param string $group Optional. The group for the cache data that should be replaced.
* Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool False if original value does not exist, true if contents were replaced
* @throws Exception Exception.
*/
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
}
/**
* Saves the data to the cache.
*
* Differs from wp_cache_add() and wp_cache_replace() in that it will always write data.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int|string $key The cache key to use for retrieval later.
* @param mixed $data The contents to store in the cache.
* @param string $group Optional. Where to group the cache contents. Enables the same key
* to be used across groups. Default empty.
* @param int $expire Optional. When to expire the cache contents, in seconds.
* Default 0 (no expiration).
* @return bool False on failure, true on success
*/
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->set( $key, $data, $group, (int) $expire );
}
/**
* Switches the internal blog ID.
*
* This changes the blog id used to create keys in blog specific groups.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param int $blog_id Site ID.
*/
function wp_cache_switch_to_blog( $blog_id ) {
global $wp_object_cache;
$wp_object_cache->switch_to_blog( $blog_id );
}
/**
* Adds a group or set of groups to the list of global groups.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param string|array $groups A group or an array of groups to add.
*/
function wp_cache_add_global_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_global_groups( $groups );
}
/**
* Adds a group or set of groups to the list of non-persistent groups.
*
* @global WP_Object_Cache $wp_object_cache Object cache global instance.
*
* @param string|array $groups A group or an array of groups to add.
*/
function wp_cache_add_non_persistent_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_non_persistent_groups( $groups );
}
/**
* Class WP_Object_Cache
*/
class WP_Object_Cache {
/**
* The Redis client.
*
* @var mixed
*/
private $redis;
/**
* The Redis server version.
*
* @var null|string
*/
private $redis_version = null;
/**
* Track if Redis is available
*
* @var bool
*/
private $redis_connected = false;
/**
* Holds the non-Redis objects.
*
* @var array
*/
public $cache = array();
/**
* Name of the used Redis client
*
* @var bool
*/
public $redis_client = null;
/**
* List of global groups.
*
* @var array
*/
public $global_groups = array(
'blog-details',
'blog-id-cache',
'blog-lookup',
'global-posts',
'networks',
'rss',
'sites',
'site-details',
'site-lookup',
'site-options',
'site-transient',
'users',
'useremail',
'userlogins',
'usermeta',
'user_meta',
'userslugs',
);
/**
* List of groups that will not be flushed.
*
* @var array
*/
public $unflushable_groups = array();
/**
* List of groups not saved to Redis.
*
* @var array
*/
public $ignored_groups = array( 'counts', 'plugins' );
/**
* Prefix used for global groups.
*
* @var string
*/
public $global_prefix = '';
/**
* Prefix used for non-global groups.
*
* @var string
*/
public $blog_prefix = '';
/**
* Track how many requests were found in cache
*
* @var int
*/
public $cache_hits = 0;
/**
* Track how may requests were not cached
*
* @var int
*/
public $cache_misses = 0;
/**
* Error message for redis connection
*
* @var int
*/
public $redis_error = '';
/**
* WP_Object_Cache constructor.
*
* @throws Exception Exception.
*/
public function __construct() {
global $blog_id, $table_prefix;
$parameters = array(
'host' => defined( 'WPHB_REDIS_HOST' ) ? WPHB_REDIS_HOST : '127.0.0.1',
'port' => defined( 'WPHB_REDIS_PORT' ) ? WPHB_REDIS_PORT : 6379,
'timeout' => 5,
'read_timeout' => 5,
'retry_interval' => 0,
);
if ( defined( 'WP_REDIS_IGNORED_GROUPS' ) && is_array( WP_REDIS_IGNORED_GROUPS ) ) {
$this->ignored_groups = array_unique( array_merge( $this->ignored_groups, array_map( array( $this, 'sanitize_key' ), WP_REDIS_IGNORED_GROUPS ) ) );
}
$client = 'predis';
if ( class_exists( 'Redis' ) && 0 !== strcasecmp( 'predis', $client ) ) {
$client = defined( 'HHVM_VERSION' ) ? 'hhvm' : 'pecl';
}
$scheme = '/' === substr( $parameters['host'], 0, 1 ) ? 'unix' : 'tcp';
try {
if ( 'unix' === $scheme && ( 'hhvm' === $client || 'pecl' === $client ) ) {
$parameters['port'] = null;
}
if ( 'hhvm' === $client ) {
/* translators: %d: HHVM no */
$this->redis_client = sprintf( 'HHVM Extension (v%s)', constant( 'HHVM_VERSION' ) );
$this->redis = new Redis();
$this->redis->connect( $parameters['host'], $parameters['port'], $parameters['timeout'], null, $parameters['retry_interval'] );
if ( $parameters['read_timeout'] ) {
$this->redis->setOption( Redis::OPT_READ_TIMEOUT, $parameters['read_timeout'] );
}
}
if ( 'pecl' === $client ) {
$phpredis_version = phpversion( 'redis' );
/* translators: %s: PECL extesnion */
$this->redis_client = sprintf( 'PECL Extension (v%s)', $phpredis_version );
$this->redis = new Redis();
if ( version_compare( $phpredis_version, '3.1.3', '>=' ) ) {
$this->redis->connect( $parameters['host'], $parameters['port'], $parameters['timeout'], null, $parameters['retry_interval'], $parameters['read_timeout'] );
} else {
$this->redis->connect( $parameters['host'], $parameters['port'], $parameters['timeout'], null, $parameters['retry_interval'] );
}
}
if ( ( 'hhvm' === $client || 'pecl' === $client ) && defined( 'WPHB_REDIS_PASSWORD' ) ) {
$this->redis->auth( WPHB_REDIS_PASSWORD );
}
if ( ( 'hhvm' === $client || 'pecl' === $client ) && defined( 'WPHB_REDIS_DB_ID' ) ) {
$this->redis->select( WPHB_REDIS_DB_ID );
}
if ( 'predis' === $client ) {
$this->redis_client = 'Predis';
// Load bundled Predis library.
if ( ! class_exists( 'Predis\Client' ) ) {
$predis_pro = sprintf(
/* translators: %s: Path to plugin directory page */
'%s/wp-hummingbird/vendor/predis/predis/autoload.php',
defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins'
);
$predis_free = sprintf(
/* translators: %s: Path to plugin directory page */
'%s/hummingbird-performance/vendor/predis/predis/autoload.php',
defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins'
);
if ( file_exists( $predis_pro ) ) {
/* @noinspection PhpIncludeInspection */
include_once $predis_pro;
} elseif ( file_exists( $predis_free ) ) {
/* @noinspection PhpIncludeInspection */
include_once $predis_free;
} else {
throw new Exception( 'Predis library not found. Re-install Hummingbird plugin or delete object-cache.php.' );
}
}
$options = array();
if ( 'unix' === $scheme ) {
$parameters['scheme'] = $scheme;
$parameters['path'] = $parameters['host'];
unset( $parameters['host'] );
unset( $parameters['port'] );
}
if ( $parameters['read_timeout'] ) {
$parameters['read_write_timeout'] = $parameters['read_timeout'];
}
if ( defined( 'WPHB_REDIS_PASSWORD' ) ) {
$options['parameters']['password'] = WPHB_REDIS_PASSWORD;
}
if ( defined( 'WPHB_REDIS_DB_ID' ) ) {
$options['parameters']['database'] = WPHB_REDIS_DB_ID;
}
$this->redis = new Predis\Client( $parameters, $options );
$this->redis->connect();
/* translators: %s: Predis client version */
$this->redis_client .= sprintf( ' (v%s)', Predis\Client::VERSION );
}
$this->redis->ping();
$server_info = $this->redis->info( 'SERVER' );
if ( isset( $server_info['redis_version'] ) ) {
$this->redis_version = $server_info['redis_version'];
} elseif ( isset( $server_info['Server']['redis_version'] ) ) {
$this->redis_version = $server_info['Server']['redis_version'];
}
$this->redis_connected = true;
} catch ( Exception $exception ) {
$this->redis_connected = false;
$this->redis_error = $exception->getMessage();
// When Redis is unavailable, fall back to the internal cache by forcing all groups to be "no redis" groups.
$this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $this->global_groups ) );
}
// Assign global and blog prefixes for use with keys.
if ( function_exists( 'is_multisite' ) ) {
$this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
$this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
}
}
/**
* Is Redis available?
*
* @return bool
*/
public function redis_status() {
return $this->redis_connected;
}
/**
* Returns the Redis instance.
*
* @return mixed
*/
public function redis_instance() {
return $this->redis;
}
/**
* Returns the Redis server version.
*
* @return null|string
*/
public function redis_version() {
return $this->redis_version;
}
/**
* Adds a value to cache.
*
* If the specified key already exists, the value is not stored and the function
* returns false.
*
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
* @param int $expiration The expiration time, defaults to 0.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function add( $key, $value, $group = 'default', $expiration = 0 ) {
return $this->add_or_replace( true, $key, $value, $group, (int) $expiration );
}
/**
* Replace a value in the cache.
*
* If the specified key doesn't exist, the value is not stored and the function
* returns false.
*
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
* @param int $expiration The expiration time, defaults to 0.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function replace( $key, $value, $group = 'default', $expiration = 0 ) {
return $this->add_or_replace( false, $key, $value, $group, (int) $expiration );
}
/**
* Add or replace a value in the cache.
*
* Add does not set the value if the key exists; replace does not replace if the value doesn't exist.
*
* @param bool $add True if should only add if value doesn't exist, false to only add when value already exists.
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
* @param int $expiration The expiration time, defaults to 0.
* @return bool Returns TRUE on success or FALSE on failure.
*/
protected function add_or_replace( $add, $key, $value, $group = 'default', $expiration = 0 ) {
$addition_suspended = function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition();
if ( $add && $addition_suspended ) {
return false;
}
$result = true;
$derived_key = $this->build_key( $key, $group );
// Save if group not excluded and redis is up.
if ( ! in_array( $group, $this->ignored_groups, true ) && $this->redis_status() ) {
$exists = $this->redis->exists( $derived_key );
if ( $add == $exists ) {
return false;
}
$expiration = $this->validate_expiration( $expiration );
if ( $expiration ) {
$result = $this->parse_redis_response( $this->redis->setex( $derived_key, $expiration, $this->maybe_serialize( $value ) ) );
} else {
$result = $this->parse_redis_response( $this->redis->set( $derived_key, $this->maybe_serialize( $value ) ) );
}
}
$exists = isset( $this->cache[ $derived_key ] );
if ( $add == $exists ) {
return false;
}
if ( $result ) {
$this->add_to_internal_cache( $derived_key, $value );
}
return $result;
}
/**
* Remove the item from the cache.
*
* @param string $key The key under which to store the value.
* @param string $group The group value appended to the $key.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function delete( $key, $group = 'default' ) {
$result = false;
$derived_key = $this->build_key( $key, $group );
if ( isset( $this->cache[ $derived_key ] ) ) {
unset( $this->cache[ $derived_key ] );
$result = true;
}
if ( $this->redis_status() && ! in_array( $group, $this->ignored_groups, true ) ) {
$result = $this->parse_redis_response( $this->redis->del( $derived_key ) );
}
return $result;
}
/**
* Invalidate all items in the cache.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function flush() {
$results = array();
$this->cache = array();
if ( $this->redis_status() ) {
if ( defined( 'WP_REDIS_SALT' ) && WP_REDIS_SALT ) {
$lua_script = $this->get_flush_closure( trim( WP_REDIS_SALT ) );
$results[] = $this->parse_redis_response( $lua_script() );
} else {
$results[] = $this->parse_redis_response( $this->redis->flushdb() );
}
}
if ( empty( $results ) ) {
return false;
}
foreach ( $results as $result ) {
if ( ! $result ) {
return false;
}
}
return true;
}
/**
* Returns a closure to flush selectively.
*
* @param string $salt The salt to be used to differentiate.
* @return callable Generated callable executing the lua script.
*/
protected function get_flush_closure( $salt ) {
if ( $this->unflushable_groups ) {
return $this->lua_flush_extended_closure( $salt );
} else {
return $this->lua_flush_closure( $salt );
}
}
/**
* Returns a closure ready to be called to flush selectively ignoring unflushable groups.
*
* @param string $salt The salt to be used to differentiate.
* @return callable Generated callable executing the lua script.
*/
protected function lua_flush_closure( $salt ) {
return function () use ( $salt ) {
if ( version_compare( $this->redis_version(), '3.2', '>=' ) ) {
$script = <<<LUA
redis.replicate_commands()
local cur = 0
local i = 0
local tmp
repeat
tmp = redis.call('SCAN', cur, 'MATCH', '{$salt}*')
cur = tonumber(tmp[1])
if tmp[2] then
for _, v in pairs(tmp[2]) do
redis.call('del', v)
i = i + 1
end
end
until 0 == cur
return i
LUA;
} else {
$script = <<<LUA2
local i = 0
for _,k in ipairs(redis.call('keys', '{$salt}*')) do
redis.call('del', k)
i = i + 1
end
return i
LUA2;
}
$args = ( $this->redis instanceof Predis\Client )
? array( $script, 0 )
: array( $script );
return call_user_func_array( array( $this->redis, 'eval' ), $args );
};
}
/**
* Returns a closure ready to be called to flush selectively.
*
* @param string $salt The salt to be used to differentiate.
* @return callable Generated callable executing the lua script.
*/
protected function lua_flush_extended_closure( $salt ) {
return function () use ( $salt ) {
$salt_length = strlen( $salt );
$non_flushable = array_map(
function ( $group ) {
return ":{$group}:";
},
$this->unflushable_groups
);
if ( version_compare( $this->redis_version(), '3.2', '>=' ) ) {
$script = <<<LUA
redis.replicate_commands()
local cur = 0
local i = 0
local d, tmp
repeat
tmp = redis.call('SCAN', cur, 'MATCH', '{$salt}*')
cur = tonumber(tmp[1])
if tmp[2] then
for _, v in pairs(tmp[2]) do
d = true
for _, s in pairs(KEYS) do
d = d and not v:find(s, {$salt_length})
if not d then break end
end
if d then
redis.call('del', v)
i = i + 1
end
end
end
until 0 == cur
return i
LUA;
} else {
$script = <<<LUA2
local i = 0
local d
for _,k in ipairs(redis.call('keys', '{$salt}*')) do
d = true
for _, s in pairs(KEYS) do
d = d and not k:find(s, {$salt_length})
if not d then break end
end
if d then
redis.call('del', k)
i = i + 1
end
end
return i
LUA2;
}
$args = ( $this->redis instanceof Predis\Client )
? array_merge( array( $script, count( $non_flushable ) ), $non_flushable )
: array( $script, $non_flushable, count( $non_flushable ) );
return call_user_func_array( array( $this->redis, 'eval' ), $args );
};
}
/**
* Retrieve object from cache.
*
* Gets an object from cache based on $key and $group.
*
* @param string $key The key under which to store the value.
* @param string $group The group value appended to the $key.
* @param bool $force Optional. Whether to force a refetch rather than relying on the local
* cache. Default false.
* @param bool|null $found Optional. Whether the key was found in the cache. Disambiguates a return of
* false, a storable value. Passed by reference. Default null.
* @return bool|mixed Cached object value.
*/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$derived_key = $this->build_key( $key, $group );
if ( isset( $this->cache[ $derived_key ] ) && ! $force ) {
$found = true;
$this->cache_hits++;
return is_object( $this->cache[ $derived_key ] ) ? clone $this->cache[ $derived_key ] : $this->cache[ $derived_key ];
} elseif ( in_array( $group, $this->ignored_groups, true ) || ! $this->redis_status() ) {
$found = false;
$this->cache_misses++;
return false;
}
$result = $this->redis->get( $derived_key );
if ( null === $result || false === $result ) {
$found = false;
$this->cache_misses++;
return false;
} else {
$found = true;
$this->cache_hits++;
// All non-numeric values are serialized.
$result = $this->maybe_unserialize( $result );
}
$this->add_to_internal_cache( $derived_key, $result );
return $result;
}
/**
* Retrieve multiple values from cache.
*
* Gets multiple values from cache, including across multiple groups. Mirrors the Memcached Object Cache plugin's
* argument and return-value formats.
* Usage: array( 'group0' => array( 'key0', 'key1', 'key2', ), 'group1' => array( 'key0' ) ).
*
* @param array $groups Array of groups and keys to retrieve.
* @return array|false Array of cached values, keys in the format $group:$key. Non-existent keys null.
*/
public function get_multi( $groups ) {
if ( empty( $groups ) || ! is_array( $groups ) ) {
return false;
}
// Retrieve requested caches and reformat results to mimic Memcached Object Cache's output.
$cache = array();
foreach ( $groups as $group => $keys ) {
if ( in_array( $group, $this->ignored_groups, true ) || ! $this->redis_status() ) {
foreach ( $keys as $key ) {
$cache[ $this->build_key( $key, $group ) ] = $this->get( $key, $group );
}
} else {
// Reformat arguments as expected by Redis.
$derived_keys = array();
foreach ( $keys as $key ) {
$derived_keys[] = $this->build_key( $key, $group );
}
// Retrieve from cache in a single request.
$group_cache = $this->redis->mget( $derived_keys );
// Build an array of values looked up, keyed by the derived cache key.
$group_cache = array_combine( $derived_keys, $group_cache );
// Restores cached data to its original data type.
$group_cache = array_map( array( $this, 'maybe_unserialize' ), $group_cache );
// Redis returns null for values not found in cache, but expected return value is false in this instance.
$group_cache = array_map( array( $this, 'filter_redis_get_multi' ), $group_cache );
$cache = array_merge( $cache, $group_cache );
}
}
// Add to the internal cache the found values from Redis.
foreach ( $cache as $key => $value ) {
if ( $value ) {
$this->cache_hits++;
$this->add_to_internal_cache( $key, $value );
} else {
$this->cache_misses++;
}
}
return $cache;
}
/**
* Sets a value in cache.
*
* The value is set whether or not this key already exists in Redis.
*
* @param string $key The key under which to store the value.
* @param mixed $value The value to store.
* @param string $group The group value appended to the $key.
* @param int $expiration The expiration time, defaults to 0.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function set( $key, $value, $group = 'default', $expiration = 0 ) {
$result = true;
$derived_key = $this->build_key( $key, $group );
// Save if group not excluded from redis and redis is up.
if ( ! in_array( $group, $this->ignored_groups, true ) && $this->redis_status() ) {
$expiration = $this->validate_expiration( $expiration );
if ( $expiration ) {
$result = $this->parse_redis_response( $this->redis->setex( $derived_key, $expiration, $this->maybe_serialize( $value ) ) );
} else {
$result = $this->parse_redis_response( $this->redis->set( $derived_key, $this->maybe_serialize( $value ) ) );
}
}
// If the set was successful, or we didn't go to redis.
if ( $result ) {
$this->add_to_internal_cache( $derived_key, $value );
}
return $result;
}
/**
* Increment a Redis counter by the amount specified.
*
* @param int|string $key The cache key to increment.
* @param int $offset Optional. The amount by which to increment the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default 'default'.
* @return false|int False on failure, the item's new value on success.
*/
public function incr( $key, $offset = 1, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
$offset = (int) $offset;
// If group is a non-Redis group, save to internal cache, not Redis.
if ( in_array( $group, $this->ignored_groups, true ) || ! $this->redis_status() ) {
$value = $this->get_from_internal_cache( $derived_key, $group );
$value += $offset;
$this->add_to_internal_cache( $derived_key, $value );
return $value;
}
// Save to Redis.
$result = $this->parse_redis_response( $this->redis->incrBy( $derived_key, $offset ) );
$this->add_to_internal_cache( $derived_key, (int) $this->redis->get( $derived_key ) );
return $result;
}
/**
* Decrement a Redis counter by the amount specified.
*
* @param int|string $key The cache key to decrement.
* @param int $offset Optional. The amount by which to decrement the item's value. Default 1.
* @param string $group Optional. The group the key is in. Default 'default'.
* @return false|int False on failure, the item's new value on success.
*/
public function decr( $key, $offset = 1, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
$offset = (int) $offset;
// If group is a non-Redis group, save to internal cache, not Redis.
if ( in_array( $group, $this->ignored_groups, true ) || ! $this->redis_status() ) {
$value = $this->get_from_internal_cache( $derived_key, $group );
$value -= $offset;
$this->add_to_internal_cache( $derived_key, $value );
return $value;
}
// Save to Redis.
$result = $this->parse_redis_response( $this->redis->decrBy( $derived_key, $offset ) );
$this->add_to_internal_cache( $derived_key, (int) $this->redis->get( $derived_key ) );
return $result;
}
/**
* Builds a key for the cached object using the prefix, group and key.
*
* @param string $key The key under which to store the value.
* @param string $group The group value appended to the $key.
*
* @return string
*/
public function build_key( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$salt = defined( 'WP_REDIS_SALT' ) ? trim( WP_REDIS_SALT ) . ':' : '';
$prefix = in_array( $group, $this->global_groups, true ) ? $this->global_prefix : $this->blog_prefix;
$key = str_replace( ':', '-', $key );
$group = str_replace( ':', '-', $group );
$prefix = trim( $prefix, '_-:$' );
return "{$salt}{$prefix}:{$group}:{$key}";
}
/**
* Convert data types when using Redis MGET
*
* When requesting multiple keys, those not found in cache are assigned the value null upon return.
* Expected value in this case is false, so we convert
*
* @param string $value Value to possibly convert.
* @return string Converted value
*/
protected function filter_redis_get_multi( $value ) {
if ( is_null( $value ) ) {
$value = false;
}
return $value;
}
/**
* Convert Redis responses into something meaningful
*
* @param mixed $response Redis response.
*
* @return bool|int|string
*/
protected function parse_redis_response( $response ) {
if ( is_bool( $response ) ) {
return $response;
}
if ( is_numeric( $response ) ) {
return $response;
}
if ( is_object( $response ) && method_exists( $response, 'getPayload' ) ) {
return $response->getPayload() === 'OK';
}
return false;
}
/**
* Simple wrapper for saving object to the internal cache.
*
* @param string $derived_key Key to save value under.
* @param mixed $value Object value.
*/
public function add_to_internal_cache( $derived_key, $value ) {
$this->cache[ $derived_key ] = $value;
}
/**
* Get a value specifically from the internal, run-time cache, not Redis.
*
* @param int|string $key Key value.
* @param int|string $group Group that the value belongs to.
*
* @return bool|mixed Value on success; false on failure.
*/
public function get_from_internal_cache( $key, $group ) {
$derived_key = $this->build_key( $key, $group );
if ( isset( $this->cache[ $derived_key ] ) ) {
return $this->cache[ $derived_key ];
}
return false;
}
/**
* In multisite, switch blog prefix when switching blogs
*
* @param int $_blog_id Blog ID.
* @return bool
*/
public function switch_to_blog( $_blog_id ) {
if ( ! function_exists( 'is_multisite' ) || ! is_multisite() ) {
return false;
}
$this->blog_prefix = $_blog_id;
return true;
}
/**
* Sets the list of global groups.
*
* @param array $groups List of groups that are global.
*/
public function add_global_groups( $groups ) {
$groups = (array) $groups;
if ( $this->redis_status() ) {
$this->global_groups = array_unique( array_merge( $this->global_groups, $groups ) );
} else {
$this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
}
}
/**
* Sets the list of groups not to be cached by Redis.
*
* @param array $groups List of groups that are to be ignored.
*/
public function add_non_persistent_groups( $groups ) {
$groups = (array) $groups;
$this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
}
/**
* Render data about current cache requests.
*/
public function stats() {
?>
<p>
<strong>Redis Status:</strong> <?php echo $this->redis_status() ? 'Connected' : 'Not Connected'; ?><br />
<strong>Redis Client:</strong> <?php echo $this->redis_client; ?><br />
<strong>Cache Hits:</strong> <?php echo $this->cache_hits; ?><br />
<strong>Cache Misses:</strong> <?php echo $this->cache_misses; ?>
</p>
<ul>
<?php foreach ( $this->cache as $group => $cache ) : ?>
<li>
<?php
printf( /* translators: %1$s - Group <a>, %2$s - Cache </a> */
'%s - %sk',
strip_tags( $group ),
number_format( strlen( serialize( $cache ) ) / 1024, 2 )
);
?>
</li>
<?php endforeach; ?>
</ul>
<?php
}
/**
* Serialize data, if needed.
*
* @param string|array|object $data Data that might be serialized.
*
* @return mixed A scalar data
*/
private function maybe_serialize( $data ) {
if ( is_array( $data ) || is_object( $data ) ) {
return serialize( $data );
}
/*
* Double serialization is required for backward compatibility.
* See https://core.trac.wordpress.org/ticket/12930
*/
if ( $this->is_serialized( $data, false ) ) {
return serialize( $data );
}
return $data;
}
/**
* Unserialize value only if it was serialized.
*
* @param string $original Maybe unserialized original, if is needed.
*
* @return mixed Unserialized data can be any type.
*/
public function maybe_unserialize( $original ) {
if ( $this->is_serialized( $original ) ) { // Don't attempt to unserialize data that wasn't serialized going in.
return @unserialize( $original );
}
return $original;
}
/**
* Check value to find if it was serialized.
*
* If $data is not an string, then returned value will always be false.
* Serialized data is always a string.
*
* @param string $data Value to check to see if was serialized.
* @param bool $strict Optional. Whether to be strict about the end of the string. Default true.
*
* @return bool False if not serialized and true if it was.
*/
private function is_serialized( $data, $strict = true ) {
// If it isn't a string, it isn't serialized.
if ( ! is_string( $data ) ) {
return false;
}
$data = trim( $data );
if ( 'N;' == $data ) {
return true;
}
if ( strlen( $data ) < 4 ) {
return false;
}
if ( ':' !== $data[1] ) {
return false;
}
if ( $strict ) {
$lastc = substr( $data, -1 );
if ( ';' !== $lastc && '}' !== $lastc ) {
return false;
}
} else {
$semicolon = strpos( $data, ';' );
$brace = strpos( $data, '}' );
// Either ; or } must exist.
if ( false === $semicolon && false === $brace ) {
return false;
}
// But neither must be in the first X characters.
if ( false !== $semicolon && $semicolon < 3 ) {
return false;
}
if ( false !== $brace && $brace < 4 ) {
return false;
}
}
$token = $data[0];
switch ( $token ) {
case 's':
if ( $strict ) {
if ( '"' !== substr( $data, -2, 1 ) ) {
return false;
}
} elseif ( false === strpos( $data, '"' ) ) {
return false;
}
// Or else fall through.
case 'a':
case 'O':
return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
case 'b':
case 'i':
case 'd':
$end = $strict ? '$' : '';
return (bool) preg_match( "/^{$token}:[0-9.E+-]+;$end/", $data );
}
return false;
}
/**
* Wrapper to validate the cache keys expiration value.
*
* @param mixed $expiration Incoming expiration value (whatever it is).
*
* @return int
*/
protected function validate_expiration( $expiration ) {
return is_int( $expiration ) || ctype_digit( $expiration ) ? (int) $expiration : 0;
}
/**
* Sanitizes a string key.
*
* Keys are used as internal identifiers. Lowercase alphanumeric characters,
* dashes, and underscores are allowed.
*
* @param string $key String key.
*
* @return string Sanitized key.
*/
public function sanitize_key( $key ) {
$key = strtolower( $key );
return preg_replace( '/[^a-z0-9_\-]/', '', $key );
}
}