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

+45 -69
View File
@@ -11,30 +11,46 @@
namespace IPS\Session;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use Exception;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Http\Useragent;
use IPS\Member;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
use function defined;
use const IPS\ACP_SESSION_TIMEOUT;
use const IPS\BYPASS_ACP_IP_CHECK;
use const IPS\DEV_DISABLE_ACP_SESSION_TIMEOUT;
use const IPS\IN_DEV;
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;
}
/**
* Admin Session Handler
*/
class _Admin extends \IPS\Session
class Admin extends Session
{
/**
* @brief Unix Timestamp of log in time
*/
public $logInTime;
public int $logInTime;
/**
* Open Session
*
* @param string $savePath Save path
* @param string $sessionName Session Name
* @return void
* @return bool
*/
public function open( $savePath, $sessionName )
public function open( string $savePath, string $sessionName ) : bool
{
return TRUE;
}
@@ -45,49 +61,49 @@ class _Admin extends \IPS\Session
* @param string $sessionId Session ID
* @return string
*/
public function read( $sessionId )
public function read( string $sessionId ) : string
{
/* Get user agent info */
$this->userAgent = \IPS\Http\Useragent::parse();
$this->userAgent = Useragent::parse();
try
{
$where = array( array( 'session_id=?', $sessionId ) );
if( !\IPS\IN_DEV OR !\IPS\DEV_DISABLE_ACP_SESSION_TIMEOUT )
if( !IN_DEV OR !DEV_DISABLE_ACP_SESSION_TIMEOUT )
{
$where[] = array( 'session_running_time>=?', ( time() - \IPS\ACP_SESSION_TIMEOUT ) );
$where[] = array( 'session_running_time>=?', ( time() - ACP_SESSION_TIMEOUT ) );
}
/* Load session */
$session = \IPS\Db::i()->select( '*', 'core_sys_cp_sessions', $where )->first();
$session = Db::i()->select( '*', 'core_sys_cp_sessions', $where )->first();
$this->logInTime = $session['session_log_in_time'];
/* Store this so plugins can access */
$this->sessionData = $session;
/* Load member */
$this->member = $session['session_member_id'] ? \IPS\Member::load( $session['session_member_id'] ) : new \IPS\Member;
$this->member = $session['session_member_id'] ? Member::load( $session['session_member_id'] ) : new Member;
if ( $this->member->member_id and !$this->member->isAdmin() )
{
throw new \DomainException('NO_ACPACCESS');
throw new DomainException('NO_ACPACCESS');
}
/* Check IP address */
if ( ( \defined( '\IPS\BYPASS_ACP_IP_CHECK' ) and !\IPS\BYPASS_ACP_IP_CHECK ) and \IPS\Settings::i()->match_ipaddress and $session['session_ip_address'] !== \IPS\Request::i()->ipAddress() )
if ( ( defined( '\IPS\BYPASS_ACP_IP_CHECK' ) and !BYPASS_ACP_IP_CHECK ) and Settings::i()->match_ipaddress and $session['session_ip_address'] !== Request::i()->ipAddress() )
{
throw new \DomainException('BAD_IP');
throw new DomainException('BAD_IP');
}
/* Return data */
return $session['session_app_data'];
}
catch ( \Exception $e )
catch ( Exception $e )
{
$this->member = new \IPS\Member;
$this->member = new Member;
$this->logInTime = 0;
$this->error = $e;
return isset( $this->sessionData['session_app_data'] ) ? $this->sessionData['session_app_data'] : '';
return $this->sessionData['session_app_data'] ?? '';
}
}
@@ -98,17 +114,17 @@ class _Admin extends \IPS\Session
* @param string $data Session Data
* @return bool
*/
public function write( $sessionId, $data )
public function write( string $sessionId, string $data ) : bool
{
\IPS\Db::i()->replace( 'core_sys_cp_sessions', array(
Db::i()->replace( 'core_sys_cp_sessions', array(
'session_id' => $sessionId,
'session_ip_address' => \IPS\Request::i()->ipAddress(),
'session_ip_address' => Request::i()->ipAddress(),
'session_member_name' => $this->member->name ?: '-',
'session_member_id' => $this->member->member_id ?: 0,
'session_location' => 'app=' . ( \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : '' ) . '&module=' . ( \IPS\Dispatcher::i()->module ? \IPS\Dispatcher::i()->module->key : '' ) . '&controller=' . \IPS\Dispatcher::i()->controller,
'session_location' => 'app=' . ( Dispatcher::i()->application ? Dispatcher::i()->application->directory : '' ) . '&module=' . ( Dispatcher::i()->module ? Dispatcher::i()->module->key : '' ) . '&controller=' . Dispatcher::i()->controller,
'session_log_in_time' => $this->logInTime,
'session_running_time' => time(),
'session_url' => \IPS\Request::i()->url(),
'session_url' => Request::i()->url(),
'session_app_data' => $data
) );
@@ -120,7 +136,7 @@ class _Admin extends \IPS\Session
*
* @return bool
*/
public function close()
public function close() : bool
{
return TRUE;
}
@@ -131,9 +147,9 @@ class _Admin extends \IPS\Session
* @param string $sessionId Session ID
* @return bool
*/
public function destroy( $sessionId )
public function destroy( string $sessionId ) : bool
{
\IPS\Db::i()->delete( 'core_sys_cp_sessions', array( 'session_id=?', $sessionId ) );
Db::i()->delete( 'core_sys_cp_sessions', array( 'session_id=?', $sessionId ) );
return TRUE;
}
@@ -143,52 +159,12 @@ class _Admin extends \IPS\Session
* @param int $lifetime Number of seconds to consider sessions expired beyond
* @return bool
*/
public function gc( $lifetime )
public function gc( int $lifetime ) : bool
{
/* We ignore $lifetime because we explicitly control how long sessions are valid for via a constant */
$lifetime = \IPS\ACP_SESSION_TIMEOUT;
$lifetime = ACP_SESSION_TIMEOUT;
\IPS\Db::i()->delete( 'core_sys_cp_sessions', array( 'session_running_time<?', ( time() - $lifetime ) ) );
Db::i()->delete( 'core_sys_cp_sessions', array( 'session_running_time<?', ( time() - $lifetime ) ) );
return TRUE;
}
/**
* Admin Log
*
* @code
\IPS\Session::i()->log( 'acplog__enhancements_enable', array( 'enhancements__foo' => TRUE ) );
* @endcode
* @param string $langKey Language key for log
* @param array $params Key/Values - keys are variables to use in sprintf on $langKey, values are booleans indicating if they are language keys themselves (TRUE) or raw data (FALSE)
* @param bool $noDupes If TRUE, will check the last log and not log again if it's the same and less than an hour ago
* @return void
*/
public function log( $langKey, $params=array(), $noDupes=FALSE )
{
if ( $noDupes )
{
try
{
$lastLog = \IPS\Db::i()->select( '*', 'core_admin_logs', array( 'member_id=?', $this->member->member_id ), 'ctime DESC', 1 )->first();
if ( $lastLog['ctime'] > ( time() - 3600 ) and $lastLog['lang_key'] == $langKey )
{
return;
}
}
catch ( \UnderflowException $e ) { }
}
\IPS\Db::i()->insert( 'core_admin_logs', array(
'member_id' => $this->member->member_id,
'member_name' => \IPS\Member::loggedIn()->name,
'ctime' => time(),
'note' => json_encode( $params ),
'ip_address' => \IPS\Request::i()->ipAddress(),
'appcomponent' => \IPS\Dispatcher::i()->application->directory,
'module' => \IPS\Dispatcher::i()->module->key,
'controller' => \IPS\Dispatcher::i()->controller,
'do' => \IPS\Request::i()->do,
'lang_key' => $langKey
) );
}
}
+212 -127
View File
@@ -1,4 +1,5 @@
<?php
/**
* @brief Front Session Handler
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
@@ -11,23 +12,58 @@
namespace IPS\Session;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use Exception;
use IPS\Api\OAuthClient;
use IPS\Application;
use IPS\Application\Module;
use IPS\Data\Store;
use IPS\Extensions\SSOAbstract;
use IPS\Platform\Bridge;
use IPS\core\ShareLinks\Service;
use IPS\DateTime;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\Http\Useragent;
use IPS\Login;
use IPS\Member;
use IPS\Member\Device;
use IPS\Request;
use IPS\Session;
use IPS\Session\Store as SessionStore;
use IPS\Settings;
use OutOfRangeException;
use UnderflowException;
use function defined;
use function in_array;
use function intval;
use function is_array;
use function is_string;
use const IPS\CACHE_PAGE_TIMEOUT;
use const IPS\OAUTH_REQUIRES_HTTPS;
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;
}
/**
* Front Session Handler
*/
class _Front extends \IPS\Session
class Front extends Session
{
const LOGIN_TYPE_MEMBER = 0;
const LOGIN_TYPE_ANONYMOUS = 1;
const LOGIN_TYPE_GUEST = 2;
const LOGIN_TYPE_SPIDER = 3;
const LOGIN_TYPE_INCOMPLETE = 4;
protected string $sessionId;
protected bool|null|array $sessionData;
/**
* Guess if the user is logged in
*
@@ -39,16 +75,21 @@ class _Front extends \IPS\Session
*
* @return bool
*/
public static function loggedIn()
public static function loggedIn(): bool
{
/* If we have a "member_id" cookie, we're probably logged in... */
if ( isset( \IPS\Request::i()->cookie['member_id'] ) and \IPS\Request::i()->cookie['member_id'] )
if ( isset( Request::i()->cookie['member_id'] ) and Request::i()->cookie['member_id'] )
{
return TRUE;
}
/* If the request sent an access token which has GraphQL acceess we'll need to check that */
if ( isset( $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] ) or isset( \IPS\Request::i()->access_token_member ) )
if ( isset( $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] ) or isset( Request::i()->access_token_member ) )
{
return TRUE;
}
if ( ( $member = Bridge::i()->liveTopicDevPreviewMember() ) AND $member instanceof Member AND $member->member_id )
{
return TRUE;
}
@@ -60,21 +101,21 @@ class _Front extends \IPS\Session
/**
* @brief Session Data
*/
protected $data = array();
protected array $data = array();
/**
* @brief Needs saving?
*/
protected $save = TRUE;
protected bool $save = TRUE;
/**
* Open Session
*
* @param string $savePath Save path
* @param string $sessionName Session Name
* @return void
* @param string $savePath Save path
* @param string $sessionName Session Name
* @return bool
*/
public function open( $savePath, $sessionName )
public function open( string $savePath, string $sessionName ) : bool
{
return TRUE;
}
@@ -82,31 +123,31 @@ class _Front extends \IPS\Session
/**
* Read Session
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return string
*/
public function read( $sessionId )
public function read( string $sessionId ): string
{
$this->sessionId = $sessionId;
/* Get user agent info */
$this->userAgent = \IPS\Http\Useragent::parse();
$this->userAgent = Useragent::parse();
$session = method_exists( SessionStore::i(), 'loadSession' ) ? SessionStore::i()->loadSession( $this->sessionId ) : NULL;
$session = \IPS\Session\Store::i()->loadSession( $this->sessionId );
/* Only use sessions with matching IP address */
if( $session and \IPS\Settings::i()->match_ipaddress and $session['ip_address'] != \IPS\Request::i()->ipAddress() )
if( $session and Settings::i()->match_ipaddress and $session['ip_address'] != Request::i()->ipAddress() )
{
$session = NULL;
}
/* Validate member_id cookie against member_id the session belongs to */
if( $session and isset( \IPS\Request::i()->cookie['member_id'] ) AND \IPS\Request::i()->cookie['member_id'] != $session['member_id'] )
if( $session and isset( Request::i()->cookie['member_id'] ) AND Request::i()->cookie['member_id'] != $session['member_id'] )
{
$session = FALSE;
}
/* If the session is for a member but the member_id cookie is not present, wipe the session */
elseif( $session AND $session['member_id'] > 0 AND empty( \IPS\Request::i()->cookie['member_id'] ) )
elseif( $session AND $session['member_id'] > 0 AND empty( Request::i()->cookie['member_id'] ) )
{
$session = FALSE;
}
@@ -124,7 +165,7 @@ class _Front extends \IPS\Session
if ( $session )
{
/* If this is a guest and the "running time" on this is less than the guest page cache, or if a member and less than 15 seconds ago, we don't need a database write */
if ( ( !$session['member_id'] and $session['running_time'] < ( time() - \IPS\CACHE_PAGE_TIMEOUT ) ) or ( $session['member_id'] and $session['running_time'] < ( time() - 15 ) ) )
if ( ( !$session['member_id'] and $session['running_time'] < ( time() - CACHE_PAGE_TIMEOUT ) ) or ( $session['member_id'] and $session['running_time'] < ( time() - 15 ) ) )
{
$this->save = TRUE;
}
@@ -136,31 +177,31 @@ class _Front extends \IPS\Session
/* Set member */
try
{
$this->member = \IPS\Member::load( (int) $session['member_id'] );
$this->member = Member::load( (int) $session['member_id'] );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
$this->member = new \IPS\Member;
$this->member = new Member;
}
}
/* We might be able to get the member from a cookie */
else
{
$this->member = new \IPS\Member;
$this->member = new Member;
}
/* If we don't have a member, but the request *did* send an access token which has GraphQL acceess (i.e. unfettered access to act as the user), then use that */
if ( !$this->member->member_id and ( isset( $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] ) or isset( \IPS\Request::i()->access_token_member ) ) and $authorizationHeader = \IPS\Request::i()->authorizationHeader() and mb_substr( $authorizationHeader, 0, 7 ) === 'Bearer ' and ( !\IPS\OAUTH_REQUIRES_HTTPS or \IPS\Request::i()->isSecure() ) )
if ( !$this->member->member_id and ( isset( $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] ) or isset( Request::i()->access_token_member ) ) and $authorizationHeader = Request::i()->authorizationHeader() and mb_substr( $authorizationHeader, 0, 7 ) === 'Bearer ' and ( !OAUTH_REQUIRES_HTTPS or Request::i()->isSecure() ) )
{
$expectedMember = \IPS\Member::load( isset( $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] ) ? $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] : \IPS\Request::i()->access_token_member );
$expectedMember = Member::load( $_SERVER['HTTP_X_IPS_ACCESSTOKENMEMBER'] ?? Request::i()->access_token_member );
if ( $expectedMember->member_id )
{
/* Start by checking the access token is valid and for this member */
try
{
$accessToken = \IPS\Api\OAuthClient::accessTokenDetails( mb_substr( $authorizationHeader, 7 ) );
$client = \IPS\Api\OAuthClient::load( $accessToken['client_id'] );
if ( \in_array($client->api_access, [ 'graphql', 'both'] ) AND $accessToken['member_id'] === $expectedMember->member_id )
$accessToken = OAuthClient::accessTokenDetails( mb_substr( $authorizationHeader, 7 ) );
$client = OAuthClient::load( $accessToken['client_id'] );
if ( in_array($client->api_access, [ 'graphql', 'both'] ) AND $accessToken['member_id'] === $expectedMember->member_id )
{
$success = TRUE;
}
@@ -169,7 +210,7 @@ class _Front extends \IPS\Session
$success = FALSE;
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
$success = FALSE;
}
@@ -177,7 +218,7 @@ class _Front extends \IPS\Session
/* Because this is effectively a log in attempt, we need to make sure the account is not locked */
try
{
\IPS\Login::checkIfAccountIsLocked( $expectedMember, $success );
Login::checkIfAccountIsLocked( $expectedMember, $success );
/* If it isn't, we can either set that we are that member... */
if ( $success )
@@ -191,7 +232,7 @@ class _Front extends \IPS\Session
$expectedMember->failedLogin();
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
// Account is locked. Do nothing.
}
@@ -200,23 +241,23 @@ class _Front extends \IPS\Session
/* If we still don't have a member, check the cookies */
$device = NULL;
if ( !$this->member->member_id and isset( \IPS\Request::i()->cookie['device_key'] ) and isset( \IPS\Request::i()->cookie['member_id'] ) and isset( \IPS\Request::i()->cookie['login_key'] ) )
if ( !$this->member->member_id and isset( Request::i()->cookie['device_key'] ) and isset( Request::i()->cookie['member_id'] ) and isset( Request::i()->cookie['login_key'] ) )
{
/* Get the member we're trying to authenticate against - do not process cookie-based login if the account is locked */
$member = \IPS\Member::load( (int) \IPS\Request::i()->cookie['member_id'] );
$member = Member::load( (int) Request::i()->cookie['member_id'] );
if ( $member->member_id and $member->unlockTime() === FALSE )
{
/* Load and authenticate device device data */
try
{
/* Authenticate */
$device = \IPS\Member\Device::loadAndAuthenticate( \IPS\Request::i()->cookie['device_key'], $member, \IPS\Request::i()->cookie['login_key'] );
$device = Device::loadAndAuthenticate( Request::i()->cookie['device_key'], $member, Request::i()->cookie['login_key'] );
/* Set member in session */
$this->member = $member;
/* Refresh the device key cookie */
\IPS\Request::i()->setCookie( 'device_key', \IPS\Request::i()->cookie['device_key'], ( new \IPS\DateTime )->add( new \DateInterval( 'P1Y' ) ) );
Request::i()->setCookie( 'device_key', Request::i()->cookie['device_key'], ( new DateTime )->add( new DateInterval( 'P1Y' ) ) );
$member->recordLogin();
$member->achievementAction( 'core', 'SessionStartDaily' );
@@ -225,21 +266,24 @@ class _Front extends \IPS\Session
$device->updateAfterAuthentication( TRUE, NULL, FALSE );
}
/* If the device_key/login_key combination wasn't valid, this may be someone trying to bruteforce... */
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
/* ... so log it as a failed login */
$member->failedLogin();
if( isset( $expectedMember ) and $expectedMember instanceof Member )
{
$expectedMember->failedLogin();
}
/* Then set us as a guest and clear out those cookies */
$this->member = new \IPS\Member;
\IPS\Request::i()->clearLoginCookies();
$this->member = new Member;
Request::i()->clearLoginCookies();
}
}
// If the member no longer exists, or the account is locked, set us as a guest and clear out those cookies
else
{
$this->member = new \IPS\Member;
\IPS\Request::i()->clearLoginCookies();
$this->member = new Member;
Request::i()->clearLoginCookies();
}
}
@@ -261,7 +305,7 @@ class _Front extends \IPS\Session
}
else
{
$type = $this->userAgent->bot ? static::LOGIN_TYPE_SPIDER : static::LOGIN_TYPE_GUEST;
$type = $this->userAgent->spider ? static::LOGIN_TYPE_SPIDER : static::LOGIN_TYPE_GUEST;
}
/* Set data */
@@ -270,21 +314,21 @@ class _Front extends \IPS\Session
'member_name' => $this->member->member_id ? $this->member->name : '',
'seo_name' => $this->member->member_id ? ( $this->member->members_seo_name ?: '' ) : '',
'member_id' => $this->member->member_id ?: 0,
'ip_address' => \IPS\Request::i()->ipAddress(),
'browser' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
'ip_address' => Request::i()->ipAddress(),
'browser' => $_SERVER['HTTP_USER_AGENT'] ?? '',
/* We do not want ajax calls to update running time as this affects appearance of being online. If no session exists, we do not want ajax polling to trigger an online list hit so we set running time for time - 31 minutes as
online lists look for running times less than 30 minutes. */
'running_time' => ( \IPS\Request::i()->isAjax() ) ? ( $session ? $session['running_time'] : time() - 1860 ) : time(),
'running_time' => ( Request::i()->isAjax() ) ? ( $session ? $session['running_time'] : time() - 1860 ) : time(),
'login_type' => $type,
'member_group' => ( $this->member->member_id ) ? $this->member->member_group_id : \IPS\Settings::i()->guest_group,
'current_appcomponent' => ( \IPS\Request::i()->isAjax() ) ? ( $session ? $session['current_appcomponent'] : '' ) : '',
'current_module' => ( \IPS\Request::i()->isAjax() ) ? ( $session ? $session['current_module'] : '' ) : '',
'current_controller' => ( \IPS\Request::i()->isAjax() ) ? ( $session ? $session['current_controller'] : NULL ) : NULL,
'current_id' => ( \IPS\Request::i()->isAjax() ) ? ( $session ? $session['current_id'] : NULL ) : \intval( \IPS\Request::i()->id ),
'member_group' => ( $this->member->member_id ) ? $this->member->member_group_id : Settings::i()->guest_group,
'current_appcomponent' => ( Request::i()->isAjax() ) ? ( $session ? $session['current_appcomponent'] : '' ) : '',
'current_module' => ( Request::i()->isAjax() ) ? ( $session ? $session['current_module'] : '' ) : '',
'current_controller' => ( Request::i()->isAjax() ) ? ( $session ? $session['current_controller'] : NULL ) : NULL,
'current_id' => ( Request::i()->isAjax() ) ? ( $session ? $session['current_id'] : NULL ) : intval( Request::i()->id ),
'uagent_key' => $this->userAgent->browser ?: '',
'uagent_version' => $this->userAgent->browserVersion ?: '',
'uagent_type' => $this->userAgent->bot ? 'search' : 'browser',
'search_thread_id' => $session ? \intval( $session['search_thread_id'] ) : 0,
'uagent_type' => $this->userAgent->spider ? 'search' : 'browser',
'search_thread_id' => $session ? intval( $session['search_thread_id'] ) : 0,
'search_thread_time' => $session ? $session['search_thread_time'] : 0,
'data' => $session ? $session['data'] : '',
'location_url' => $session ? $session['location_url'] : NULL,
@@ -292,37 +336,48 @@ class _Front extends \IPS\Session
'location_data' => $session ? $session['location_data'] : NULL,
'location_permissions' => $session ? $session['location_permissions'] : NULL,
'theme_id' => $session ? $session['theme_id'] : 0,
'in_editor' => ( \IPS\Request::i()->isAjax() ) ? ( $session ? $session['in_editor'] : 0 ) : 0,
'in_editor' => ( Request::i()->isAjax() ) ? ( $session ? $session['in_editor'] : 0 ) : 0,
);
/* Is this a spider? */
if( $this->userAgent->bot )
if( $this->userAgent->spider )
{
/* Is this Facebook? Do we need to treat them as a user of a different group? */
if( $this->userAgent->bot == 'facebook' )
if( $this->userAgent->spider == 'facebook' )
{
if( \IPS\core\ShareLinks\Service::load( 'facebook', 'share_key' )->enabled )
if( Service::load( 'facebook', 'share_key' )->enabled )
{
if( $this->userAgent->facebookIpVerified( \IPS\Request::i()->ipAddress() ) AND \IPS\Settings::i()->fbc_bot_group != \IPS\Settings::i()->guest_group )
if( $this->userAgent->facebookIpVerified( Request::i()->ipAddress() ) AND Settings::i()->fbc_bot_group != Settings::i()->guest_group )
{
$this->member->member_group_id = \IPS\Settings::i()->fbc_bot_group;
$this->member->member_group_id = Settings::i()->fbc_bot_group;
}
}
}
}
/* Session read() method MUST return a string, or this can result in PHP errors */
return (string) $this->data['data'];
$result = (string) $this->data['data'];
foreach( Application::allExtensions( 'core', 'SSO', FALSE ) as $ext )
{
/* @var SSOAbstract $ext */
if( $ext->isEnabled() )
{
return $ext->onSessionRead( $this, $result );
}
}
return $result;
}
/**
* Set Session Member
*
* @param \IPS\Member $member Member object
* @param Member $member Member object
* @return void
*/
public function setMember( $member )
public function setMember( Member $member ) : void
{
parent::setMember( $member );
@@ -336,11 +391,11 @@ class _Front extends \IPS\Session
/**
* Write Session
*
* @param string $sessionId Session ID
* @param string $data Session Data
* @param string $sessionId Session ID
* @param string $data Session Data
* @return bool
*/
public function write( $sessionId, $data )
public function write( string $sessionId, string $data ): bool
{
if ( !isset( $this->data['data'] ) or $data !== $this->data['data'] or $this->data['member_id'] != $this->member->member_id )
{
@@ -348,31 +403,34 @@ class _Front extends \IPS\Session
}
/* Don't update if instant notifications are checking to reduce overhead on the session table */
if ( \IPS\Request::i()->isAjax() and isset( \IPS\Request::i()->app ) and \IPS\Request::i()->app === 'core' and isset( \IPS\Request::i()->controller ) and \IPS\Request::i()->controller === 'ajax' and isset( \IPS\Request::i()->do ) and \IPS\Request::i()->do === 'instantNotifications' )
if ( Request::i()->isAjax() and isset( Request::i()->app ) and Request::i()->app === 'core' and isset( Request::i()->controller ) and Request::i()->controller === 'ajax' and isset( Request::i()->do ) and Request::i()->do === 'instantNotifications' )
{
$this->save = FALSE;
}
/* Don't update if there is a hit on the manifest. Why do we use the url()? When page caching grabs and returns, the \IPS\Request::i()->do/controller variables are no populated */
if ( isset( \IPS\Request::i()->url()->hiddenQueryString['controller'] ) and \IPS\Request::i()->url()->hiddenQueryString['controller'] === 'metatags' and isset( \IPS\Request::i()->url()->hiddenQueryString['do'] ) and \IPS\Request::i()->url()->hiddenQueryString['do'] === 'manifest' )
if ( isset( Request::i()->url()->hiddenQueryString['controller'] ) and Request::i()->url()->hiddenQueryString['controller'] === 'metatags' and isset( Request::i()->url()->hiddenQueryString['do'] ) and Request::i()->url()->hiddenQueryString['do'] === 'manifest' )
{
$this->save = FALSE;
}
/* Don't update if there is a hit on the serviceworker */
if ( isset( \IPS\Request::i()->app ) and \IPS\Request::i()->app === 'core' and isset( \IPS\Request::i()->controller ) and \IPS\Request::i()->controller === 'serviceworker' )
if ( isset( Request::i()->app ) and Request::i()->app === 'core' and isset( Request::i()->controller ) and Request::i()->controller === 'serviceworker' )
{
$this->save = FALSE;
}
$this->data['member_name'] = $this->member->member_id ? $this->member->name : '';
$this->data['member_id'] = $this->member->member_id ?: NULL;
$this->data['data'] = $data;
$this->setLocationData();
if ( $this->save === TRUE and ( !empty( \IPS\Request::i()->cookie ) or $this->userAgent->bot or $this->member->member_id ) ) // If a guest and cookies are disabled we do not write to database to prevent duplicate sessions unless it's a search engine, which we deal with separately
if ( $this->save === TRUE and ( !empty( Request::i()->cookie ) or $this->userAgent->spider or $this->member->member_id ) ) // If a guest and cookies are disabled we do not write to database to prevent duplicate sessions unless it's a search engine, which we deal with separately
{
\IPS\Session\Store::i()->updateSession( $this->data );
if( method_exists( '\IPS\Session\Store', 'updateSession' ) )
{
SessionStore::i()->updateSession( $this->data );
}
}
return TRUE;
@@ -383,7 +441,7 @@ class _Front extends \IPS\Session
*
* @return void
*/
public function noUpdate()
public function noUpdate() : void
{
$this->save = FALSE;
}
@@ -391,18 +449,18 @@ class _Front extends \IPS\Session
/**
* @brief Stored engine
*/
protected static $engine = NULL;
protected static mixed $engine = NULL;
/**
* Clear sessions - abstracted so it can be called externally without initiating a session
*
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @return void
*/
public static function clearSessions( $timeout )
public static function clearSessions( int $timeout ) : void
{
/* Cannot change this from a static method as it is called on garbage collection */
\IPS\Session\Store::i()->clearSessions( $timeout );
SessionStore::i()->clearSessions( $timeout );
}
/**
@@ -410,9 +468,9 @@ class _Front extends \IPS\Session
*
* @return void
*/
public function startSearch()
public function startSearch() : void
{
$this->data['search_thread_id'] = \IPS\Db::i()->thread_id;
$this->data['search_thread_id'] = Db::i()->thread_id;
$this->data['search_thread_time'] = time();
}
@@ -421,7 +479,7 @@ class _Front extends \IPS\Session
*
* @return void
*/
public function endSearch()
public function endSearch() : void
{
$this->data['search_thread_id'] = 0;
$this->data['search_thread_time'] = 0;
@@ -430,12 +488,12 @@ class _Front extends \IPS\Session
/**
* Set a theme ID
*
* @param int $themeId The theme id, of course
* @param int $themeId The theme id, of course
* @return void
*/
public function setTheme( $themeId )
public function setTheme( int $themeId ) : void
{
if( !\IPS\Dispatcher::hasInstance() OR \IPS\Request::i()->isAjax() )
if( !Dispatcher::hasInstance() OR Request::i()->isAjax() )
{
return;
}
@@ -444,13 +502,13 @@ class _Front extends \IPS\Session
$this->save = TRUE;
}
/**
* Get the theme ID
*
* @return int
* @return int|null
*/
public function getTheme()
public function getTheme(): ?int
{
if ( isset( $this->data['theme_id'] ) and $this->data['theme_id'] )
{
@@ -465,17 +523,17 @@ class _Front extends \IPS\Session
*
* @return void
*/
public function setLocationData()
public function setLocationData() : void
{
if( !\IPS\Dispatcher::hasInstance() OR \IPS\Request::i()->isAjax() )
if( !Dispatcher::hasInstance() OR Request::i()->isAjax() )
{
return;
}
$this->data['current_appcomponent'] = \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : '';
$this->data['current_module'] = \IPS\Dispatcher::i()->module ? \IPS\Dispatcher::i()->module->key : '';
$this->data['current_controller'] = \IPS\Dispatcher::i()->controller;
$this->data['current_id'] = \intval( \IPS\Request::i()->id );
$this->data['current_appcomponent'] = Dispatcher::i()->application ? Dispatcher::i()->application->directory : '';
$this->data['current_module'] = Dispatcher::i()->module ? Dispatcher::i()->module->key : '';
$this->data['current_controller'] = Dispatcher::i()->controller;
$this->data['current_id'] = intval( Request::i()->id );
}
/**
@@ -483,7 +541,7 @@ class _Front extends \IPS\Session
*
* @return void
*/
public function setUsingEditor()
public function setUsingEditor() : void
{
$this->data['in_editor'] = time();
}
@@ -491,15 +549,15 @@ class _Front extends \IPS\Session
/**
* Set the session location
*
* @param \IPS\Http\Url $url URL
* @param array $groupIds Permission data
* @param string $lang Language string
* @param array $data Language data. Keys are the words, value is a boolean indicating if it's a language key (TRUE) or should be displayed as-is (FALSE)
* @param Url $url URL
* @param mixed $groupIds Permission data
* @param string $lang Language string
* @param array $data Language data. Keys are the words, value is a boolean indicating if it's a language key (TRUE) or should be displayed as-is (FALSE)
* @return void
*/
public function setLocation( \IPS\Http\Url $url, $groupIds, $lang, $data=array() )
public function setLocation( Url $url, mixed $groupIds, string $lang, array $data=array() ) : void
{
if( !\IPS\Dispatcher::hasInstance() OR \IPS\Request::i()->isAjax() )
if( !Dispatcher::hasInstance() OR Request::i()->isAjax() )
{
return;
}
@@ -507,7 +565,7 @@ class _Front extends \IPS\Session
$this->data['location_url'] = (string) $url;
$this->data['location_lang'] = $lang;
$this->data['location_data'] = json_encode( $data );
$this->data['current_id'] = \intval( \IPS\Request::i()->id );
$this->data['current_id'] = intval( Request::i()->id );
if ( !$this->data['current_appcomponent'] )
{
@@ -520,21 +578,21 @@ class _Front extends \IPS\Session
$groupIds = (string) $groupIds;
}
$groupIds = \is_string( $groupIds ) ? explode( ',', $groupIds ) : ( $groupIds ?: NULL );
$groupIds = is_string( $groupIds ) ? explode( ',', $groupIds ) : ( $groupIds ?: NULL );
$app = \IPS\Application::load( $this->data['current_appcomponent'] );
$app = Application::load( $this->data['current_appcomponent'] );
if ( !$app->enabled )
{
$groupIds = $groupIds ? array_intersect( $groupIds, explode( ',', $app->disabled_groups ) ) : explode( ',', $app->disabled_groups );
}
$modulePermissions = \IPS\Application\Module::get( $this->data['current_appcomponent'], $this->data['current_module'], 'front' )->permissions();
$modulePermissions = Module::get( $this->data['current_appcomponent'], $this->data['current_module'], 'front' )->permissions();
if ( $modulePermissions['perm_view'] !== '*' )
{
$groupIds = $groupIds ? array_intersect( $groupIds, explode( ',', $modulePermissions['perm_view'] ) ) : explode( ',', $modulePermissions['perm_view'] );
}
$this->data['location_permissions'] = ( $groupIds !== NULL ) ? ( \is_string( $groupIds ) ? $groupIds : implode( ',', $groupIds ) ) : NULL;
$this->data['location_permissions'] = ( $groupIds !== NULL ) ? ( is_string( $groupIds ) ? $groupIds : implode( ',', $groupIds ) ) : NULL;
$this->save = TRUE;
}
@@ -542,10 +600,10 @@ class _Front extends \IPS\Session
/**
* Get the session location
*
* @param array $row Row from sessions
* @param array $row Row from sessions
* @return string|null
*/
public static function getLocation( $row )
public static function getLocation( array $row ): ?string
{
$location = NULL;
@@ -556,7 +614,7 @@ class _Front extends \IPS\Session
try
{
if ( $row['location_permissions'] === NULL or $row['location_permissions'] === '*' or \IPS\Member::loggedIn()->inGroup( explode( ',', $row['location_permissions'] ), TRUE ) )
if ( $row['location_permissions'] === NULL or $row['location_permissions'] === '*' or Member::loggedIn()->inGroup( explode( ',', $row['location_permissions'] ), TRUE ) )
{
$sprintf = array();
$data = json_decode( $row['location_data'], TRUE );
@@ -565,17 +623,17 @@ class _Front extends \IPS\Session
{
foreach ( $data as $key => $parse )
{
$value = htmlspecialchars( $parse ? \IPS\Member::loggedIn()->language()->get( $key ) : $key, ENT_DISALLOWED, 'UTF-8', FALSE );
$value = htmlspecialchars( $parse ? Member::loggedIn()->language()->get( $key ) : $key, ENT_DISALLOWED, 'UTF-8', FALSE );
$sprintf[] = $value;
}
}
$location = \IPS\Member::loggedIn()->language()->addToStack( htmlspecialchars( $row['location_lang'], ENT_DISALLOWED, 'UTF-8', FALSE ), FALSE, array( 'htmlsprintf' => $sprintf ) );
$location = Member::loggedIn()->language()->addToStack( htmlspecialchars( $row['location_lang'], ENT_DISALLOWED, 'UTF-8', FALSE ), FALSE, array( 'htmlsprintf' => $sprintf ) );
$location = "<a href='" . htmlspecialchars( $row['location_url'], ENT_DISALLOWED, 'UTF-8', FALSE ) . "'>" . $location . "</a>";
}
}
catch ( \UnderflowException $e ){ }
catch ( UnderflowException $e ){ }
return $location;
}
@@ -583,10 +641,10 @@ class _Front extends \IPS\Session
/**
* Set the session "login_type"
*
* @param int $type Type as defined by the class constants
* @param int $type Type as defined by the class constants
* @return void
*/
public function setType( $type )
public function setType( int $type ) : void
{
if ( $this->data['login_type'] !== $type )
{
@@ -603,8 +661,8 @@ class _Front extends \IPS\Session
$this->data['login_type'] = $type;
break;
default:
throw new \OutOfRangeException();
break;
throw new OutOfRangeException();
}
}
@@ -613,7 +671,7 @@ class _Front extends \IPS\Session
*
* @return void
*/
public function setAnon()
public function setAnon() : void
{
$this->setType( static::LOGIN_TYPE_ANONYMOUS );
}
@@ -621,9 +679,9 @@ class _Front extends \IPS\Session
/**
* Set the session as anonymous
*
* @return void
* @return bool
*/
public function getAnon()
public function getAnon() : bool
{
return (bool) $this->data['login_type'] == static::LOGIN_TYPE_ANONYMOUS;
}
@@ -633,7 +691,7 @@ class _Front extends \IPS\Session
*
* @return bool
*/
public function close()
public function close() : bool
{
return TRUE;
}
@@ -641,30 +699,57 @@ class _Front extends \IPS\Session
/**
* Destroy Session
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return bool
*/
public function destroy( $sessionId )
public function destroy( string $sessionId ): bool
{
if ( isset( $_SESSION['wizardKey'] ) )
{
$dataKey = $_SESSION['wizardKey'];
unset( \IPS\Data\Store::i()->$dataKey );
unset( Store::i()->$dataKey );
}
\IPS\Session\Store::i()->deleteSession( $sessionId );
if( method_exists( '\IPS\Session\Store', 'deleteSession' ) )
{
SessionStore::i()->deleteSession( $sessionId );
}
return TRUE;
}
/**
* Garbage Collection
*
* @param int $lifetime Number of seconds to consider sessions expired beyond
* @param int $lifetime Number of seconds to consider sessions expired beyond
* @return bool
*/
public function gc( $lifetime )
public function gc( int $lifetime ): bool
{
static::clearSessions( $lifetime );
return TRUE;
}
/**
* @inheritDoc
* @return void
*/
public function init(): void
{
if ( ( $member = Bridge::i()->liveTopicDevPreviewMember() ) AND $member instanceof Member AND $member->member_id )
{
$this->setMember( $member );
}
parent::init();
foreach( Application::allExtensions( 'core', 'SSO', FALSE ) as $ext )
{
/* @var SSOAbstract $ext */
if( $ext->isEnabled() )
{
$ext->onSessionInit( $this );
}
}
}
}
+114 -56
View File
@@ -11,59 +11,76 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateTimeZone;
use IPS\Content\Item;
use IPS\Dispatcher\Setup;
use IPS\Http\Useragent;
use IPS\Session\Admin;
use IPS\Session\Front;
use RuntimeException;
use StdClass;
use UnderflowException;
use function defined;
use function function_exists;
use function get_called_class;
use function get_class;
use function in_array;
use function substr;
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;
}
/**
* Session Handler
*/
abstract class _Session
abstract class Session
{
/**
* @brief Singleton Instance
*/
protected static $instance = NULL;
protected static mixed $instance = NULL;
/**
* @brief User agent information
* @see \IPS\Http\Useragent::parse()
* @see Useragent::parse
*/
public $userAgent = NULL;
public ?Useragent $userAgent = NULL;
/**
* @brief Session record - stored so plugins can access
*/
protected $sessionData = NULL;
protected bool|null|array $sessionData = NULL;
/**
* Get instance
*
* @return static
* @return static|Front|Admin|StdClass
*/
public static function i()
public static function i(): static|Front|Admin|StdClass
{
if( static::$instance === NULL )
{
$classname = \get_called_class();
$classname = get_called_class();
if ( $classname === 'IPS\Session' )
{
if( class_exists( 'IPS\Dispatcher', FALSE ) )
{
$location = ( \IPS\Dispatcher::hasInstance() ) ? mb_ucfirst( \IPS\Dispatcher::i()->controllerLocation ) : 'Front';
$location = ( Dispatcher::hasInstance() ) ? IPS::mb_ucfirst( Dispatcher::i()->controllerLocation ) : 'Front';
$classname = 'IPS\Session\\' . $location;
}
else
{
throw new \RuntimeException('LOCATION_UNKNOWN');
throw new RuntimeException('LOCATION_UNKNOWN');
}
}
else
{
$location = \substr( $classname, 12 );
$location = substr( $classname, 12 );
}
if ( class_exists( $classname ) )
@@ -75,7 +92,7 @@ abstract class _Session
session_cache_limiter('');
/* Name the session */
$name = session_name( ( \IPS\COOKIE_PREFIX !== NULL ) ? \IPS\COOKIE_PREFIX . 'IPSSession' . $location : 'IPSSession' . $location );
$name = session_name( ( COOKIE_PREFIX !== NULL ) ? COOKIE_PREFIX . 'IPSSession' . $location : 'IPSSession' . $location );
/* Set the handler */
session_write_close();
@@ -84,9 +101,9 @@ abstract class _Session
/* Make sure we use HTTP-Only cookies */
session_set_cookie_params(
'0',
( \IPS\COOKIE_PATH !== NULL ) ? \IPS\COOKIE_PATH : '/',
( \IPS\COOKIE_DOMAIN !== NULL ) ? \IPS\COOKIE_DOMAIN : '',
( \IPS\COOKIE_BYPASS_SSLONLY !== TRUE ) ? ( mb_substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) : FALSE,
( COOKIE_PATH !== NULL ) ? COOKIE_PATH : '/',
( COOKIE_DOMAIN !== NULL ) ? COOKIE_DOMAIN : '',
( !COOKIE_BYPASS_SSLONLY ) ? ( mb_substr( Settings::i()->base_url, 0, 5 ) == 'https' ) : FALSE,
TRUE
);
@@ -101,12 +118,12 @@ abstract class _Session
}
else
{
static::$instance = new \StdClass;
static::$instance->member = new \IPS\Member;
static::$instance = new StdClass;
static::$instance->member = new Member;
static::$instance->csrfKey = '';
/* Upgrader starts session already */
if ( !\IPS\Dispatcher::hasInstance() or !\IPS\Dispatcher::i() instanceof \IPS\Dispatcher\Setup )
if ( !Dispatcher::hasInstance() or !Dispatcher::i() instanceof Setup )
{
if( session_status() !== PHP_SESSION_ACTIVE )
{
@@ -122,30 +139,30 @@ abstract class _Session
/**
* @brief Session ID
*/
public $id = NULL;
public ?string $id = NULL;
/**
* @brief Currently logged in member
*/
public $member = NULL;
public ?Member $member = NULL;
/**
* @brief CSRF Key
*/
public $csrfKey = '';
public string $csrfKey = '';
/**
* @brief Validation Error
*/
public $error = NULL;
public ?string $error = NULL;
/**
* Set Session Member
*
* @param \IPS\Member $member Member object
* @param Member $member Member object
* @return void
*/
public function setMember( $member )
public function setMember( Member $member ) : void
{
/* PHP 7.0.2 had a bug reported where session_regenerate_id() does not close opened sessions properly and in some situations can cause PHP to hang or crash.
* This issue is fixed in PHP 7.1.0 - https://bugs.php.net/bug.php?id=71394 */
@@ -166,7 +183,7 @@ abstract class _Session
*
* @return void
*/
public function init()
public function init() : void
{
/* Set ID */
$this->id = session_id();
@@ -180,7 +197,7 @@ abstract class _Session
$save = FALSE;
/* Set the last activity (but not if this is an ajax request or a partially registered member as we delete where last_visit=0) */
if ( isset( $this->data ) and ! \IPS\Request::i()->isAjax() and ( $this->member->email and $this->member->name ) )
if ( isset( $this->data ) and ! Request::i()->isAjax() and ( $this->member->email and $this->member->name ) )
{
if ( time() - $this->member->last_activity > 3600 or !$this->member->last_visit )
{
@@ -195,10 +212,10 @@ abstract class _Session
}
/* Set timezone */
if ( isset( \IPS\Request::i()->cookie['ipsTimezone'] ) and \IPS\Request::i()->cookie['ipsTimezone'] !== $this->member->timezone and \in_array( \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] ), \DateTimeZone::listIdentifiers() ) )
if ( isset( Request::i()->cookie['ipsTimezone'] ) and Request::i()->cookie['ipsTimezone'] !== $this->member->timezone and in_array( DateTime::getFixedTimezone( Request::i()->cookie['ipsTimezone'] ), DateTimeZone::listIdentifiers() ) )
{
$save = TRUE;
$this->member->timezone = \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] );
$this->member->timezone = DateTime::getFixedTimezone( Request::i()->cookie['ipsTimezone'] );
}
/* Save */
@@ -210,9 +227,9 @@ abstract class _Session
else
{
/* Ensure any loggedIn cookies are removed */
if( isset( \IPS\Request::i()->cookie['loggedIn'] ) )
if( isset( Request::i()->cookie['loggedIn'] ) )
{
\IPS\Request::i()->setCookie( 'loggedIn', NULL );
Request::i()->setCookie( 'loggedIn', NULL );
}
}
}
@@ -222,7 +239,7 @@ abstract class _Session
*
* @return void
*/
public function noUpdate()
public function noUpdate() : void
{
/* Overridden methods do something (or not) */
}
@@ -232,52 +249,53 @@ abstract class _Session
*
* @return void
*/
public function csrfCheck()
public function csrfCheck() : void
{
$token = (string) \IPS\Request::i()->csrfKey;
$token = (string) Request::i()->csrfKey;
/* Guests may provide the csrf token via a header */
if ( !\IPS\Member::loggedIn()->member_id && isset( $_SERVER['HTTP_X_CSRF_TOKEN'] ) )
if ( !Member::loggedIn()->member_id && isset( $_SERVER['HTTP_X_CSRF_TOKEN'] ) )
{
$token = $_SERVER['HTTP_X_CSRF_TOKEN'];
}
if ( !\IPS\Login::compareHashes( (string) $this->csrfKey, $token ) )
if ( !Login::compareHashes( $this->csrfKey, $token ) )
{
\IPS\Output::i()->error( 'generic_error', '2S119/1', 403, 'admin_csrf_error' );
Output::i()->error( 'generic_error', '2S119/1', 403, 'admin_csrf_error' );
}
}
/**
* Moderator Log
* @code
\IPS\Session::i()->modLog( 'modlog__spammer_flagged', array( $this->name => FALSE ) );
* \IPS\Session::i()->modLog( 'modlog__spammer_flagged', array( $this->name => FALSE ) );
* @endcode
* @param string $langKey Language key for log
* @param string $langKey Language key for log
* @param array $params Key/Values - keys are variables to use in sprintf on $langKey, values are booleans indicating if they are language keys themselves (TRUE) or raw data (FALSE)
* @param \IPS\Content\Item|NULL $item If moderation action is specific to an item
* @param Item|null $item If moderation action is specific to an item
* @return void
*/
public function modLog( $langKey, $params=array(), $item=null )
public function modLog( string $langKey, array $params=array(), Item $item=null ) : void
{
$class = NULL;
if ( $item instanceof \IPS\Content\Item )
if ( $item instanceof Item )
{
$class = \get_class( $item );
/* @var Item $class */
$class = get_class( $item );
$idColumn = $class::$databaseColumnId;
}
\IPS\Db::i()->insert( 'core_moderator_logs', array(
'member_id' => \IPS\Member::loggedIn()->member_id,
'member_name' => \IPS\Member::loggedIn()->name,
Db::i()->insert( 'core_moderator_logs', array(
'member_id' => Member::loggedIn()->member_id,
'member_name' => Member::loggedIn()->name,
'ctime' => time(),
'note' => json_encode( $params ),
'ip_address' => \IPS\Request::i()->ipAddress(),
'appcomponent' => \IPS\Dispatcher::i()->application->directory,
'module' => \IPS\Dispatcher::i()->module->key,
'controller' => \IPS\Dispatcher::i()->controller,
'do' => \IPS\Request::i()->do,
'ip_address' => Request::i()->ipAddress(),
'appcomponent' => Dispatcher::i()->application->directory,
'module' => Dispatcher::i()->module->key,
'controller' => Dispatcher::i()->controller,
'do' => Request::i()->do,
'lang_key' => $langKey,
'class' => $class,
'item_id' => $item ? $item->$idColumn : NULL,
@@ -289,9 +307,9 @@ abstract class _Session
*
* @return void
*/
public function regenerateCsrfKey()
public function regenerateCsrfKey() : void
{
$this->csrfKey = md5( \IPS\SUITE_UNIQUE_KEY . "&{$this->member->email}& " . ( $this->member->member_id ? $this->member->joined->getTimestamp() : 0 ) . '&' . $this->id );
$this->csrfKey = md5( SUITE_UNIQUE_KEY . "&{$this->member->email}& " . ( $this->member->member_id ? $this->member->joined->getTimestamp() : 0 ) . '&' . $this->id );
}
/**
@@ -299,11 +317,11 @@ abstract class _Session
*
* @return int
*/
public static function sessionLifetime()
public static function sessionLifetime(): int
{
$timeout = 1440;
if( \function_exists('ini_get') )
if( function_exists('ini_get') )
{
$phpTimeout = @ini_get('session.gc_maxlifetime');
$timeout = $phpTimeout ?: $timeout;
@@ -311,4 +329,44 @@ abstract class _Session
return $timeout;
}
/**
* Admin Log
*
* @code
\IPS\Session::i()->log( 'acplog__enhancements_enable', array( 'enhancements__foo' => TRUE ) );
* @endcode
* @param string $langKey Language key for log
* @param array $params Key/Values - keys are variables to use in sprintf on $langKey, values are booleans indicating if they are language keys themselves (TRUE) or raw data (FALSE)
* @param bool $noDupes If TRUE, will check the last log and not log again if it's the same and less than an hour ago
* @return void
*/
public function log( string $langKey, array $params=array(), bool $noDupes=FALSE ) : void
{
if ( $noDupes )
{
try
{
$lastLog = Db::i()->select( '*', 'core_admin_logs', array( 'member_id=?', $this->member->member_id ), 'ctime DESC', 1 )->first();
if ( $lastLog['ctime'] > ( time() - 3600 ) and $lastLog['lang_key'] == $langKey )
{
return;
}
}
catch ( UnderflowException $e ) { }
}
Db::i()->insert( 'core_admin_logs', array(
'member_id' => $this->member->member_id,
'member_name' => Member::loggedIn()->name,
'ctime' => time(),
'note' => json_encode( $params ),
'ip_address' => Request::i()->ipAddress(),
'appcomponent' => Dispatcher::i()->application->directory,
'module' => Dispatcher::i()->module->key,
'controller' => Dispatcher::i()->controller,
'do' => Request::i()->do,
'lang_key' => $langKey
) );
}
}
+49 -37
View File
@@ -11,16 +11,28 @@
namespace IPS\Session;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Session\Store\Database;
use IPS\Session\Store\Redis;
use RedisException;
use RuntimeException;
use function defined;
use const IPS\CACHE_CONFIG;
use const IPS\CACHE_METHOD;
use const IPS\REDIS_CONFIG;
use const IPS\REDIS_ENABLED;
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;
}
/**
* Session Handler
*/
abstract class _Store
abstract class Store
{
/**
* @brief Just return a count
@@ -40,14 +52,14 @@ abstract class _Store
/**
* @brief Instance
*/
protected static $instance = NULL;
protected static mixed $instance = NULL;
/**
* Returns the engine object
*
* @return class
* @return mixed
*/
public static function i()
public static function i(): mixed
{
if ( static::$instance === NULL )
{
@@ -56,26 +68,26 @@ abstract class _Store
try
{
/* Try and use Redis */
$writeConnection = \IPS\Redis::i()->connection('write');
$readConnection = \IPS\Redis::i()->connection('read');
$writeConnection = \IPS\Redis::i()->connection('read');
if( !$writeConnection OR !$readConnection )
{
throw new \RedisException;
throw new RedisException;
}
/* No exceptions means it worked */
static::$instance = new \IPS\Session\Store\Redis;
static::$instance = new Redis;
}
catch( \Exception $e )
catch( Exception $e )
{
/* Something went wrong, so fall back */
static::$instance = new \IPS\Session\Store\Database;
static::$instance = new Database;
}
}
else
{
static::$instance = new \IPS\Session\Store\Database;
static::$instance = new Database;
}
}
@@ -85,26 +97,26 @@ abstract class _Store
/**
* Load the session from the storage engine
*
* @param string $sessionId Session ID
* @return array
* @param string $sessionId Session ID
* @return array|null
*/
abstract public function loadSession( $sessionId );
abstract public function loadSession( string $sessionId ): ?array;
/**
* Update the session storage engine
*
* @param array $data Session data to store
* @param array $data Session data to store
* @return void
*/
abstract public function updateSession( $data );
abstract public function updateSession( array $data ) : void;
/**
* Delete from the session engine
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return void
*/
abstract public function deleteSession( $sessionId );
abstract public function deleteSession( string $sessionId ) : void;
/**
* Delete from the session engine
@@ -114,47 +126,47 @@ abstract class _Store
* @param array|NULL $keepSessionIds Array of session ids to keep [optional]
* @return void
*/
abstract public function deleteByMember( int $memberId, string $userAgent=NULL, array $keepSessionIds=NULL );
abstract public function deleteByMember( int $memberId, string $userAgent=NULL, array $keepSessionIds=NULL ) : void;
/**
* Delete from the session engine
*
* @param int $memberId You can probably figure this out right?
* @param int $memberId You can probably figure this out right?
* @return array|FALSE
*/
abstract public function getLatestMemberSession( $memberId );
abstract public function getLatestMemberSession( int $memberId ): array|FALSE;
/**
* Fetch all online users (but not spiders)
*
* @param int $flags Bitwise flags
* @param string $sort Sort direction
* @param array|NULL $limit Limit [ offset, limit ]
* @param int $memberGroup Limit by a specific member group ID
* @param boolean $showAnonymous Show anonymously logged in peoples?
* @return array
* @param int $flags Bitwise flags
* @param string $sort Sort direction
* @param array|null $limit Limit [ offset, limit ]
* @param int|null $memberGroup Limit by a specific member group ID
* @param boolean $showAnonymous Show anonymously logged in peoples?
* @return array|int
*/
abstract public function getOnlineUsers( $flags=0, $sort='desc', $limit=NULL, $memberGroup=NULL, $showAnonymous=FALSE );
abstract public function getOnlineUsers( int $flags=0, string $sort='desc', array $limit=NULL, int $memberGroup=NULL, bool $showAnonymous=FALSE ): array|int;
/**
* Fetch all members active at a specific location
*
* @param string $app Application directory (core, forums, etc)
* @param string $module Module
* @param string $controller Controller
* @param int $id Current item ID (empty if none)
* @param string $url Current viewing URL
* @param string $app Application directory (core, forums, etc)
* @param string $module Module
* @param string $controller Controller
* @param int $id Current item ID (empty if none)
* @param string $url Current viewing URL
* @return array
*/
abstract public function getOnlineMembersByLocation( $app, $module, $controller, $id, $url );
abstract public function getOnlineMembersByLocation( string $app, string $module, string $controller, ?int $id, string $url ): array;
/**
* Clear sessions - abstracted so it can be called externally without initiating a session
*
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @return void
*/
public static function clearSessions( $timeout )
public static function clearSessions( int $timeout )
{
/* Session engines can overload this */
}
+76 -62
View File
@@ -11,57 +11,71 @@
namespace IPS\Session\Store;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Useragent;
use IPS\Member;
use IPS\Request;
use IPS\Session\Front;
use IPS\Session\Store;
use UnderflowException;
use function count;
use function defined;
use function is_array;
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;
}
/**
* Database Session Handler
*/
class _Database extends \IPS\Session\Store
class Database extends Store
{
/**
* Load the session from the storage engine
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return array|NULL
*/
public function loadSession( $sessionId )
public function loadSession( string $sessionId ): ?array
{
$session = NULL;
/* Get from the database */
try
{
/* If it looks like we're logged in, join the member row to save a query later */
if ( \IPS\Session\Front::loggedIn() )
if ( Front::loggedIn() )
{
$session = \IPS\Db::i()->select( '*', 'core_sessions', array( 'id=?', $sessionId ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_MULTIDIMENSIONAL_JOINS )->join( 'core_members', 'core_members.member_id=core_sessions.member_id' )->first();
$session = Db::i()->select( '*', 'core_sessions', array( 'id=?', $sessionId ), NULL, NULL, NULL, NULL, Db::SELECT_MULTIDIMENSIONAL_JOINS )->join( 'core_members', 'core_members.member_id=core_sessions.member_id' )->first();
if ( $session['core_members']['member_id'] )
{
\IPS\Member::constructFromData( $session['core_members'], FALSE );
Member::constructFromData( $session['core_members'], FALSE );
}
$session = $session['core_sessions'];
}
/* If we're not logged in, just look at the session */
else
{
$userAgent = \IPS\Http\Useragent::parse();
$userAgent = Useragent::parse();
/* Spiders match by IP and useragent */
if ( $userAgent->bot )
if ( $userAgent->spider )
{
$session = \IPS\Db::i()->select( '*', 'core_sessions', array( 'id=? OR ( ip_address=? AND browser=? )', $sessionId, \IPS\Request::i()->ipAddress(), $_SERVER['HTTP_USER_AGENT'] ) )->first();
$session = Db::i()->select( '*', 'core_sessions', array( 'id=? OR ( ip_address=? AND browser=? )', $sessionId, Request::i()->ipAddress(), $_SERVER['HTTP_USER_AGENT'] ) )->first();
}
/* Normal users don't */
else
{
$session = \IPS\Db::i()->select( '*', 'core_sessions', array( 'id=?', $sessionId ) )->first();
$session = Db::i()->select( '*', 'core_sessions', array( 'id=?', $sessionId ) )->first();
}
}
}
catch ( \UnderflowException $e ) { }
catch ( UnderflowException $e ) { }
return $session;
}
@@ -69,23 +83,23 @@ class _Database extends \IPS\Session\Store
/**
* Update the session storage engine
*
* @param string $data Session Data
* @param array $data Session Data
* @return void
*/
public function updateSession( $data )
public function updateSession( array $data ) : void
{
\IPS\Db::i()->insert( 'core_sessions', $data, TRUE );
Db::i()->insert( 'core_sessions', $data, TRUE );
}
/**
* Delete from the session engine
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return void
*/
public function deleteSession( $sessionId )
public function deleteSession( string $sessionId ) : void
{
\IPS\Db::i()->delete( 'core_sessions', array( 'id=?', $sessionId ) );
Db::i()->delete( 'core_sessions', array( 'id=?', $sessionId ) );
}
/**
@@ -96,7 +110,7 @@ class _Database extends \IPS\Session\Store
* @param array|NULL $keepSessionIds Array of session ids to keep [optional]
* @return void
*/
public function deleteByMember( int $memberId, string $userAgent=NULL, array $keepSessionIds=NULL )
public function deleteByMember( int $memberId, string $userAgent=NULL, array $keepSessionIds=NULL ) : void
{
$where = array( array( 'member_id=?', $memberId ) );
@@ -105,27 +119,27 @@ class _Database extends \IPS\Session\Store
$where[] = array( 'browser=?', $userAgent );
}
if ( \is_array( $keepSessionIds ) AND \count( $keepSessionIds ) )
if ( is_array( $keepSessionIds ) AND count( $keepSessionIds ) )
{
$where[] = array( \IPS\Db::i()->in( 'id', $keepSessionIds, TRUE ) );
$where[] = array( Db::i()->in( 'id', $keepSessionIds, TRUE ) );
}
\IPS\Db::i()->delete( 'core_sessions', $where );
Db::i()->delete( 'core_sessions', $where );
}
/**
* Delete from the session engine
*
* @param int $memberId You can probably figure this out right?
* @param int $memberId You can probably figure this out right?
* @return array|FALSE
*/
public function getLatestMemberSession( $memberId )
public function getLatestMemberSession( int $memberId ): array|FALSE
{
try
{
return \IPS\Db::i()->select( '*', 'core_sessions', array( 'member_id=?', $memberId ), 'running_time DESC' )->first();
return Db::i()->select( '*', 'core_sessions', array( 'member_id=?', $memberId ), 'running_time DESC' )->first();
}
catch ( \UnderflowException $e )
catch ( UnderflowException $e )
{
return FALSE;
}
@@ -136,56 +150,56 @@ class _Database extends \IPS\Session\Store
*
* @return array or session IDs
*/
public function getSessionIds()
public function getSessionIds(): array
{
return iterator_to_array( \IPS\Db::i()->select( 'id', 'core_sessions' ) );
return iterator_to_array( Db::i()->select( 'id', 'core_sessions' ) );
}
/**
* Clear sessions - abstracted so it can be called externally without initiating a session
*
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @return void
*/
public static function clearSessions( $timeout )
public static function clearSessions( int $timeout ) : void
{
\IPS\Db::i()->delete( 'core_sessions', array( 'running_time<?', ( time() - $timeout ) ) );
Db::i()->delete( 'core_sessions', array( 'running_time<?', ( time() - $timeout ) ) );
}
/**
* Fetch all online users (but not spiders)
*
* @param int $flags Bitwise flags
* @param string $sort Sort direction
* @param array|NULL $limit Limit [ offset, limit ]
* @param int $memberGroup Limit by a specific member group ID
* @param boolean $showAnonymous Show anonymously logged in peoples?
* @return array
* @param int $flags Bitwise flags
* @param string $sort Sort direction
* @param array|null $limit Limit [ offset, limit ]
* @param int|null $memberGroup Limit by a specific member group ID
* @param boolean $showAnonymous Show anonymously logged in peoples?
* @return array|int
*/
public function getOnlineUsers( $flags=0, $sort='desc', $limit=NULL, $memberGroup=NULL, $showAnonymous=FALSE )
public function getOnlineUsers( int $flags=0, string $sort='desc', array $limit=NULL, int $memberGroup=NULL, bool $showAnonymous=FALSE ): array|int
{
/* Query */
$where = array(
array( 's.running_time>?', \IPS\DateTime::create()->sub( new \DateInterval( 'PT30M' ) )->getTimeStamp() ),
array( "s.login_type!=?", \IPS\Session\Front::LOGIN_TYPE_SPIDER )
array( 's.running_time>?', DateTime::create()->sub( new DateInterval( 'PT30M' ) )->getTimeStamp() ),
array( "s.login_type!=?", Front::LOGIN_TYPE_SPIDER )
);
if ( ! $showAnonymous )
{
if( \IPS\Member::loggedIn()->member_id )
if( Member::loggedIn()->member_id )
{
$where[] = array( "(s.login_type!=? OR s.member_id=?)", \IPS\Session\Front::LOGIN_TYPE_ANONYMOUS, \IPS\Member::loggedIn()->member_id );
$where[] = array( "(s.login_type!=? OR s.member_id=?)", Front::LOGIN_TYPE_ANONYMOUS, Member::loggedIn()->member_id );
}
else
{
$where[] = array( "s.login_type!=?", \IPS\Session\Front::LOGIN_TYPE_ANONYMOUS );
$where[] = array( "s.login_type!=?", Front::LOGIN_TYPE_ANONYMOUS );
}
}
if ( ! $flags and ! $limit )
{
/* Simple query for PHP processing */
return iterator_to_array( \IPS\Db::i()->select( 's.id,s.member_id,s.member_name,s.seo_name,s.member_group,s.login_type', array( 'core_sessions', 's' ), $where, 's.running_time ' . $sort )->setKeyField('id') );
return iterator_to_array( Db::i()->select( 's.id,s.member_id,s.member_name,s.seo_name,s.member_group,s.login_type', array( 'core_sessions', 's' ), $where, 's.running_time ' . $sort )->setKeyField('id') );
}
else
{
@@ -202,7 +216,7 @@ class _Database extends \IPS\Session\Store
$where = array(
array(
"core_sessions.id IN(?)",
\IPS\Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $memberSubWhere, NULL, NULL, 'member_id' ),
Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $memberSubWhere, NULL, NULL, 'member_id' ),
)
);
}
@@ -211,7 +225,7 @@ class _Database extends \IPS\Session\Store
$where = array(
array(
"core_sessions.id IN(?)",
\IPS\Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $guestSubWhere, NULL, NULL, 'ip_address' )
Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $guestSubWhere, NULL, NULL, 'ip_address' )
)
);
}
@@ -220,8 +234,8 @@ class _Database extends \IPS\Session\Store
$where = array(
array(
"( core_sessions.id IN(?) OR core_sessions.id IN(?) )",
\IPS\Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $memberSubWhere, NULL, NULL, 'member_id' ),
\IPS\Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $guestSubWhere, NULL, NULL, 'ip_address' )
Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $memberSubWhere, NULL, NULL, 'member_id' ),
Db::i()->select( 'MAX(id)', array( 'core_sessions', 's' ), $guestSubWhere, NULL, NULL, 'ip_address' )
)
);
}
@@ -247,44 +261,44 @@ class _Database extends \IPS\Session\Store
/* Just fetching a count? */
if ( $flags & static::ONLINE_COUNT_ONLY )
{
return \IPS\Db::i()->select( 'COUNT(*)', 'core_sessions', $where )->first();
return Db::i()->select( 'COUNT(*)', 'core_sessions', $where )->first();
}
return iterator_to_array( \IPS\Db::i()->select( '*', 'core_sessions', $where, 'core_sessions.running_time ' . $sort, $limit )->setKeyField('id') );
return iterator_to_array( Db::i()->select( '*', 'core_sessions', $where, 'core_sessions.running_time ' . $sort, $limit )->setKeyField('id') );
}
}
/**
* Fetch all members active at a specific location
*
* @param string $app Application directory (core, forums, etc)
* @param string $module Module
* @param string $controller Controller
* @param int $id Current item ID (empty if none)
* @param string $url Current viewing URL
* @param string $app Application directory (core, forums, etc)
* @param string $module Module
* @param string $controller Controller
* @param int $id Current item ID (empty if none)
* @param string $url Current viewing URL
* @return array
*/
public function getOnlineMembersByLocation( $app, $module, $controller, $id, $url )
public function getOnlineMembersByLocation( string $app, string $module, string $controller, ?int $id, string $url ): array
{
$members = array();
$where = array(
array( 'core_sessions.login_type=' . \IPS\Session\Front::LOGIN_TYPE_MEMBER ),
array( 'core_sessions.login_type=' . Front::LOGIN_TYPE_MEMBER ),
array( 'core_sessions.current_appcomponent=?', $app ),
array( 'core_sessions.current_module=?', $module ),
array( 'core_sessions.current_controller=?', $controller ),
array( 'core_sessions.running_time>' . \IPS\DateTime::create()->sub( new \DateInterval( 'PT30M' ) )->getTimeStamp() ),
array( 'core_sessions.running_time>' . DateTime::create()->sub( new DateInterval( 'PT30M' ) )->getTimeStamp() ),
array( 'core_sessions.location_url IS NOT NULL AND location_url LIKE ?', "{$url}%" ),
array( 'core_sessions.member_id IS NOT NULL' )
);
if( $id )
{
$where[] = array( 'core_sessions.current_id = ?', \IPS\Request::i()->id );
$where[] = array( 'core_sessions.current_id = ?', Request::i()->id );
}
foreach( \IPS\Db::i()->select( 'core_sessions.member_id,core_sessions.member_name,core_sessions.seo_name,core_sessions.member_group,core_sessions.login_type,core_sessions.in_editor', 'core_sessions', $where, 'core_sessions.running_time DESC' ) as $row )
foreach( Db::i()->select( 'core_sessions.member_id,core_sessions.member_name,core_sessions.seo_name,core_sessions.member_group,core_sessions.login_type,core_sessions.in_editor', 'core_sessions', $where, 'core_sessions.running_time DESC' ) as $row )
{
if( $row['login_type'] == \IPS\Session\Front::LOGIN_TYPE_MEMBER and $row['member_name'] )
if( $row['login_type'] == Front::LOGIN_TYPE_MEMBER and $row['member_name'] )
{
$members[ $row['member_id'] ] = $row;
}
+139 -128
View File
@@ -11,21 +11,37 @@
namespace IPS\Session\Store;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Member;
use IPS\Member\Group;
use IPS\Redis as RedisClass;
use IPS\Session\Front;
use IPS\Session\Store;
use IPS\Settings;
use RedisException;
use function array_slice;
use function count;
use function defined;
use function in_array;
use function is_array;
use function is_null;
use const IPS\TEXT_ENCRYPTION_KEY;
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 Session Handler
*/
class _Redis extends \IPS\Session\Store
class Redis extends Store
{
/**
* @brief Default expiration for keys in seconds
*/
static protected $ttl = 1800; #30 mins
static protected int $ttl = 1800; #30 mins
/**
* Return the hash we will use to obfuscate the session ID
@@ -34,29 +50,29 @@ class _Redis extends \IPS\Session\Store
*/
public static function getHash(): string
{
if ( \IPS\TEXT_ENCRYPTION_KEY )
if ( TEXT_ENCRYPTION_KEY )
{
return \IPS\TEXT_ENCRYPTION_KEY;
return TEXT_ENCRYPTION_KEY;
}
return \IPS\Settings::i()->sql_pass;
return Settings::i()->sql_pass;
}
/**
* Load the session from the storage engine
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return array|NULL
*/
public function loadSession( $sessionId )
public function loadSession( string $sessionId ): ?array
{
if ( $result = \IPS\Redis::i()->hGetAll( static::_key( 'session_id_' . md5( $sessionId . static::getHash() ) ) ) )
if ( $result = RedisClass::i()->hGetAll( static::_key( 'session_id_' . md5( $sessionId . static::getHash() ) ) ) )
{
try
{
return \IPS\Redis::i()->decode( $result['data'] );
return RedisClass::i()->decode( $result['data'] );
}
catch( \RedisException $e ){}
catch( RedisException $e ){}
}
return NULL;
@@ -65,73 +81,73 @@ class _Redis extends \IPS\Session\Store
/**
* Update the session storage engine
*
* @param array $data Session data to store
* @param array $data Session data to store
* @return void
*/
public function updateSession( $data )
public function updateSession( array $data ) : void
{
/* Groups are loaded into memory so this does not cause a query */
$group = \IPS\Member\Group::load( $data['member_group'] );
$group = Group::load( $data['member_group'] );
/* Update the specific session */
\IPS\Redis::i()->del( static::_key( 'session_id_' . md5( $data['id'] . static::getHash() ) ) );
\IPS\Redis::i()->hMSet( static::_key( 'session_id_' . md5( $data['id'] . static::getHash() ) ), array(
RedisClass::i()->del( static::_key( 'session_id_' . md5( $data['id'] . static::getHash() ) ) );
RedisClass::i()->hMSet( static::_key( 'session_id_' . md5( $data['id'] . static::getHash() ) ), array(
'member_id' => $data['member_id'],
'member_name' => $data['member_name'],
'seo_name' => $data['seo_name'],
'member_group' => $data['member_group'],
'login_type' => $data['login_type'],
'in_editor' => $data['in_editor'],
'data' => \IPS\Redis::i()->encode( $data )
'data' => RedisClass::i()->encode( $data )
), static::$ttl );
/* Update the list of sessions for the online list [ microtime => sessionID ] */
\IPS\Redis::i()->zAdd( static::_key( 'session_map' ), time(), 'session_id_' . md5( $data['id'] . static::getHash() ), static::$ttl );
RedisClass::i()->zAdd( static::_key( 'session_map' ), time(), 'session_id_' . md5( $data['id'] . static::getHash() ), static::$ttl );
/* Update users list */
if ( $data['uagent_type'] == 'search' )
{
/* Make a unique row based on IP and user-agent to prevent multiple rows for each spider */
\IPS\Redis::i()->zAdd( static::_key( 'session_online_spiders' ), time(), md5( $data['ip_address'] . $data['browser'] ), static::$ttl );
RedisClass::i()->zAdd( static::_key( 'session_online_spiders' ), time(), md5( $data['ip_address'] . $data['browser'] ), static::$ttl );
}
else if ( $data['member_id'] )
{
\IPS\Redis::i()->zAdd( static::_key( 'session_online_users' ), time(), $data['member_id'] . '__' . 'session_id_' . md5( $data['id'] . static::getHash() ), static::$ttl );
RedisClass::i()->zAdd( static::_key( 'session_online_users' ), time(), $data['member_id'] . '__' . 'session_id_' . md5( $data['id'] . static::getHash() ), static::$ttl );
}
else
{
/* A guest may have one ip address but multiple devices, but we don't really need to track that */
\IPS\Redis::i()->zAdd( static::_key( 'session_online_guests' ), time(), md5( $data['ip_address'] ), static::$ttl );
RedisClass::i()->zAdd( static::_key( 'session_online_guests' ), time(), md5( $data['ip_address'] ), static::$ttl );
}
/* Delete old items */
if ( ! \IPS\Redis::i()->get( static::_key( 'session_cleanup' ) ) )
if ( ! RedisClass::i()->get( static::_key( 'session_cleanup' ) ) )
{
/* Do a little clean up */
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_map' ), 0, time() - static::$ttl );
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_online_spiders' ), 0, time() - static::$ttl );
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_online_users' ), 0, time() - static::$ttl );
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_online_guests' ), 0, time() - static::$ttl );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_map' ), 0, time() - static::$ttl );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_online_spiders' ), 0, time() - static::$ttl );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_online_users' ), 0, time() - static::$ttl );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_online_guests' ), 0, time() - static::$ttl );
/* And do it again in 3ish mins */
\IPS\Redis::i()->setEx( static::_key( 'session_cleanup' ), 180, time() );
RedisClass::i()->setEx( static::_key( 'session_cleanup' ), 180, time() );
}
}
/**
* Delete from the session engine
*
* @param string $sessionId Session ID
* @param string $sessionId Session ID
* @return void
*/
public function deleteSession( $sessionId )
public function deleteSession( string $sessionId ) : void
{
$data = $this->loadSession( $sessionId );
\IPS\Redis::i()->del( static::_key( 'session_id_' . md5( $sessionId . static::getHash() ) ) );
\IPS\Redis::i()->zRem( static::_key( 'session_map' ), 'session_id_' . md5( $sessionId . static::getHash() ) );
\IPS\Redis::i()->zRem( static::_key( 'session_online_spiders' ), md5( $data['ip_address'] . $data['browser'] ) );
\IPS\Redis::i()->zRem( static::_key( 'session_online_users' ), $data['member_id'] . '__' . 'session_id_' . md5( $data['id'] . static::getHash() ) );
\IPS\Redis::i()->zRem( static::_key( 'session_online_guests' ), md5( $data['ip_address'] ) );
RedisClass::i()->del( static::_key( 'session_id_' . md5( $sessionId . static::getHash() ) ) );
RedisClass::i()->zRem( static::_key( 'session_map' ), 'session_id_' . md5( $sessionId . static::getHash() ) );
RedisClass::i()->zRem( static::_key( 'session_online_spiders' ), md5( $data['ip_address'] . $data['browser'] ) );
RedisClass::i()->zRem( static::_key( 'session_online_users' ), $data['member_id'] . '__' . 'session_id_' . md5( $data['id'] . static::getHash() ) );
RedisClass::i()->zRem( static::_key( 'session_online_guests' ), md5( $data['ip_address'] ) );
}
/**
@@ -142,33 +158,33 @@ class _Redis extends \IPS\Session\Store
* @param array|NULL $keepSessionIds Array of session ids to keep [optional]
* @return void
*/
public function deleteByMember( int $memberId, string $userAgent=NULL, array $keepSessionIds=NULL )
public function deleteByMember( int $memberId, string $userAgent=NULL, array $keepSessionIds=NULL ) : void
{
if ( ! \is_array( $keepSessionIds ) and ! \is_null( $keepSessionIds ) )
if ( ! is_array( $keepSessionIds ) and ! is_null( $keepSessionIds ) )
{
$keepSessionIds = array( $keepSessionIds );
}
$sessionMap = \IPS\Redis::i()->zRange( static::_key( 'session_online_users' ), 0, -1 );
$sessionMap = RedisClass::i()->zRange( static::_key( 'session_online_users' ), 0, -1 );
foreach( $sessionMap as $index => $redisKey )
{
$key = explode( '__', $redisKey );
$session = \IPS\Redis::i()->hMGet( $key[1], array( 'data' ) );
$session = RedisClass::i()->hMGet( $key[1], array( 'data' ) );
try
{
$sessionMap[ $index ] = \IPS\Redis::i()->decode( $session['data'] );
$sessionMap[ $index ] = RedisClass::i()->decode( $session['data'] );
}
catch( \RedisException $e ){}
catch( RedisException $e ){}
}
foreach( $sessionMap as $session )
{
/* SessionMap may not link to a valid session, remove if that is the case */
if( !\is_array( $session ) )
if( !is_array( $session ) )
{
\IPS\Redis::i()->zRem( static::_key( 'session_map' ), $session );
RedisClass::i()->zRem( static::_key( 'session_map' ), $session );
continue;
}
@@ -183,7 +199,7 @@ class _Redis extends \IPS\Session\Store
$delete = false;
}
if ( $keepSessionIds and \is_array( $keepSessionIds ) and \in_array( $session['id'], $keepSessionIds ) )
if ( $keepSessionIds and in_array( $session['id'], $keepSessionIds ) )
{
$delete = false;
}
@@ -200,9 +216,9 @@ class _Redis extends \IPS\Session\Store
*
* @return array of session IDs
*/
public function getSessionIds()
public function getSessionIds(): array
{
if ( $result = \IPS\Redis::i()->zRangeByScore( static::_key( 'session_map' ), '-inf', '+inf', array( 'withscores' => false ) ) )
if ( $result = RedisClass::i()->zRangeByScore( static::_key( 'session_map' ), '-inf', '+inf', array( 'withscores' => false ) ) )
{
return $result;
}
@@ -213,14 +229,14 @@ class _Redis extends \IPS\Session\Store
/**
* Delete from the session engine
*
* @param int $memberId You can probably figure this out right?
* @param int $memberId You can probably figure this out right?
* @return array|FALSE
*/
public function getLatestMemberSession( $memberId )
public function getLatestMemberSession( int $memberId ): array|FALSE
{
$redis = \IPS\Redis::i()->zRevRangeByScore( static::_key( 'session_online_users' ), '+inf', '-inf', array('withscores' => FALSE, 'alpha' => TRUE ) );
$redis = RedisClass::i()->zRevRangeByScore( static::_key( 'session_online_users' ), '+inf', '-inf', array('withscores' => FALSE, 'alpha' => TRUE ) );
if( \is_array( $redis ) )
if( is_array( $redis ) )
{
foreach ( $redis as $data )
{
@@ -228,13 +244,13 @@ class _Redis extends \IPS\Session\Store
if ( $id == $memberId )
{
if ( $result = \IPS\Redis::i()->hGetAll( static::_key( $sessionKey ) ) )
if ( $result = RedisClass::i()->hGetAll( static::_key( $sessionKey ) ) )
{
try
{
return \IPS\Redis::i()->decode( $result['data'] );
return RedisClass::i()->decode( $result['data'] );
}
catch ( \RedisException $e ) {}
catch ( RedisException $e ) {}
}
}
}
@@ -246,31 +262,31 @@ class _Redis extends \IPS\Session\Store
/**
* Clear sessions - abstracted so it can be called externally without initiating a session
*
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @param int $timeout Sessions older than the number of seconds provided will be deleted
* @return void
*/
public static function clearSessions( $timeout )
public static function clearSessions( int $timeout ) : void
{
/* Remove the public facing items. This is only called by PHP's session gc so individual sessions do not need removing as they are cleaned by Redis' TTL */
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_map' ), 0, time() - $timeout );
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_online_spiders' ), 0, time() - $timeout );
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_online_users' ), 0, time() - $timeout );
\IPS\Redis::i()->zRemRangeByScore( static::_key( 'session_online_guests' ), 0, time() - $timeout );
\IPS\Redis::i()->del( static::_key( 'session_onlinelist' ) );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_map' ), 0, time() - $timeout );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_online_spiders' ), 0, time() - $timeout );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_online_users' ), 0, time() - $timeout );
RedisClass::i()->zRemRangeByScore( static::_key( 'session_online_guests' ), 0, time() - $timeout );
RedisClass::i()->del( static::_key( 'session_onlinelist' ) );
}
/**
* Redis key
*/
protected static $_redisKey;
protected static ?string $_redisKey = NULL;
/**
* Returns a key to be stored with Redis
*
* @param string $key Key suffix
* @param string $key Key suffix
* @return string
*/
protected static function _key( $key )
protected static function _key( string $key ): string
{
/* Only manage the session_onlist key which is prone to corruption. We don't want to wipe out the online list each time this file fails */
if ( $key == 'session_onlinelist' )
@@ -278,11 +294,11 @@ class _Redis extends \IPS\Session\Store
if ( !static::$_redisKey )
{
/* Last access ensures that the data is not stale if we fail back to MySQL and then go back to Redis later */
if ( !( static::$_redisKey = \IPS\Redis::i()->get( 'redisKey_session' ) ) OR ! \IPS\Redis::i()->get( 'redisStore_lastAccess' ) )
if ( !( static::$_redisKey = RedisClass::i()->get( 'redisKey_session' ) ) OR ! RedisClass::i()->get( 'redisStore_lastAccess' ) )
{
static::$_redisKey = md5( mt_rand() );
\IPS\Redis::i()->setex( 'redisKey_session', 604800, static::$_redisKey );
\IPS\Redis::i()->setex( 'redisStore_lastAccess', ( 3 * 3600 ), time() );
RedisClass::i()->setex( 'redisKey_session', 604800, static::$_redisKey );
RedisClass::i()->setex( 'redisStore_lastAccess', ( 3 * 3600 ), time() );
}
}
@@ -299,54 +315,49 @@ class _Redis extends \IPS\Session\Store
*
* @return void
*/
protected static function resetKey()
protected static function resetKey() : void
{
static::$_redisKey = md5( mt_rand() );
\IPS\Redis::i()->setex( 'redisKey_session', 604800, static::$_redisKey );
RedisClass::i()->setex( 'redisKey_session', 604800, static::$_redisKey );
}
/**
* Redis key
*/
protected $fetchAttempt = 0;
/**
* @var array|null cache online user data after fetching it
*/
protected ?array $onlineUsers = null;
protected int $fetchAttempt = 0;
/**
* Fetch all online users (but not spiders)
*
* @param int $flags Bitwise flags
* @param string $sort Sort direction
* @param array|NULL $limit Limit [ offset, limit ]
* @param int $memberGroup Limit by a specific member group ID
* @param boolean $showAnonymous Show anonymously logged in peoples?
* @return array
* @param int $flags Bitwise flags
* @param string $sort Sort direction
* @param array|null $limit Limit [ offset, limit ]
* @param int|null $memberGroup Limit by a specific member group ID
* @param boolean $showAnonymous Show anonymously logged in peoples?
* @return array|int
*/
public function getOnlineUsers( $flags=0, $sort='desc', $limit=NULL, $memberGroup=NULL, $showAnonymous=FALSE )
public function getOnlineUsers( int $flags=0, string $sort='desc', array $limit=NULL, int $memberGroup=NULL, bool $showAnonymous=FALSE ): array|int
{
if( $this->onlineUsers === NULL )
try
{
try
{
$this->onlineUsers = \IPS\Redis::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
}
catch ( \RedisException $e )
{
/* Something went wrong, so reset the key to force a new file */
static::resetKey();
}
$results = RedisClass::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
}
if ( ! $this->onlineUsers )
catch ( RedisException $e )
{
/* Something went wrong, so reset the key to force a new file */
static::resetKey();
$results = NULL;
}
if ( ! $results )
{
/* Ensure file is deleted */
try
{
\IPS\Redis::i()->del( static::_key( 'session_onlinelist' ) );
RedisClass::i()->del( static::_key( 'session_onlinelist' ) );
}
catch ( \RedisException $e )
catch ( RedisException $e )
{
/* Something went wrong, so reset the key to force a new file */
static::resetKey();
@@ -354,30 +365,30 @@ class _Redis extends \IPS\Session\Store
$options = array(
'sort' => $sort === 'asc' ? 'asc' : 'desc',
'store' => \IPS\Redis::i()->prefix . static::_key( 'session_onlinelist' ),
'store' => RedisClass::i()->prefix . static::_key( 'session_onlinelist' ),
'alpha' => true,
'by' => 'nosort ' . $sort === 'asc' ? 'asc' : 'desc',
'ttl' => 30, /* This ensures the stored file session_online only lasts for 30 seconds */
'get' => array(
static::_key( \IPS\Redis::i()->prefix ) . '*->member_id',
static::_key( \IPS\Redis::i()->prefix ) . '*->member_name',
static::_key( \IPS\Redis::i()->prefix ) . '*->seo_name',
static::_key( \IPS\Redis::i()->prefix ) . '*->member_group',
static::_key( \IPS\Redis::i()->prefix ) . '*->login_type',
static::_key( \IPS\Redis::i()->prefix ) . '*->data'
static::_key( RedisClass::i()->prefix ) . '*->member_id',
static::_key( RedisClass::i()->prefix ) . '*->member_name',
static::_key( RedisClass::i()->prefix ) . '*->seo_name',
static::_key( RedisClass::i()->prefix ) . '*->member_group',
static::_key( RedisClass::i()->prefix ) . '*->login_type',
static::_key( RedisClass::i()->prefix ) . '*->data'
)
);
\IPS\Redis::i()->sort( static::_key( 'session_map' ), $options );
RedisClass::i()->sort( static::_key( 'session_map' ), $options );
try
{
/* Force expiration in 60 seconds to prevent stale caches hanging around */
\IPS\Redis::i()->expire( static::_key( 'session_onlinelist' ), 60 );
$this->onlineUsers = \IPS\Redis::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
/* Force expiration in 30 seconds to prevent stale caches hanging around */
RedisClass::i()->expire( static::_key( 'session_onlinelist' ), 30 );
$results = RedisClass::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
}
catch ( \RedisException $e )
catch ( RedisException $e )
{
$this->fetchAttempt++;
@@ -387,11 +398,11 @@ class _Redis extends \IPS\Session\Store
if ( $this->fetchAttempt < 2 )
{
/* And try again, but only once more to prevent an infinite loop */
return $this->getOnlineUsers( $flags, $sort, $limit, $memberGroup, $showAnonymous );
return $this->getOnlineUsers($flags, $sort, $limit, $memberGroup, $showAnonymous);
}
else
{
$this->onlineUsers = array();
$results = array();
}
}
}
@@ -403,7 +414,7 @@ class _Redis extends \IPS\Session\Store
$return = array();
$i = 0;
while( $i < \count( $this->onlineUsers ) )
while( $i < count( $results ) )
{
$fields = array();
$data = NULL;
@@ -413,9 +424,9 @@ class _Redis extends \IPS\Session\Store
{
try
{
$data = \IPS\Redis::i()->decode( $this->onlineUsers[ $i++ ] );
$data = RedisClass::i()->decode( $results[ $i++ ] );
}
catch( \RedisException $e )
catch( RedisException $e )
{
$data = NULL;
}
@@ -423,15 +434,15 @@ class _Redis extends \IPS\Session\Store
/* login_type must be cast as an integer or else anonymous state can be lost when adjustSessions() runs */
elseif( $field === 'login_type' )
{
$fields[ $field ] = (int) $this->onlineUsers[ $i++ ];
$fields[ $field ] = (int) $results[ $i++ ];
}
else
{
$fields[ $field ] = $this->onlineUsers[ $i++ ];
$fields[ $field ] = $results[ $i++ ];
}
}
if ( \is_array( $data ) AND \count( $data ) )
if ( is_array( $data ) AND count( $data ) )
{
/* Have we already fetched this member? */
if ( $fields['member_id'] and isset( $return[ $fields['member_id'] ] ) )
@@ -446,12 +457,12 @@ class _Redis extends \IPS\Session\Store
}
/* Ignore spiders */
if ( ! $fields['member_id'] and ! ( $data['login_type'] == \IPS\Session\Front::LOGIN_TYPE_GUEST or $data['login_type'] == \IPS\Session\Front::LOGIN_TYPE_INCOMPLETE ) )
if ( ! $fields['member_id'] and ! ( $data['login_type'] == Front::LOGIN_TYPE_GUEST or $data['login_type'] == Front::LOGIN_TYPE_INCOMPLETE ) )
{
continue;
}
$return[ $fields['member_id'] ? $fields['member_id'] : $data['ip_address'] ] = array_merge( $data, $fields );
$return[ $fields['member_id'] ?: $data['ip_address'] ] = array_merge( $data, $fields );
}
}
@@ -482,7 +493,7 @@ class _Redis extends \IPS\Session\Store
continue;
}
if ( ! $showAnonymous and $data['login_type'] == \IPS\Session\Front::LOGIN_TYPE_ANONYMOUS and ( !\IPS\Member::loggedIn()->member_id OR $data['member_id'] != \IPS\Member::loggedIn()->member_id ) )
if ( ! $showAnonymous and $data['login_type'] == Front::LOGIN_TYPE_ANONYMOUS and ( !Member::loggedIn()->member_id OR $data['member_id'] != Member::loggedIn()->member_id ) )
{
continue;
}
@@ -493,7 +504,7 @@ class _Redis extends \IPS\Session\Store
/* Count only? */
if ( $flags & static::ONLINE_COUNT_ONLY )
{
return \count( $members );
return count( $members );
}
/* Hooray for PHP 7 */
@@ -504,7 +515,7 @@ class _Redis extends \IPS\Session\Store
if ( $limit )
{
return \array_slice( $members, $limit[0], $limit[1], TRUE );
return array_slice( $members, $limit[0], $limit[1], TRUE );
}
return $members;
@@ -516,18 +527,18 @@ class _Redis extends \IPS\Session\Store
/**
* Fetch all members active at a specific location
*
* @param string $app Application directory (core, forums, etc)
* @param string $module Module
* @param string $controller Controller
* @param int $id Current item ID (empty if none)
* @param string $url Current viewing URL
* @param string $app Application directory (core, forums, etc)
* @param string $module Module
* @param string $controller Controller
* @param int $id Current item ID (empty if none)
* @param string $url Current viewing URL
* @return array
*/
public function getOnlineMembersByLocation( $app, $module, $controller, $id, $url )
public function getOnlineMembersByLocation( string $app, string $module, string $controller, ?int $id, string $url ): array
{
$members = array();
foreach( $this->getOnlineUsers( static::ONLINE_MEMBERS, 'desc' ) as $member )
foreach($this->getOnlineUsers(static::ONLINE_MEMBERS, 'desc') as $member )
{
if ( $member['current_appcomponent'] == $app and $member['current_module'] == $module and $member['current_controller'] == $controller and $member['current_id'] == $id )
{