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

+256 -172
View File
@@ -539,202 +539,240 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
{
$form->add( new \IPS\Helpers\Form\YesNo( 'club_rules_reacknowledge', FALSE, FALSE, [], NULL, NULL, NULL, 'club_rules_reacknowledge' ) );
}
if ( $values = $form->values() )
return $form;
}
/**
* Process Club Form
*
* @param $values
* @param bool $acp TRUE if editing in the ACP
* @param bool $new TRUE if creating new
* @param array|null $availableTypes If creating new, the available types
* @return \IPS\Helpers\Form|NULL
*/
public function processForm($values, bool $acp, bool $new = FALSE, array $availableTypes = NULL): ?\IPS\Helpers\Form
{
$this->name = $values['club_name'];
/* If there is only one type available, set it. */
if( \is_array( $availableTypes ) AND \count( $availableTypes ) == 1 )
{
$this->name = $values['club_name'];
$values['club_type'] = key( $availableTypes );
}
/* If there is only one type available, set it. */
if( \is_array( $availableTypes ) AND \count( $availableTypes ) == 1 )
{
$values['club_type'] = key( $availableTypes );
}
$needToUpdatePermissions = FALSE;
if ( $acp )
{
if ( $this->type != $values['club_type'] )
{
$this->type = $values['club_type'];
$needToUpdatePermissions = TRUE;
}
if ( $this->owner != $values['club_owner'] )
{
$this->owner = $values['club_owner'];
$this->addMember( $values['club_owner'], \IPS\Member\Club::STATUS_LEADER, TRUE );
}
}
elseif ( $new )
$needToUpdatePermissions = FALSE;
if ( $acp )
{
if ( $this->type != $values['club_type'] )
{
$this->type = $values['club_type'];
$this->owner = \IPS\Member::loggedIn();
$needToUpdatePermissions = TRUE;
}
$this->about = $values['club_about'];
$this->profile_photo = (string) $values['club_profile_photo'];
if( $values['club_profile_photo'] )
if ( $this->owner != $values['club_owner'] )
{
$this->profile_photo_uncropped = (string) $values['club_profile_photo'];
$this->owner = $values['club_owner'];
$this->addMember( $values['club_owner'], Club::STATUS_LEADER, TRUE );
/* Update purchases for commission */
if( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on )
{
\IPS\Db::i()->update( 'nexus_purchases', array( 'ps_pay_to' => $this->owner ), array( "ps_pay_to IS NOT NULL and ps_app=? and ps_type=? and ps_item_id=?", 'core', 'club', $this->id ) );
}
}
if ( isset( $values['club_location'] ) )
}
elseif ( $new )
{
$this->type = $values['club_type'];
$this->owner = \IPS\Member::loggedIn();
}
$this->about = $values['club_about'];
$this->profile_photo = (string) $values['club_profile_photo'];
if( $values['club_profile_photo'] )
{
$this->profile_photo_uncropped = (string) $values['club_profile_photo'];
}
if ( isset( $values['club_location'] ) )
{
$this->location_json = json_encode( $values['club_location'] );
if ( $values['club_location']->lat and $values['club_location']->long )
{
$this->location_json = json_encode( $values['club_location'] );
if ( $values['club_location']->lat and $values['club_location']->long )
{
$this->location_lat = $values['club_location']->lat;
$this->location_long = $values['club_location']->long;
}
else
{
$this->location_lat = NULL;
$this->location_long = NULL;
}
$this->location_lat = $values['club_location']->lat;
$this->location_long = $values['club_location']->long;
}
else
{
$this->location_json = NULL;
$this->location_lat = NULL;
$this->location_long = NULL;
}
if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and \IPS\Member::loggedIn()->group['gbw_paid_clubs'] )
{
switch ( $values['club_membership_fee'] )
{
case 'free':
$this->fee = NULL;
$this->renewal_term = 0;
$this->renewal_units = NULL;
$this->renewal_price = NULL;
break;
case 'paid':
$this->fee = json_encode( $values['club_fee'] );
if ( $values['club_renewals'] and $values['club_renewal_term'] )
{
$term = $values['club_renewal_term']->getTerm();
$this->renewal_term = $term['term'];
$this->renewal_units = $term['unit'];
$this->renewal_price = json_encode( $values['club_renewal_term']->cost );
}
else
{
$this->renewal_term = 0;
$this->renewal_units = NULL;
$this->renewal_price = NULL;
}
break;
}
}
}
else
{
$this->location_json = NULL;
$this->location_lat = NULL;
$this->location_long = NULL;
}
if ( array_key_exists( 'club_show_membertab', $values ) )
if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and \IPS\Member::loggedIn()->group['gbw_paid_clubs'] )
{
switch ( $values['club_membership_fee'] )
{
$this->show_membertab = $values['club_show_membertab'];
}
else
{
/* Default is "Everybody" */
$this->show_membertab = "nonmember";
}
case 'free':
$this->fee = NULL;
$this->renewal_term = 0;
$this->renewal_units = NULL;
$this->renewal_price = NULL;
break;
$this->save();
if ( $values['show_rules'] )
{
$this->rules = $values['club_rules'];
\IPS\File::claimAttachments( ( $new ) ? "club-rules-new" : "club-rules-{$this->id}", $this->id, NULL, 'rules' );
$this->rules_required = (bool) $values['club_rules_required'];
/* Do we need to reset the acknowledge flags? */
if ( !$new AND \array_key_exists( 'club_rules_reacknowledge', $values ) )
{
if ( $values['club_rules_reacknowledge'] )
case 'paid':
$this->fee = json_encode( $values['club_fee'] );
if ( $values['club_renewals'] and $values['club_renewal_term'] )
{
\IPS\Db::i()->update( 'core_clubs_memberships', array( "rules_acknowledged" => FALSE ), array( "club_id=?", $this->id ) );
}
}
}
else
{
$this->rules = NULL;
$this->rules_required = FALSE;
if ( !$new )
{
/* If this isn't a new club, update all memberships in case they decide to re-add rules later on */
\IPS\Db::i()->update( "core_clubs_memberships", array( 'rules_acknowledged' => FALSE ), array( "club_id=?", $this->id ) );
}
}
if ( $new )
{
$this->addMember( \IPS\Member::loggedIn(), \IPS\Member\Club::STATUS_LEADER );
$this->acknowledgeRules( \IPS\Member::loggedIn() );
}
$this->recountMembers();
$customFieldValues = array();
foreach ( \IPS\Member\Club\CustomField::roots() as $field )
{
if ( isset( $values["core_clubfield_{$field->id}"] ) )
{
$helper = $field->buildHelper();
if ( $helper instanceof \IPS\Helpers\Form\Upload )
{
$customFieldValues[ "field_{$field->id}" ] = (string) $values["core_clubfield_{$field->id}"];
$term = $values['club_renewal_term']->getTerm();
$this->renewal_term = $term['term'];
$this->renewal_units = $term['unit'];
$this->renewal_price = json_encode( $values['club_renewal_term']->cost );
}
else
{
$customFieldValues[ "field_{$field->id}" ] = $helper::stringValue( $values["core_clubfield_{$field->id}"] );
$this->renewal_term = 0;
$this->renewal_units = NULL;
$this->renewal_price = NULL;
}
if ( $field->type === 'Editor' )
{
$field->claimAttachments( $this->id );
}
}
break;
}
if ( \count( $customFieldValues ) )
{
$customFieldValues['club_id'] = $this->id;
\IPS\Db::i()->insert( 'core_clubs_fieldvalues', $customFieldValues, TRUE );
}
if ( $needToUpdatePermissions )
{
foreach ( $this->nodes() as $node )
{
try
{
$nodeClass = $node['node_class'];
$node = $nodeClass::load( $node['node_id'] );
$node->setPermissionsToClub( $this );
}
catch ( \Exception $e ) { }
}
}
if( $new and \IPS\Settings::i()->clubs_require_approval and !$this->approved )
{
$this->sendModeratorApprovalNotification();
}
if( $new and $this->approved )
{
\IPS\Api\Webhook::fire( 'club_created', $this );
}
else if( !$new AND isset( $this->changed['approved'] ) )
{
$this->onApprove();
}
return NULL;
}
return $form;
if ( array_key_exists( 'club_show_membertab', $values ) )
{
$this->show_membertab = $values['club_show_membertab'];
}
else
{
/* Default is "Everybody" */
$this->show_membertab = "nonmember";
}
$this->save();
if ( $values['show_rules'] )
{
$this->rules = $values['club_rules'];
\IPS\File::claimAttachments( ( $new ) ? "club-rules-new" : "club-rules-{$this->id}", $this->id, NULL, 'rules' );
$this->rules_required = (bool) $values['club_rules_required'];
/* Do we need to reset the acknowledge flags? */
if ( !$new AND \array_key_exists( 'club_rules_reacknowledge', $values ) )
{
if ( $values['club_rules_reacknowledge'] )
{
\IPS\Db::i()->update( 'core_clubs_memberships', array( "rules_acknowledged" => FALSE ), array( "club_id=?", $this->id ) );
}
}
}
else
{
$this->rules = NULL;
$this->rules_required = FALSE;
if ( !$new )
{
/* If this isn't a new club, update all memberships in case they decide to re-add rules later on */
\IPS\Db::i()->update( "core_clubs_memberships", array( 'rules_acknowledged' => FALSE ), array( "club_id=?", $this->id ) );
}
}
if ( $new )
{
$this->addMember( \IPS\Member::loggedIn(), Club::STATUS_LEADER );
$this->acknowledgeRules( \IPS\Member::loggedIn() );
}
$this->recountMembers();
$customFieldValues = array();
foreach ( \IPS\Member\Club\CustomField::roots() as $field )
{
if ( isset( $values["core_clubfield_{$field->id}"] ) )
{
$helper = $field->buildHelper();
if ( $helper instanceof \IPS\Helpers\Form\Upload )
{
$customFieldValues[ "field_{$field->id}" ] = (string) $values["core_clubfield_{$field->id}"];
}
else
{
$customFieldValues[ "field_{$field->id}" ] = $helper::stringValue( $values["core_clubfield_{$field->id}"] );
}
if ( $field->type === 'Editor' )
{
$field->claimAttachments( $this->id );
}
}
}
if ( \count( $customFieldValues ) )
{
$customFieldValues['club_id'] = $this->id;
\IPS\Db::i()->insert( 'core_clubs_fieldvalues', $customFieldValues, TRUE );
}
if ( $needToUpdatePermissions )
{
foreach ( $this->nodes() as $node )
{
try
{
$nodeClass = $node['node_class'];
$node = $nodeClass::load( $node['node_id'] );
$node->setPermissionsToClub( $this );
}
catch ( \Exception $e ) { }
}
}
if( $new and \IPS\Settings::i()->clubs_require_approval and !$this->approved )
{
$this->sendModeratorApprovalNotification();
}
if( $new and $this->approved )
{
\IPS\Api\Webhook::fire( 'club_created', $this );
}
else if( !$new AND isset( $this->changed['approved'] ) )
{
$this->onApprove();
}
return NULL;
}
/**
* Redirect after save
*
* @param \IPS\Member\Club $old A clone of the club as it was before
* @param \IPS\Member\Club\ $new The club now
* @return array
*/
public static function renewalChanges(Club $old, Club $new)
{
$changes = array();
foreach ( array( 'renewal_term', 'renewal_units', 'renewal_price' ) as $k )
{
if ( $old->$k != $new->$k )
{
$changes[ $k ] = $old->$k;
}
}
return $changes;
}
/**
@@ -2191,4 +2229,50 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
\IPS\Api\Webhook::fire( 'club_created', $this );
$this->owner->achievementAction( 'core', 'NewClub', $this );
}
/**
* Update existing purchases
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param array $changes The old values
* @param bool $cancelBillingAgreementIfNecessary If making changes to renewal terms, TRUE will cancel associated billing agreements. FALSE will skip that change
* @return void
*/
public function updatePurchase( \IPS\nexus\Purchase $purchase, $changes=array(), $cancelBillingAgreementIfNecessary=FALSE )
{
$tax = NULL;
if ( $purchase->tax )
{
try
{
$tax = \IPS\nexus\Tax::load( $purchase->tax );
}
catch ( \OutOfRangeException $e ) { }
}
$currency = $purchase->renewal_currency ?: $purchase->member->defaultCurrency( );
$price = json_decode( $this->renewal_price, TRUE );
$purchase->renewals = new \IPS\nexus\Purchase\RenewalTerm(
new \IPS\nexus\Money( $price[$currency]['amount'], $currency ),
new \DateInterval( 'P' . $this->renewal_term . mb_strtoupper( $this->renewal_units ) ),
$tax
);
if ( $cancelBillingAgreementIfNecessary and $billingAgreement = $purchase->billing_agreement )
{
if ( array_key_exists( 'renewal_price', $changes ) and !empty( $changes['renewal_price'] ) )
{
try
{
$billingAgreement->cancel();
$billingAgreement->save();
}
catch ( \Exception $e ) { }
}
}
$purchase->save();
}
}
+1 -1
View File
@@ -262,7 +262,7 @@ class _Device extends \IPS\Patterns\ActiveRecord
{
try
{
$location = \IPS\GeoLocation::getByIp( \IPS\Request::i()->ipAddress() );
$location = \IPS\GeoLocation::getRequesterLocation();
}
catch ( \Exception $e )
{
+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;
}
}
+304
View File
@@ -0,0 +1,304 @@
<?php
/**
* @brief Privacy Action
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 07 March 2023
*/
namespace IPS\Member;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* PrivacyAction Model
*/
class _PrivacyAction extends \IPS\Patterns\ActiveRecord
{
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_member_privacy_actions';
/**
* @brief [ActiveRecord] Multiton Map
*/
protected static $multitonMap = [];
/**
* @brief PII request
*/
const TYPE_REQUEST_PII = 'pii_download';
/**
* @brief Account deletion request
*/
const TYPE_REQUEST_DELETE = 'delete_account';
/**
* @brief Account deletion requires validation
*/
const TYPE_REQUEST_DELETE_VALIDATION = 'delete_account_validation';
/**
* Request the PII Data
*
* @param \IPS\Member|NULL $member
* @return void
*/
public static function requestPiiData( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
$obj = new static;
$obj->member_id = $member->member_id;
$obj->request_date = time();
$obj->action = static::TYPE_REQUEST_PII;
$obj->save();
\IPS\core\AdminNotification::send( 'core', 'PiiDataRequest', NULL, TRUE, $member );
}
/**
* Can the member request his PII Data?
*
* @param \IPS\Member|NULL $member
* @return bool
*/
public static function canRequestPiiData( \IPS\Member $member = NULL ): bool
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $member->member_id AND !static::hasPiiRequest($member) )
{
return TRUE;
}
return FALSE;
}
/**
* Can the member download his PII Data?
*
* @param \IPS\Member|NULL $member
* @return bool
*/
public static function canDownloadPiiData( \IPS\Member $member = NULL ): bool
{
$member = $member ?: \IPS\Member::loggedIn();
if( !$member->member_id )
{
return FALSE;
}
if ( \IPS\Db::i()->select( 'count(*)', static::$databaseTable, [ 'member_id=? AND action=? AND approved=?', $member->member_id, static::TYPE_REQUEST_PII, 1 ] )->first() > 0 )
{
return TRUE;
}
return FALSE;
}
/**
* Get the deletion request by member and key
*
* @param \IPS\Member $member
* @param string $key
* @return static
* @throws \OutOfRangeException
*/
public static function getDeletionRequestByMemberAndKey( \IPS\Member $member, string $key ): static
{
try
{
$where = [];
$where[] = ['member_id=?', $member->member_id];
$where[] = ['vkey=?', $key];
$where[] = [ \IPS\Db::i()->in( 'action',[static::TYPE_REQUEST_DELETE, static::TYPE_REQUEST_DELETE_VALIDATION ] )];
return static::constructFromData(\IPS\Db::i()->select( '*', static::$databaseTable, $where )->first() );
}
catch( \UnderflowException $e ){
throw new \OutOfRangeException;
}
}
/**
* Is a PII Data Request pending for this member?
*
* @param \IPS\Member|NULL $member
* @param bool $approved
* @return bool
*/
public static function hasPiiRequest( \IPS\Member $member = NULL, bool $approved = FALSE ): bool
{
$member = $member ?: \IPS\Member::loggedIn();
try
{
\IPS\Db::i()->select( '*', static::$databaseTable, [ 'member_id=? AND action=? AND approved=?', $member->member_id, static::TYPE_REQUEST_PII, (int) $approved ] )->first();
return TRUE;
}
catch( \UnderflowException $e ){}
return FALSE;
}
/**
* Approve the PII Request
*
* @return void
*/
public function approvePiiRequest()
{
$this->approved = TRUE;
$this->save();
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'pii_data', $this, array( $this ) );
$notification->recipients->attach( $this->member );
$notification->send();
static::resetPiiAcpNotifications();
}
/**
* Reject the PII Data Request
*
* @return void
*/
public function rejectPiiRequest()
{
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'pii_data_rejected', $this, array( $this ) );
$notification->recipients->attach( $this->member );
$notification->send();
$this->delete();
static::resetPiiAcpNotifications();
}
/**
* Get the member object
*
* @return \IPS\Member
*/
public function get_member(): \IPS\Member
{
return \IPS\Member::load( $this->member_id );
}
/**
* Reset the PII request ACP notifications
*
* @return void
*/
public static function resetPiiAcpNotifications()
{
if ( !\IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( 'action=? AND approved=?', static::TYPE_REQUEST_PII, 0 ) )->first() )
{
\IPS\core\AdminNotification::remove( 'core', 'PiiDataRequest' );
}
}
/**
* Reset the account deletion ACP notifications
*
* @return void
*/
public static function resetDeletionAcpNotifications()
{
if ( !\IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( 'action=?', static::TYPE_REQUEST_DELETE ) )->first() )
{
\IPS\core\AdminNotification::remove( 'core', 'AccountDeletion' );
}
}
/**
* Can the current member request account deletion?
*
* @param \IPS\Member|NULL $member
* @return bool
*/
public static function canDeleteAccount( \IPS\Member $member = NULL ): bool
{
$member = $member ?: \IPS\Member::loggedIn();
return \IPS\Db::i()->select( 'count(*)', static::$databaseTable, [ 'member_id=? AND action=?', $member->member_id, static::TYPE_REQUEST_DELETE ] )->first() == 0;
}
/**
* Create and log the account deletion request
*
* @param \IPS\Member|NULL $member
* @return void
*/
public static function requestAccountDeletion( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
$obj = new static;
$obj->member_id = $member->member_id;
$obj->request_date = time();
$obj->action = static::TYPE_REQUEST_DELETE_VALIDATION;
$vkey = md5( $member->members_pass_hash . \IPS\Login::generateRandomString() );
$obj->vkey = $vkey;
\IPS\Email::buildFromTemplate( 'core', 'account_deletion_confirmation', array( $member, $vkey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
$obj->save();
}
/**
* Confirm the account deletion request
*
* @return void
*/
public function confirmAccountDeletion()
{
$this->action = static::TYPE_REQUEST_DELETE;
\IPS\core\AdminNotification::send( 'core', 'AccountDeletion', NULL, TRUE, $this->member );
$this->member->logHistory( 'core', 'privacy', array( 'type' => 'account_deletion_requested' ) );
$this->save();
}
/**
* Delete the account
*
* @return void
*/
public function deleteAccount()
{
/** @var \IPS\Member $member */
$member = $this->member;
$member->delete( TRUE, FALSE );
static::resetDeletionAcpNotifications();
\IPS\Session::i()->log( 'acplog__members_deleted_id', array( $member->name => FALSE, $member->member_id => FALSE ) );
}
/**
* Reject the account deletion request
*
* @return void
*/
public function rejectDeletionRequest()
{
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'account_del_request_rejected', $this, array( $this ) );
$notification->recipients->attach( $this->member );
$notification->send();
$this->delete();
static::resetDeletionAcpNotifications();
}
/**
* Reset all ACP notifications and caches if there are any, should be called after a member was deleted or merged
*
* @return void
*/
public static function resetAcpNotificationCounts()
{
static::resetPiiAcpNotifications();
static::resetDeletionAcpNotifications();
}
}