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

@@ -33,6 +33,7 @@ use IPS\Db;
use IPS\Dispatcher;
use IPS\Email;
use IPS\Events\Event;
use IPS\Extensions\MemberACPManagementAbstract;
use IPS\Extensions\SSOAbstract;
use IPS\File;
use IPS\GeoLocation;
@@ -86,12 +87,16 @@ use function chr;
use function count;
use function defined;
use function get_class;
use function implode;
use function in_array;
use function intval;
use function IPS\Cicloud\isManaged;
use function is_array;
use function is_numeric;
use function is_string;
use function iterator_to_array;
use function mb_stripos;
use function method_exists;
use function str_replace;
use function strlen;
use function strtotime;
@@ -148,7 +153,7 @@ class members extends Controller
$filters += array( 'members_filter_locked' => '( failed_login_count>=' . (int) Settings::i()->ipb_bruteforce_attempts . ' OR failed_mfa_attempts>=' . (int) Settings::i()->security_questions_tries . ')' );
}
return array_merge( Bridge::i()->bounceMemberViewGetTableFilters(), $filters );
return $filters;
}
/**
@@ -184,8 +189,25 @@ class members extends Controller
{
unset( $joins[1] );
}
/* Extensions */
$joinedTables = [ 'core_validating', 'core_pfields_content', 'core_admin_permission_rows', 'core_members' ];
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
foreach( $ext->acpJoins() as $join )
{
/* Try to limit the craziness here, you wouldn't believe the things we've seen... */
$tableToJoin = is_array( $join['from'] ) ? $join['from'][0] : $join['from'];
if( !in_array( $tableToJoin, $joinedTables ) )
{
$joins[] = $join;
$joinedTables[] = $tableToJoin;
}
}
}
return array_merge( Bridge::i()->bounceMemberViewGetTableJoins(), $joins );
return $joins;
}
/**
@@ -513,8 +535,22 @@ class members extends Controller
},
);
/* Extensions */
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
foreach( $ext->acpColumns() as $column => $callback )
{
$table->include[] = $column;
if( $callback !== null )
{
$table->parsers[ $column ] = $callback;
}
}
}
/* Specify the buttons */
if ( Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_add' ) and Handler::findMethod( 'IPS\Login\Handler\Standard' ) )
if( static::canAddMembers() )
{
Output::i()->sidebar['actions']['add'] = array(
'primary' => true,
@@ -524,6 +560,7 @@ class members extends Controller
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('members_add') )
);
}
$table->rowButtons = function( $row )
{
$member = Member::constructFromData( $row );
@@ -602,7 +639,7 @@ class members extends Controller
)
);
$return['unflag'] = array(
'icon' => 'flag ipsType_spammer',
'icon' => 'flag i-color_warning',
'title' => 'spam_unflag',
'link' => Url::internal( 'app=core&module=members&controller=members&do=spam&id=' . $member->member_id . '&status=0' )->csrf()->getSafeUrlFromFilters(),
'hidden' => !$member->members_bitoptions['bw_is_spammer'],
@@ -1206,7 +1243,10 @@ class members extends Controller
public function add() : void
{
/* Check permissions */
Dispatcher::i()->checkAcpPermission( 'member_add' );
if( !static::canAddMembers() )
{
Output::i()->error( 'no_standard_login_handler', '2C114/54', 403 );
}
/* Build form */
$form = new Form;
@@ -1351,6 +1391,8 @@ class members extends Controller
if( Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_history' ) )
{
$history = new History( $member->acpUrl()->setQueryString( array( 'do' => 'history' ) ), array( array( 'log_member=?', $member->member_id ) ), TRUE, FALSE, TRUE );
$history->sortBy = 'log_date';
$history->sortDirection = 'desc';
$history->tableTemplate = array( Theme::i()->getTemplate( 'memberprofile', 'core' ), 'historyTable' );
$history->rowsTemplate = array( Theme::i()->getTemplate( 'memberprofile', 'core' ), 'historyRows' );
$history->limit = 20;
@@ -2122,6 +2164,108 @@ class members extends Controller
}
Output::i()->output = Theme::i()->getTemplate('global')->block( 'photo', $form, FALSE );
}
/**
* Add a login link for a member
*
* This method handles the addition of a new login method (or linking an existing method) for a specific member.
* It ensures appropriate permissions, validates inputs, verifies the member, and processes the form submission.
* In case of errors, appropriate exceptions or error messages are triggered.
*
* @return void
*/
protected function loginAdd(): void
{
/* Check permission */
Dispatcher::i()->checkAcpPermission( 'member_edit' );
/* Managed Only */
if( !\IPS\CIC OR ( \IPS\CIC AND !isManaged() ) )
{
Output::i()->error( 'node_error', '2C114/17', 403, '' );
}
/* Load Member and Method */
try
{
$member = Member::load( Request::i()->id );
if( !$member->member_id )
{
throw new OutOfRangeException;
}
if ( $member->isAdmin() )
{
Dispatcher::i()->checkAcpPermission( 'member_edit_admin' );
}
}
catch ( OutOfRangeException $e )
{
Output::i()->error( 'node_error', '2C114/18', 403, '' );
}
$loginHandlers = [];
$existingLinks = iterator_to_array( Db::i()->select( 'token_login_method', 'core_login_links', [ 'token_member=?', $member->member_id ] )->setKeyField('token_login_method') );
foreach( Login::methods() as $method )
{
if( in_array( $method->_id, $existingLinks ) )
{
continue;
}
if( $method::class != 'IPS\Login\Handler\Standard' AND $method::class != 'IPS\convert\extensions\core\LoginHandler\Converter' )
{
$loginHandlers[ $method->_id ] = $method->_title;
}
}
if( !count( $loginHandlers ) )
{
Output::i()->error( 'login_identifier_none', '2C114/19', 403, '' );
}
$form = new Form;
$form->ajaxOutput = TRUE;
$form->add( new Select( 'add_login_link_handler', NULL, TRUE, [ 'options' => $loginHandlers ] ) );
$form->add( new Text( 'add_login_link_identifier', NULL, TRUE, customValidationCode: function( $value ) {
try
{
Db::i()->select( '*', 'core_login_links', [ 'token_login_method=? AND token_identifier=?', (int) \IPS\Request::i()->add_login_link_handler, $value ] )->first();
throw new DomainException( 'login_identifier_exists' );
}
catch( UnderflowException ) {}
} ) );
if( $values = $form->values() )
{
try
{
Db::i()->insert( 'core_login_links', [
'token_login_method' => $values['add_login_link_handler'],
'token_member' => Request::i()->id,
'token_identifier' => $values['add_login_link_identifier'],
'token_linked' => 1
]);
Session::i()->log( 'acplog__members_added_login', array( 'core_login_' . $values['add_login_link_handler'] => TRUE, $member->name => FALSE ) );
Output::i()->redirect( Url::internal( "app=core&module=members&controller=members&do=view&id={$member->member_id}" ), 'saved' );
}
catch( Db\Exception $e )
{
if( $e->getCode() == 1062 )
{
Output::i()->error( 'login_identifier_exists', '3C114/1A', 400, '' );
# Duplicate entry
}
throw new $e;
}
}
/* Display */
Output::i()->title = Member::loggedIn()->language()->addToStack( 'add_login_link' );
Output::i()->output = $form;
}
/**
* Change Login Method Settings
@@ -3601,6 +3745,13 @@ class members extends Controller
unset( Member::loggedIn()->language()->words[ 'core_pfield_' . $fieldId . '_desc' ] );
}
}
foreach( Login::methods() as $method )
{
if( $method::class != 'IPS\Login\Handler\Standard' AND $method::class != 'IPS\convert\extensions\core\LoginHandler\Converter' )
{
$importOptions['import_login_links'][ 'core_login_' . $method->_id ] = $method->_title;
}
}
return new Select( $key, $value, FALSE, array( 'options' => $importOptions, 'toggles' => array( 'member_id' => array( 'elImportMemberIdWarning' ) ) ) );
}
);
@@ -3832,7 +3983,7 @@ class members extends Controller
$member = new Member;
$member->member_group_id = $wizardData['import_members_fallback_group'];
$member->members_bitoptions['password_reset_forced'] = TRUE;
$profileFields = array();
$loginLinks = $profileFields = [];
foreach ( $line as $k => $v )
{
if ( isset( $wizardData['columns'][ $k ] ) )
@@ -3886,6 +4037,22 @@ class members extends Controller
}
catch( OutOfRangeException $e ) { }
}
elseif ( mb_substr( $wizardData['columns'][ $k ], 0, 11 ) == 'core_login_' )
{
$handlerId = (int) mb_substr( $wizardData['columns'][ $k ], 11 );
if( Db::i()->select( 'COUNT(token_member)', 'core_login_links', [ 'token_login_method=? AND token_identifier=?', $handlerId, $v ] )->first() )
{
throw new DomainException( sprintf( Member::loggedIn()->language()->get( 'import_login_link_in_use' ), $v ) );
}
$loginLinks[ $handlerId ] = [
'token_login_method' => $handlerId,
'token_identifier' => $v,
'token_linked' => 1
];
$member->members_bitoptions['password_reset_forced'] = FALSE;
}
else
{
switch ( $wizardData['columns'][ $k ] )
@@ -4043,6 +4210,18 @@ class members extends Controller
{
Db::i()->replace( 'core_pfields_content', array_merge( array( 'member_id' => $member->member_id ), $profileFields ) );
}
if( count( $loginLinks ) )
{
foreach( $loginLinks as $loginLink )
{
try
{
Db::i()->insert( 'core_login_links', array_merge( $loginLink, [ 'token_member' => $member->member_id ] ) );
}
catch( Db\Exception $e ) { }
}
}
Session::i()->log( 'acplog__members_created', array( $member->name => FALSE ) );
}
catch ( DomainException $e )
@@ -4196,6 +4375,27 @@ class members extends Controller
}
}
foreach( Login::methods() as $method )
{
if( $method::class != 'IPS\Login\Handler\Standard' AND $method::class != 'IPS\convert\extensions\core\LoginHandler\Converter' )
{
$columns[ 'core_login_' . $method->_id ] = \IPS\Member::loggedIn()->language()->addToStack( 'import_login_links_suffix', FALSE, [ 'sprintf' => [ $method->_title ] ] );
}
}
/* Extensions */
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
foreach( $ext->exportColumns() as $column => $lang )
{
if( !array_key_exists( $column, $columns ) )
{
$columns[ $column ] = $lang;
}
}
}
return $columns;
}
@@ -4268,6 +4468,7 @@ class members extends Controller
/* Do we need to join profile field data? */
$select = array( 'core_members.*' );
$customFields = array();
foreach ( $wizardData['columns'] as $column )
{
if ( mb_substr( $column, 0, 7 ) == 'pfield_' )
@@ -4300,8 +4501,8 @@ class members extends Controller
$mrData['count'] = $query->first();
}
/* Compile query */
$query = Db::i()->select( implode( ',', $select ), 'core_members', $where, 'core_members.member_id', array( $mrData['offset'], $doPerLoop ) );
/* Compile member_id query. Just getting the member_ids first is more efficient on larger tables */
$query = Db::i()->select( 'core_members.member_id', 'core_members', $where, 'core_members.member_id', array( $mrData['offset'], $doPerLoop ) );
/* Run callbacks */
foreach ( Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' ) as $key => $extension )
@@ -4314,25 +4515,50 @@ class members extends Controller
}
}
/* Finished? */
if ( !$query->count() )
/* Get the IDs */
$memberIds = iterator_to_array( $query );
/* Nothing to do? */
if ( ! count( $memberIds ) )
{
if( count( $mrData['removedData'] ) AND ( !isset( $wizardData['includeInsecure'] ) OR !$wizardData['includeInsecure'] ) )
{
$_SESSION['removedData'] = $mrData['removedData'];
}
/* Finish here as we got everything */
Output::i()->redirect( $baseUrl->setQueryString( array( 'buildDone' => 1 ) ) );
}
/* Now prepare the actual query to get all the data */
$where[] = [ Db::i()->in( 'core_members.member_id', $memberIds ) ];
$dataQuery = Db::i()->select( implode( ',', $select ), 'core_members', $where, 'core_members.member_id' );
/* Run callbacks */
foreach ( Application::allExtensions( 'core', 'MemberFilter', TRUE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'queryCallback' ) )
{
/* Grab our fields and add to the form */
if( !empty( $wizardData['filters'][ $key ] ) )
{
$data = $wizardData['filters'][ $key ];
$extension->queryCallback( $data, $dataQuery );
}
}
}
/* Open file */
$fh = fopen( $wizardData['file'], 'a' );
/* Run */
foreach ( $query as $member )
$done = 0;
foreach ( $dataQuery as $member )
{
$dataToWrite = $this->_getDataToWriteCsv( $member, $wizardData, $mrData );
$done++; /* This is here because the query fetched 2500 but fewer than 2500 may be processed throwing the last bit out */
if( $dataToWrite === FALSE )
{
continue;
@@ -4344,6 +4570,19 @@ class members extends Controller
/* Close and loop */
fclose( $fh );
if ( $done < $doPerLoop )
{
if( count( $mrData['removedData'] ) AND ( !isset( $wizardData['includeInsecure'] ) OR !$wizardData['includeInsecure'] ) )
{
$_SESSION['removedData'] = $mrData['removedData'];
}
/* Finish here as we got everything */
Output::i()->redirect( $baseUrl->setQueryString( array( 'buildDone' => 1 ) ) );
}
$mrData['offset'] += $doPerLoop;
return array( $mrData, Member::loggedIn()->language()->addToStack('export_members_processing'), ( $mrData['offset'] > $mrData['count'] ) ? 100 : ( 100 / $mrData['count'] ) * $mrData['offset'] );
},
@@ -4449,9 +4688,32 @@ class members extends Controller
default => $member[ 'field_' . $fieldId ] ? Field::load( $fieldId )->displayValue( $member[ 'field_' . $fieldId ], FALSE, NULL, 0, NULL, TRUE, ',' ) : ''
};
}
else if ( mb_substr( $column, 0, 11 ) == 'core_login_' )
{
$handlerId = mb_substr( $column, 11 );
try
{
$valueToWrite = Db::i()->select( 'token_identifier', 'core_login_links', [ 'token_login_method=? AND token_member=?', $handlerId, $member['member_id'] ] )->first();
}
catch( UnderflowException ){}
}
else
{
$valueToWrite = $member[ $column ];
/* Is this part of an extension? */
foreach( Application::allExtensions( 'core', 'MemberACPManagement' ) as $ext )
{
/* @var MemberACPManagementAbstract $ext */
if( array_key_exists( $column, $ext->exportColumns() ) )
{
$valueToWrite = $ext->exportColumnValue( $column, $member );
break;
}
}
if( empty( $valueToWrite ) )
{
$valueToWrite = $member[ $column ] ?? '';
}
}
break;
}
@@ -4954,4 +5216,26 @@ class members extends Controller
Bridge::i()->bounceMemberViewRequestEmailUnblock();
}
/**
* Check if the functionality to manually add members is enabled
*
* @return bool
*/
protected static function canAddMembers() : bool
{
/* ACP restriction first */
if( !Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_add' ) )
{
return false;
}
/* Check if the standard method is available and enabled */
if( $handler = Handler::findMethod( Standard::class ) )
{
return $handler->enabled;
}
return false;
}
}