(string) self::conf( 'CTPS_REDIS_HOST', '127.0.0.1' ), 'port' => (int) self::conf( 'CTPS_REDIS_PORT', 6379 ), 'socket' => $socket, 'database' => (int) self::conf( 'CTPS_REDIS_DB', 0 ), 'timeout' => (float) self::conf( 'CTPS_REDIS_TIMEOUT', 1 ), 'maxttl' => (int) self::conf( 'CTPS_REDIS_MAXTTL', 0 ), 'password' => '' !== self::conf( 'CTPS_REDIS_PASSWORD', '' ) && null !== self::conf( 'CTPS_REDIS_PASSWORD', '' ), 'target' => $socket ? 'unix:' . $socket : self::conf( 'CTPS_REDIS_HOST', '127.0.0.1' ) . ':' . (int) self::conf( 'CTPS_REDIS_PORT', 6379 ), 'prefix' => self::salt(), ); } /** * Same algorithm as WP_Object_Cache::build_salt() in the drop-in. */ public static function salt() { if ( defined( 'WP_CACHE_KEY_SALT' ) && '' !== trim( (string) WP_CACHE_KEY_SALT ) ) { $raw = rtrim( trim( (string) WP_CACHE_KEY_SALT ), ':' ); $clean = preg_replace( '/[^A-Za-z0-9_.\-]/', '', $raw ); if ( '' !== $clean && $clean === $raw && strlen( $clean ) <= 64 ) { return $clean; } return 'wp_' . substr( md5( (string) WP_CACHE_KEY_SALT ), 0, 12 ); } $prefix = isset( $GLOBALS['table_prefix'] ) ? (string) $GLOBALS['table_prefix'] : ''; $name = defined( 'DB_NAME' ) ? (string) DB_NAME : ''; $host = defined( 'DB_HOST' ) ? (string) DB_HOST : ''; return 'wp_' . substr( md5( 'ctps|' . $host . '|' . $name . '|' . $prefix ), 0, 12 ); } /** * Opens a test connection with the configured constants. * * @return Redis|WP_Error */ private static function connect() { if ( ! class_exists( 'Redis' ) ) { return new WP_Error( 'ctps_oc_no_extension', __( 'The PHP Redis extension (phpredis) is not installed on this server.', 'celero-adaptive-speed' ) ); } $c = self::config(); $timeout = $c['timeout'] > 0 ? $c['timeout'] : 1.0; try { $redis = new Redis(); $ok = $c['socket'] ? $redis->connect( $c['socket'], 0, $timeout, null, 0, $timeout ) : $redis->connect( $c['host'], $c['port'], $timeout, null, 0, $timeout ); if ( ! $ok ) { throw new RedisException( 'Connection refused' ); } } catch ( Throwable $e ) { /* translators: 1: Redis address (host:port or socket), 2: technical error message */ return new WP_Error( 'ctps_oc_connect', sprintf( __( 'Cannot connect to Redis at %1$s: %2$s', 'celero-adaptive-speed' ), $c['target'], $e->getMessage() ) ); } try { $password = self::conf( 'CTPS_REDIS_PASSWORD', null ); if ( null !== $password && '' !== $password && array() !== $password && ! $redis->auth( $password ) ) { throw new RedisException( (string) $redis->getLastError() ); } } catch ( Throwable $e ) { /* translators: %s: technical error message */ return new WP_Error( 'ctps_oc_auth', sprintf( __( 'Redis refused the password (CTPS_REDIS_PASSWORD): %s', 'celero-adaptive-speed' ), $e->getMessage() ) ); } try { if ( $c['database'] && ! $redis->select( $c['database'] ) ) { throw new RedisException( (string) $redis->getLastError() ); } } catch ( Throwable $e ) { /* translators: %d: Redis database number */ return new WP_Error( 'ctps_oc_select', sprintf( __( 'Cannot select Redis database %d (CTPS_REDIS_DB).', 'celero-adaptive-speed' ), $c['database'] ) ); } return $redis; } /** * phpredis is loaded and Redis answers PING with the configured settings. * * @return true|WP_Error */ public static function available( $refresh = false ) { if ( null !== self::$available && ! $refresh ) { return self::$available; } $redis = self::connect(); if ( is_wp_error( $redis ) ) { self::$available = $redis; return $redis; } try { $pong = $redis->ping(); $redis->close(); } catch ( Throwable $e ) { $pong = false; } if ( true === $pong || ( is_string( $pong ) && false !== stripos( $pong, 'PONG' ) ) ) { self::$available = true; } else { /* translators: %s: Redis address (host:port or socket) */ self::$available = new WP_Error( 'ctps_oc_ping', sprintf( __( 'Redis at %s does not answer PING.', 'celero-adaptive-speed' ), self::config()['target'] ) ); } return self::$available; } /* --------------------------------------------------------------------- * Status * ------------------------------------------------------------------ */ /** * @return array { * @type string $state none|ours-active|ours-fallback|foreign * @type string $label Short translated label. * @type string $message Translated explanation ('' when all is fine). * @type bool $loaded Our drop-in serves the current request. * @type string $reason Fallback reason from the drop-in ('' if none). * @type string $file_version Version of the installed drop-in. * @type string $template_version Version shipped with the plugin. * @type bool $outdated Installed drop-in older/newer than the template. * @type string $foreign_name Name of another plugin's drop-in. * @type array|null $stats WP_Object_Cache::get_stats() when ours is loaded. * } */ public static function status() { $out = array( 'state' => self::STATE_NONE, 'label' => __( 'Not active', 'celero-adaptive-speed' ), 'message' => '', 'loaded' => false, 'reason' => '', 'file_version' => '', 'template_version' => self::file_version( self::template() ), 'outdated' => false, 'foreign_name' => '', 'stats' => null, ); if ( ! self::dropin_exists() ) { return $out; } if ( ! self::is_ours() ) { $name = ''; if ( function_exists( 'get_file_data' ) ) { $data = get_file_data( self::target(), array( 'name' => 'Plugin Name' ) ); $name = (string) $data['name']; } $out['state'] = self::STATE_FOREIGN; $out['foreign_name'] = '' !== $name ? $name : __( 'unknown plugin', 'celero-adaptive-speed' ); $out['label'] = __( 'Another plugin\'s object cache is installed', 'celero-adaptive-speed' ); /* translators: %s: name of the other object cache plugin */ $out['message'] = sprintf( __( 'wp-content/object-cache.php belongs to %s. Adaptive Speed will not replace it: remove it from that plugin first if you want to use the Adaptive Speed Redis cache.', 'celero-adaptive-speed' ), $out['foreign_name'] ); return $out; } $out['file_version'] = self::file_version(); $out['outdated'] = '' !== $out['template_version'] && $out['file_version'] !== $out['template_version']; if ( self::is_loaded() ) { $cache = $GLOBALS['wp_object_cache']; $st = $cache->get_status(); $out['loaded'] = true; $out['reason'] = (string) $st['reason']; $out['stats'] = method_exists( $cache, 'get_stats' ) ? $cache->get_stats() : null; $connected = ! empty( $st['connected'] ) && ( null === $out['stats'] || ! empty( $out['stats']['connected'] ) ); if ( ! $connected && null !== $out['stats'] ) { $st = $out['stats']['status']; $out['reason'] = (string) $st['reason']; } if ( $connected ) { $out['state'] = self::STATE_ACTIVE; $out['label'] = __( 'Active: Redis connected', 'celero-adaptive-speed' ); } else { $out['state'] = self::STATE_FALLBACK; $out['label'] = __( 'Redis not reachable: in-memory cache in use', 'celero-adaptive-speed' ); $out['message'] = self::reason_message( $out['reason'], $st ); } } else { // Installed but not running in this request (e.g. just enabled): test the connection. $ok = self::available(); if ( true === $ok ) { $out['state'] = self::STATE_ACTIVE; $out['label'] = __( 'Active: Redis connected', 'celero-adaptive-speed' ); } else { $out['state'] = self::STATE_FALLBACK; $out['reason'] = 'connect-failed'; $out['label'] = __( 'Redis not reachable: in-memory cache in use', 'celero-adaptive-speed' ); $out['message'] = $ok->get_error_message(); } } if ( $out['outdated'] && '' === $out['message'] ) { $out['message'] = __( 'The installed object-cache.php is from another version of Adaptive Speed: turn the Redis cache off and on again to update it.', 'celero-adaptive-speed' ); } return $out; } private static function reason_message( $reason, array $st ) { $error = isset( $st['error'] ) ? (string) $st['error'] : ''; switch ( $reason ) { case 'disabled': /* translators: %s: name of the switch (ctps-off file or a wp-config.php constant) */ return sprintf( __( 'The Redis cache is turned off by the emergency switch (%s). WordPress is using a temporary in-memory cache.', 'celero-adaptive-speed' ), isset( $st['disabled_by'] ) ? $st['disabled_by'] : '' ); case 'no-extension': return __( 'The PHP Redis extension (phpredis) is not installed on this server. WordPress is using a temporary in-memory cache.', 'celero-adaptive-speed' ); case 'backoff': return __( 'Redis did not respond a few moments ago: it will be tried again shortly. Meanwhile WordPress is using a temporary in-memory cache.', 'celero-adaptive-speed' ); case 'runtime-error': /* translators: %s: technical error message */ return sprintf( __( 'Redis stopped responding while the page was loading, so WordPress switched to a temporary in-memory cache. Details: %s', 'celero-adaptive-speed' ), $error ); default: /* translators: %s: technical error message */ return sprintf( __( 'Cannot connect to Redis, so WordPress is using a temporary in-memory cache. The site works but is slower. Details: %s', 'celero-adaptive-speed' ), $error ); } } /** * Text for an admin notice, or '' when there is nothing to report. */ public static function notice() { $s = self::status(); if ( self::STATE_FALLBACK === $s['state'] || ( self::STATE_ACTIVE === $s['state'] && $s['outdated'] ) ) { return $s['message']; } return ''; } /* --------------------------------------------------------------------- * Enable / disable / flush * ------------------------------------------------------------------ */ /** * Installs (or updates) our drop-in. * * @return true|WP_Error */ public static function enable() { if ( is_multisite() ) { return new WP_Error( 'ctps_oc_multisite', __( 'Adaptive Speed does not support multisite networks yet.', 'celero-adaptive-speed' ) ); } $template = self::template(); $target = self::target(); if ( ! is_file( $template ) || ! is_readable( $template ) ) { return new WP_Error( 'ctps_oc_template', __( 'The drop-in template (dropins/object-cache.tpl) is missing from the plugin folder. Reinstall Adaptive Speed.', 'celero-adaptive-speed' ) ); } if ( file_exists( $target ) && ! self::is_ours( $target ) ) { return new WP_Error( 'ctps_oc_foreign', self::foreign_message() ); } $ok = self::available( true ); if ( is_wp_error( $ok ) ) { return $ok; } return self::install( true ); } /** * Refreshes our drop-in when the plugin ships a different version * (call after a plugin update). Does nothing if the file is not ours. * * @return bool|WP_Error True if updated, false if nothing to do. */ public static function maybe_update() { if ( ! self::is_ours() ) { return false; } if ( self::file_version() === self::file_version( self::template() ) ) { return false; } return self::install( false ); } /** * Atomic copy: write a temporary file next to the target, then link it * in place (fails if a file appeared meanwhile) or rename over our own. */ private static function install( $flush ) { $template = self::template(); $target = self::target(); $dir = dirname( $target ); if ( ! is_dir( $dir ) || ! wp_is_writable( $dir ) ) { return new WP_Error( 'ctps_oc_not_writable', __( 'The wp-content folder is not writable, so object-cache.php cannot be created. Check the folder permissions.', 'celero-adaptive-speed' ) ); } if ( file_exists( $target ) && ! wp_is_writable( $target ) ) { return new WP_Error( 'ctps_oc_not_writable', __( 'The wp-content/object-cache.php file is not writable. Check its permissions.', 'celero-adaptive-speed' ) ); } $tmp = $dir . '/.object-cache.php.ctps-' . uniqid( '', true ); if ( ! @copy( $template, $tmp ) || md5_file( $tmp ) !== md5_file( $template ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors wp_delete_file( $tmp ); return new WP_Error( 'ctps_oc_write', __( 'Could not write wp-content/object-cache.php.', 'celero-adaptive-speed' ) ); } @chmod( $tmp, 0644 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- sets standard 0644 on the object-cache.php drop-in temp copy before the atomic rename $exists = file_exists( $target ); if ( $exists && ! self::is_ours( $target ) ) { wp_delete_file( $tmp ); return new WP_Error( 'ctps_oc_foreign', self::foreign_message() ); } $done = false; if ( ! $exists && function_exists( 'link' ) ) { $done = @link( $tmp, $target ); // phpcs:ignore WordPress.PHP.NoSilencedErrors if ( $done ) { wp_delete_file( $tmp ); } elseif ( file_exists( $target ) && ! self::is_ours( $target ) ) { wp_delete_file( $tmp ); return new WP_Error( 'ctps_oc_foreign', self::foreign_message() ); } } if ( ! $done ) { $done = @rename( $tmp, $target ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions.rename_rename -- atomic install of the object-cache.php drop-in in wp-content; WP_Filesystem::move() is not atomic } if ( ! $done ) { wp_delete_file( $tmp ); return new WP_Error( 'ctps_oc_write', __( 'Could not write wp-content/object-cache.php.', 'celero-adaptive-speed' ) ); } if ( function_exists( 'opcache_invalidate' ) ) { @opcache_invalidate( $target, true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors } if ( $flush ) { // Redis may hold keys from an earlier activation: the database changed since. self::flush_redis_prefix(); } return true; } /** * Removes our drop-in (never a foreign one) and empties the cache. * * @return true|WP_Error */ public static function disable() { $target = self::target(); if ( ! file_exists( $target ) ) { self::remove_markers(); return true; } if ( ! self::is_ours( $target ) ) { return new WP_Error( 'ctps_oc_foreign', self::foreign_message() ); } wp_delete_file( $target ); if ( file_exists( $target ) ) { return new WP_Error( 'ctps_oc_delete', __( 'Could not delete wp-content/object-cache.php. Check the permissions of the wp-content folder.', 'celero-adaptive-speed' ) ); } if ( function_exists( 'opcache_invalidate' ) ) { @opcache_invalidate( $target, true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors } if ( self::is_loaded() ) { wp_cache_flush(); if ( method_exists( $GLOBALS['wp_object_cache'], 'detach' ) ) { $GLOBALS['wp_object_cache']->detach(); } } else { self::flush_redis_prefix(); if ( function_exists( 'wp_cache_flush' ) ) { wp_cache_flush(); } } self::remove_markers(); return true; } /** * Empties the object cache, only when our drop-in is the active one. * * @return bool */ public static function flush() { if ( ! self::is_loaded() ) { return false; } return (bool) wp_cache_flush(); } /** * Deletes this site's keys straight from Redis (SCAN on the prefix), * for when the drop-in is not the cache of the current request. * * @return int|false Keys deleted, false if Redis is not reachable. */ public static function flush_redis_prefix() { $redis = self::connect(); if ( is_wp_error( $redis ) ) { return false; } $deleted = 0; try { $redis->setOption( Redis::OPT_SCAN, Redis::SCAN_RETRY ); $pattern = addcslashes( self::salt(), '\\*?[]^' ) . ':*'; $it = null; do { $keys = $redis->scan( $it, $pattern, 1000 ); if ( $keys ) { $n = $redis->unlink( $keys ); if ( false === $n ) { $n = $redis->del( $keys ); } $deleted += (int) $n; } } while ( $it > 0 ); $redis->close(); } catch ( Throwable $e ) { return false; } return $deleted; } private static function remove_markers() { $name = '.ctps-redis-down-' . self::salt(); foreach ( array( WP_CONTENT_DIR . '/cache/', WP_CONTENT_DIR . '/', rtrim( sys_get_temp_dir(), '/\\' ) . '/' ) as $dir ) { if ( file_exists( $dir . $name ) ) { wp_delete_file( $dir . $name ); } } } private static function foreign_message() { $name = ''; if ( function_exists( 'get_file_data' ) && file_exists( self::target() ) ) { $data = get_file_data( self::target(), array( 'name' => 'Plugin Name' ) ); $name = (string) $data['name']; } /* translators: %s: name of the other object cache plugin */ return sprintf( __( 'Another object cache is already installed in wp-content/object-cache.php (%s). Adaptive Speed will not overwrite it: disable it from its own plugin first.', 'celero-adaptive-speed' ), '' !== $name ? $name : __( 'unknown plugin', 'celero-adaptive-speed' ) ); } /* --------------------------------------------------------------------- * Plugin lifecycle * ------------------------------------------------------------------ */ /** * For register_deactivation_hook: a drop-in without its plugin cannot be * managed from wp-admin any more, so it goes away with the plugin. */ public static function on_deactivate() { if ( self::is_ours() ) { self::disable(); } } /** * For uninstall.php (require this file first). */ public static function on_uninstall() { if ( self::is_ours() ) { self::disable(); } else { self::remove_markers(); } } }