Version 4.7.13

This commit is contained in:
Neo committed 2025-12-19 16:14:56 -08:00
1 parent fd22d99e69
commit 7124a02564
954 files changed
+29277 -59308

No files matched your search

+334 -12
View File
@@ -120,6 +120,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
'new_device_email' => 16777216, // Send an email when a new device is used to log in.
'no_solved_reenage' => 33554432, // User does not want to get topic re-engagement emails
'datalayer_pii_optout' => 67108864, // User has opted in to having their PII collected by datalayer
'email_messages_bounce' => 134217728, // User's email keeps returning hard bounces so they need to change it
// 268435456 - cookie_optout moved to cookie preferences
)
)
);
@@ -133,7 +135,6 @@ class _Member extends \IPS\Patterns\ActiveRecord
'last_visit',
'last_activity',
'profilesync_lastsync',
];
/* !Follow */
@@ -203,7 +204,12 @@ class _Member extends \IPS\Patterns\ActiveRecord
if ( !static::$loggedInMember->member_id and isset( \IPS\Request::i()->cookie['ipsTimezone'] ) )
{
static::$loggedInMember->timezone = \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] );
/* Check for valid timezone identifier */
$tz = \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] );
if( \in_array( $tz, \DateTimeZone::listIdentifiers() ) )
{
static::$loggedInMember->timezone = $tz;
}
}
}
@@ -331,6 +337,47 @@ class _Member extends \IPS\Patterns\ActiveRecord
}
}
/**
* Allow optional cookies
*
* @param bool $allow
* @return bool
*/
public function setAllowOptionalCookies( bool $allow = TRUE ): bool
{
$expire = ( new \IPS\DateTime )->add( new \DateInterval('P6M' ) );
\IPS\Request::i()->setCookie( 'cookie_consent', 1, $expire, FALSE );
if( !$allow )
{
/* Remove optional cookies */
foreach( \IPS\Request::i()->cookie as $cookieName => $value )
{
if( !\in_array( $cookieName, \IPS\Request::i()->getEssentialCookies() ) )
{
\IPS\Request::i()->setCookie( $cookieName, NULL);
}
}
\IPS\Request::i()->setCookie( 'cookie_consent_optional', NULL );
}
else
{
\IPS\Request::i()->setCookie( 'cookie_consent_optional', 1, $expire, FALSE );
}
return $allow;
}
/**
* Are optional cookies allowed?
*
* @return bool
*/
public function get_optionalCookiesAllowed(): bool
{
return isset( \IPS\Request::i()->cookie[ 'cookie_consent' ] ) AND isset( \IPS\Request::i()->cookie[ 'cookie_consent_optional' ] );
}
/**
* [ActiveRecord] Delete Record
*
@@ -411,6 +458,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
/* @note The completed column is added in the 4.4.0 Beta 1 routine, so we need to verify the upgrade has been performed otherwise
the auto-upgrader can replace this file before the column is added, and an SQL error can prevent the admin from completing the
auto-upgrade process */
$fireRegistrationCompletedWebhook = FALSE;
if ( \IPS\Application::load( 'core' )->long_version > 104000 )
{
if( $this->completed AND ( !$this->name OR !$this->email ) )
@@ -421,22 +469,32 @@ class _Member extends \IPS\Patterns\ActiveRecord
elseif( !$this->completed AND $this->name AND $this->email )
{
$this->completed = TRUE;
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
$fireRegistrationCompletedWebhook = TRUE;
}
}
/* If the email was changed, make sure that the bounce flag is unset so the member no longer sees the error message */
if ( isset( $changes['email'] ) )
{
$this->members_bitoptions['email_messages_bounce'] = 0;
}
parent::save();
if ( $new )
{
$this->memberSync( 'onCreateAccount' );
/* Profile Fields */
\IPS\Db::i()->insert( 'core_pfields_content', array( 'member_id' => $this->member_id ), TRUE );
$this->memberSync( 'onCreateAccount' );
}
else
{
if( $fireRegistrationCompletedWebhook )
{
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
}
/* Run member sync, but not if the only change is the last_activity timestamp */
if( \count( $this->changedCustomFields ) > 0 OR ( \count( $changes ) > 0 AND !( \count( $changes ) === 1 AND isset( $changes['last_activity'] ) ) ) )
{
@@ -784,6 +842,12 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
$this->members_bitoptions['validating'] = FALSE;
}
/* Revoke oAuth tokens */
if( $value )
{
\IPS\Db::i()->update( 'core_oauth_server_access_tokens', array( 'status' => 'revoked' ), array( 'member_id=?', $this->member_id ) );
}
}
/**
@@ -2011,7 +2075,14 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
return $this->_lang;
}
/* If in API use the lang set by dispatcher */
if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'api' )
{
$this->_lang = \IPS\Dispatcher::i()->_setLanguage();
return $this->_lang;
}
/* If in setup, create a "dummy" language */
if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'install' )
{
@@ -3418,7 +3489,6 @@ class _Member extends \IPS\Patterns\ActiveRecord
* @apiresponse string title Member title
* @clientapiresponse string timezone Member timezone
* @apiresponse string formattedName Username with group formatting
* @apiresponse string ipAddress IP address used during registration
* @apiresponse \IPS\Member\Group primaryGroup Primary group
* @clientapiresponse [\IPS\Member\Group] secondaryGroups Secondary groups
* @clientapiresponse string email Email address
@@ -3445,7 +3515,17 @@ class _Member extends \IPS\Patterns\ActiveRecord
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL, $otherFields = NULL )
{
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
try
{
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
}
catch( \OutOfRangeException $e )
{
\IPS\Log::log( "{$this->name} has an invalid group ({$this->_data['member_group_id']}) during API call. Resetting.", 'api_invalid_group' );
$group = \IPS\Member\Group::load( \IPS\Settings::i()->member_group ); // Intentially no catch here as that means things are really broken.
$this->member_group_id = \IPS\Settings::i()->member_group;
$this->save();
}
$secondaryGroups = array();
foreach ( array_filter( array_map( "intval", explode( ',', $this->_data['mgroup_others'] ) ) ) as $secondaryGroupId )
@@ -3471,7 +3551,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
foreach( $profileFields as $field )
{
if ( !$authorizedMember or
if ( !$authorizedMember or
$field['pf_contains_pii'] == 1 AND ( $authorizedMember->member_id == $this->member_id ) or
$field['pf_member_hide'] == 'all' or
( $field['pf_member_hide'] == 'owner' and ( $authorizedMember->member_id == $this->member_id OR $authorizedMember->modPermissions() OR $authorizedMember->isAdmin() ) ) or
( $field['pf_member_hide'] == 'staff' and ( $authorizedMember->modPermissions() OR $authorizedMember->isAdmin() ) )
@@ -3914,9 +3995,11 @@ class _Member extends \IPS\Patterns\ActiveRecord
* @param string $type Request type
* @param string $emailAddress Email address to check, NULL for existing email address
* @param int $spamCode If set, will modify by reference with the raw value from the spam service
* @param bool $disposable If set, will modify by reference if the user used a disposable email.
* @param bool $geoBlock If set, will modify by reference if the user is in a moderated / blocked country.
* @return int|NULL Action code based on spam service response, or NULL for no action
*/
public function spamService( $type='register', $emailAddress=NULL, &$spamCode=NULL )
public function spamService( $type='register', $emailAddress=NULL, &$spamCode=NULL, &$disposable=FALSE, &$geoBlock=FALSE )
{
$email = $emailAddress ?: $this->email;
@@ -4004,10 +4087,156 @@ class _Member extends \IPS\Patterns\ActiveRecord
break;
}
}
/* GeoLocation */
if ( $type === 'register' AND $action == 1 )
{
$action = $this->geoSpamCheck( $geoBlock );
}
/* If the normal spam service and geolocation didn't pick up anything, check for a disposable email. */
if ( $type === 'register' AND $action == 1 )
{
$action = $this->disposableEmailCheck( $emailAddress, $disposable );
}
return $action;
}
/**
* GeoLocation Block Check
*
* @param bool $geoBlock If set, will modify by reference if the user is in a moderated / blocked country.
* @return int|NULL
*/
public function geoSpamCheck( &$geoBlock ): ?int
{
/* If Geo is enabled, don't bother. */
if ( !\IPS\Settings::i()->ipsgeoip )
{
return 1;
}
/* See we have anything configured. If we don't, don't bother. */
$settings = \IPS\Settings::i()->spam_geo_settings ? json_decode( \IPS\Settings::i()->spam_geo_settings, true ) : array();
if ( !\count( $settings ) )
{
return 1;
}
try
{
$location = \IPS\GeoLocation::getRequesterLocation();
}
catch( \BadMethodCallException | \IPS\Http\Request\Exception | \RuntimeException | \OutOfRangeException $e )
{
/* If it fails for any reason, don't bother. */
\IPS\Log::debug( $e, 'spam-service' );
return 1;
}
/* All good, start checking. */
if ( !isset( $settings[ $location->country ] ) )
{
/* Not in the list, we're good. */
return 1;
}
/* See what action we're taking. */
switch( $settings[$location->country] )
{
case 'moderate':
\IPS\Settings::i()->reg_auth_type = 'admin';
$geoBlock = TRUE;
return 2;
case 'block':
return 4;
}
}
/**
* Check for disposable email address domain
*
* @param string $emailAddress Email to check, or NULL for current.
* @param bool $disposable If set, will modify by reference if the user used a disposable email.
* @return int|NULL
*/
public function disposableEmailCheck( $emailAddress, &$disposable=FALSE ): ?int
{
$email = $emailAddress ?: $this->email;
try
{
$response = \IPS\Http\Url::ips( 'spam/disposable' )->request()->login( \IPS\Settings::i()->ipb_reg_number, '' )->post( array(
'email' => $email,
) );
if ( $response->httpResponseCode != 200 )
{
throw new \DomainException( print_r( $response, TRUE ) );
}
$result = (bool) $response->decodeJson()['result'];
}
catch ( \Exception $e )
{
\IPS\Log::debug( $e, 'spam-service' );
$result = false;
}
$action = NULL;
if ( $result )
{
$disposable = TRUE;
$action = \IPS\Settings::i()->spam_service_disposable;
/* Perform Action */
switch( $action )
{
/* Proceed with registration */
case 1:
break;
/* Flag for admin approval */
case 2:
\IPS\Settings::i()->reg_auth_type = 'admin';
break;
/* Approve the account, but ban it */
case 3:
$this->temp_ban = -1;
$this->members_bitoptions['bw_is_spammer'] = TRUE;
break;
/* Deny registration - we return the code and the controller is expected to show an error */
case 4:
break;
/* Moderate posts */
case 5:
$days = json_decode( \IPS\Settings::i()->spam_service_days, TRUE );
if ( isset( $days['disposable'] ) )
{
if ( $days['disposable'] == -1 )
{
$this->mod_posts = -1;
}
else if ( $days['disposable'] > 0 )
{
$this->mod_posts = \IPS\DateTime::ts( time() )->add( new \DateInterval( "P{$days['disposable']}D" ) )->getTimestamp();
}
}
break;
}
}
return $action;
}
/**
* Member Sync
*
@@ -4597,6 +4826,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
/* Reset the flag */
$this->members_bitoptions['validating'] = FALSE;
$this->save();
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
/* Sync */
$this->memberSync( 'onValidate' );
@@ -5931,7 +6162,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
foreach( $members as $data )
{
list( $type, $memberId ) = explode( '-', $data );
[ $type, $memberId ] = explode( '-', $data );
$insertId = \IPS\Db::i()->insert( 'core_achievements_log_milestones', [
'milestone_member_id' => $memberId,
'milestone_rule' => $ruleId,
@@ -6190,4 +6421,95 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
return NULL;
}
/**
* Get the member's personal information
*
* @return \IPS\Xml\SimpleXML
*/
public function getPiiData(): \IPS\Xml\SimpleXML
{
/* Init */
$xml = \IPS\Xml\SimpleXML::create('member_export');
$xml->addAttribute( 'created', \IPS\DateTime::ts( time() )->rfc3339() );
/* Get the data */
foreach( \IPS\Application::allExtensions( 'core', 'MemberExportPersonalInformation', TRUE, 'core' ) AS $key => $ext )
{
if ( $data = $ext->getData( $this ) and \is_array( $data ) and \count( $data ) )
{
$child = $xml->addChild( $key );
foreach( $data as $k => $v )
{
$child->addChild( $k, $v );
}
}
}
return $xml;
}
/**
* Is there a deletion request pending?
*
* @return bool
*/
public function get_isDeletionPending(): bool
{
$where = [];
$where[] = ['member_id=?', $this->member_id];
$where[] = [ \IPS\Db::i()->in( 'action',[\IPS\Member\PrivacyAction::TYPE_REQUEST_DELETE, \IPS\Member\PrivacyAction::TYPE_REQUEST_DELETE_VALIDATION ] ) ];
return !\IPS\Db::i()->select( 'count(*)', \IPS\Member\PrivacyAction::$databaseTable, $where )->first() == 0;
}
/**
* Delete all logged IP addresses which were logged after a certain time
*
* @param int $time
* @return void
*/
public static function pruneAllLoggedIpAddresses(int $time )
{
foreach ( \IPS\Content::routedClasses( FALSE, TRUE ) as $class )
{
try
{
$class::pruneIpAddresses( \IPS\Settings::i()->ip_address_prune );
}
catch( \Exception $ex ) { }
}
/* PMs and Ratings are treated extra */
\IPS\Db::i()->update( 'core_message_posts', array( 'msg_ip_address' => '' ), array( "msg_ip_address != '' AND msg_date <= " . $time ) );
\IPS\Db::i()->update( 'core_ratings', array( 'ip' => '' ), array( "ip != '' AND rating_date IS NOT NULL AND rating_date <= " . $time ) );
foreach ( \IPS\Application::allExtensions( 'core', 'IpAddresses', FALSE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'pruneIpAddress'))
{
$extension->pruneIpAddress( $time );
}
}
}
/**
* Can this member request that his account gets deleted via the frontend
*
* @return bool
*/
public function canUseAccountDeletion(): bool
{
if( \IPS\Settings::i()->right_to_be_forgotten_type != 'on' )
{
return FALSE;
}
if( \IPS\Member::loggedIn()->isAdmin() )
{
return FALSE;
}
return TRUE;
}
}