208 lines
5.2 KiB
PHP
208 lines
5.2 KiB
PHP
<?php
|
|
/**
|
|
* @brief Redis Cache Class
|
|
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
|
* @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 );
|
|
}
|
|
} |