Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 12 November 2018 */ namespace IPS\Output\Cache; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Redis Cache Class */ class _Redis extends \IPS\Output\Cache { /** * Turn on debugging which logs stats about cache hits and misses in Redis * * @var bool */ protected $debug = FALSE; /** * Server supports this method? * * @return bool */ public static function supported() { if ( class_exists('Redis') ) { try { $connection = \IPS\Redis::i()->connection('read'); if( !$connection ) { return FALSE; } return TRUE; } catch( \RedisException $e ) { return FALSE; } } return FALSE; } /** * Redis key */ protected $_redisKey; /** * Get random string used in the keys to identify this site compared to other sites * * @return string|FALSE Value from the _datastore; FALSE if key doesn't exist */ protected function _getRedisKey() { if ( !$this->_redisKey ) { if ( !( $this->_redisKey = \IPS\Redis::i()->get( 'redisKeyOutputCache' ) ) ) { $this->_redisKey = md5( mt_rand() ) . '_pg_'; \IPS\Redis::i()->setex( 'redisKeyOutputCache', 604800, $this->_redisKey ); } } return $this->_redisKey; } /** * Abstract Method: Get * * @param string $key * @return array|FALSE Value from the _datastore; FALSE if key doesn't exist */ protected function _get( $key ) { try { $value = \IPS\Redis::i()->get( $this->_getRedisKey() . '_' . $key ); if ( ! $value ) { if ( $this->debug ) { \IPS\Redis::i()->zRemRangeByScore('guest_page_failures_key_gone', 0, time() - \IPS\CACHE_PAGE_TIMEOUT ); \IPS\Redis::i()->zAdd( 'guest_page_failures_key_gone', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT ); } return FALSE; } if ( ! $decrypted = \IPS\Text\Encrypt::fromTag( $value )->decrypt() ) { if ( $this->debug ) { \IPS\Redis::i()->zRemRangeByScore('guest_page_failures_decrypt', 0, time() - \IPS\CACHE_PAGE_TIMEOUT ); \IPS\Redis::i()->zAdd( 'guest_page_failures_decrypt', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT ); } return FALSE; } if ( ! $unzipped = @gzdecode( $decrypted ) ) { if ( $this->debug ) { \IPS\Redis::i()->zRemRangeByScore('guest_page_failures_gz', 0, time() - \IPS\CACHE_PAGE_TIMEOUT ); \IPS\Redis::i()->zAdd( 'guest_page_failures_gz', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT ); } return FALSE; } if ( ! $data = json_decode( $unzipped, TRUE ) ) { if ( $this->debug ) { \IPS\Redis::i()->zRemRangeByScore('guest_page_failures_json_decode', 0, time() - \IPS\CACHE_PAGE_TIMEOUT ); \IPS\Redis::i()->zAdd( 'guest_page_failures_json_decode', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT ); } return FALSE; } if ( $this->debug ) { \IPS\Redis::i()->zRemRangeByScore('guest_page_hits', 0, time() - \IPS\CACHE_PAGE_TIMEOUT ); \IPS\Redis::i()->zAdd( 'guest_page_hits', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT ); } return array( 'output' => $data['value'], 'meta' => $data['meta'], 'expires' => $data['expires'] ); } catch( \RedisException $e ) { \IPS\Redis::i()->resetConnection( $e ); return FALSE; } } /** * Store value using cache method if available or falling back to the database * * @param string $key Key * @param mixed $value Value * @param mixed $meta Meta data (contentType, headers, etc) * @param \IPS\DateTime $expire Expiration if using database * @return bool */ protected function _set( $key, $value, $meta, \IPS\DateTime $expire ) { try { if ( $this->debug ) { \IPS\Redis::i()->zRemRangeByScore('guest_page_created', 0, time() - \IPS\CACHE_PAGE_TIMEOUT ); \IPS\Redis::i()->zAdd( 'guest_page_created', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT ); } /* json_encode > gzencode > crypt */ return (bool) \IPS\Redis::i()->setex( $this->_getRedisKey() . '_' . $key, $expire->getTimestamp() - time(), \IPS\Text\Encrypt::fromPlaintext( gzencode( json_encode( array( 'value' => $value, 'meta' => $meta, 'expires' => $expire->getTimestamp() ) ) ) )->tag() ); } catch( \RedisException $e ) { \IPS\Redis::i()->resetConnection( $e ); return FALSE; } } /** * Remove all expired caches * * @param boolean $truncate Truncate the table * @return void */ public function deleteExpired( $truncate=FALSE ) { /* Redis handles this for us automatically */ } /** * Abstract Method: Clear All Caches * * @return void */ public function clearAll() { $this->_redisKey = md5( mt_rand() ) . '_pg_'; \IPS\Redis::i()->setex( 'redisKeyOutputCache', 604800, $this->_redisKey ); } }