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

+87 -68
View File
@@ -11,16 +11,35 @@
namespace IPS\Member;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use Exception;
use IPS\DateTime;
use IPS\Db;
use IPS\Email;
use IPS\GeoLocation;
use IPS\Http\Useragent;
use IPS\Login;
use IPS\Login\Handler;
use IPS\Member;
use IPS\Patterns\ActiveRecord;
use IPS\Request;
use IPS\Settings;
use OutOfRangeException;
use UnderflowException;
use function defined;
use function is_numeric;
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;
}
/**
* Known Member Device Model
*/
class _Device extends \IPS\Patterns\ActiveRecord
class Device extends ActiveRecord
{
/**
* @brief Login keys are valid for 3 months
@@ -30,43 +49,43 @@ class _Device extends \IPS\Patterns\ActiveRecord
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
protected static array $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_members_known_devices';
public static ?string $databaseTable = 'core_members_known_devices';
/**
* @brief [ActiveRecord] ID Database Column
*/
public static $databaseColumnId = 'device_key';
public static string $databaseColumnId = 'device_key';
/**
* @brief Is this a known device?
*/
public $known = TRUE;
public bool $known = TRUE;
/**
* Load device for current request, or create one if there isn't one
*
* @param \IPS\Member $member The member being authenticated
* @param bool $sendNewDeviceEmail If true, and the associated setting is enabled, and this is a new device, an email will be sent to the member. Only set to FALSE if the user is registering and while the MFA where the email would be redundant.
* @return \IPS\Member\Device
* @param Member $member The member being authenticated
* @param bool $sendNewDeviceEmail If true, and the associated setting is enabled, and this is a new device, an email will be sent to the member. Only set to FALSE if the user is registering and while the MFA where the email would be redundant.
* @return Device
*/
public static function loadOrCreate( \IPS\Member $member, $sendNewDeviceEmail=TRUE )
public static function loadOrCreate( Member $member, bool $sendNewDeviceEmail=TRUE ): Device
{
if ( isset( \IPS\Request::i()->cookie['device_key'] ) and mb_strlen( \IPS\Request::i()->cookie['device_key'] ) === 32 )
if ( isset( Request::i()->cookie['device_key'] ) and mb_strlen( Request::i()->cookie['device_key'] ) === 32 )
{
try
{
$device = static::loadAndAuthenticate( \IPS\Request::i()->cookie['device_key'], $member );
$device = static::loadAndAuthenticate( Request::i()->cookie['device_key'], $member );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
$device = new static;
$device->known = FALSE;
$device->device_key = \IPS\Request::i()->cookie['device_key'];
$device->device_key = Request::i()->cookie['device_key'];
$device->member_id = $member->member_id;
if ( $sendNewDeviceEmail )
@@ -87,7 +106,7 @@ class _Device extends \IPS\Patterns\ActiveRecord
}
}
\IPS\Request::i()->setCookie( 'device_key', $device->device_key, ( new \IPS\DateTime )->add( new \DateInterval( 'P1Y' ) ) );
Request::i()->setCookie( 'device_key', $device->device_key, ( new DateTime )->add( new DateInterval( 'P1Y' ) ) );
return $device;
}
@@ -95,13 +114,13 @@ class _Device extends \IPS\Patterns\ActiveRecord
/**
* Load, but only if it is valid for a particular member and optionally login key
*
* @param string $deviceId The device ID
* @param \IPS\Member $member The member
* @param string|null $loginKey If you also want to authenticate by login key, the login key to check
* @return \IPS\Member\Device
* @throws \OutOfRangeException
* @param string $deviceId The device ID
* @param Member $member The member
* @param string|null $loginKey If you also want to authenticate by login key, the login key to check
* @return Device
* @throws OutOfRangeException
*/
public static function loadAndAuthenticate( $deviceId, \IPS\Member $member, $loginKey = NULL )
public static function loadAndAuthenticate( string $deviceId, Member $member, string $loginKey = NULL ): Device
{
/* Load the device */
$device = static::load( $deviceId, NULL, array( 'member_id=?', $member->member_id ) );
@@ -110,15 +129,15 @@ class _Device extends \IPS\Patterns\ActiveRecord
if ( $loginKey !== NULL )
{
/* Login keys expire after 3 months - if it has been more than 3 months since it was generted, do not authenticate */
if ( $device->last_seen < ( new \IPS\DateTime )->sub( new \DateInterval( static::LOGIN_KEY_VALIDITY ) )->getTimestamp() )
if ( $device->last_seen < ( new DateTime )->sub( new DateInterval( static::LOGIN_KEY_VALIDITY ) )->getTimestamp() )
{
throw new \OutOfRangeException;
throw new OutOfRangeException;
}
/* Validate login key is valid - if there is no login_key set for the device, it is because the device has been deauthorized */
if ( !$device->login_key OR !\IPS\Login::compareHashes( (string) $device->login_key, (string) $loginKey ) )
if ( !$device->login_key OR !Login::compareHashes( (string) $device->login_key, $loginKey ) )
{
throw new \OutOfRangeException;
throw new OutOfRangeException;
}
}
@@ -129,20 +148,20 @@ class _Device extends \IPS\Patterns\ActiveRecord
/**
* Create a new device with unique key
*
* @return \IPS\Member\Device
* @return Device
*/
public static function createNew()
public static function createNew(): Device // we need the underscore version as long as we have monkey patching
{
do
{
$deviceKey = \IPS\Login::generateRandomString();
$deviceKey = Login::generateRandomString();
try
{
\IPS\Db::i()->select( 'device_key', 'core_members_known_devices', array( 'device_key=?', $deviceKey ) )->first();
Db::i()->select( 'device_key', 'core_members_known_devices', array( 'device_key=?', $deviceKey ) )->first();
$generatedDeviceKeyInUse = TRUE;
}
catch ( \UnderflowException $e )
catch ( UnderflowException $e )
{
$generatedDeviceKeyInUse = FALSE;
}
@@ -157,18 +176,18 @@ class _Device extends \IPS\Patterns\ActiveRecord
/**
* Update after logging in / automatic authentication with current request's user agent / IP address, etc.
*
* @param bool|null $rememberMe Remember me? NULL can be provided if the login is being processed from somewhere that doesn't ask
* @param \IPS\Login\Handler $loginHandler The login handler which processed the login, or NULL if updating an existing login. Can also be empty string for logins that weren't processed by any handler (such as after registration or using the lost password feature)
* @param bool $refreshLoginKey If login key should be refreshed (FALSE for automatic logins which will need the same login key subsequently)
* @param book $setCookies Should the cookies be set ( FALSE for ACP login )
* @param bool|null $rememberMe Remember me? NULL can be provided if the login is being processed from somewhere that doesn't ask
* @param Handler|null $loginHandler The login handler which processed the login, or NULL if updating an existing login. Can also be empty string for logins that weren't processed by any handler (such as after registration or using the lost password feature)
* @param bool $refreshLoginKey If login key should be refreshed (FALSE for automatic logins which will need the same login key subsequently)
* @param bool $setCookies Should the cookies be set ( FALSE for ACP login )
* @return void
*/
public function updateAfterAuthentication( $rememberMe, \IPS\Login\Handler $loginHandler=NULL, $refreshLoginKey=TRUE, $setCookies = TRUE )
public function updateAfterAuthentication( ?bool $rememberMe, Handler $loginHandler=NULL, bool $refreshLoginKey=TRUE, bool $setCookies = TRUE ) : void
{
$this->user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : NULL;
$this->user_agent = $_SERVER['HTTP_USER_AGENT'] ?? NULL;
if ( $refreshLoginKey )
{
$this->login_key = $rememberMe ? \IPS\Login::generateRandomString() : NULL;
$this->login_key = $rememberMe ? Login::generateRandomString() : NULL;
}
$this->last_seen = time();
if ( $loginHandler !== NULL )
@@ -177,27 +196,27 @@ class _Device extends \IPS\Patterns\ActiveRecord
}
$this->save();
$this->logIpAddress( \IPS\Request::i()->ipAddress() );
$this->logIpAddress( Request::i()->ipAddress() );
if ( $setCookies )
{
$cookieExpiration = ( new \IPS\DateTime )->add( new \DateInterval( static::LOGIN_KEY_VALIDITY ) );
$cookieExpiration = ( new DateTime )->add( new DateInterval( static::LOGIN_KEY_VALIDITY ) );
if ( $rememberMe === NULL )
{
\IPS\Request::i()->setCookie( 'member_id', $this->member_id, $cookieExpiration );
\IPS\Request::i()->setCookie( 'loggedIn', time(), $cookieExpiration, FALSE );
Request::i()->setCookie( 'member_id', $this->member_id, $cookieExpiration );
Request::i()->setCookie( 'loggedIn', time(), $cookieExpiration, FALSE );
}
elseif ( $rememberMe === TRUE )
{
\IPS\Request::i()->setCookie( 'member_id', $this->member_id, $cookieExpiration );
\IPS\Request::i()->setCookie( 'login_key', $this->login_key, $cookieExpiration );
\IPS\Request::i()->setCookie( 'loggedIn', time(), $cookieExpiration, FALSE );
Request::i()->setCookie( 'member_id', $this->member_id, $cookieExpiration );
Request::i()->setCookie( 'login_key', $this->login_key, $cookieExpiration );
Request::i()->setCookie( 'loggedIn', time(), $cookieExpiration, FALSE );
}
else
{
\IPS\Request::i()->setCookie( 'member_id', $this->member_id, NULL ); // Just tells the guest caching mechanism that we are logged in, so it can expire on the session end
\IPS\Request::i()->setCookie( 'login_key', NULL ); // Clear it in case they previously had chosen "Remember Me"
\IPS\Request::i()->setCookie( 'loggedIn', time(), $cookieExpiration, FALSE );
Request::i()->setCookie( 'member_id', $this->member_id, NULL ); // Just tells the guest caching mechanism that we are logged in, so it can expire on the session end
Request::i()->setCookie( 'login_key', NULL ); // Clear it in case they previously had chosen "Remember Me"
Request::i()->setCookie( 'loggedIn', time(), $cookieExpiration, FALSE );
}
}
}
@@ -205,12 +224,12 @@ class _Device extends \IPS\Patterns\ActiveRecord
/**
* Log an IP address as been having used by this devuce
*
* @param string $ipAddress The IP Address
* @param string $ipAddress The IP Address
* @return void
*/
public function logIpAddress( $ipAddress )
public function logIpAddress( string $ipAddress ) : void
{
\IPS\Db::i()->insert( 'core_members_known_ip_addresses', array(
Db::i()->insert( 'core_members_known_ip_addresses', array(
'device_key' => $this->device_key,
'member_id' => $this->member_id,
'ip_address' => $ipAddress,
@@ -221,27 +240,27 @@ class _Device extends \IPS\Patterns\ActiveRecord
/**
* Get user agent data
*
* @return \IPS\Http\Useragent
* @return Useragent
*/
public function userAgent()
public function userAgent(): Useragent
{
return \IPS\Http\Useragent::parse( $this->user_agent );
return Useragent::parse( $this->user_agent );
}
/**
* Get login method
*
* @return \IPS\Login\Handler|NULL
* @return Handler|NULL
*/
public function loginMethod()
public function loginMethod(): ?Handler
{
if ( \is_numeric( $this->login_handler ) )
if ( is_numeric( $this->login_handler ) )
{
try
{
return \IPS\Login\Handler::load( $this->login_handler );
return Handler::load( $this->login_handler );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
return NULL;
}
@@ -254,33 +273,33 @@ class _Device extends \IPS\Patterns\ActiveRecord
*
* @return void
*/
protected function sendNewDeviceEmail()
protected function sendNewDeviceEmail() : void
{
$member = \IPS\Member::load( $this->member_id );
$member = Member::load( $this->member_id );
if ( \IPS\Settings::i()->new_device_email and $member->members_bitoptions['new_device_email'] )
if ( Settings::i()->new_device_email and $member->members_bitoptions['new_device_email'] )
{
try
{
$location = \IPS\GeoLocation::getRequesterLocation();
$location = GeoLocation::getRequesterLocation();
}
catch ( \Exception $e )
catch ( Exception $e )
{
$location = NULL;
}
\IPS\Email::buildFromTemplate( 'core', 'new_device', array( $member, $this, $location ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
Email::buildFromTemplate( 'core', 'new_device', array( $member, $this, $location ), Email::TYPE_TRANSACTIONAL )->send( $member );
}
$member->logHistory( 'core', 'login', array( 'type' => 'new_device', 'device' => $this->device_key, 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : NULL ), FALSE );
$member->logHistory( 'core', 'login', array( 'type' => 'new_device', 'device' => $this->device_key, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? NULL ), FALSE );
}
/**
* Get the WHERE clause for save()
*
* @return void
* @return array
*/
protected function _whereClauseForSave()
protected function _whereClauseForSave() : array
{
return array( 'device_key=? AND member_id=?', $this->device_key, $this->member_id );
}