Version 5.0.13
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user