Version 5.0.13

This commit is contained in:
Neo committed 2025-12-19 19:04:57 -08:00
1 parent b89858b2ff
commit 5efe744c5f
1407 files changed
+69690 -30177

No files matched your search

+152 -28
View File
@@ -34,7 +34,6 @@ use IPS\core\ProfileFields\Api\Field;
use IPS\core\ProfileFields\Api\FieldGroup;
use IPS\core\Reports\Report;
use IPS\Data\Store;
use IPS\Db;
use IPS\Db\Select;
use IPS\Events\Event;
use IPS\Helpers\CoverPhoto;
@@ -57,7 +56,6 @@ use IPS\Member\UserMenu;
use IPS\Patterns\ActiveRecord;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Platform\Bridge;
use IPS\Settings;
use IPS\Text\Encrypt;
use IPS\Xml\SimpleXML;
use OutOfRangeException;
@@ -92,6 +90,7 @@ class Member extends ActiveRecord
{
Followable::followersCount as public _followersCountBase;
Followable::followers as public _followersBase;
Followable::follow as public _followBase;
}
/**
@@ -207,7 +206,7 @@ class Member extends 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
'email_messages_bounce' => 134217728, // @deprecated User's email keeps returning hard bounces so they need to change it
// 268435456 - cookie_optout moved to cookie preferences
'expert_user_disabled' => 536870912, // User does not want to be a community expert
'expert_user_blocked' => 1073741824 // Admin has blocked expert user
@@ -542,12 +541,6 @@ class Member extends ActiveRecord
}
}
/* 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 )
@@ -564,8 +557,8 @@ class Member extends ActiveRecord
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'] ) ) ) )
/* Run member sync, but not if the only change is the last_activity timestamp and if we're not just updating the member's language during language() init when no language is set */
if( ( Member::loggedIn()->member_id != $this->member_id or $this->_lang !== null ) and ( count( $this->changedCustomFields ) > 0 OR ( count( $changes ) > 0 AND !( count( $changes ) === 1 AND isset( $changes['last_activity'] ) ) ) ) )
{
Event::fire( 'onProfileUpdate', $this, array( 'changes' => array_merge( $changes, $this->changedCustomFields ) ) );
}
@@ -781,7 +774,7 @@ class Member extends ActiveRecord
/* Run sync */
$this->logHistory( 'core', 'account', array( 'type' => 'spammer', 'set' => TRUE, 'actions' => $actions ) );
Event::fire( 'onSetAsSpammer', $this );
Webhook::fire( 'member_flagged_as_spammer', $this );
/* Notify admin */
AdminNotification::send( 'core', 'Spammer', NULL, TRUE, $this->member_id, Member::loggedIn() );
@@ -865,6 +858,7 @@ class Member extends ActiveRecord
/* Run sync */
Event::fire( 'onUnSetAsSpammer', $this );
Webhook::fire( 'member_unflagged_as_spammer', $this );
}
}
@@ -908,6 +902,8 @@ class Member extends ActiveRecord
{
Db::i()->update( 'core_oauth_server_access_tokens', array( 'status' => 'revoked' ), array( 'member_id=?', $this->member_id ) );
}
Webhook::fire( 'member_ban_state_changed', [$this, $value] );
}
/**
@@ -1692,6 +1688,31 @@ class Member extends ActiveRecord
return static::_followers( 'member', $this->member_id ?: 0, $privacy, $frequencyTypes, $date, $limit, $order, FALSE, null );
}
/**
* Follow this object
*
* @param string $frequency ( 'none', 'immediate', 'daily', 'weekly' )
* @param bool $public
* @param Member|null $member
* @return void
*/
public function follow( string $frequency, bool $public=true, ?Member $member=null ) : void
{
$member = $member ?: Member::loggedIn();
$this->_followBase( $frequency, $public, $member );
if( $public )
{
$this->achievementAction( 'core', 'FollowMember', [
'giver' => $member
] );
$notification = new Notification( Application::load( 'core' ), 'member_follow', $member, array( $member ) );
$notification->recipients->attach( $this );
$notification->send();
}
}
/**
* Record a failed login attempt
*
@@ -1823,7 +1844,7 @@ class Member extends ActiveRecord
/**
* Get timezone
*
* @return string
* @return string|null
*/
public function get_timezone(): ?string
{
@@ -2043,6 +2064,12 @@ class Member extends ActiveRecord
return false;
}
/* Is the member in a group that is allowed to be an expert? This setting might have been changed. */
if( Settings::i()->ips_experts_allowed_groups !== '*' and ! $this->inGroup( explode( ',', Settings::i()->ips_experts_allowed_groups ) ) )
{
$this->isExpert = false;
}
if ( $this->isExpert === null )
{
try
@@ -2343,7 +2370,7 @@ class Member extends ActiveRecord
{
if( $this->_url === NULL )
{
$this->_url = Url::internal( "app=core&module=members&controller=profile&id={$this->member_id}", 'front', 'profile', $this->members_seo_name );
$this->_url = Url::internal( "app=core&module=members&controller=profile&id={$this->member_id}", 'front', 'profile', ( $this->members_seo_name ?? '-' ) );
}
return $this->_url;
@@ -3172,6 +3199,21 @@ class Member extends ActiveRecord
return $this->_following[ $_key ];
}
/**
* Clear the cached result used by the following method
*
* @param string $app Application key
* @param string $area Area
* @param int $id Item ID
* @return void
*/
public function clearFollowingCache( string $app, string $area, int $id ): void
{
$key = md5( $app . $area . $id );
unset( $this->_following[ $key ] );
}
/**
* Admin CP Restrictions
*
@@ -3778,12 +3820,18 @@ class Member extends ActiveRecord
* @clientapiresponse datetime|null lastVisit Last distinct visit date on the site.
* @clientapiresponse datetime|null lastPost Latest content submission date.
* @apiresponse int profileViews Number of times member's profile has been viewed
* @clientapiresponse bool spammer Is the member flagged as a spammer?
* @clientapiresponse bool banned Is the member currently banned?
* @apiresponse string birthday Member birthday in MM/DD/YYYY format (or MM/DD format if no year has been supplied).
* @apiresponse array login_methods Array of login methods used <int, array{name: string, id: int|string}>, key is login method ID.
* @apiresponse [\IPS\core\ProfileFields\Api\FieldGroup] customFields Custom profile fields. For requests using an OAuth Access Token for a particular member, only fields the authorized user can view will be included
* @apiresponse [\IPS\core\Achievements\Rank] rank Rank
* @apiresponse int achievements_points Points
* @apiresponse bool allowAdminEmails Whether or not this member wants to receive admin emails
* @clientapiresponse bool allowAdminEmails Whether or not this member wants to receive admin emails
* @apiresponse bool completed Whether or not the registration is completed
* @clientapiresponse int totalMessages Total number of Personal Messages
* @clientapiresponse int unreadMessages Total number of unread Personal Messages
* @apiresponse [IPS\core\Achievements\Badge] badges All badges received by this member
*/
public function apiOutput( Member $authorizedMember = NULL, array $otherFields = NULL ): array
{
@@ -3837,7 +3885,7 @@ class Member extends ActiveRecord
$fields[ $profileFieldGroup ] = ( new FieldGroup( $this->language()->get( 'core_pfieldgroups_' . $profileFieldGroup ), $groupValues ) )->apiOutput( $authorizedMember );
}
}
catch( UnderflowException $e ) { } # Guests will not have any profile field information.
catch( UnderflowException $e ) { } # Guests will not have any profile field information
$return = array();
$return['id'] = $this->member_id;
@@ -3881,8 +3929,29 @@ class Member extends ActiveRecord
if( !$authorizedMember OR $authorizedMember->member_id === $this->member_id )
{
$return['birthday'] = $this->bday_month ? ( $this->bday_month . '/' . $this->bday_day . ( $this->bday_year ? '/' . $this->bday_year : '' ) ) : NULL;
/* Login Links */
$return['login_methods'] = [];
foreach( Db::i()->select( '*', 'core_login_links', [ 'token_member=? and token_linked=?', $this->member_id, 1 ] ) as $link )
{
/* Only include enabled methods */
if( isset( Login::methods()[ $link['token_login_method'] ] ) )
{
$return['login_methods'][ $link['token_login_method'] ] = [
'name' => Login::methods()[ $link['token_login_method'] ]->_title,
'id' => $link['token_identifier']
];
}
}
}
$return['profileViews'] = $this->members_profile_views;
if( $authorizedMember === null or $authorizedMember->member_id == $this->member_id )
{
$return['spammer'] = (bool) $this->members_bitoptions['bw_is_spammer'];
$return['banned'] = (bool) $this->isBanned();
}
$return['customFields'] = $fields;
if ( !$authorizedMember )
@@ -3898,9 +3967,29 @@ class Member extends ActiveRecord
$return['rank'] = $this->rank() ? $this->rank()->apiOutput( $authorizedMember ) : NULL;
$return['achievements_points'] = $this->achievements_points;
$return['allowAdminEmails'] = $this->allow_admin_mails;
if( $authorizedMember === null or $authorizedMember->member_id == $this->member_id )
{
$return['allowAdminEmails'] = $this->allow_admin_mails;
}
$return['completed'] = $this->completed;
$counts = \IPS\Db::i()->select( 'COUNT(map_id) as total_count, COUNT(CASE WHEN map_has_unread = 1 THEN 1 END) as unread_messages', 'core_message_topic_user_map', [ 'map_user_id=?', $this->member_id ] )->first();
if( $authorizedMember === null or $authorizedMember->member_id == $this->member_id )
{
$return['totalMessages'] = $counts['total_count'];
$return['unreadMessages'] = $counts['unread_messages'];
}
/* Add member badges */
$return['badges'] = array();
foreach( $this->recentBadges( NULL ) as $badge )
{
$return['badges'][] = $badge->apiOutput();
}
return $return;
}
@@ -4695,7 +4784,7 @@ class Member extends ActiveRecord
catch ( OutOfRangeException $e )
{
/* Log the error as part of the failure audit trail */
\IPS\Log::log( "Group promotion found a member (#{$this->member_id}: {$this->name} [Group ID {$this->member_group_id}] with an invalid member group", 'group_promotion' );
Log::log( "Group promotion found a member (#{$this->member_id}: {$this->name} [Group ID {$this->member_group_id}] with an invalid member group", 'group_promotion' );
return;
}
@@ -5223,11 +5312,27 @@ class Member extends ActiveRecord
*/
protected function _sendWelcomeEmail() : void
{
try
if( Settings::i()->reg_welcome_email )
{
Email::buildFromTemplate( 'core', 'registration_complete', array( $this ), Email::TYPE_TRANSACTIONAL )->send( $this );
if( $this->language()->checkKeyExists( 'reg_welcome_email_message' ) )
{
$message = $this->language()->get( 'reg_welcome_email_message' );
}
else
{
$message = $this->language()->addToStack( 'email_reg_complete', false, [ 'sprintf' => [ Settings::i()->board_name ] ] );
if( $this->members_pass_hash )
{
$message .= $this->language()->addToStack( 'email_reg_complete_pass' );
}
}
try
{
Email::buildFromTemplate( 'core', 'registration_complete', array( $this, $message ), Email::TYPE_TRANSACTIONAL )->send( $this );
}
catch( ErrorException $e ) { }
}
catch( ErrorException $e ) { }
}
/**
@@ -5264,7 +5369,7 @@ class Member extends ActiveRecord
return FALSE;
}
}
/**
* Profile Sync
*
@@ -5274,7 +5379,7 @@ class Member extends ActiveRecord
{
$profileSync = $this->profilesync;
foreach (Login::methods() as $method )
foreach ( Login::methods() as $method )
{
if ( $method->canProcess( $this ) )
{
@@ -5285,6 +5390,16 @@ class Member extends ActiveRecord
$profileSync[ $type ] = array( 'handler' => $method->_id, 'ref' => NULL, 'error' => NULL );
}
}
/* Perform any extra profile sync the login handler may need */
try
{
if( method_exists( $method, 'extraProfileSync' ) )
{
$method->extraProfileSync( $this );
}
}
catch( Login\Exception $e ) {}
}
}
@@ -5571,7 +5686,7 @@ class Member extends ActiveRecord
if ( $this->member_id )
{
/* Set this only if this was not called by the task system, otherwise we'll set the member who triggered the task as moderator */
if ( Dispatcher::hasInstance() and !Dispatcher::i()->inDestructor and $by === NULL )
if ( Dispatcher::hasInstance() and isset( Dispatcher::i()->inDestructor ) and !Dispatcher::i()->inDestructor and $by === NULL )
{
$by = Session::i()->member; // Not \IPS\Member::loggedIn() because if this is an admin logged in as a member, we want to log that the action was done by the admin
}
@@ -6300,6 +6415,12 @@ class Member extends ActiveRecord
continue;
}
/* Cloudy stuff */
if( !Bridge::i()->checkAchievementRule( array_merge( [ 'action' => "{$app}_{$extension}" ], $ruleData ), $this ) )
{
continue;
}
if ( !$ruleData['filters'] or $extensionObject->filtersMatch( $this, $ruleData['filters'], $extra ) )
{
/* Did we already award for this milestone? */
@@ -6364,6 +6485,9 @@ class Member extends ActiveRecord
}
}
}
/* Fire the post-achievement here */
Bridge::i()->postAchievement( $ruleId, array_merge( [ 'action' => "{$app}_{$extension}" ], $ruleData ), $this, $extra );
}
}
@@ -6631,7 +6755,7 @@ class Member extends ActiveRecord
{
$lastLogin = Db::i()->select( 'member_timestamp', 'core_members_logins', [ 'member_id=?', $this->member_id ], 'member_timestamp DESC', 1 )->first();
}
catch( \UnderflowException $e )
catch( UnderflowException $e )
{
/* This is the first login, or they haven't logged in since upgrading from < 106100 */
try
@@ -6642,7 +6766,7 @@ class Member extends ActiveRecord
return false;
}
}
catch( \UnderflowException $e )
catch( UnderflowException $e )
{
/* This could be a new install, or IN_DEV where upgrade history isn't available */
return false;
@@ -6658,7 +6782,7 @@ class Member extends ActiveRecord
}
/* If this login is within the period, don't send an email */
if( $lastLoginObj > DateTime::create()->sub( new \DateInterval( static::LOGIN_INACTIVITY_NOTIFICATION ) ) )
if( $lastLoginObj > DateTime::create()->sub( new DateInterval( static::LOGIN_INACTIVITY_NOTIFICATION ) ) )
{
return false;
}
@@ -6669,7 +6793,7 @@ class Member extends ActiveRecord
{
$location = GeoLocation::getRequesterLocation();
}
catch ( \Exception $e )
catch ( Exception $e )
{
$location = NULL;
}