Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
1 parent 517a5e1f70
commit 8ba7d43898
1933 files changed
+65613 -11332

No files matched your search

+219 -38
View File
@@ -115,7 +115,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
'link_pref_unread' => 524288, // Take me to comments I haven't read
'link_pref_last' => 1048576, // Take me to the latest comment
'mobile_notifications' => 2097152, // Has the user has enabled push notifications on at least 1 mobile device?
'link_pref_first' => 4194304 // Take me to the first comment
'link_pref_first' => 4194304, // Take me to the first comment
'datalayer_event_fired' => 8388608,
)
)
);
@@ -289,7 +290,6 @@ class _Member extends \IPS\Patterns\ActiveRecord
$this->members_bitoptions['show_pm_popup'] = ( \IPS\Settings::i()->notification_prefs_popup ) ? TRUE : FALSE;
$this->members_bitoptions['email_notifications_once'] = ( \IPS\Settings::i()->notification_prefs_one_per_view ) ? TRUE : FALSE;
$this->members_bitoptions['disable_notification_sounds'] = ( \IPS\Settings::i()->notification_prefs_sound ) ? FALSE : TRUE;
$this->_data['auto_track'] = json_encode( array(
'content' => \IPS\Settings::i()->auto_follow_new_content ? 1 : 0,
@@ -342,7 +342,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
\IPS\core\AdminNotification::remove( 'core', 'NewRegValidate' );
}
if ( $latestReg = \IPS\Db::i()->select( 'MAX(joined)', 'core_members', array( 'email<>?', '' ) )->first() )
if ( $latestReg = \IPS\Db::i()->select( 'joined', 'core_members', array( 'email != ?', '' ), 'joined DESC', 1 )->first() )
{
\IPS\core\AdminNotification::remove( 'core', 'NewRegComplete', NULL, \IPS\DateTime::ts( $latestReg ) );
}
@@ -413,7 +413,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
else
{
/* Run member sync, but not if the only change is the last_activity timestamp */
if( \count( $this->changedCustomFields ) > 0 or \count( $changes ) > 1 OR !isset( $changes['last_activity'] ) )
if( \count( $this->changedCustomFields ) > 0 OR ( \count( $changes ) > 0 AND !( \count( $changes ) === 1 AND isset( $changes['last_activity'] ) ) ) )
{
$this->memberSync( 'onProfileUpdate', array( 'changes' => array_merge( $changes, $this->changedCustomFields ) ) );
}
@@ -513,7 +513,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
return \IPS\Member::loggedIn()->language()->addToStack('guest');
}
return $this->member_id ? $this->_data['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'guest_name_shown', NULL, array( 'sprintf' => array( $this->_data['name'] ) ) );
return $this->member_id ? $this->_data['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'guest_name_shown', FALSE, array( 'sprintf' => array( $this->_data['name'] ) ) );
}
/**
@@ -1664,12 +1664,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
*/
public function get_timezone()
{
if ( \in_array( $this->_data['timezone'], [ 'America/Buenos_Aires', 'America/Catamarca', 'America/Cordoba', 'America/Jujuy', 'America/La_Rioja', 'America/Mendoza', 'America/Rio_Gallegos', 'America/Salta', 'America/San_Juan', 'America/San_Luis', 'America/Tucuman', 'America/Ushuaia' ] ) ) // https://bugs.webkit.org/show_bug.cgi?id=218542
{
return 'America/Argentina/' . \substr( $this->_data['timezone'], 8 );
}
return $this->_data['timezone'];
return \IPS\DateTime::getFixedTimezone( $this->_data['timezone'] );
}
/* !Photos */
@@ -1974,6 +1969,13 @@ class _Member extends \IPS\Patterns\ActiveRecord
return FALSE;
}
/* If the group is forced to be anonymous */
if( $this->group['g_hide_online_list'] == 1 )
{
return TRUE;
}
/* Anonymous status is not allowed */
if( $this->group['g_hide_online_list'] == 2 )
{
return FALSE;
@@ -2193,9 +2195,10 @@ class _Member extends \IPS\Patterns\ActiveRecord
* Profile Fields
*
* @param int $location \IPS\core\ProfileFields\Field::PROFILE for profile, \IPS\core\ProfileFields\Field::REG for registration screen, \IPS\core\ProfileFields\Field::STAFF for ModCP/ACP, \IPS\core\ProfileFields\Field::EDIT for member editing
* @param bool $raw Returns the raw value if true or the display value if false. Useful for comparisons for field types like Yes/NO to see if a value is set.
* @return array
*/
public function profileFields( $location = 0 )
public function profileFields( $location = 0, $raw = FALSE )
{
if ( !$this->member_id )
{
@@ -2218,7 +2221,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
if( !empty( $values ) )
{
foreach ( \IPS\core\ProfileFields\Field::values( $values, $location ) as $group => $fields )
foreach ( \IPS\core\ProfileFields\Field::values( $values, $location, $raw ) as $group => $fields )
{
$this->rawProfileFieldsData[ 'core_pfieldgroups_' . $group ] = $fields;
}
@@ -2784,7 +2787,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
*/
public function passwordResetForced( ?\IPS\Http\Url $ref = NULL ): ?\IPS\Http\Url
{
if ( $this->members_bitoptions['password_reset_forced'] AND $this->members_pass_hash )
if ( $this->members_bitoptions['password_reset_forced'] AND !$this->members_pass_hash )
{
return \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&area=password', 'front', 'settings_password' )->setQueryString( 'ref', base64_encode( $ref ) );
}
@@ -2797,6 +2800,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
* @note This property is public so it can be populated en-masse in areas that may benefit from doing so
*/
public $notificationsConfiguration = NULL;
static protected $extCache = [];
/**
* Notifications Configuration
@@ -2808,6 +2813,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
if( $this->notificationsConfiguration === NULL )
{
$this->notificationsConfiguration = array();
$defaultNotifications = [];
foreach (
\IPS\Db::i()->select(
@@ -2821,13 +2827,52 @@ class _Member extends \IPS\Patterns\ActiveRecord
) {
if ( $row['preference'] === NULL or !$row['editable'] )
{
$this->notificationsConfiguration[ $row['notification_key'] ] = explode( ',', $row['default'] );
$defaultNotifications[ $row['notification_key'] ] = explode( ',', $row['default'] );
}
else
{
$this->notificationsConfiguration[ $row['notification_key'] ] = array_diff( explode( ',', $row['preference'] ), explode( ',', $row['disabled'] ) );
}
}
/* Only cycle through if this member has any non-default notifications */
if( \count( $this->notificationsConfiguration ) )
{
/* Cache extension references */
if ( !\count( static::$extCache ) )
{
static::$extCache = \IPS\Application::allExtensions( 'core', 'Notifications' );
}
foreach ( $defaultNotifications as $k => $v )
{
foreach ( static::$extCache as $extension )
{
if ( method_exists( $extension, 'configurationOptions' ) )
{
foreach ( $extension->configurationOptions( null ) as $optionKey => $option )
{
if ( $option['type'] == 'standard' and \in_array( $k, $option['notificationTypes'] ) and \count( $option['notificationTypes'] ) > 1 )
{
if ( !empty( $this->notificationsConfiguration[ $option['notificationTypes'][0] ] ) )
{
$defaultNotifications[ $k ] = $this->notificationsConfiguration[ $option['notificationTypes'][0] ];
\IPS\Db::i()->insert( 'core_notification_preferences', [
'member_id' => $this->member_id,
'notification_key' => $k,
'preference' => implode( ',', $this->notificationsConfiguration[ $option['notificationTypes'][0] ] )
] );
}
continue 2;
}
}
}
}
}
}
$this->notificationsConfiguration = array_merge( $this->notificationsConfiguration, $defaultNotifications );
}
return $this->notificationsConfiguration;
@@ -3027,7 +3072,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
return FALSE;
}
if ( ! \in_array( $this->member_id, static::$modBadge ) )
if ( ! array_key_exists( $this->member_id, static::$modBadge ) )
{
static::$modBadge[ $this->member_id ] = false;
@@ -3445,6 +3490,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
* @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
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL, $otherFields = NULL )
{
@@ -3545,6 +3591,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
$return['rank'] = $this->rank() ? $this->rank()->apiOutput( $authorizedMember ) : NULL;
$return['achievements_points'] = $this->achievements_points;
$return['allowAdminEmails'] = (bool) $this->allow_admin_emails;
return $return;
}
@@ -3941,7 +3988,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'spam-service' );
\IPS\Log::debug( $e, 'spam-service' );
$spamCode = 0;
}
@@ -4332,6 +4379,42 @@ class _Member extends \IPS\Patterns\ActiveRecord
}
return TRUE;
}
/**
* Get a unique member hash for use in RSS keys and such.
*
* If the member has a current style password hash, the md5() of that value will be returned. Otherwise,
* one will be generated, stored in a new column in the members table, and its md5() will be returned.
*
* @return string
*/
public function getUniqueMemberHash()
{
/* If this is a guest, just return a random string. */
if ( !$this->member_id )
{
return \IPS\Login::generateRandomString();
}
/* Return the password hash if we already have it */
if ( !empty( $this->members_pass_hash ) )
{
return md5( ( $this->members_pass_hash ?: $this->email ) . $this->members_pass_salt );
}
/* Do we already have one? */
if ( !empty( $this->unique_hash ) )
{
return $this->unique_hash;
}
/* Otherwise, generate one */
$this->unique_hash = md5( \IPS\Login::generateRandomString(32) );
$this->save();
return $this->unique_hash;
}
/**
* Perform a database update on all members
@@ -5135,11 +5218,11 @@ class _Member extends \IPS\Patterns\ActiveRecord
if ( $item::$firstCommentRequired and isset( $commentClass::$databaseColumnMap['author'] ) )
{
$filters[ $commentClass ] = \IPS\Member::loggedIn()->language()->addToStack('leaderboard_tab_x', NULL, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack("{$commentClass::$title}_pl_lc") ) ) );
$filters[ $commentClass ] = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_tab_x', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack("{$commentClass::$title}_pl_lc") ) ) );
}
elseif ( isset( $item::$databaseColumnMap['author'] ) )
{
$filters[ $item ] = \IPS\Member::loggedIn()->language()->addToStack('leaderboard_tab_x', NULL, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack("{$item::$title}_pl_lc") ) ) );
$filters[ $item ] = \IPS\Member::loggedIn()->language()->addToStack( 'leaderboard_tab_x', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack("{$item::$title}_pl_lc") ) ) );
}
}
}
@@ -5566,7 +5649,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
try
{
return \IPS\DateTime::ts( \IPS\Db::i()->select( 'datetime', 'core_points_log', [ 'member=? AND new_rank=?', $this->member_id, $rank->id ], 'datetime DESC', 1 )->first() );
return \IPS\DateTime::ts( \IPS\Db::i()->select( 'datetime', 'core_points_log', [ '`member`=? AND new_rank=?', $this->member_id, $rank->id ], 'datetime DESC', 1 )->first() );
}
catch ( \UnderflowException $e ) { }
}
@@ -5602,7 +5685,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
if ( ! $this->rankHistory )
{
$return = [ 'earned' => NULL, 'not_earned' => NULL ];
$query = iterator_to_array( \IPS\Db::i()->select( 'new_rank, datetime', 'core_points_log', ['member=? and new_rank IS NOT NULL', $this->member_id] )->setKeyField('new_rank')->setValueField('datetime') );
$query = iterator_to_array( \IPS\Db::i()->select( 'new_rank, datetime', 'core_points_log', ['`member`=? and new_rank IS NOT NULL', $this->member_id] )->setKeyField('new_rank')->setValueField('datetime') );
$currentRank = $this->rank();
foreach ( \IPS\core\Achievements\Rank::getStore() as $rank )
@@ -5627,6 +5710,30 @@ class _Member extends \IPS\Patterns\ActiveRecord
return $this->rankHistory;
}
/**
* Return the number of days since the last rank upgrade
*
* @return int
*/
public function daysSinceLastRank(): int
{
$history = $this->rankHistory();
$lastRank = NULL;
foreach( $history['earned'] as $earned )
{
if ( $earned['rank']->id == $this->rank()->id )
{
break;
}
$lastRank = $earned;
}
$time = ( $lastRank and $lastRank['time'] ) ? $lastRank['time'] : $this->joined;
return \intval( \IPS\DateTime::ts( time() )->diff( $time )->days );
}
/**
* @brief List of badge IDs that this member has earned
*/
@@ -5641,7 +5748,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
if ( $this->_badgeIds === NULL )
{
$this->_badgeIds = iterator_to_array( \IPS\Db::i()->select( 'badge', 'core_member_badges', [ 'member=?', $this->member_id ] ) );
$this->_badgeIds = iterator_to_array( \IPS\Db::i()->select( 'badge', 'core_member_badges', [ '`member`=?', $this->member_id ] ) );
}
return $this->_badgeIds;
}
@@ -5660,7 +5767,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
if ( $this->_badgeCount === NULL )
{
$this->_badgeCount = \IPS\Db::i()->select( 'count(*)', 'core_member_badges', [ 'member=?', $this->member_id ] )->first();
$this->_badgeCount = \IPS\Db::i()->select( 'count(*)', 'core_member_badges', [ '`member`=?', $this->member_id ] )->first();
}
return $this->_badgeCount;
}
@@ -5674,7 +5781,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
public function recentBadges( $numberToGet ): \IPS\Patterns\ActiveRecordIterator
{
return new \IPS\Patterns\ActiveRecordIterator(
\IPS\Db::i()->select( 'core_badges.*, core_member_badges.member, core_member_badges.badge, core_member_badges.datetime, core_member_badges.rule, core_member_badges.action_log, core_member_badges.actor, core_member_badges.recognize', 'core_member_badges', [ 'member=?', $this->member_id ], 'datetime DESC', $numberToGet )
\IPS\Db::i()->select( 'core_badges.*, core_member_badges.member, core_member_badges.badge, core_member_badges.datetime, core_member_badges.rule, core_member_badges.action_log, core_member_badges.actor, core_member_badges.recognize', 'core_member_badges', [ '`member`=?', $this->member_id ], 'datetime DESC', $numberToGet )
->join( 'core_badges', 'core_member_badges.badge=core_badges.id' ),
'IPS\core\Achievements\Badge'
);
@@ -5697,9 +5804,14 @@ class _Member extends \IPS\Patterns\ActiveRecord
return FALSE;
}
if ( ! $this->member_id )
{
return FALSE;
}
if ( static::$rulesExcluded === NULL )
{
static::$rulesExcluded = json_decode( \IPS\Settings::i()->rules_exclude_groups, TRUE );;
static::$rulesExcluded = json_decode( \IPS\Settings::i()->rules_exclude_groups, TRUE );
}
if ( ! \is_array( static::$rulesExcluded ) or ! \count( static::$rulesExcluded ) )
@@ -5740,6 +5852,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
if ( $enabled === TRUE )
{
$extensionObject = \IPS\Application::load( $app )->extensions( 'core', 'AchievementAction' )[$extension];
/* Log it first, because doing so allows us to tell if we have already ran the rules
for this action (e.g. if someone unlikes and relikes a post, we don't want to keep
doing the actions for that every time: only once) */
@@ -5753,9 +5866,15 @@ class _Member extends \IPS\Patterns\ActiveRecord
return;
}
}
else
{
/* There are no enabled rules with this extension */
return;
}
/* Work out what we have to dish out */
$awards = [];
$milestoneHitWithRules = [];
foreach ( \IPS\core\Achievements\Rule::getStore()["{$app}_{$extension}"] as $ruleId => $ruleData )
{
if ( ! $ruleData['enabled'] )
@@ -5764,9 +5883,20 @@ class _Member extends \IPS\Patterns\ActiveRecord
}
if ( !$ruleData['filters'] or $extensionObject->filtersMatch( $this, $ruleData['filters'], $extra ) )
{
{
/* Did we already award for this milestone? */
if ( \IPS\core\Achievements\Rule::ruleHasMilestone( $ruleData['filters'] ) )
{
$milestoneHitWithRules[ $ruleId ] = [];
}
if ( $ruleData['points_subject'] or $ruleData['badge_subject'] )
{
if ( array_key_exists( $ruleId, $milestoneHitWithRules ) )
{
$milestoneHitWithRules[ $ruleId ][ 'subject-' . $this->member_id ] = 'subject-' . $this->member_id;
}
if ( !isset( $awards[ $this->member_id ] ) )
{
$awards[ $this->member_id ] = [ 'points' => [], 'badges' => [], 'actor' => [] ];
@@ -5782,10 +5912,10 @@ class _Member extends \IPS\Patterns\ActiveRecord
$awards[ $this->member_id ]['badges'][ $ruleId ] = $ruleData['badge_subject'];
}
}
if ( $ruleData['points_other'] or $ruleData['badge_other'] )
{
foreach ( $extensionObject->awardOther( $extra ) as $member )
foreach ( $extensionObject->awardOther( $extra, $ruleData['filters'] ) as $member )
{
/* If the "other" member is also the subject member (e.g. member is replying to their own topic), we
only give them the "subject" points */
@@ -5793,7 +5923,12 @@ class _Member extends \IPS\Patterns\ActiveRecord
{
continue;
}
if ( array_key_exists( $ruleId, $milestoneHitWithRules ) )
{
$milestoneHitWithRules[ $ruleId ][ 'other-' . $member->member_id ] = 'other-' . $member->member_id;
}
/* Otherwise carry on as usual... */
if ( !isset( $awards[ $member->member_id ] ) )
{
@@ -5804,7 +5939,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
$awards[ $member->member_id ]['actor']['other'] = 'other';
$awards[ $member->member_id ]['points'][ $ruleId ] = $ruleData['points_other'];
}
if ( $ruleData['badge_subject'] and !\in_array( $ruleData['badge_subject'], $member->badgeIds() ) )
if ( $ruleData['badge_other'] and !\in_array( $ruleData['badge_other'], $member->badgeIds() ) )
{
$awards[ $member->member_id ]['actor']['other'] = 'other';
$awards[ $member->member_id ]['badges'][ $ruleId ] = $ruleData['badge_other'];
@@ -5813,25 +5948,61 @@ class _Member extends \IPS\Patterns\ActiveRecord
}
}
}
/* If we have stuff to dish out... */
if ( $awards )
{
$alreadyHitMilestone = [];
/* Insert these milestone maps */
foreach( $milestoneHitWithRules as $ruleId => $members )
{
foreach( $members as $data )
{
list( $type, $memberId ) = explode( '-', $data );
$insertId = \IPS\Db::i()->insert( 'core_achievements_log_milestones', [
'milestone_member_id' => $memberId,
'milestone_rule' => $ruleId,
'milestone_log_id' => $logId,
'milestone_type' => $type,
], FALSE, TRUE );
if ( ! $insertId )
{
$alreadyHitMilestone[ $memberId ][] = $ruleId;
}
}
}
foreach ( $awards as $memberId => $memberAwards )
{
if ( $memberAwards['points'] )
{
\IPS\Member::load( $memberId )->awardPoints( array_sum( $memberAwards['points'] ), $logId, array_keys( $memberAwards['points'] ), $memberAwards['actor'], 0, $date );
foreach( $memberAwards['points'] as $ruleId => $data )
{
if ( isset( $alreadyHitMilestone[ $memberId ] ) and \in_array( $ruleId, $alreadyHitMilestone[ $memberId ] ) )
{
unset( $memberAwards['points'][ $ruleId ] );
}
}
if ( \array_key_exists( 'points', $memberAwards ) and \count( $memberAwards['points'] ) )
{
\IPS\Member::load( $memberId )->awardPoints( array_sum( $memberAwards['points'] ), $logId, array_keys( $memberAwards['points'] ), $memberAwards['actor'], 0, $date );
}
}
if ( $memberAwards['badges'] )
{
foreach ( array_unique( $memberAwards['badges'] ) as $ruleId => $badgeId )
{
try
if ( ! isset( $alreadyHitMilestone[ $memberId ] ) or ! \in_array( $ruleId, $alreadyHitMilestone[ $memberId ] ) )
{
\IPS\Member::load( $memberId )->awardBadge( \IPS\core\Achievements\Badge::load( $badgeId ), $logId, $ruleId, $memberAwards['actor'], 0, $date );
try
{
\IPS\Member::load( $memberId )->awardBadge( \IPS\core\Achievements\Badge::load( $badgeId ), $logId, $ruleId, $memberAwards['actor'], 0, $date );
}
catch ( \OutOfRangeException $e ) { }
}
catch ( \OutOfRangeException $e ) { }
}
}
}
@@ -5886,10 +6057,12 @@ class _Member extends \IPS\Patterns\ActiveRecord
/* Did that push them over threshold for a new rank? Don't send a notification if rebuilds are in progress, and a random post made live triggers a new badge or award */
if ( $newRank and ! \IPS\Settings::i()->achievements_rebuilding )
{
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_rank', $this, [ $this ], [ $newRank->id ] );
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_rank', $this, [ $newRank, $this ], [ $newRank->id ] );
$notification->recipients->attach( $this );
$notification->send();
}
$this->save();
if ( isset( static::$_memberRecognizedPoints[ $this->member_id ] ) )
{
@@ -5932,10 +6105,18 @@ class _Member extends \IPS\Patterns\ActiveRecord
/* Don't send a notification if rebuilds are in progress, and a random post made live triggers a new badge or award */
if ( ! \IPS\Settings::i()->achievements_rebuilding )
{
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_badge', $this, [$badge], [$badge->id] );
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_badge', $this, [ $this, $badge ], [ $badge->id ] );
$notification->recipients->attach( $this );
$notification->send();
}
$data = [
'member' => $this->apiOutput(),
'badge' => $badge->apiOutput(),
];
\IPS\Api\Webhook::fire( 'member_badge_awarded', $data );
$this->save();
}
public static $_memberRecognizedPoints = array();