Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+123 -110
View File
@@ -10,55 +10,66 @@
namespace IPS;
use function class_exists;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use BadMethodCallException;
use IPS\Session\Store;
use IPS\Text\Encrypt;
use Redis as PHPRedis;
use RedisException;
use function array_slice;
use function count;
use function defined;
use function stristr;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Redis Cache Class
*
* @mixin PHPRedis
*/
class _Redis
class Redis
{
/**
* @brief Multiton Store
*/
protected static $multitons = array();
protected static array $multitons = array();
/**
* @brief Connections Store
* @var \Redis[]
*/
protected static $connections = array();
protected static array $connections = array();
/**
* @brief Default expiration for keys in seconds
*/
protected static $ttl = 604800; #7 days
protected static int $ttl = 604800; #7 days
/**
* @brief Log what redis is up to
*/
public static $log = array();
public static array $log = array();
/**
* @brief Log what redis is up to
* @brief Prefix
*/
public $prefix = NULL;
public ?string $prefix = NULL;
/**
* @brief Unpack the config once
*/
protected static $config = NULL;
protected static ?array $config = NULL;
/**
* Writes made
*/
protected static $writes = array();
protected static array $writes = array();
/**
* @var array
@@ -68,26 +79,26 @@ class _Redis
/**
* Get instance
*
* @param NULL|array $configuration Configuration to use (NULL to use \IPS\REDIS_CONFIG or \IPS\CACHE_CONFIG)
* @param NULL|string $identifier Identifier (to support multiple instances)
* @return \Redis
* @param array|null $configuration Configuration to use (NULL to use \IPS\REDIS_CONFIG or \IPS\CACHE_CONFIG)
* @param string|null $identifier Identifier (to support multiple instances)
* @return Redis
*/
public static function i( $configuration=NULL, $identifier=NULL )
public static function i( array $configuration=NULL, string $identifier=NULL ) : Redis
{
if ( static::$config === NULL )
{
$config = ( \defined( '\IPS\REDIS_CONFIG' ) and \IPS\REDIS_CONFIG !== NULL ) ? \IPS\REDIS_CONFIG : \IPS\CACHE_CONFIG;
$config = ( defined( '\IPS\REDIS_CONFIG' ) and REDIS_CONFIG !== NULL ) ? REDIS_CONFIG : CACHE_CONFIG;
static::$config = $configuration ?: json_decode( $config, true );
}
$identifier = $identifier ? $identifier : '_MAIN';
$identifier = $identifier ?: '_MAIN';
if ( ! isset( static::$multitons[ $identifier ] ) )
{
static::$multitons[ $identifier ] = new static;
/* Set the prefix with the most obvious comment in the world */
static::$multitons[ $identifier ]->prefix = \IPS\SUITE_UNIQUE_KEY . '_';
static::$multitons[ $identifier ]->prefix = SUITE_UNIQUE_KEY . '_';
}
/* Return */
@@ -101,31 +112,31 @@ class _Redis
*/
public function __destruct()
{
if ( \IPS\DEBUG_LOG and \count( static::$writes ) > 50 )
if ( DEBUG_LOG and count( static::$writes ) > 50 )
{
$slice = implode( ', ', \array_slice( static::$writes, 0, 50 ) );
\IPS\Log::debug( "Large number of Redis writes: " . $slice, 'redis_writes' );
$slice = implode( ', ', array_slice( static::$writes, 0, 50 ) );
Log::debug( "Large number of Redis writes: " . $slice, 'redis_writes' );
}
}
/**
* @brief Chosen reader for this session
*/
protected static $reader = NULL;
protected static ?int $reader = NULL;
/**
* Connect to Redis
*
* @param string|NULL $identifier Identifier
* @return \Redis
* @throws \RedisException If connection cannot be established
* @throws \BadMethodCallException If Redis PHP extension is not installed
* @param string|null $identifier Identifier
* @return PHPRedis
* @throws RedisException If connection cannot be established
* @throws BadMethodCallException If Redis PHP extension is not installed
*/
public function connection( $identifier=NULL )
public function connection( string $identifier=NULL ): PHPRedis
{
if ( ! class_exists('Redis') )
{
throw new \BadMethodCallException;
throw new BadMethodCallException;
}
$useConfig = NULL;
@@ -140,7 +151,7 @@ class _Redis
{
if ( static::$reader === NULL )
{
static::$reader = rand( 0, \count( static::$config['read'] ) - 1 );
static::$reader = rand( 0, count( static::$config['read'] ) - 1 );
}
$useConfig = static::$config['read'][ static::$reader ];
@@ -164,7 +175,7 @@ class _Redis
{
try
{
static::$connections[ $identifier ] = new \Redis;
static::$connections[ $identifier ] = new PHPRedis;
/* Track connection requests */
if ( ! isset( static::$connectionAttempts[ $identifier ] ) )
@@ -184,7 +195,7 @@ class _Redis
if( @static::$connections[ $identifier ]->connect( $useConfig['server'], $useConfig['port'], 1, '', 500 ) === FALSE ) # Connect with a 1 second timeout, and 500ms between connection attempts
{
unset( static::$connections[ $identifier ] );
throw new \RedisException('CANNOT_CONNECT');
throw new RedisException('CANNOT_CONNECT');
}
else
{
@@ -193,33 +204,33 @@ class _Redis
if( static::$connections[ $identifier ]->auth( $useConfig['password'] ) === FALSE )
{
unset( static::$connections[ $identifier ] );
throw new \RedisException;
throw new RedisException;
}
}
}
if( static::$connections[ $identifier ] !== NULL )
{
static::$connections[ $identifier ]->setOption( \Redis::OPT_SERIALIZER, \Redis::SERIALIZER_NONE );
static::$connections[ $identifier ]->setOption( \Redis::OPT_PREFIX, $this->prefix );
static::$connections[ $identifier ]->setOption( PHPRedis::OPT_SERIALIZER, PHPRedis::SERIALIZER_NONE );
static::$connections[ $identifier ]->setOption( PHPRedis::OPT_PREFIX, $this->prefix );
}
/* If connection times out, connect can return TRUE and we won't know until our next attempt to talk to the server,
so we should ping now to verify we were able to connect successfully */
static::$connections[ $identifier ]->ping();
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "Redis connected (" . $identifier . ' ' . $useConfig['server'] . ")" );
}
if ( \count( static::$connections ) === 1 ) # Only set the shutdown function if this is the first connection so we don't get multiple shutdown functions
if ( count( static::$connections ) === 1 ) # Only set the shutdown function if this is the first connection so we don't get multiple shutdown functions
{
register_shutdown_function( function( $object ){
try
{
/* First we have to make sure sessions have written */
if( \IPS\Session\Store::i() instanceof \IPS\Session\Store\Redis )
if( Store::i() instanceof Store\Redis)
{
session_write_close();
}
@@ -232,11 +243,11 @@ class _Redis
/* Reset stored connections so they can be re-connected correctly if tasks run after this shutdown proceses */
static::$connections = array();
}
catch( \RedisException $e ){}
catch( RedisException $e ){}
}, $this );
}
}
catch( \RedisException $e )
catch( RedisException $e )
{
/* Unset this connection */
if ( isset( static::$connections[ $identifier ] ) )
@@ -250,7 +261,7 @@ class _Redis
if( !isset( static::$connections[ $identifier ] ) )
{
throw new \RedisException('CANNOT_CONNECT');
throw new RedisException('CANNOT_CONNECT');
}
return static::$connections[ $identifier ];
@@ -259,23 +270,23 @@ class _Redis
/**
* Call methods
*
* @param string $method Method
* @param string $method Method
* @param mixed $args Arguments
* @return mixed
*/
public function __call( $method, $args )
public function __call( string $method, mixed $args )
{
if ( method_exists( 'Redis', $method ) )
{
$type = ( \stristr( $method, 'get' ) or \stristr( $method, 'RevRange' ) or $method === 'lRange' ) ? 'read' : 'write';
$type = ( stristr( $method, 'get' ) or stristr( $method, 'RevRange' ) ) ? 'read' : 'write';
$return = $this->connection( $type )->$method( ...$args );
if ( $type === 'write' and \count( $args ) )
if ( $type === 'write' and count( $args ) )
{
static::$writes[] = $args[0];
}
if ( \IPS\REDIS_LOG and \count( $args ) )
if ( REDIS_LOG and count( $args ) )
{
if ( preg_match( '#^[a-f0-9]{32}_str__#', $args[0] ) )
{
@@ -286,26 +297,27 @@ class _Redis
}
return $return;
}
}
return null;
}
/**
* Add one or more members to a sorted set or update its score if it already exists
* Overloaded here so it can add a TTL to prevent permanent keys
*
* @param string $key Key
* @param float $score Score
* @param string $value Value
* @param int|NULL $ttl TTL in seconds
* @return 1 if the element is added. 0 otherwise.
* @param string $key Key
* @param float $score Score
* @param string $value Value
* @param int|null $ttl TTL in seconds
* @return int 1 if the element is added. 0 otherwise.
*/
public function zAdd( $key, $score, $value, $ttl=NULL )
public function zAdd( string $key, float $score, string $value, int $ttl=NULL ): int
{
$return = $this->connection('write')->zAdd( $this->key( $key ), $score, $value );
$this->connection('write')->expire( $this->key( $key ), ( $ttl ? $ttl : static::$ttl ) );
$this->connection('write')->expire( $this->key( $key ), ( $ttl ?: static::$ttl ) );
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) zAdd " . $key . " = " . $return, json_encode( $value ) );
}
@@ -317,18 +329,18 @@ class _Redis
* Fills in a whole hash. Non-string values are converted to string, using the standard (string) cast. NULL values are stored as empty strings.
* Overloaded here so it can add a TTL to prevent permanent keys
*
* @param string $key Key
* @param array $value Value
* @param int|NULL $ttl TTL in seconds
* @param string $key Key
* @param array $value Value
* @param int|null $ttl TTL in seconds
* @return boolean
*/
public function hMSet( $key, $value, $ttl=NULL )
public function hMSet( string $key, array $value, int $ttl=NULL ): bool
{
$return = $this->connection('write')->hMSet( $this->key( $key ), $value );
$this->connection('write')->expire( $this->key( $key ), ( $ttl ? $ttl : static::$ttl ) );
$this->connection('write')->expire( $this->key( $key ), ( $ttl ?: static::$ttl ) );
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) hMSet " . $key . " = " . $return, json_encode( $value ) );
}
@@ -340,16 +352,16 @@ class _Redis
* Set the string value in argument as value of the key, with a time to live
* Overloaded here so it can be logged
*
* @param string $key Key
* @param int|NULL $ttl TTL in seconds
* @param string $value Value
* @param string $key Key
* @param int|null $ttl TTL in seconds
* @param string $value Value
* @return boolean
*/
public function setEx( $key, $ttl, $value )
public function setEx( string $key, ?int $ttl, string $value ): bool
{
$return = $this->connection('write')->setEx( $this->key( $key ), $ttl, $value );
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) setEx " . $key . " = " . $return, json_encode( $value ) );
}
@@ -361,11 +373,11 @@ class _Redis
* Sort the elements in a list, set or sorted set.
* Overloaded here so we can adjust the key
*
* @param string $key Key
* @param array $options Options: array(key => value, ...) - optional
* @return array
* @param string $key Key
* @param array $options Options: array(key => value, ...) - optional
* @return array|int
*/
public function sort( $key, $options=array() )
public function sort( string $key, array $options=array() ): array|int
{
$return = $this->connection('write')->sort( $this->key( $key ), $options );
@@ -374,7 +386,7 @@ class _Redis
$this->connection('write')->expire( $this->key( $options['store'] ), ( isset( $options['ttl'] ) and $options['ttl'] ) ? $options['ttl'] : static::$ttl );
}
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) sort " . $key, json_encode( $return ) );
}
@@ -386,15 +398,15 @@ class _Redis
* Returns the whole hash, as an array of strings indexed by strings.
* Overloaded here so it can be logged
*
* @param string $key Key
* @param string $key Key
* @return array
*/
public function hGetAll( $key )
public function hGetAll( string $key ): array
{
/* Make sure we read */
$return = $this->connection('read')->hGetAll( $this->key( $key ) );
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(read) hGetAll " . $key, json_encode( $return ) );
}
@@ -406,10 +418,10 @@ class _Redis
* Publish a message to Redis PubSub
*
* @param string $key The message key
* @param mixed $value The payload
* @param mixed $value The payload
* @param boolean $encrypted Should the payload be encrypted? Has no effect in non-CiC environments.
*/
public function publish( string $key, $value=array(), bool $encrypted=FALSE ) :int
public function publish(string $key, mixed $value=array(), bool $encrypted=FALSE ) :int
{
return $this->connection('write')->publish( $key, $this->encode($value, $encrypted) );
}
@@ -418,18 +430,18 @@ class _Redis
* Increments the score of a member from a sorted set by a given amount.
* Overloaded here so it can be logged and a ttl set
*
* @param string $key Key
* @param int $inc Value to increment
* @param string $value Value
* @param int|NULL $ttl TTL in seconds
* @param string $key Key
* @param int $inc Value to increment
* @param string $value Value
* @param int|null $ttl TTL in seconds
* @return boolean
*/
public function zIncrBy( $key, $inc, $value, $ttl=NULL )
public function zIncrBy( string $key, int $inc, string $value, int $ttl=NULL ): bool
{
$return = $this->connection('write')->zIncrBy( $this->key( $key ), $inc, $value );
$this->connection('write')->expire( $this->key( $key ), ( $ttl ? $ttl : static::$ttl ) );
$this->connection('write')->expire( $this->key( $key ), ( $ttl ?: static::$ttl ) );
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) zIncrBy " . $key . " = " . $return, json_encode( $value ) );
}
@@ -440,10 +452,10 @@ class _Redis
/**
* Strip prefixes from keys as PHP redis will handle this
*
* @param string $key Key
* @param string $key Key
* @return string
*/
protected function key( $key )
protected function key( string $key ): string
{
if ( $this->prefix )
{
@@ -460,13 +472,14 @@ class _Redis
* Encode
*
* @param mixed $value Value
* @param bool $encryptIfCic Encrypt for CiC
* @return string
*/
public function encode( $value, $encryptIfCic=TRUE )
public function encode(mixed $value, bool $encryptIfCic=TRUE ): string
{
if ( \IPS\CIC && $encryptIfCic )
if ( CIC && $encryptIfCic )
{
return \IPS\Text\Encrypt::fromPlaintext( json_encode( $value ) )->tag();
return Encrypt::fromPlaintext( json_encode( $value ) )->tag();
}
else
{
@@ -480,15 +493,15 @@ class _Redis
* @param mixed $value Value
* @return mixed
*/
public function decode( $value )
public function decode( mixed $value ): mixed
{
if ( \IPS\CIC )
if (CIC)
{
$decoded = json_decode( \IPS\Text\Encrypt::fromTag( $value )->decrypt(), TRUE );
$decoded = json_decode( Encrypt::fromTag( $value )->decrypt(), TRUE );
if( $decoded === NULL )
{
throw new \RedisException('DECODE_ERROR');
throw new RedisException('DECODE_ERROR');
}
}
else
@@ -502,22 +515,22 @@ class _Redis
/**
* Reset connection
*
* @param \RedisException|NULL $e If this was called as a result of an exception, log that to the debug log
* @param RedisException|NULL $e If this was called as a result of an exception, log that to the debug log
* @return void
*/
public function resetConnection( \RedisException $e = NULL )
public function resetConnection( RedisException $e = NULL ) : void
{
$message = '';
if ( $e !== NULL )
{
$message = $e->getMessage();
\IPS\Log::debug( $e, 'redis_exception' );
Log::debug( $e, 'redis_exception' );
}
static::$multitons = array();
if ( \IPS\REDIS_LOG )
if (REDIS_LOG)
{
static::$log[ microtime() ] = array( 'redis', "Redis connections reset " . $message );
}
@@ -528,9 +541,9 @@ class _Redis
*
* @return bool
*/
public function test()
public function test(): bool
{
return ( \count( static::$connections ) ) ? TRUE : FALSE;
return (bool) count( static::$connections );
}
/**
@@ -540,11 +553,11 @@ class _Redis
* @param string $pattern Pattern (* to fetch all)
* @param boolean $keyNamesOnly Return names only
* @return array
* @throws \RedisException
* @throws RedisException
*/
public function debugGetKeys( $pattern='*', $keyNamesOnly=FALSE )
public function debugGetKeys( string $pattern='*', bool $keyNamesOnly=FALSE ): array
{
$this->connection('write')->setOption( \Redis::OPT_SCAN, \Redis::SCAN_RETRY );
$this->connection('write')->setOption( PHPRedis::OPT_SCAN, PHPRedis::SCAN_RETRY );
$return = array();
$iterator = NULL;
@@ -560,31 +573,31 @@ class _Redis
{
$key = $this->key( $key );
$type = $this->connection('write')->type( $key );
$ttl = \IPS\Redis::i()->ttl( $key );
$ttl = $this->ttl( $key );
switch( $type )
{
case \Redis::REDIS_STRING:
case PHPRedis::REDIS_STRING:
if ( mb_stristr( $key, '_pg__page_' ) )
{
$return[ $key . ' (TTL: ' . $ttl . ')' ] = @gzdecode( \IPS\Text\Encrypt::fromCipher( \IPS\Redis::i()->get( $key ) )->decrypt() );
$return[ $key . ' (TTL: ' . $ttl . ')' ] = @gzdecode( Encrypt::fromCipher( $this->get( $key ) )->decrypt() );
}
else
{
$return[ $key . ' (TTL: ' . $ttl . ')' ] = \IPS\Redis::i()->decode( $this->connection('write')->get( $key ) );
$return[ $key . ' (TTL: ' . $ttl . ')' ] = Redis::i()->decode( $this->connection('write')->get( $key ) );
}
break;
case \Redis::REDIS_ZSET:
case PHPRedis::REDIS_ZSET:
$return[ $key . ' (TTL: ' . $ttl . ')' ] = $this->connection('write')->zRange( $key, 0, -1, TRUE );
break;
case \Redis::REDIS_HASH:
case PHPRedis::REDIS_HASH:
$return[ $key . ' (TTL: ' . $ttl . ')' ] = $this->connection('write')->hGetAll( $key );
if ( isset( $return[ $key . ' (TTL: ' . $ttl . ')' ]['data'] ) )
{
$return[ $key . ' (TTL: ' . $ttl . ')' ]['data'] = \IPS\Redis::i()->decode( $return[ $key . ' (TTL: ' . $ttl . ')' ]['data'] );
$return[ $key . ' (TTL: ' . $ttl . ')' ]['data'] = Redis::i()->decode( $return[ $key . ' (TTL: ' . $ttl . ')' ]['data'] );
}
break;
case \Redis::REDIS_LIST:
case PHPRedis::REDIS_LIST:
$return[ $key . ' (TTL: ' . $ttl . ')' ] = $this->connection('write')->lRange( $key, 0, -1 );
break;
}