Seditio Source
Root |
./othercms/ips_4.3.4/system/Redis/Redis.php
<?php
/**
 * @brief        Redis Engine 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        11 Sept 2017
 */

namespace IPS;

/* 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
{
   
/**
     * @brief    Multiton Store
     */
   
protected static $multitons = array();
   
   
/**
     * @brief    Connections Store
     */
   
protected static $connections = array();
   
   
/**
     * @brief    Default expiration for keys in seconds
     */
   
protected static $ttl = 604800; #7 days
   
    /**
     * @brief Log what redis is up to
     */
   
public static $log = array();
   
   
/**
     * @brief Log what redis is up to
     */
   
public $prefix = NULL;
   
   
/**
     * @brief Unpack the config once
     */
   
protected static $config = NULL;

   
/**
     * @brief Can we encrypt?
     */
   
protected $canEncrypt    = FALSE;
   
   
/**
     * Get instance
     *
     * @return    \Redis
     */
   
public static function i( $configuration=NULL, $identifier=NULL )
    {
        if ( static::
$config === NULL )
        {
            static::
$config = $configuration ?: json_decode( \IPS\CACHE_CONFIG, true );
        }
       
       
$identifier = $identifier ? $identifier : '_MAIN';
       
        if ( ! isset( static::
$multitons[ $identifier ] ) )
        {
            static::
$multitons[ $identifier ] = new self;
           
           
/* Set the prefix with the most obvious comment in the world */
           
static::$multitons[ $identifier ]->prefix = \IPS\SUITE_UNIQUE_KEY . '_';
        }
       
       
/* Return */
       
return static::$multitons[ $identifier ];
    }

   
/**
     * Constructor
     *
     * @return void
     */
   
public function __construct()
    {
       
$this->canEncrypt = \IPS\REDIS_ENCRYPT === TRUE and function_exists( 'openssl_encrypt' ) and in_array( 'aes-256-ctr', openssl_get_cipher_methods() );
    }
   
   
/**
     * Connect to Redis
     *
     * @param    string    $identifier
     * @return    \Redis
     */
   
protected function connection( $identifier=NULL )
    {
        if ( !
class_exists('Redis') )
        {
            throw new \
BadMethodCallException;
        }
       
       
$useConfig = NULL;
        if ( isset( static::
$config['write'] ) )
        {
           
/* We have multiple servers for read and one for write */
           
if ( $identifier === 'write' )
            {
               
$useConfig = static::$config['write'];
            }
            else if (
$identifier === 'read' )
            {
               
$randomReaderIndex = rand( 0, count( static::$config['read'] ) - 1 );
               
$useConfig = static::$config['read'][ $randomReaderIndex ];
               
$identifier = 'read' . $randomReaderIndex;
            }
            else
            {
               
/* Set up the writer first as the default server */
               
$identifier = 'write';
               
$useConfig = static::$config['write'];
            }
        }
        else
        {
           
/* We have only passed through one server */
           
$identifier = 'single';
           
$useConfig = static::$config;
        }

        if ( ! isset( static::
$connections[ $identifier ] ) )
        {
            try
            {
                static::
$connections[ $identifier ] = new \Redis;

               
/* PHP Redis uses many PHP internals to connect, and these can throw ErrorException when they fail but we want a consistent exception */
               
if( @static::$connections[ $identifier ]->connect( $useConfig['server'], $useConfig['port'], 2 ) === FALSE )
                {
                    unset( static::
$connections[ $identifier ] );
                    throw new \
RedisException('CANNOT_CONNECT');
                }
                else
                {
                    if( isset(
$useConfig['password'] ) and $useConfig['password'] )
                    {
                        if( static::
$connections[ $identifier ]->auth( $useConfig['password'] ) === FALSE )
                        {
                            unset( static::
$connections[ $identifier ] );
                            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 );
                }
               
               
/* 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 )
                {
                    static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "Redis connected (" . $identifier . ' ' . $useConfig['server'] . ")"  );
                }
               
                if (
count( static::$connections ) === 1 )
                {
                   
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 )
                            {
                               
session_write_close();
                            }

                            foreach( static::
$connections as $key => $connection )
                            {
                               
$connection->close();
                            }
                           
                           
/* Reset stored connections so they can be re-connected correctly if tasks run after this shutdown proceses */
                           
static::$connections = array();
                        }
                        catch( \
RedisException $e ){}
                    },
$this );
                }

            }
            catch( \
RedisException $e )
            {
               
$this->resetConnection( $e );
            }
        }

        if( !isset( static::
$connections[ $identifier ] ) )
        {
            throw new \
RedisException('CANNOT_CONNECT');
        }
       
        return static::
$connections[ $identifier ];
    }
   
   
/**
     * Call methods
     *
     * @param    string    $method
     * @param    mixed    $args
     * @return mixed
     */
   
public function __call( $method, $args )
    {
        if (
method_exists( 'Redis', $method ) )
        {
           
$type = ( \stristr( $method, 'get' ) or \stristr( $method, 'RevRange' ) ) ? 'read' : 'write';
           
$return = call_user_func_array( array( $this->connection( $type ), $method ), $args );
           
            if ( \
IPS\REDIS_LOG )
            {
                static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "({$type}) {$method} " . $args[0], json_encode( $args ) );
            }
           
            return
$return;
        }    
    }
   
   
/**
     * 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
     * @param    float        $score
     * @param    string        $value
     * @param    int|NULL    $ttl    TTL in seconds
     * @return    1 if the element is added. 0 otherwise.
     */
   
public function zAdd( $key, $score, $value, $ttl=NULL )
    {
       
$return = $this->connection('write')->zAdd( $this->key( $key ), $score, $value );
       
       
$this->connection('write')->expire( $this->key( $key ), ( $ttl ? $ttl : static::$ttl ) );
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) zAdd " . $key . " = " . $return, json_encode( $value ) );
        }
       
        return
$return;
    }
   
   
/**
     * 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
     * @param    array        $value
     * @param    int|NULL    $ttl    TTL in seconds
     * @return    boolean
     */
   
public function hMSet( $key, $value, $ttl=NULL )
    {
       
$return = $this->connection('write')->hMSet( $this->key( $key ), $value );
       
       
$this->connection('write')->expire( $this->key( $key ), ( $ttl ? $ttl : static::$ttl ) );
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) hMSet " . $key . " = " . $return, json_encode( $value ) );
        }
       
        return
$return;
    }
   
   
/**
     * 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
     * @param    int|NULL    $ttl    TTL in seconds
     * @param    string        $value
     * @return    boolean
     */
   
public function setEx( $key, $ttl, $value )
    {
       
$return = $this->connection('write')->setEx( $this->key( $key ), $ttl, $value );
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) setEx " . $key . "  = " . $return, json_encode( $value ) );
        }
       
        return
$return;
    }
   
   
/**
     * Sort the elements in a list, set or sorted set.
     * Overloaded here so we can adjust the key
     *
     * @param    string        $key
     * @param    array        $options    Options: array(key => value, ...) - optional
     * @return    array
     */
   
public function sort( $key, $options=array() )
    {
       
$return = $this->connection('write')->sort( $this->key( $key ), $options );
       
        if ( isset(
$options['store'] ) )
        {
           
$this->connection('write')->expire( $this->key( $options['store'] ), ( isset( $options['ttl'] ) and $options['ttl'] ) ? $options['ttl'] : static::$ttl );
        }
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(read) sort " . $key, json_encode( $return ) );
        }
       
        return
$return;
    }
   
   
/**
     * Returns the whole hash, as an array of strings indexed by strings.
     * Overloaded here so it can be logged
     *
     * @param    string        $key
     * @return    array
     */
   
public function hGetAll( $key )
    {
       
/* Make sure we read */
       
$return = $this->connection('read')->hGetAll( $this->key( $key ) );
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(read) hGetAll " . $key, json_encode( $return ) );
        }
       
        return
$return;
    }
   
   
/**
     * 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
     * @param    int            $inc    Value to increment
     * @param    string        $value
     * @param    int|NULL    $ttl    TTL in seconds
     * @return    boolean
     */
   
public function zIncrBy( $key, $inc, $value, $ttl=NULL )
    {
       
$return = $this->connection('write')->zIncrBy( $this->key( $key ), $inc, $value );
       
$this->connection('write')->expire( $this->key( $key ), ( $ttl ? $ttl : static::$ttl ) );
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "(write) zIncrBy " . $key . "  = " . $return, json_encode( $value ) );
        }
       
        return
$return;
    }
   
   
/**
     * Strip prefixes from keys as PHP redis will handle this
     *
     * @param    string    $key
     * @return    string
     */
   
protected function key( $key )
    {
        if (
$this->prefix )
        {
            if (
mb_substr( $key, 0, mb_strlen( $this->prefix ) ) == $this->prefix )
            {
                return
str_replace( $this->prefix, '', $key );
            }
        }
       
        return
$key;
    }

   
/**
     * @brief    Cached encryption key
     */
   
protected $encryptionKey = NULL;
   
   
/**
     * Encryption key
     *
     * @return    string
     */
   
protected function _encryptionKey()
    {
        if(
$this->encryptionKey !== NULL )
        {
            return
$this->encryptionKey;
        }

       
$this->encryptionKey = \IPS\Settings::i()->sql_pass;
        if (
function_exists( 'openssl_digest' ) and in_array( 'sha256', openssl_get_md_methods() ) )
        {
           
$this->encryptionKey = openssl_digest( $this->encryptionKey, 'sha256', TRUE );
        }

        return
$this->encryptionKey;
    }
       
   
/**
     * Encode
     *
     * @param    mixed    $value    Value
     * @return    string
     */
   
public function encode( $value )
    {
       
$value = json_encode( $value );
               
        if (
$this->canEncrypt )
        {
           
$iv = \IPS\Login::generateRandomString( 16 );
           
$value = $iv . openssl_encrypt( $value, 'aes-256-ctr', $this->_encryptionKey(), OPENSSL_RAW_DATA, $iv );
        }
       
        return
$value;
    }
   
   
/**
     * Decode
     *
     * @param    mixed    $value    Value
     * @return    mixed
     */
   
public function decode( $value )
    {
        if (
$this->canEncrypt )
        {
           
$value = @openssl_decrypt( \substr( $value, 16 ), 'aes-256-ctr', $this->_encryptionKey(), OPENSSL_RAW_DATA, \substr( $value, 0, 16 ) );
        }
       
        return
json_decode( $value, TRUE );
    }
   
   
/**
     * Log a page hit
     *
     * @param    \IPS\Http\Url|NULL    $url        URL to log (or null)
     * @return void
     */
   
public function logPageHit( $url=NULL )
    {
       
$url = $url ? $url : \IPS\Request::i()->url();
       
$key = 'counter-member';
       
        if ( ! \
IPS\Member::loggedIn()->member_id )
        {
           
$key = \IPS\Session::i()->userAgent->spider ? 'counter-spider' : 'counter-guest';
        }
       
       
$value = $this->connection('write')->hIncrBy( $this->key( $key ), ( \IPS\Session::i()->userAgent->spider ? '/' : ( $url->getFurlQuery() ?: '/' ) ), 1 );
       
$this->connection('write')->expire( $this->key( $key ), static::$ttl );
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ sprintf( '%.4f', microtime(true) ) ] = array( 'redis', "Page hit logged " . $key . "  = " . ($url->getFurlQuery() ?: '/'), json_encode( $value ) );
        }
    }
   
   
/**
     * Reset connection
     *
     * @param    \RedisException|NULL    If this was called as a result of an exception, log that to the debug log
     * @return void
     */
   
public function resetConnection( \RedisException $e = NULL )
    {
        if (
$e !== NULL )
        {
            \
IPS\Log::debug( $e, 'redis_exception' );
        }

        static::
$multitons = array();
       
        if ( \
IPS\REDIS_LOG )
        {
            static::
$log[ microtime() ] = array( 'redis', "Redis connections reset" );
        }
    }
   
   
/**
     * Debug method to fetch all keys. Never use in production!
     *
     * @param    string    $pattern
     * @param    boolean    $keyNamesOnly
     * @return array
     */
   
public function debugGetKeys( $pattern='*', $keyNamesOnly=FALSE )
    {
       
$this->connection('write')->setOption( \Redis::OPT_SCAN, \Redis::SCAN_RETRY );
       
       
$return = array();
       
$iterator = NULL;
        while(
$keys = $this->connection('write')->scan( $iterator, $this->prefix . $this->key( $pattern ) ) )
        {
            if (
$keyNamesOnly)
            {
               
$return = array_merge( $return, $keys );
            }
            else
            {
                foreach(
$keys as $key )
                {
                   
$key = $this->key( $key );
                   
$type = $this->connection('write')->type( $key );
                   
$ttl = \IPS\Redis::i()->ttl( $key );

                    switch(
$type )
                    {
                        case \
Redis::REDIS_STRING:
                           
$return[ $key . '(' . $ttl . ')' ] = \IPS\Redis::i()->decode( $this->connection('write')->get( $key ) );
                        break;
                        case \
Redis::REDIS_ZSET:
                           
$return[ $key . '(' . $ttl . ')' ] = $this->connection('write')->zRange( $key, 0, -1, TRUE );
                        break;
                        case \
Redis::REDIS_HASH:
                           
$return[ $key . '(' . $ttl . ')' ] = $this->connection('write')->hGetAll( $key );
                            if ( isset(
$return[ $key . '(' . $ttl . ')' ]['data'] ) )
                            {
                               
$return[ $key . '(' . $ttl . ')' ]['data'] = \IPS\Redis::i()->decode( $return[ $key . '(' . $ttl . ')' ]['data'] );
                            }
                        break;
                        case \
Redis::REDIS_LIST:
                           
$return[ $key . '(' . $ttl . ')' ] = $this->connection('write')->lRange( $key, 0, -1 );
                        break;
                    }
                }
            }
        }
       
        return
$return;
    }
}