diff --git a/composer.json b/composer.json index 3e6ffe0..28615c6 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "helsingborg-stad/lix-calculator": "^3.0.0", "helsingborg-stad/media-usage": "^3.0.0", "helsingborg-stad/mod-my-pages": "^1.0.0", - "helsingborg-stad/modularity": "^3.0.1", + "helsingborg-stad/modularity": "^3.3.1", "helsingborg-stad/modularity-contact-banner": "^2.0.0", "helsingborg-stad/modularity-entryscape": "^2.0.0", "helsingborg-stad/modularity-form-builder": "^2.0.0", @@ -90,25 +90,24 @@ "helsingborg-stad/wpmu-network-admin-url": "^1.0.0", "helsingborg-stad/wpmu-mu-plugins-url-everywhere": "~1.0.0", "humanmade/s3-uploads": "^3.0.4", - "johnpbloch/wordpress": "6.3.1", + "johnpbloch/wordpress": "~6.3.1", "municipio-se/modularity-noticeboard": "^1.0.0", "wikimedia/composer-merge-plugin": "dev-master", - "wpackagist-plugin/acf-extended": "0.8.8.7", - "wpackagist-plugin/autodescription": ">=2.9.0", - "wpackagist-plugin/cookies-and-content-security-policy": "^1.44", - "wpackagist-plugin/fakerpress": "0.5.2", - "wpackagist-plugin/kirki": "^5.0", - "wpackagist-plugin/litespeed-cache": "^5.3.0", - "wpackagist-plugin/network-plugin-auditor": "^1.10.0", - "wpackagist-plugin/nginx-helper": "^2.2", - "wpackagist-plugin/redirection": "^4.3.1", - "wpackagist-plugin/redis-cache": "2.0.15", - "wpackagist-plugin/simple-smtp": "^1.3.2", - "wpackagist-plugin/user-switching": ">=1.5.1", - "wpackagist-plugin/username-changer": "3.1.3", - "wpackagist-plugin/varnish-http-purge": "^4.1.1", - "wpackagist-plugin/wp-multi-network": "^2.5.2", - "wpackagist-plugin/wp-nested-pages": "3.1.4", + "wpackagist-plugin/autodescription": "~4.2.8", + "wpackagist-plugin/cookies-and-content-security-policy": "~2.17", + "wpackagist-plugin/fakerpress": "~0.6.1", + "wpackagist-plugin/kirki": "~5.0", + "wpackagist-plugin/litespeed-cache": "~5.3.0", + "wpackagist-plugin/network-plugin-auditor": "~1.10.0", + "wpackagist-plugin/nginx-helper": "~2.2", + "wpackagist-plugin/redirection": "~5.3.10", + "wpackagist-plugin/redis-cache": "2.4.4", + "wpackagist-plugin/simple-smtp": "~1.3.2", + "wpackagist-plugin/user-switching": "~1.5.1", + "wpackagist-plugin/username-changer": "~3.2.2", + "wpackagist-plugin/varnish-http-purge": "~5.1.3", + "wpackagist-plugin/wp-multi-network": "~2.5.2", + "wpackagist-plugin/wp-nested-pages": "~3.2.4", "wpackagist-plugin/wp-sentry-integration":"~6.24", "wpackagist-plugin/performant-translations":"~1.0.3" }, diff --git a/wp-content/object-cache.php b/wp-content/object-cache.php index 304aab2..001776e 100644 --- a/wp-content/object-cache.php +++ b/wp-content/object-cache.php @@ -1,13 +1,14 @@ add( $key, $value, $group, $expiration ); } +/** + * Adds multiple values to the cache in one call. + * + * @param array $data Array of keys and values to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expire Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if cache key and group already exist. + */ +function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) { + global $wp_object_cache; + + return $wp_object_cache->add_multiple( $data, $group, $expire ); +} + /** * Closes the cache. * @@ -80,18 +123,54 @@ function wp_cache_delete( $key, $group = '', $time = 0 ) { return $wp_object_cache->delete( $key, $group, $time ); } +/** + * Deletes multiple values from the cache in one call. + * + * @param array $keys Array of keys under which the cache to deleted. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if the contents were not deleted. + */ +function wp_cache_delete_multiple( array $keys, $group = '' ) { + global $wp_object_cache; + + return $wp_object_cache->delete_multiple( $keys, $group ); +} + /** * Invalidate all items in the cache. If `WP_REDIS_SELECTIVE_FLUSH` is `true`, * only keys prefixed with the `WP_REDIS_PREFIX` are flushed. * - * @param int $delay Number of seconds to wait before invalidating the items. - * * @return bool Returns TRUE on success or FALSE on failure. */ -function wp_cache_flush( $delay = 0 ) { +function wp_cache_flush() { + global $wp_object_cache; + + return $wp_object_cache->flush(); +} + +/** + * Removes all cache items in a group. + * + * @param string $group Name of group to remove from cache. + * @return true Returns TRUE on success or FALSE on failure. + */ +function wp_cache_flush_group( $group ) +{ + global $wp_object_cache; + + return $wp_object_cache->flush_group( $group ); +} + +/** + * Removes all cache items from the in-memory runtime cache. + * + * @return bool True on success, false on failure. + */ +function wp_cache_flush_runtime() { global $wp_object_cache; - return $wp_object_cache->flush( $delay ); + return $wp_object_cache->flush_runtime(); } /** @@ -165,10 +244,14 @@ function wp_cache_init() { define( 'WP_REDIS_PREFIX', WP_CACHE_KEY_SALT ); } + // Set unique prefix for sites hosted on Cloudways + if ( ! defined( 'WP_REDIS_PREFIX' ) && isset( $_SERVER['cw_allowed_ip'] ) ) { + define( 'WP_REDIS_PREFIX', getenv( 'HTTP_X_APP_USER' ) ); + } + if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) { - $fail_gracefully = ! defined( 'WP_REDIS_GRACEFUL' ) || WP_REDIS_GRACEFUL; + $fail_gracefully = defined( 'WP_REDIS_GRACEFUL' ) && WP_REDIS_GRACEFUL; - // We need to override this WordPress global in order to inject our cache. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_object_cache = new WP_Object_Cache( $fail_gracefully ); } @@ -211,6 +294,22 @@ function wp_cache_set( $key, $value, $group = '', $expiration = 0 ) { return $wp_object_cache->set( $key, $value, $group, $expiration ); } +/** + * Sets multiple values to the cache in one call. + * + * @param array $data Array of keys and values to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expire Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false on failure. + */ +function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) { + global $wp_object_cache; + + return $wp_object_cache->set_multiple( $data, $group, $expire ); +} + /** * Switch the internal blog id. * @@ -256,7 +355,6 @@ function wp_cache_add_non_persistent_groups( $groups ) { * Object cache class definition */ class WP_Object_Cache { - /** * The Redis client. * @@ -309,7 +407,7 @@ class WP_Object_Cache { /** * List of global groups. * - * @var array + * @var array */ public $global_groups = [ 'blog-details', @@ -349,6 +447,13 @@ class WP_Object_Cache { 'themes', ]; + /** + * List of groups and their types. + * + * @var array + */ + public $group_type = []; + /** * Prefix used for global groups. * @@ -359,9 +464,9 @@ class WP_Object_Cache { /** * Prefix used for non-global groups. * - * @var string + * @var int */ - public $blog_prefix = ''; + public $blog_prefix = 0; /** * Track how many requests were found in cache. @@ -378,25 +483,25 @@ class WP_Object_Cache { public $cache_misses = 0; /** - * Track how long request took. + * The amount of Redis commands made. * * @var int */ - public $cache_time = 0; + public $cache_calls = 0; /** - * Track how may calls were made. + * The amount of microseconds (μs) waited for Redis commands. * - * @var int + * @var float */ - public $cache_calls = 0; + public $cache_time = 0; /** * Instantiate the Redis class. * * @param bool $fail_gracefully Handles and logs errors if true throws exceptions otherwise. */ - public function __construct( $fail_gracefully = true ) { + public function __construct( $fail_gracefully = false ) { global $blog_id, $table_prefix; $this->fail_gracefully = $fail_gracefully; @@ -415,6 +520,14 @@ public function __construct( $fail_gracefully = true ) { $this->unflushable_groups = array_map( [ $this, 'sanitize_key_part' ], WP_REDIS_UNFLUSHABLE_GROUPS ); } + $this->cache_group_types(); + + if ( function_exists( '_doing_it_wrong' ) ) { + if ( defined( 'WP_REDIS_TRACE' ) && WP_REDIS_TRACE ) { + _doing_it_wrong( __FUNCTION__ , 'Tracing feature was removed.' , '2.1.2' ); + } + } + $client = $this->determine_client(); $parameters = $this->build_parameters(); @@ -426,6 +539,9 @@ public function __construct( $fail_gracefully = true ) { case 'phpredis': $this->connect_using_phpredis( $parameters ); break; + case 'relay': + $this->connect_using_relay( $parameters ); + break; case 'credis': $this->connect_using_credis( $parameters ); break; @@ -436,7 +552,13 @@ public function __construct( $fail_gracefully = true ) { } if ( defined( 'WP_REDIS_CLUSTER' ) ) { - $this->diagnostics[ 'ping' ] = $this->redis->ping( current( array_values( WP_REDIS_CLUSTER ) ) ); + $connectionId = is_string( WP_REDIS_CLUSTER ) + ? WP_REDIS_CLUSTER + : current( $this->build_cluster_connection_array() ); + + $this->diagnostics[ 'ping' ] = $client === 'predis' + ? $this->redis->getClientBy( 'id', $connectionId )->ping() + : $this->redis->ping( $connectionId ); } else { $this->diagnostics[ 'ping' ] = $this->redis->ping(); } @@ -455,6 +577,25 @@ public function __construct( $fail_gracefully = true ) { } } + /** + * Set group type array + * + * @return void + */ + protected function cache_group_types() { + foreach ( $this->global_groups as $group ) { + $this->group_type[ $group ] = 'global'; + } + + foreach ( $this->unflushable_groups as $group ) { + $this->group_type[ $group ] = 'unflushable'; + } + + foreach ( $this->ignored_groups as $group ) { + $this->group_type[ $group ] = 'ignored'; + } + } + /** * Determine the Redis client. * @@ -489,6 +630,7 @@ protected function build_parameters() { 'timeout' => 1, 'read_timeout' => 1, 'retry_interval' => null, + 'persistent' => false, ]; $settings = [ @@ -511,19 +653,15 @@ protected function build_parameters() { } } - if ( isset( $parameters['password'] ) ) { - $password = $parameters['password']; - - if ( is_null( $password ) || $password === '' ) { - unset( $parameters['password'] ); - } + if ( isset( $parameters[ 'password' ] ) && $parameters[ 'password' ] === '' ) { + unset( $parameters[ 'password' ] ); } return $parameters; } /** - * Connect to Redis using the PhpRedis (PECL) extention. + * Connect to Redis using the PhpRedis (PECL) extension. * * @param array $parameters Connection parameters built by the `build_parameters` method. * @return void @@ -538,45 +676,129 @@ protected function connect_using_phpredis( $parameters ) { $this->diagnostics[ 'shards' ] = WP_REDIS_SHARDS; } elseif ( defined( 'WP_REDIS_CLUSTER' ) ) { + if ( is_string( WP_REDIS_CLUSTER ) ) { + $this->redis = new RedisCluster( WP_REDIS_CLUSTER ); + } else { + $args = [ + 'cluster' => $this->build_cluster_connection_array(), + 'timeout' => $parameters['timeout'], + 'read_timeout' => $parameters['read_timeout'], + 'persistent' => $parameters['persistent'], + ]; + + if ( isset( $parameters['password'] ) && version_compare( $version, '4.3.0', '>=' ) ) { + $args['password'] = $parameters['password']; + } + + $this->redis = new RedisCluster( null, ...array_values( $args ) ); + $this->diagnostics += $args; + } + } else { + $this->redis = new Redis(); + $args = [ - 'cluster' => array_values( WP_REDIS_CLUSTER ), + 'host' => $parameters['host'], + 'port' => $parameters['port'], 'timeout' => $parameters['timeout'], - 'read_timeout' => $parameters['read_timeout'], + '', + 'retry_interval' => (int) $parameters['retry_interval'], ]; - if ( isset( $parameters['password'] ) && version_compare( $version, '4.3.0', '>=' ) ) { + if ( version_compare( $version, '3.1.3', '>=' ) ) { + $args['read_timeout'] = $parameters['read_timeout']; + } + + if ( strcasecmp( 'tls', $parameters['scheme'] ) === 0 ) { + $args['host'] = sprintf( + '%s://%s', + $parameters['scheme'], + str_replace( 'tls://', '', $parameters['host'] ) + ); + + if ( version_compare( $version, '5.3.0', '>=' ) && defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) { + $args['others']['stream'] = WP_REDIS_SSL_CONTEXT; + } + } + + if ( strcasecmp( 'unix', $parameters['scheme'] ) === 0 ) { + $args['host'] = $parameters['path']; + $args['port'] = -1; + } + + call_user_func_array( [ $this->redis, 'connect' ], array_values( $args ) ); + + if ( isset( $parameters['password'] ) ) { $args['password'] = $parameters['password']; + $this->redis->auth( $parameters['password'] ); } - $this->redis = new RedisCluster( null, ...array_values( $args ) ); + if ( isset( $parameters['database'] ) ) { + if ( ctype_digit( (string) $parameters['database'] ) ) { + $parameters['database'] = (int) $parameters['database']; + } + + $args['database'] = $parameters['database']; + + if ( $parameters['database'] ) { + $this->redis->select( $parameters['database'] ); + } + } $this->diagnostics += $args; + } + + if ( defined( 'WP_REDIS_SERIALIZER' ) && ! empty( WP_REDIS_SERIALIZER ) ) { + $this->redis->setOption( Redis::OPT_SERIALIZER, WP_REDIS_SERIALIZER ); + + if ( function_exists( '_doing_it_wrong' ) ) { + _doing_it_wrong( __FUNCTION__ , 'The `WP_REDIS_SERIALIZER` configuration constant has been deprecated, use `WP_REDIS_IGBINARY` instead.', '2.3.1' ); + } + } + } + + /** + * Connect to Redis using the Relay extension. + * + * @param array $parameters Connection parameters built by the `build_parameters` method. + * @return void + */ + protected function connect_using_relay( $parameters ) { + $version = phpversion( 'relay' ); + + $this->diagnostics[ 'client' ] = sprintf( 'Relay (v%s)', $version ); + + if ( defined( 'WP_REDIS_SHARDS' ) ) { + throw new Exception('Relay does not support sharding.'); + } elseif ( defined( 'WP_REDIS_CLUSTER' ) ) { + throw new Exception('Relay does not cluster connections.'); } else { - $this->redis = new Redis(); + $this->redis = new Relay\Relay; $args = [ 'host' => $parameters['host'], 'port' => $parameters['port'], 'timeout' => $parameters['timeout'], - null, - 'retry_interval' => $parameters['retry_interval'], + '', + 'retry_interval' => (int) $parameters['retry_interval'], ]; + $args['read_timeout'] = $parameters['read_timeout']; + if ( strcasecmp( 'tls', $parameters['scheme'] ) === 0 ) { $args['host'] = sprintf( '%s://%s', $parameters['scheme'], str_replace( 'tls://', '', $parameters['host'] ) ); + + if ( defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) { + $args['others']['stream'] = WP_REDIS_SSL_CONTEXT; + } } if ( strcasecmp( 'unix', $parameters['scheme'] ) === 0 ) { $args['host'] = $parameters['path']; - $args['port'] = null; - } - - if ( version_compare( $version, '3.1.3', '>=' ) ) { - $args['read_timeout'] = $parameters['read_timeout']; + $args['port'] = -1; } call_user_func_array( [ $this->redis, 'connect' ], array_values( $args ) ); @@ -587,8 +809,8 @@ protected function connect_using_phpredis( $parameters ) { } if ( isset( $parameters['database'] ) ) { - if ( ctype_digit( $parameters['database'] ) ) { - $parameters['database'] = intval( $parameters['database'] ); + if ( ctype_digit( (string) $parameters['database'] ) ) { + $parameters['database'] = (int) $parameters['database']; } $args['database'] = $parameters['database']; @@ -602,7 +824,11 @@ protected function connect_using_phpredis( $parameters ) { } if ( defined( 'WP_REDIS_SERIALIZER' ) && ! empty( WP_REDIS_SERIALIZER ) ) { - $this->redis->setOption( Redis::OPT_SERIALIZER, WP_REDIS_SERIALIZER ); + $this->redis->setOption( Relay\Relay::OPT_SERIALIZER, WP_REDIS_SERIALIZER ); + + if ( function_exists( '_doing_it_wrong' ) ) { + _doing_it_wrong( __FUNCTION__ , 'The `WP_REDIS_SERIALIZER` configuration constant has been deprecated, use `WP_REDIS_IGBINARY` instead.', '2.3.1' ); + } } } @@ -618,13 +844,18 @@ protected function connect_using_predis( $parameters ) { // Load bundled Predis library. if ( ! class_exists( 'Predis\Client' ) ) { - $predis = sprintf( - '%s/redis-cache/dependencies/predis/predis/autoload.php', - defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : WP_CONTENT_DIR . '/plugins' - ); - - if ( is_readable( $predis ) ) { - require_once $predis; + $predis = '/dependencies/predis/predis/autoload.php'; + + $pluginDir = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR . '/redis-cache' : null; + $contentDir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR . '/plugins/redis-cache' : null; + $pluginPath = defined( 'WP_REDIS_PLUGIN_PATH' ) ? WP_REDIS_PLUGIN_PATH : null; + + if ( $pluginDir && is_readable( $pluginDir . $predis ) ) { + require_once $pluginDir . $predis; + } elseif ( $contentDir && is_readable( $contentDir . $predis ) ) { + require_once $contentDir . $predis; + } elseif ( $pluginPath && is_readable( $pluginPath . $predis ) ) { + require_once $pluginPath . $predis; } else { throw new Exception( 'Predis library not found. Re-install Redis Cache plugin or delete the object-cache.php.' @@ -646,13 +877,17 @@ protected function connect_using_predis( $parameters ) { } elseif ( defined( 'WP_REDIS_SERVERS' ) ) { $servers = WP_REDIS_SERVERS; $parameters['servers'] = $servers; - $options['replication'] = true; + $options['replication'] = 'predis'; } elseif ( defined( 'WP_REDIS_CLUSTER' ) ) { - $servers = WP_REDIS_CLUSTER; + $servers = $this->build_cluster_connection_array(); $parameters['cluster'] = $servers; $options['cluster'] = 'redis'; } + if ( strcasecmp( 'unix', $parameters['scheme'] ) === 0 ) { + unset($parameters['host'], $parameters['port']); + } + if ( isset( $parameters['read_timeout'] ) && $parameters['read_timeout'] ) { $parameters['read_write_timeout'] = $parameters['read_timeout']; } @@ -664,11 +899,31 @@ protected function connect_using_predis( $parameters ) { } if ( isset( $parameters['password'] ) ) { - $options['parameters']['password'] = WP_REDIS_PASSWORD; + if ( is_array( $parameters['password'] ) ) { + $options['parameters']['username'] = WP_REDIS_PASSWORD[0]; + $options['parameters']['password'] = WP_REDIS_PASSWORD[1]; + } else { + $options['parameters']['password'] = WP_REDIS_PASSWORD; + } } } } + if ( isset( $parameters['password'] ) ) { + if ( is_array( $parameters['password'] ) ) { + $parameters['username'] = array_shift( $parameters['password'] ); + $parameters['password'] = implode( '', $parameters['password'] ); + } + + if ( defined( 'WP_REDIS_USERNAME' ) ) { + $parameters['username'] = WP_REDIS_USERNAME; + } + } + + if ( defined( 'WP_REDIS_SSL_CONTEXT' ) && ! empty( WP_REDIS_SSL_CONTEXT ) ) { + $parameters['ssl'] = WP_REDIS_SSL_CONTEXT; + } + $this->redis = new Predis\Client( $servers ?: $parameters, $options ); $this->redis->connect(); @@ -689,6 +944,8 @@ protected function connect_using_predis( $parameters ) { * @return void */ protected function connect_using_credis( $parameters ) { + _doing_it_wrong( __FUNCTION__ , 'Credis support will be removed in future versions.' , '2.0.26' ); + $client = 'Credis'; $creds_path = sprintf( @@ -754,19 +1011,24 @@ protected function connect_using_credis( $parameters ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $url_components = parse_url( $connection_string ); - parse_str( $url_components['query'], $add_params ); + if ( isset( $url_components['query'] ) ) { + parse_str( $url_components['query'], $add_params ); + } if ( ! $is_cluster && isset( $add_params['alias'] ) ) { $add_params['master'] = 'master' === $add_params['alias']; } $add_params['host'] = $url_components['host']; + $add_params['port'] = $url_components['port']; if ( ! isset( $add_params['alias'] ) ) { $add_params['alias'] = "redis-$index"; } $clients[ $index ] = array_merge( $parameters, $add_params ); + + unset($add_params); } $this->redis = new Credis_Cluster( $clients ); @@ -778,7 +1040,7 @@ protected function connect_using_credis( $parameters ) { $params = array_filter( $_client ); if ( $params ) { - $connection_string .= '?' . http_build_query( $params, null, '&' ); + $connection_string .= '?' . http_build_query( $params, '', '&' ); } $clients[ $index ] = $connection_string; @@ -790,7 +1052,7 @@ protected function connect_using_credis( $parameters ) { 'host' => $parameters['scheme'] === 'unix' ? $parameters['path'] : $parameters['host'], 'port' => $parameters['port'], 'timeout' => $parameters['timeout'], - 'persistent' => null, + 'persistent' => '', 'database' => $parameters['database'], 'password' => isset( $parameters['password'] ) ? $parameters['password'] : null, ]; @@ -815,12 +1077,14 @@ protected function connect_using_credis( $parameters ) { } /** - * Connect to Redis using HHVM's Redis extention. + * Connect to Redis using HHVM's Redis extension. * * @param array $parameters Connection parameters built by the `build_parameters` method. * @return void */ protected function connect_using_hhvm( $parameters ) { + _doing_it_wrong( __FUNCTION__ , 'HHVM support will be removed in future versions.' , '2.0.26' ); + $this->redis = new Redis(); // Adjust host and port if the scheme is `unix`. @@ -846,8 +1110,8 @@ protected function connect_using_hhvm( $parameters ) { } if ( isset( $parameters['database'] ) ) { - if ( ctype_digit( $parameters['database'] ) ) { - $parameters['database'] = intval( $parameters['database'] ); + if ( ctype_digit( (string) $parameters['database'] ) ) { + $parameters['database'] = (int) $parameters['database']; } if ( $parameters['database'] ) { @@ -876,7 +1140,13 @@ public function fetch_info() { } if ( defined( 'WP_REDIS_CLUSTER' ) ) { - $info = $this->redis->info( current( array_values( WP_REDIS_CLUSTER ) ) ); + $connectionId = is_string( WP_REDIS_CLUSTER ) + ? 'SERVER' + : current( $this->build_cluster_connection_array() ); + + $info = $this->determine_client() === 'predis' + ? $this->redis->getClientBy( 'id', $connectionId )->info() + : $this->redis->info( $connectionId ); } else { $info = $this->redis->info(); } @@ -931,6 +1201,130 @@ public function add( $key, $value, $group = 'default', $expiration = 0 ) { return $this->add_or_replace( true, $key, $value, $group, $expiration ); } + /** + * Adds multiple values to the cache in one call. + * + * @param array $data Array of keys and values to be added. + * @param string $group Optional. Where the cache contents are grouped. + * @param int $expire Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if cache key and group already exist. + */ + public function add_multiple( array $data, $group = 'default', $expire = 0 ) { + if ( function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition() ) { + return array_combine( array_keys( $data ), array_fill( 0, count( $data ), false ) ); + } + + if ( + $this->redis_status() && + method_exists( $this->redis, 'pipeline' ) && + ! $this->is_ignored_group( $group ) + ) { + return $this->add_multiple_at_once( $data, $group, $expire ); + } + + $values = []; + + foreach ( $data as $key => $value ) { + $values[ $key ] = $this->add( $key, $value, $group, $expire ); + } + + return $values; + } + + /** + * Adds multiple values to the cache in one call. + * + * @param array $data Array of keys and values to be added. + * @param string $group Optional. Where the cache contents are grouped. + * @param int $expire Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if cache key and group already exist. + */ + protected function add_multiple_at_once( array $data, $group = 'default', $expire = 0 ) + { + $keys = array_keys( $data ); + + $san_group = $this->sanitize_key_part( $group ); + + $tx = $this->redis->pipeline(); + + $orig_exp = $expire; + $expire = $this->validate_expiration( $expire ); + $derived_keys = []; + + foreach ( $data as $key => $value ) { + /** + * Filters the cache expiration time + * + * @param int $expiration The time in seconds the entry expires. 0 for no expiry. + * @param string $key The cache key. + * @param string $group The cache group. + * @param mixed $orig_exp The original expiration value before validation. + */ + $expire = apply_filters( 'redis_cache_expiration', $expire, $key, $group, $orig_exp ); + + $san_key = $this->sanitize_key_part( $key ); + $derived_key = $derived_keys[ $key ] = $this->fast_build_key( $san_key, $san_group ); + + $args = [ $derived_key, $this->maybe_serialize( $value ) ]; + + if ( $this->is_predis() ) { + $args[] = 'nx'; + + if ( $expire ) { + $args[] = 'ex'; + $args[] = $expire; + } + } else { + if ( $expire ) { + $args[] = [ 'nx', 'ex' => $expire ]; + } else { + $args[] = [ 'nx' ]; + } + } + + $tx->set( ...$args ); + } + + try { + $start_time = microtime( true ); + + $method = $this->is_predis() ? 'execute' : 'exec'; + + $results = array_map( function ( $response ) { + return (bool) $this->parse_redis_response( $response ); + }, $tx->{$method}() ?: [] ); + + if ( count( $results ) !== count( $keys ) ) { + $tx->discard(); + + return array_fill_keys( $keys, false ); + } + + $results = array_combine( $keys, $results ); + + foreach ( $results as $key => $result ) { + if ( $result ) { + $this->add_to_internal_cache( $derived_keys[ $key ], $data[ $key ] ); + } + } + + $execute_time = microtime( true ) - $start_time; + + $this->cache_calls++; + $this->cache_time += $execute_time; + } catch ( Exception $exception ) { + $this->handle_exception( $exception ); + + return array_combine( $keys, array_fill( 0, count( $keys ), false ) ); + } + + return $results; + } + /** * Replace a value in the cache. * @@ -960,19 +1354,21 @@ public function replace( $key, $value, $group = 'default', $expiration = 0 ) { * @return bool Returns TRUE on success or FALSE on failure. */ protected function add_or_replace( $add, $key, $value, $group = 'default', $expiration = 0 ) { - $cache_addition_suspended = function_exists( 'wp_suspend_cache_addition' ) - ? wp_suspend_cache_addition() - : false; + $cache_addition_suspended = function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition(); if ( $add && $cache_addition_suspended ) { return false; } $result = true; - $derived_key = $this->build_key( $key, $group ); + + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + + $derived_key = $this->fast_build_key( $san_key, $san_group ); // Save if group not excluded and redis is up. - if ( ! $this->is_ignored_group( $group ) && $this->redis_status() ) { + if ( ! $this->is_ignored_group( $san_group ) && $this->redis_status() ) { try { $orig_exp = $expiration; $expiration = $this->validate_expiration( $expiration ); @@ -992,7 +1388,7 @@ protected function add_or_replace( $add, $key, $value, $group = 'default', $expi if ( $add ) { $args = [ $derived_key, $this->maybe_serialize( $value ) ]; - if ( $this->redis instanceof Predis\Client ) { + if ( $this->is_predis() ) { $args[] = 'nx'; if ( $expiration ) { @@ -1034,7 +1430,7 @@ protected function add_or_replace( $add, $key, $value, $group = 'default', $expi } } - $exists = isset( $this->cache[ $derived_key ] ); + $exists = array_key_exists( $derived_key, $this->cache ); if ( (bool) $add === $exists ) { return false; @@ -1054,18 +1450,22 @@ protected function add_or_replace( $add, $key, $value, $group = 'default', $expi * @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' ) { + public function delete( $key, $group = 'default', $deprecated = false ) { $result = false; - $derived_key = $this->build_key( $key, $group ); - if ( isset( $this->cache[ $derived_key ] ) ) { + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + + $derived_key = $this->fast_build_key( $san_key, $san_group ); + + if ( array_key_exists( $derived_key, $this->cache ) ) { unset( $this->cache[ $derived_key ] ); $result = true; } $start_time = microtime( true ); - if ( $this->redis_status() && ! $this->is_ignored_group( $group ) ) { + if ( $this->redis_status() && ! $this->is_ignored_group( $san_group ) ) { try { $result = $this->parse_redis_response( $this->redis->del( $derived_key ) ); } catch ( Exception $exception ) { @@ -1096,19 +1496,107 @@ public function delete( $key, $group = 'default' ) { } /** - * Invalidate all items in the cache. If `WP_REDIS_SELECTIVE_FLUSH` is `true`, - * only keys prefixed with the `WP_REDIS_PREFIX` are flushed. + * Deletes multiple values from the cache in one call. * - * @param int $delay Number of seconds to wait before invalidating the items. - * @return bool Returns TRUE on success or FALSE on failure. + * @param array $keys Array of keys to be deleted. + * @param string $group Optional. Where the cache contents are grouped. + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if the contents were not deleted. */ - public function flush( $delay = 0 ) { - $delay = abs( intval( $delay ) ); + public function delete_multiple( array $keys, $group = 'default' ) { + if ( + $this->redis_status() && + method_exists( $this->redis, 'pipeline' ) && + ! $this->is_ignored_group( $group ) + ) { + return $this->delete_multiple_at_once( $keys, $group ); + } + + $values = []; + + foreach ( $keys as $key ) { + $values[ $key ] = $this->delete( $key, $group ); + } + + return $values; + } + + /** + * Deletes multiple values from the cache in one call. + * + * @param array $keys Array of keys to be deleted. + * @param string $group Optional. Where the cache contents are grouped. + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if the contents were not deleted. + */ + protected function delete_multiple_at_once( array $keys, $group = 'default' ) { + $start_time = microtime( true ); + + try { + $tx = $this->redis->pipeline(); - if ( $delay ) { - sleep( $delay ); + foreach ( $keys as $key ) { + $derived_key = $this->build_key( (string) $key, $group ); + + $tx->del( $derived_key ); + + unset( $this->cache[ $derived_key ] ); + } + + $method = $this->is_predis() ? 'execute' : 'exec'; + + $results = array_map( function ( $response ) { + return (bool) $this->parse_redis_response( $response ); + }, $tx->{$method}() ?: [] ); + + if ( count( $results ) !== count( $keys ) ) { + $tx->discard(); + + return array_fill_keys( $keys, false ); + } + + $execute_time = microtime( true ) - $start_time; + } catch ( Exception $exception ) { + $this->handle_exception( $exception ); + + return array_combine( $keys, array_fill( 0, count( $keys ), false ) ); + } + + if ( function_exists( 'do_action' ) ) { + foreach ( $keys as $key ) { + /** + * Fires on every cache key deletion + * + * @since 1.3.3 + * @param string $key The cache key. + * @param string $group The group value appended to the $key. + * @param float $execute_time Execution time for the request in seconds. + */ + do_action( 'redis_object_cache_delete', $key, $group, $execute_time ); + } } + return array_combine( $keys, $results ); + } + + /** + * Removes all cache items from the in-memory runtime cache. + * + * @return bool True on success, false on failure. + */ + public function flush_runtime() { + $this->cache = []; + + return true; + } + + /** + * Invalidate all items in the cache. If `WP_REDIS_SELECTIVE_FLUSH` is `true`, + * only keys prefixed with the `WP_REDIS_PREFIX` are flushed. + * + * @return bool True on success, false on failure. + */ + public function flush() { $results = []; $this->cache = []; @@ -1173,12 +1661,12 @@ public function flush( $delay = 0 ) { * * @since 1.3.5 * @param null|array $results Array of flush results. - * @param int $delay Given number of seconds to waited before invalidating the items. + * @param int $deprecated Unused. Default 0. * @param bool $seletive Whether a selective flush took place. * @param string $salt The defined key prefix. * @param float $execute_time Execution time for the request in seconds. */ - do_action( 'redis_object_cache_flush', $results, $delay, $selective, $salt, $execute_time ); + do_action( 'redis_object_cache_flush', $results, 0, $selective, $salt, $execute_time ); } } @@ -1195,6 +1683,81 @@ public function flush( $delay = 0 ) { return true; } + /** + * Removes all cache items in a group. + * + * @param string $group Name of group to remove from cache. + * @return bool Returns TRUE on success or FALSE on failure. + */ + public function flush_group( $group ) + { + $san_group = $this->sanitize_key_part( $group ); + + if ( is_multisite() && ! $this->is_global_group( $san_group ) ) { + $salt = str_replace( "{$this->blog_prefix}:{$san_group}", "*:{$san_group}", $this->fast_build_key( '*', $san_group ) ); + } else { + $salt = $this->fast_build_key( '*', $san_group ); + } + + foreach ( $this->cache as $key => $value ) { + if ( strpos( $key, "{$san_group}:" ) === 0 || strpos( $key, ":{$san_group}:" ) !== false ) { + unset( $this->cache[ $key ] ); + } + } + + if ( in_array( $san_group, $this->unflushable_groups ) ) { + return false; + } + + if ( ! $this->redis_status() ) { + return false; + } + + $results = []; + + $start_time = microtime( true ); + $script = $this->lua_flush_closure( $salt, false ); + + try { + if ( defined( 'WP_REDIS_CLUSTER' ) ) { + foreach ( $this->redis->_masters() as $master ) { + $redis = new Redis; + $redis->connect( $master[0], $master[1] ); + $results[] = $this->parse_redis_response( $script() ); + unset( $redis ); + } + } else { + $results[] = $this->parse_redis_response( $script() ); + } + } catch ( Exception $exception ) { + $this->handle_exception( $exception ); + + return false; + } + + if ( function_exists( 'do_action' ) ) { + $execute_time = microtime( true ) - $start_time; + + /** + * Fires on every group cache flush + * + * @param null|array $results Array of flush results. + * @param string $salt The defined key prefix. + * @param float $execute_time Execution time for the request in seconds. + * @since 2.2.3 + */ + do_action( 'redis_object_cache_flush_group', $results, $salt, $execute_time ); + } + + foreach ( $results as $result ) { + if ( ! $result ) { + return false; + } + } + + return true; + } + /** * Returns a closure to flush selectively. * @@ -1234,10 +1797,11 @@ function ( $character ) { * Returns a closure ready to be called to flush selectively ignoring unflushable groups. * * @param string $salt The salt to be used to differentiate. + * @param bool $escape ... * @return callable Generated callable executing the lua script. */ - protected function lua_flush_closure( $salt ) { - $salt = $this->glob_quote( $salt ); + protected function lua_flush_closure( $salt, $escape = true ) { + $salt = $escape ? $this->glob_quote( $salt ) : $salt; return function () use ( $salt ) { $script = <<redis instanceof Predis\Client ) - ? [ $script, 0 ] - : [ $script ]; + $args = $this->is_predis() ? [ $script, 0 ] : [ $script ]; return call_user_func_array( [ $this->redis, 'eval' ], $args ); }; @@ -1315,7 +1877,7 @@ function ( $group ) { $script = 'redis.replicate_commands()' . "\n" . $script; } - $args = ( $this->redis instanceof Predis\Client ) + $args = $this->is_predis() ? array_merge( [ $script, count( $unflushable ) ], $unflushable ) : [ $script, $unflushable, count( $unflushable ) ]; @@ -1337,13 +1899,16 @@ function ( $group ) { * @return bool|mixed Cached object value. */ public function get( $key, $group = 'default', $force = false, &$found = null ) { - $derived_key = $this->build_key( $key, $group ); + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + $derived_key = $this->fast_build_key( $san_key, $san_group ); - if ( isset( $this->cache[ $derived_key ] ) && ! $force ) { + if ( array_key_exists( $derived_key, $this->cache ) && ! $force ) { $found = true; $this->cache_hits++; + $value = $this->get_from_internal_cache( $derived_key ); - return $this->get_from_internal_cache( $derived_key ); + return $value; } elseif ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) { $found = false; $this->cache_misses++; @@ -1420,7 +1985,7 @@ public function get( $key, $group = 'default', $force = false, &$found = null ) * @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. - * @return array Array of values organized into groups. + * @return array|false Array of values organized into groups. */ public function get_multiple( $keys, $group = 'default', $force = false ) { if ( ! is_array( $keys ) ) { @@ -1429,15 +1994,25 @@ public function get_multiple( $keys, $group = 'default', $force = false ) { $cache = []; $derived_keys = []; + $start_time = microtime( true ); + + $san_group = $this->sanitize_key_part( $group ); foreach ( $keys as $key ) { - $derived_keys[ $key ] = $this->build_key( $key, $group ); + $san_key = $this->sanitize_key_part( $key ); + $derived_keys[ $key ] = $this->fast_build_key( $san_key, $san_group ); } if ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) { foreach ( $keys as $key ) { - $cache[ $key ] = $this->get_from_internal_cache( $derived_keys[ $key ] ); - $cache[ $key ] === false ? $this->cache_misses++ : $this->cache_hits++; + $value = $this->get_from_internal_cache( $derived_keys[ $key ] ); + $cache[ $key ] = $value; + + if ($value === false) { + $this->cache_misses++; + } else { + $this->cache_hits++; + } } return $cache; @@ -1449,6 +2024,7 @@ public function get_multiple( $keys, $group = 'default', $force = false ) { if ( $value === false ) { $this->cache_misses++; + } else { $cache[ $key ] = $value; $this->cache_hits++; @@ -1459,7 +2035,7 @@ public function get_multiple( $keys, $group = 'default', $force = false ) { $remaining_keys = array_filter( $keys, function ( $key ) use ( $cache ) { - return ! isset( $cache[ $key ] ); + return ! array_key_exists( $key, $cache ); } ); @@ -1468,23 +2044,28 @@ function ( $key ) use ( $cache ) { } $start_time = microtime( true ); + $results = []; - try { - $remaining_ids = array_map( - function ( $key ) use ( $derived_keys ) { - return $derived_keys[ $key ]; - }, - $remaining_keys - ); + $remaining_ids = array_map( + function ( $key ) use ( $derived_keys ) { + return $derived_keys[ $key ]; + }, + $remaining_keys + ); + try { $results = array_combine( $remaining_keys, $this->redis->mget( $remaining_ids ) + ?: array_fill( 0, count( $remaining_ids ), false ) ); } catch ( Exception $exception ) { $this->handle_exception( $exception ); - $cache = array_fill( 0, count( $derived_keys ) - 1, false ); + $results = array_combine( + $remaining_keys, + array_fill( 0, count( $remaining_ids ), false ) + ); } $execute_time = microtime( true ) - $start_time; @@ -1499,7 +2080,6 @@ function ( $key ) use ( $derived_keys ) { } else { $cache[ $key ] = $this->maybe_unserialize( $value ); $this->add_to_internal_cache( $derived_keys[ $key ], $cache[ $key ] ); - $this->cache_hits++; } } @@ -1509,8 +2089,8 @@ function ( $key ) use ( $derived_keys ) { * Fires on every cache get multiple request * * @since 2.0.6 - * @param mixed $value Value of the cache entry. - * @param string $key The cache key. + * @param array $keys Array of keys under which the cache contents are stored. + * @param array $cache Cache items. * @param string $group The group value appended to the $key. * @param bool $force Whether a forced refetch has taken place rather than relying on the local cache. * @param float $execute_time Execution time for the request in seconds. @@ -1551,10 +2131,13 @@ function ( $key ) use ( $derived_keys ) { */ public function set( $key, $value, $group = 'default', $expiration = 0 ) { $result = true; - $derived_key = $this->build_key( $key, $group ); - $start_time = microtime( true ); + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + + $derived_key = $this->fast_build_key( $san_key, $san_group ); + // Save if group not excluded from redis and redis is up. if ( ! $this->is_ignored_group( $group ) && $this->redis_status() ) { $orig_exp = $expiration; @@ -1583,8 +2166,9 @@ public function set( $key, $value, $group = 'default', $expiration = 0 ) { return false; } + $execute_time = microtime( true ) - $start_time; $this->cache_calls++; - $this->cache_time += ( microtime( true ) - $start_time ); + $this->cache_time += $execute_time; } // If the set was successful, or we didn't go to redis. @@ -1611,6 +2195,126 @@ public function set( $key, $value, $group = 'default', $expiration = 0 ) { return $result; } + /** + * Sets multiple values to the cache in one call. + * + * @param array $data Array of key and value to be set. + * @param string $group Optional. Where the cache contents are grouped. + * @param int $expire Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is always true. + */ + public function set_multiple( array $data, $group = 'default', $expire = 0 ) { + if ( + $this->redis_status() && + method_exists( $this->redis, 'pipeline' ) && + ! $this->is_ignored_group( $group ) + ) { + return $this->set_multiple_at_once( $data, $group, $expire ); + } + + $values = []; + + foreach ( $data as $key => $value ) { + $values[ $key ] = $this->set( $key, $value, $group, $expire ); + } + + return $values; + } + + /** + * Sets multiple values to the cache in one call. + * + * @param array $data Array of key and value to be set. + * @param string $group Optional. Where the cache contents are grouped. + * @param int $expiration Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is always true. + */ + protected function set_multiple_at_once( array $data, $group = 'default', $expiration = 0 ) + { + $start_time = microtime( true ); + + $san_group = $this->sanitize_key_part( $group ); + $derived_keys = []; + + $orig_exp = $expiration; + $expiration = $this->validate_expiration( $expiration ); + $expirations = []; + + $tx = $this->redis->pipeline(); + $keys = array_keys( $data ); + + foreach ( $data as $key => $value ) { + $san_key = $this->sanitize_key_part( $key ); + $derived_key = $derived_keys[ $key ] = $this->fast_build_key( $san_key, $san_group ); + + /** + * Filters the cache expiration time + * + * @param int $expiration The time in seconds the entry expires. 0 for no expiry. + * @param string $key The cache key. + * @param string $group The cache group. + * @param mixed $orig_exp The original expiration value before validation. + */ + $expiration = $expirations[ $key ] = apply_filters( 'redis_cache_expiration', $expiration, $key, $group, $orig_exp ); + + if ( $expiration ) { + $tx->setex( $derived_key, $expiration, $this->maybe_serialize( $value ) ); + } else { + $tx->set( $derived_key, $this->maybe_serialize( $value ) ); + } + } + + try { + $method = $this->is_predis() ? 'execute' : 'exec'; + + $results = array_map( function ( $response ) { + return (bool) $this->parse_redis_response( $response ); + }, $tx->{$method}() ?: [] ); + + if ( count( $results ) !== count( $keys ) ) { + $tx->discard(); + + return array_fill_keys( $keys, false ); + } + + $results = array_combine( $keys, $results ); + + foreach ( $results as $key => $result ) { + if ( $result ) { + $this->add_to_internal_cache( $derived_keys[ $key ], $data[ $key ] ); + } + } + } catch ( Exception $exception ) { + $this->handle_exception( $exception ); + + return array_combine( $keys, array_fill( 0, count( $keys ), false ) ); + } + + $execute_time = microtime( true ) - $start_time; + + $this->cache_calls++; + $this->cache_time += $execute_time; + + if ( function_exists( 'do_action' ) ) { + foreach ( $data as $key => $value ) { + /** + * Fires on every cache set + * + * @param string $key The cache key. + * @param mixed $value Value of the cache entry. + * @param string $group The group value appended to the $key. + * @param int $expiration The time in seconds the entry expires. 0 for no expiry. + * @param float $execute_time Execution time for the request in seconds. + */ + do_action( 'redis_object_cache_set', $key, $value, $group, $expirations[ $key ], $execute_time ); + } + } + + return $results; + } + /** * Increment a Redis counter by the amount specified * @@ -1621,7 +2325,12 @@ public function set( $key, $value, $group = 'default', $expiration = 0 ) { */ public function increment( $key, $offset = 1, $group = 'default' ) { $offset = (int) $offset; - $derived_key = $this->build_key( $key, $group ); + $start_time = microtime( true ); + + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + + $derived_key = $this->fast_build_key( $san_key, $san_group ); // If group is a non-Redis group, save to internal cache, not Redis. if ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) { @@ -1632,8 +2341,6 @@ public function increment( $key, $offset = 1, $group = 'default' ) { return $value; } - $start_time = microtime( true ); - try { $result = $this->parse_redis_response( $this->redis->incrBy( $derived_key, $offset ) ); @@ -1674,8 +2381,13 @@ public function incr( $key, $offset = 1, $group = 'default' ) { * @return int|bool */ public function decrement( $key, $offset = 1, $group = 'default' ) { - $derived_key = $this->build_key( $key, $group ); $offset = (int) $offset; + $start_time = microtime( true ); + + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + + $derived_key = $this->fast_build_key( $san_key, $san_group ); // If group is a non-Redis group, save to internal cache, not Redis. if ( $this->is_ignored_group( $group ) || ! $this->redis_status() ) { @@ -1686,10 +2398,7 @@ public function decrement( $key, $offset = 1, $group = 'default' ) { return $value; } - $start_time = microtime( true ); - try { - // 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 ) ); @@ -1707,6 +2416,19 @@ public function decrement( $key, $offset = 1, $group = 'default' ) { return $result; } + /** + * Alias of `decrement()`. + * + * @see self::decrement() + * @param string $key The key name. + * @param int $offset Optional. The decrement. Defaults to 1. + * @param string $group Optional. The key group. Default is 'default'. + * @return int|bool + */ + public function decr( $key, $offset = 1, $group = 'default' ) { + return $this->decrement( $key, $offset, $group ); + } + /** * Render data about current cache requests * Used by the Debug bar plugin @@ -1723,13 +2445,13 @@ public function stats() { diagnostics['client'] ?: 'Unknown'; ?>
Cache Hits: - cache_hits ); ?> + cache_hits; ?>
Cache Misses: - cache_misses ); ?> + cache_misses; ?>
Cache Size: - cache ) ) / 1024, 2 ); ?> kB + cache ) ) / 1024, 2 ); ?> KB

'...', 'hits' => $this->cache_hits, 'misses' => $this->cache_misses, 'ratio' => $total > 0 ? round( $this->cache_hits / ( $total / 100 ), 1 ) : 100, @@ -1766,7 +2486,7 @@ function ( $keys ) { ], 'errors' => empty( $this->errors ) ? null : $this->errors, 'meta' => [ - 'Client' => $this->diagnostics['client'] ?: 'Unknown', + 'Client' => $this->diagnostics['client'] ?? 'Unknown', 'Redis Version' => $this->redis_version, ], ]; @@ -1775,8 +2495,8 @@ function ( $keys ) { /** * 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. + * @param string $key The key under which to store the value, pre-sanitized. + * @param string $group The group value appended to the $key, pre-sanitized. * * @return string */ @@ -1785,12 +2505,28 @@ public function build_key( $key, $group = 'default' ) { $group = 'default'; } - $salt = defined( 'WP_REDIS_PREFIX' ) ? trim( WP_REDIS_PREFIX ) : ''; - $prefix = $this->is_global_group( $group ) ? $this->global_prefix : $this->blog_prefix; + $san_key = $this->sanitize_key_part( $key ); + $san_group = $this->sanitize_key_part( $group ); + + return $this->fast_build_key($san_key, $san_group); + } + + /** + * Builds a key for the cached object using the prefix, group and key. + * + * @param string $key The key under which to store the value, pre-sanitized. + * @param string $group The group value appended to the $key, pre-sanitized. + * + * @return string + */ + public function fast_build_key( $key, $group = 'default' ) { + if ( empty( $group ) ) { + $group = 'default'; + } - $key = $this->sanitize_key_part( $key ); - $group = $this->sanitize_key_part( $group ); + $salt = defined( 'WP_REDIS_PREFIX' ) ? trim( WP_REDIS_PREFIX ) : ''; + $prefix = $this->is_global_group( $group ) ? $this->global_prefix : $this->blog_prefix; $prefix = trim( $prefix, '_-:$' ); return "{$salt}{$prefix}:{$group}:{$key}"; @@ -1809,31 +2545,43 @@ protected function sanitize_key_part( $part ) { /** * Checks if the given group is part the ignored group array * - * @param string $group Name of the group to check. + * @param string $group Name of the group to check, pre-sanitized. * @return bool */ protected function is_ignored_group( $group ) { - return in_array( $this->sanitize_key_part( $group ), $this->ignored_groups, true ); + return $this->is_group_of_type( $group, 'ignored' ); } /** * Checks if the given group is part the global group array * - * @param string $group Name of the group to check. + * @param string $group Name of the group to check, pre-sanitized. * @return bool */ protected function is_global_group( $group ) { - return in_array( $this->sanitize_key_part( $group ), $this->global_groups, true ); + return $this->is_group_of_type( $group, 'global' ); } /** * Checks if the given group is part the unflushable group array * - * @param string $group Name of the group to check. + * @param string $group Name of the group to check, pre-sanitized. * @return bool */ protected function is_unflushable_group( $group ) { - return in_array( $this->sanitize_key_part( $group ), $this->unflushable_groups, true ); + return $this->is_group_of_type( $group, 'unflushable' ); + } + + /** + * Checks the type of the given group + * + * @param string $group Name of the group to check, pre-sanitized. + * @param string $type Type of the group to check. + * @return bool + */ + private function is_group_of_type( $group, $type ) { + return isset( $this->group_type[ $group ] ) + && $this->group_type[ $group ] == $type; } /** @@ -1880,7 +2628,7 @@ public function add_to_internal_cache( $derived_key, $value ) { * @return bool|mixed Value on success; false on failure. */ public function get_from_internal_cache( $derived_key ) { - if ( ! isset( $this->cache[ $derived_key ] ) ) { + if ( ! array_key_exists( $derived_key, $this->cache ) ) { return false; } @@ -1902,7 +2650,7 @@ public function switch_to_blog( $_blog_id ) { return false; } - $this->blog_prefix = $_blog_id; + $this->blog_prefix = (int) $_blog_id; return true; } @@ -1920,6 +2668,8 @@ public function add_global_groups( $groups ) { } else { $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) ); } + + $this->cache_group_types(); } /** @@ -1928,9 +2678,16 @@ public function add_global_groups( $groups ) { * @param array $groups List of groups that are to be ignored. */ public function add_non_persistent_groups( $groups ) { - $groups = (array) $groups; + /** + * Filters list of groups to be added to {@see self::$ignored_groups} + * + * @since 2.1.7 + * @param string[] $groups List of groups to be ignored. + */ + $groups = apply_filters( 'redis_cache_add_non_persistent_groups', (array) $groups ); $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) ); + $this->cache_group_types(); } /** @@ -1942,6 +2699,7 @@ public function add_unflushable_groups( $groups ) { $groups = (array) $groups; $this->unflushable_groups = array_unique( array_merge( $this->unflushable_groups, $groups ) ); + $this->cache_group_types(); } /** @@ -1950,7 +2708,7 @@ public function add_unflushable_groups( $groups ) { * @param mixed $expiration Incoming expiration value (whatever it is). */ protected function validate_expiration( $expiration ) { - $expiration = is_int( $expiration ) || ctype_digit( $expiration ) ? (int) $expiration : 0; + $expiration = is_int( $expiration ) || ctype_digit( (string) $expiration ) ? (int) $expiration : 0; if ( defined( 'WP_REDIS_MAXTTL' ) ) { $max = (int) WP_REDIS_MAXTTL; @@ -2115,14 +2873,14 @@ protected function handle_exception( $exception ) { // 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 ) ); + error_log( $exception ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + if ( ! $this->fail_gracefully ) { - throw $exception; + $this->show_error_and_die( $exception ); } $this->errors[] = $exception->getMessage(); - error_log( $exception ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - if ( function_exists( 'do_action' ) ) { /** * Fires on every cache error @@ -2134,6 +2892,91 @@ protected function handle_exception( $exception ) { } } + /** + * Show Redis connection error screen, or load custom `/redis-error.php`. + * + * @return void + */ + protected function show_error_and_die( Exception $exception ) { + wp_load_translations_early(); + + add_filter( 'pre_determine_locale', function () { + return defined( 'WPLANG' ) ? WPLANG : 'en_US'; + } ); + + // Load custom DB error template, if present. + if ( file_exists( WP_CONTENT_DIR . '/redis-error.php' ) ) { + require_once WP_CONTENT_DIR . '/redis-error.php'; + die(); + } + + $verbose = wp_installing() + || defined( 'WP_ADMIN' ) + || ( defined( 'WP_DEBUG' ) && WP_DEBUG ); + + $message = '

' . __( 'Error establishing a Redis connection', 'redis-cache' ) . "

\n"; + + if ( $verbose ) { + $message .= "

" . $exception->getMessage() . "

\n"; + + $message .= '

' . sprintf( + // translators: %s = Formatted wp-config.php file name. + __( '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' ), + 'wp-config.php' + ) . "

\n"; + + $message .= "\n"; + + $message .= '

' . sprintf( + // translators: %s = Link to installation instructions. + __( 'If you need help, please read the installation instructions.', 'redis-cache' ), + 'https://github.com/rhubarbgroup/redis-cache/blob/develop/INSTALL.md' + ) . "

\n"; + } + + $message .= '

' . sprintf( + // translators: %1$s = Formatted object-cache.php file name, %2$s = Formatted wp-content directory name. + __( 'To disable Redis, delete the %1$s file in the %2$s directory.', 'redis-cache' ), + 'object-cache.php', + '/wp-content/' + ) . "

\n"; + + wp_die( $message ); + } + + /** + * Builds a clean connection array out of redis clusters array. + * + * @return array + */ + protected function build_cluster_connection_array() { + $cluster = array_values( WP_REDIS_CLUSTER ); + + foreach ( $cluster as $key => $server ) { + $connection_string = parse_url( $server ); + + $cluster[ $key ] = sprintf( + "%s:%s", + $connection_string['host'], + $connection_string['port'] + ); + } + + return $cluster; + } + + /** + * Check whether Predis client is in use. + * + * @return bool + */ + protected function is_predis() { + return $this->redis instanceof Predis\Client; + } + /** * Allows access to private properties for backwards compatibility. *