2261 lines
71 KiB
PHP
2261 lines
71 KiB
PHP
<?php
|
|
/**
|
|
* @brief User CP Controller
|
|
* @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 10 Jun 2013
|
|
*/
|
|
|
|
namespace IPS\core\modules\front\system;
|
|
|
|
/* To prevent PHP errors (extending class does not exist) revealing path */
|
|
|
|
use BadMethodCallException;
|
|
use DateInterval;
|
|
use DOMXpath;
|
|
use Exception;
|
|
use IPS\Api\OAuthClient;
|
|
use IPS\Application;
|
|
use IPS\core\ShareLinks\Service;
|
|
use IPS\DateTime;
|
|
use IPS\Db;
|
|
use IPS\Dispatcher\Controller;
|
|
use IPS\Email;
|
|
use IPS\Extensions\SSOAbstract;
|
|
use IPS\File;
|
|
use IPS\GeoLocation;
|
|
use IPS\Helpers\Form;
|
|
use IPS\Helpers\Form\Checkbox;
|
|
use IPS\Helpers\Form\Editor;
|
|
use IPS\Helpers\Form\Email as FormEmail;
|
|
use IPS\Helpers\Form\Password;
|
|
use IPS\Helpers\Form\Radio;
|
|
use IPS\Helpers\Form\Text;
|
|
use IPS\Helpers\Form\YesNo;
|
|
use IPS\Helpers\Wizard;
|
|
use IPS\Http\Url;
|
|
use IPS\Login;
|
|
use IPS\Login\Exception as LoginException;
|
|
use IPS\Login\Handler;
|
|
use IPS\Login\Success;
|
|
use IPS\Member;
|
|
use IPS\Member\Device;
|
|
use IPS\Member\PrivacyAction;
|
|
use IPS\Member\ProfileStep;
|
|
use IPS\MFA\MFAHandler;
|
|
use IPS\Output;
|
|
use IPS\Patterns\ActiveRecordIterator;
|
|
use IPS\Request;
|
|
use IPS\Session;
|
|
use IPS\Session\Front;
|
|
use IPS\Session\Store;
|
|
use IPS\Settings as SettingsClass;
|
|
use IPS\Text\Encrypt;
|
|
use IPS\Theme;
|
|
use IPS\Xml\DOMDocument;
|
|
use OutOfRangeException;
|
|
use UnderflowException;
|
|
use function count;
|
|
use function defined;
|
|
use function in_array;
|
|
use function is_array;
|
|
use function is_numeric;
|
|
use function is_string;
|
|
use function json_decode;
|
|
use function strlen;
|
|
|
|
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
|
{
|
|
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* User CP Controller
|
|
*/
|
|
class settings extends Controller
|
|
{
|
|
/**
|
|
* @brief These properties are used to specify datalayer context properties.
|
|
*
|
|
*/
|
|
public static array $dataLayerContext = array(
|
|
'community_area' => [ 'value' => 'settings', 'odkUpdate' => true]
|
|
);
|
|
|
|
/**
|
|
* Execute
|
|
*
|
|
* @return void
|
|
*/
|
|
public function execute() : void
|
|
{
|
|
/* Only logged in members */
|
|
if ( !Member::loggedIn()->member_id and !in_array( Request::i()->do, array( 'mfarecovery', 'mfarecoveryvalidate', 'invite' ) ) )
|
|
{
|
|
Output::i()->error( 'no_module_permission_guest', '2C122/1', 403, '' );
|
|
}
|
|
|
|
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js('front_system.js', 'core' ) );
|
|
|
|
Output::i()->sidebar['enabled'] = FALSE;
|
|
parent::execute();
|
|
}
|
|
|
|
/**
|
|
* Settings
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function manage() : void
|
|
{
|
|
/* Work out output */
|
|
$area = Request::i()->area ?: 'overview';
|
|
$methodName = "_{$area}";
|
|
if ( method_exists( $this, $methodName ) )
|
|
{
|
|
$output = $this->$methodName();
|
|
}
|
|
else
|
|
{
|
|
foreach( Application::allExtensions( 'core', 'AccountSettings', TRUE, 'core' ) as $ext )
|
|
{
|
|
if( $ext->getTab() == $area )
|
|
{
|
|
if( isset( Request::i()->action ) AND method_exists( $ext, Request::i()->action ) )
|
|
{
|
|
$method = Request::i()->action;
|
|
$output = $ext->$method();
|
|
}
|
|
else
|
|
{
|
|
$output = $ext->getContent();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Display */
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack('settings');
|
|
Output::i()->breadcrumb[] = array( NULL, Member::loggedIn()->language()->addToStack('settings') );
|
|
if ( !Request::i()->isAjax() )
|
|
{
|
|
if ( Request::i()->service )
|
|
{
|
|
$area = "{$area}_" . Request::i()->service;
|
|
}
|
|
|
|
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/settings.css' ) );
|
|
|
|
if ( $output )
|
|
{
|
|
Output::i()->output .= $this->_wrapOutputInTemplate( $area, $output );
|
|
}
|
|
}
|
|
elseif ( $output )
|
|
{
|
|
Output::i()->output .= $output;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap output in template
|
|
*
|
|
* @param string $area Active area
|
|
* @param string $output Output
|
|
* @return string
|
|
*/
|
|
protected function _wrapOutputInTemplate( string $area, string $output ) : string
|
|
{
|
|
/* What can we do? */
|
|
$tabs = [
|
|
'overview' => [
|
|
'icon' => 'circle-user',
|
|
'url' => Url::internal( "app=core&module=system&controller=settings", "front", "settings" )
|
|
]
|
|
];
|
|
|
|
/* Can change email? */
|
|
if( SettingsClass::i()->allow_email_changes != 'disabled' )
|
|
{
|
|
$tabs['email'] = [
|
|
'icon' => 'envelope',
|
|
'title' => 'email_address'
|
|
];
|
|
if( Member::loggedIn()->members_bitoptions['email_messages_bounce'] )
|
|
{
|
|
$tabs['email']['warning'] = true;
|
|
}
|
|
}
|
|
|
|
/* Can change password? */
|
|
$canChangePassword = ( SettingsClass::i()->allow_password_changes == 'redirect' );
|
|
if ( SettingsClass::i()->allow_password_changes == 'normal' )
|
|
{
|
|
foreach ( Login::methods() as $method )
|
|
{
|
|
if ( $method->canChangePassword( Member::loggedIn() ) )
|
|
{
|
|
$canChangePassword = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( $canChangePassword )
|
|
{
|
|
$tabs['password'] = [ 'icon' => 'key' ];
|
|
}
|
|
|
|
/* MFA */
|
|
$canConfigureMfa = FALSE;
|
|
foreach ( MFAHandler::handlers() as $handler )
|
|
{
|
|
if ( $handler->isEnabled() and $handler->memberCanUseHandler( Member::loggedIn() ) )
|
|
{
|
|
$canConfigureMfa = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( $canConfigureMfa OR !Member::loggedIn()->group['g_hide_online_list'] OR Member::loggedIn()->canUseAccountDeletion() OR SettingsClass::i()->pii_type != 'off' )
|
|
{
|
|
$tabs['mfa'] = [
|
|
'icon' => 'lock',
|
|
'title' => 'ucp_mfa'
|
|
];
|
|
}
|
|
|
|
/* Devices */
|
|
if( SettingsClass::i()->device_management )
|
|
{
|
|
$tabs['devices'] = [
|
|
'icon' => 'laptop',
|
|
'title' => 'ucp_devices'
|
|
];
|
|
}
|
|
|
|
/* Can change username? */
|
|
if( Member::loggedIn()->group['g_dname_changes'] )
|
|
{
|
|
$tabs['username'] = [ 'icon' => 'user' ];
|
|
}
|
|
|
|
/* Content Preferences */
|
|
$tabs['links'] = [
|
|
'icon' => 'file-pen',
|
|
'title' => 'profile_settings_cvb'
|
|
];
|
|
|
|
/* Signature */
|
|
if( Member::loggedIn()->canEditSignature() )
|
|
{
|
|
$tabs['signature'] = [ 'icon' => 'pencil' ];
|
|
}
|
|
|
|
/* Login methods */
|
|
foreach( Login::methods() as $method )
|
|
{
|
|
if( $method->showInUcp( Member::loggedIn() ) )
|
|
{
|
|
$tabs['login_' . $method->id] = [
|
|
'url' => Url::internal( "app=core&module=system&controller=settings&area=login&service=" . $method->id, "front", "settings_login" ),
|
|
'title' => $method->_title
|
|
];
|
|
|
|
$icon = $method->logoForUcp();
|
|
if( is_string( $icon ) )
|
|
{
|
|
$tabs['login_' . $method->id]['icon'] = 'brands fa-' . $icon;
|
|
}
|
|
else
|
|
{
|
|
$tabs['login_' . $method->id]['image'] = $icon;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Show our own oauth clients? */
|
|
$showApps = (bool) Db::i()->select( 'COUNT(*)', 'core_oauth_clients', array( array( 'oauth_enabled=1 AND oauth_ucp=1' ) ) )->first();
|
|
if( $showApps )
|
|
{
|
|
$tabs['apps'] = [
|
|
'icon' => 'cubes',
|
|
'title' => 'oauth_apps'
|
|
];
|
|
}
|
|
|
|
/* Extensions */
|
|
foreach( Application::allExtensions( 'core', 'AccountSettings', TRUE, 'core' ) as $ext )
|
|
{
|
|
if( $key = $ext->getTab() )
|
|
{
|
|
$tabs[ $key ] = [
|
|
'icon' => $ext::$icon,
|
|
'title' => $ext->getTitle(),
|
|
'warning' => $ext->showWarning()
|
|
];
|
|
}
|
|
}
|
|
|
|
/* Return */
|
|
return Theme::i()->getTemplate( 'system' )->settings( $area, $output, $tabs );
|
|
}
|
|
|
|
/**
|
|
* Overview
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _overview() : string
|
|
{
|
|
$loginMethods = array();
|
|
$canChangePassword = FALSE;
|
|
|
|
foreach ( Login::methods() as $method )
|
|
{
|
|
if ( $method->showInUcp( Member::loggedIn() ) )
|
|
{
|
|
if ( $method->canProcess( Member::loggedIn() ) )
|
|
{
|
|
try
|
|
{
|
|
$name = $method->userProfileName( Member::loggedIn() );
|
|
|
|
$loginMethods[ $method->id ] = array(
|
|
'title' => $method->_title,
|
|
'blurb' => $name ? Member::loggedIn()->language()->addToStack( 'profilesync_headline', FALSE, array( 'sprintf' => array( $name ) ) ) : Member::loggedIn()->language()->addToStack( 'profilesync_signed_in' ),
|
|
'icon' => $method->userProfilePhoto( Member::loggedIn() )
|
|
);
|
|
}
|
|
catch ( LoginException $e )
|
|
{
|
|
$loginMethods[ $method->id ] = array( 'title' => $method->_title, 'blurb' => Member::loggedIn()->language()->addToStack('profilesync_reauth_needed') );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$loginMethods[ $method->id ] = array( 'title' => $method->_title, 'blurb' => Member::loggedIn()->language()->addToStack('profilesync_not_synced') );
|
|
}
|
|
}
|
|
|
|
|
|
if ( $method->canChangePassword( Member::loggedIn() ) )
|
|
{
|
|
$canChangePassword = TRUE;
|
|
}
|
|
}
|
|
|
|
if( SettingsClass::i()->allow_password_changes == 'disabled' )
|
|
{
|
|
$canChangePassword = FALSE;
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsOverview( $loginMethods, $canChangePassword );
|
|
}
|
|
|
|
/**
|
|
* Email
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _email() : string
|
|
{
|
|
if ( SettingsClass::i()->allow_email_changes == 'redirect' )
|
|
{
|
|
Output::i()->redirect( Url::external( SettingsClass::i()->allow_email_changes_target ) );
|
|
}
|
|
|
|
if( SettingsClass::i()->allow_email_changes != 'normal' )
|
|
{
|
|
Output::i()->error( 'no_module_permission', '2C122/U', 403, '' );
|
|
}
|
|
|
|
if( Member::loggedIn()->isAdmin() )
|
|
{
|
|
return Theme::i()->getTemplate( 'system' )->settingsEmail();
|
|
}
|
|
|
|
$mfaOutput = MFAHandler::accessToArea( 'core', 'EmailChange', Url::internal( 'app=core&module=system&controller=settings&area=email', 'front', 'settings_email' ) );
|
|
|
|
if ( $mfaOutput )
|
|
{
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=email', 'front', 'settings_email' ) );
|
|
}
|
|
Output::i()->output = $mfaOutput;
|
|
}
|
|
|
|
/* Do we have any pending validation emails? */
|
|
try
|
|
{
|
|
$pending = Db::i()->select( '*', 'core_validating', array( 'member_id=? AND email_chg=1', Member::loggedIn()->member_id ), 'entry_date DESC' )->first();
|
|
}
|
|
catch( UnderflowException $e )
|
|
{
|
|
$pending = null;
|
|
}
|
|
|
|
/* Build the form */
|
|
$form = new Form;
|
|
$form->class = 'ipsForm_collapseTablet';
|
|
|
|
$currentEmail = htmlspecialchars( Member::loggedIn()->email, ENT_DISALLOWED, 'UTF-8', FALSE );
|
|
$form->addDummy( 'current_email', Member::loggedIn()->members_bitoptions["email_messages_bounce"] ? Theme::i()->getTemplate( 'global', 'core' )->memberEmailBlockedMessage( $currentEmail ) : $currentEmail );
|
|
$form->add( new FormEmail(
|
|
'new_email',
|
|
'',
|
|
TRUE,
|
|
array( 'accountEmail' => Member::loggedIn() )
|
|
) );
|
|
|
|
|
|
/* Handle submissions */
|
|
$values = NULL;
|
|
if ( !$mfaOutput and $values = $form->values() )
|
|
{
|
|
$_SESSION['newEmail'] = $values['new_email'];
|
|
}
|
|
if ( isset( $_SESSION['newEmail'] ) )
|
|
{
|
|
/* Reauthenticate */
|
|
$login = new Login( Url::internal( 'app=core&module=system&controller=settings&area=email', 'front', 'settings_email' ), Login::LOGIN_REAUTHENTICATE );
|
|
|
|
/* After re-authenticating, change the email */
|
|
$error = NULL;
|
|
try
|
|
{
|
|
|
|
if ( !count( $login->buttonMethods() ) or $success = $login->authenticate() )
|
|
{
|
|
/* Send a validation email if we need to */
|
|
if ( SettingsClass::i()->reg_auth_type == 'user' or SettingsClass::i()->reg_auth_type == 'admin_user' )
|
|
{
|
|
$vid = Login::generateRandomString();
|
|
$plainSecurityKey = Login::generateRandomString();
|
|
|
|
Db::i()->insert( 'core_validating', array(
|
|
'vid' => $vid,
|
|
'member_id' => Member::loggedIn()->member_id,
|
|
'entry_date' => time(),
|
|
'email_chg' => TRUE,
|
|
'ip_address' => Request::i()->ipAddress(),
|
|
'new_email' => $_SESSION['newEmail'],
|
|
'email_sent' => time(),
|
|
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag()
|
|
) );
|
|
|
|
Member::loggedIn()->members_bitoptions['validating'] = TRUE;
|
|
Member::loggedIn()->save();
|
|
|
|
Email::buildFromTemplate( 'core', 'email_change', array( Member::loggedIn(), $vid, $plainSecurityKey, $_SESSION['newEmail'] ), Email::TYPE_TRANSACTIONAL )->send( $_SESSION['newEmail'], array(), array(), NULL, NULL, array( 'Reply-To' => SettingsClass::i()->email_in ) );
|
|
|
|
unset( $_SESSION['newEmail'] );
|
|
|
|
Output::i()->redirect( Url::internal( '' ) );
|
|
}
|
|
|
|
/* If we don't need validation, just change it */
|
|
else
|
|
{
|
|
$oldEmail = Member::loggedIn()->email;
|
|
Member::loggedIn()->changeEmail( $_SESSION['newEmail'] );
|
|
|
|
/* Invalidate sessions except this one */
|
|
Member::loggedIn()->invalidateSessionsAndLogins( Session::i()->id );
|
|
if( isset( Request::i()->cookie['login_key'] ) )
|
|
{
|
|
Device::loadOrCreate( Member::loggedIn() )->updateAfterAuthentication( TRUE );
|
|
}
|
|
|
|
unset( $_SESSION['newEmail'] );
|
|
|
|
/* Send a confirmation email */
|
|
Email::buildFromTemplate( 'core', 'email_address_changed', array( Member::loggedIn(), $oldEmail ), Email::TYPE_TRANSACTIONAL )->send( $oldEmail, array(), array(), NULL, NULL, array( 'Reply-To' => SettingsClass::i()->email_in ) );
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=email', 'front', 'settings' ), 'email_changed' );
|
|
}
|
|
}
|
|
}
|
|
catch ( LoginException|Exception $e )
|
|
{
|
|
$error = $e->getMessage();
|
|
}
|
|
|
|
/* Otherwise show the reauthenticate form */
|
|
return Theme::i()->getTemplate( 'system' )->settingsEmail( NULL, $login, $error );
|
|
|
|
}
|
|
return Theme::i()->getTemplate( 'system' )->settingsEmail( $form );
|
|
}
|
|
|
|
/**
|
|
* Password
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _password() : string
|
|
{
|
|
if ( SettingsClass::i()->allow_password_changes == 'redirect' )
|
|
{
|
|
Output::i()->redirect( Url::external( SettingsClass::i()->allow_password_changes_target ) );
|
|
}
|
|
|
|
if( SettingsClass::i()->allow_password_changes != 'normal' )
|
|
{
|
|
Output::i()->error( 'no_module_permission', '2C122/T', 403, '' );
|
|
}
|
|
|
|
$canChangePassword = FALSE;
|
|
|
|
foreach ( Login::methods() as $method )
|
|
{
|
|
if ( $method->canChangePassword( Member::loggedIn() ) )
|
|
{
|
|
$canChangePassword = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !$canChangePassword )
|
|
{
|
|
Output::i()->error( 'no_module_permission', '3C122/W', 403, '' );
|
|
}
|
|
|
|
if( Member::loggedIn()->isAdmin() )
|
|
{
|
|
return Theme::i()->getTemplate( 'system' )->settingsPassword();
|
|
}
|
|
|
|
$mfaOutput = MFAHandler::accessToArea( 'core', 'PasswordChange', Url::internal( 'app=core&module=system&controller=settings&area=password', 'front', 'settings_password' ) );
|
|
if ( $mfaOutput )
|
|
{
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=password', 'front', 'settings_password' ) );
|
|
}
|
|
Output::i()->output = $mfaOutput;
|
|
}
|
|
|
|
$form = new Form;
|
|
$form->class = 'ipsForm_collapseTablet';
|
|
if ( !Member::loggedIn()->members_bitoptions['password_reset_forced'] )
|
|
{
|
|
$form->add( new Password( 'current_password', '', TRUE, array( 'protect' => TRUE, 'validateFor' => Member::loggedIn(), 'bypassProfanity' => Text::BYPASS_PROFANITY_ALL, 'htmlAutocomplete' => "current-password" ) ) );
|
|
}
|
|
$form->add( new Password( 'new_password', '', TRUE, array( 'protect' => TRUE, 'showMeter' => SettingsClass::i()->password_strength_meter, 'checkStrength' => TRUE, 'strengthMember' => Member::loggedIn(), 'bypassProfanity' => Text::BYPASS_PROFANITY_ALL, 'htmlAutocomplete' => "new-password" ) ) );
|
|
$form->add( new Password( 'confirm_new_password', '', TRUE, array( 'protect' => TRUE, 'confirm' => 'new_password', 'bypassProfanity' => Text::BYPASS_PROFANITY_ALL, 'htmlAutocomplete' => "new-password" ) ) );
|
|
|
|
if ( !$mfaOutput and $values = $form->values() )
|
|
{
|
|
/* Change password */
|
|
Member::loggedIn()->changePassword( $values['new_password'] );
|
|
|
|
/* Invalidate sessions except this one */
|
|
Member::loggedIn()->invalidateSessionsAndLogins( Session::i()->id );
|
|
if( isset( Request::i()->cookie['login_key'] ) )
|
|
{
|
|
Device::loadOrCreate( Member::loggedIn() )->updateAfterAuthentication( TRUE );
|
|
}
|
|
|
|
/* Delete any pending validation emails */
|
|
Db::i()->delete( 'core_validating', array( 'member_id=? AND lost_pass=1', Member::loggedIn()->member_id ) );
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=password&success=1', 'front', 'settings' ) );
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsPassword( $form );
|
|
}
|
|
|
|
|
|
/**
|
|
* Devices
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _devices() : string
|
|
{
|
|
/* Can users manage devices? */
|
|
if ( ! SettingsClass::i()->device_management )
|
|
{
|
|
Output::i()->error( 'no_module_permission', '2C122/S' );
|
|
}
|
|
|
|
$mfaOutput = MFAHandler::accessToArea( 'core', 'DeviceManagement', Url::internal( 'app=core&module=system&controller=settings&area=devices', 'front', 'settings_devices' ) );
|
|
if ( $mfaOutput )
|
|
{
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=devices', 'front', 'settings_devices' ) );
|
|
}
|
|
Output::i()->output = $mfaOutput;
|
|
return Theme::i()->getTemplate( 'system' )->settingsDevices( array(), array() );
|
|
}
|
|
|
|
$devices = new ActiveRecordIterator( Db::i()->select( '*', 'core_members_known_devices', array( 'member_id=? AND last_seen>?', Member::loggedIn()->member_id, ( new \DateTime )->sub( new DateInterval( Device::LOGIN_KEY_VALIDITY ) )->getTimestamp() ), 'last_seen DESC' ), 'IPS\Member\Device' );
|
|
|
|
$locations = array();
|
|
$ipAddresses = array();
|
|
foreach ( $devices as $device )
|
|
{
|
|
try
|
|
{
|
|
$log = Db::i()->select( '*', 'core_members_known_ip_addresses', array( 'member_id=? AND device_key=?', Member::loggedIn()->member_id, $device->device_key ), 'last_seen DESC' )->first();
|
|
}
|
|
catch ( UnderflowException $e )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( SettingsClass::i()->ipsgeoip )
|
|
{
|
|
if ( !array_key_exists( $log['ip_address'], $locations ) )
|
|
{
|
|
try
|
|
{
|
|
$locations[ $log['ip_address'] ] = GeoLocation::getByIp( $log['ip_address'] );
|
|
}
|
|
catch ( Exception $e )
|
|
{
|
|
$locations[ $log['ip_address'] ] = Member::loggedIn()->language()->addToStack('unknown');
|
|
}
|
|
}
|
|
|
|
$ipAddresses[ $log['device_key'] ][ $log['ip_address'] ] = array(
|
|
'location' => $locations[ $log['ip_address'] ],
|
|
'date' => $log['last_seen']
|
|
);
|
|
}
|
|
else
|
|
{
|
|
$ipAddresses[ $log['device_key'] ][ $log['ip_address'] ] = array(
|
|
'date' => $log['last_seen']
|
|
);
|
|
}
|
|
}
|
|
|
|
$oauthClients = OAuthClient::roots();
|
|
$apps = array();
|
|
foreach ( Db::i()->select( '*', 'core_oauth_server_access_tokens', array( 'member_id=?', Member::loggedIn()->member_id ), 'issued DESC' ) as $accessToken )
|
|
{
|
|
if ( $accessToken['device_key'] and isset( $oauthClients[ $accessToken['client_id'] ] ) )
|
|
{
|
|
$apps[ $accessToken['device_key'] ][ $accessToken['client_id'] ] = array(
|
|
'date' => $accessToken['issued'],
|
|
);
|
|
}
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsDevices( $devices, $ipAddresses, $apps, $oauthClients );
|
|
}
|
|
|
|
/**
|
|
* Secure Account
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function secureAccount() : void
|
|
{
|
|
/* Only logged in members */
|
|
if ( !Member::loggedIn()->member_id )
|
|
{
|
|
Output::i()->error( 'no_module_permission_guest', '2C122/Q', 403, '' );
|
|
}
|
|
|
|
$canChangePassword = FALSE;
|
|
foreach ( Login::methods() as $method )
|
|
{
|
|
if ( $method->canChangePassword( Member::loggedIn() ) )
|
|
{
|
|
$canChangePassword = TRUE;
|
|
}
|
|
}
|
|
|
|
$canConfigureMfa = FALSE;
|
|
$hasConfiguredMfa = FALSE;
|
|
foreach ( MFAHandler::handlers() as $handler )
|
|
{
|
|
if ( $handler->isEnabled() and $handler->memberCanUseHandler( Member::loggedIn() ) )
|
|
{
|
|
$canConfigureMfa = TRUE;
|
|
|
|
if ( $handler->memberHasConfiguredHandler( Member::loggedIn() ) )
|
|
{
|
|
$hasConfiguredMfa = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$loginMethods = array();
|
|
foreach ( Login::methods() as $method )
|
|
{
|
|
if ( $method->showInUcp( Member::loggedIn() ) )
|
|
{
|
|
if ( $method->canProcess( Member::loggedIn() ) )
|
|
{
|
|
try
|
|
{
|
|
$name = $method->userProfileName( Member::loggedIn() );
|
|
|
|
$loginMethods[ $method->id ] = array(
|
|
'title' => $method->_title,
|
|
'blurb' => $name ? Member::loggedIn()->language()->addToStack( 'profilesync_headline', FALSE, array( 'sprintf' => array( $name ) ) ) : Member::loggedIn()->language()->addToStack( 'profilesync_headline' ),
|
|
'icon' => $method->userProfilePhoto( Member::loggedIn() )
|
|
);
|
|
}
|
|
catch ( LoginException $e )
|
|
{
|
|
$loginMethods[ $method->id ] = array( 'title' => $method->_title, 'blurb' => Member::loggedIn()->language()->addToStack('profilesync_reauth_needed') );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$oauthApps = Db::i()->select( 'COUNT(DISTINCT client_id)', 'core_oauth_server_access_tokens', array( "member_id=? AND oauth_enabled=1 AND oauth_ucp=1 AND status='active'", Member::loggedIn()->member_id ) )
|
|
->join( 'core_oauth_clients', 'oauth_client_id=client_id' )
|
|
->first();
|
|
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( 'secure_account' );
|
|
Output::i()->breadcrumb[] = array( Url::internal( 'app=core&module=system&controller=settings', 'front', 'settings' ), Member::loggedIn()->language()->addToStack('settings') );
|
|
Output::i()->breadcrumb[] = array( NULL, Member::loggedIn()->language()->addToStack('secure_account') );
|
|
Output::i()->output = Theme::i()->getTemplate( 'system' )->settingsSecureAccount( $canChangePassword, $canConfigureMfa, $hasConfiguredMfa, $loginMethods, $oauthApps );
|
|
}
|
|
|
|
/**
|
|
* Disable Automatic Login
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function disableAutomaticLogin() : string
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
try
|
|
{
|
|
$device = Device::loadAndAuthenticate( Request::i()->device, Member::loggedIn() );
|
|
$device->login_key = NULL;
|
|
$device->save();
|
|
|
|
Db::i()->update( 'core_oauth_server_access_tokens', array( 'status' => 'revoked' ), array( 'member_id=? AND device_key=?', $device->member_id, $device->device_key ) );
|
|
|
|
Member::loggedIn()->logHistory( 'core', 'login', array( 'type' => 'logout', 'device' => $device->device_key ) );
|
|
|
|
Store::i()->deleteByMember( $device->member_id, $device->user_agent, array( Session::i()->id ) );
|
|
}
|
|
catch ( Exception $e ) { }
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=devices', 'front', 'settings_devices' ) );
|
|
}
|
|
|
|
/**
|
|
* MFA
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _mfa() : string
|
|
{
|
|
Output::i()->bypassCsrfKeyCheck = true;
|
|
|
|
/* Validate password */
|
|
if ( !isset( $_SESSION['passwordForMfa'] ) )
|
|
{
|
|
$login = new Login( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ), Login::LOGIN_REAUTHENTICATE );
|
|
$usernamePasswordMethods = $login->usernamePasswordMethods();
|
|
$buttonMethods = $login->buttonMethods();
|
|
|
|
/* Only prompt for re-authentication if it is possible */
|
|
if( $usernamePasswordMethods OR $buttonMethods )
|
|
{
|
|
$_SESSION['mfaValidationRequired'] = TRUE;
|
|
$error = NULL;
|
|
try
|
|
{
|
|
if ( $success = $login->authenticate() )
|
|
{
|
|
$_SESSION['passwordForMfa'] = TRUE;
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
|
|
}
|
|
}
|
|
catch ( LoginException $e )
|
|
{
|
|
$error = $e->getMessage();
|
|
}
|
|
return Theme::i()->getTemplate( 'system' )->settingsMfaPassword( $login, $error );
|
|
}
|
|
else
|
|
{
|
|
/* If we don't have any methods available, we have only the standard login */
|
|
$_SESSION['mfaValidationRequired'] = FALSE;
|
|
}
|
|
}
|
|
|
|
/* Get our handlers and the output, even if it's just for a backdrop */
|
|
$handlers = array();
|
|
foreach ( MFAHandler::handlers() as $key => $handler )
|
|
{
|
|
if ( $handler->isEnabled() and $handler->memberCanUseHandler( Member::loggedIn() ) )
|
|
{
|
|
$handlers[ $key ] = $handler;
|
|
}
|
|
}
|
|
$output = Theme::i()->getTemplate( 'system' )->settingsMfa( $handlers );
|
|
|
|
/* Do MFA check */
|
|
$mfaOutput = MFAHandler::accessToArea( 'core', 'SecurityQuestions', Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
|
|
if ( $mfaOutput )
|
|
{
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
|
|
}
|
|
return $output . $mfaOutput;
|
|
}
|
|
|
|
/* We got past the prompts */
|
|
$_SESSION['passwordValidatedForMfa'] = TRUE;
|
|
|
|
/* Do any enabling/disabling */
|
|
if ( isset( Request::i()->act ) )
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
/* Get the handler */
|
|
$key = Request::i()->type;
|
|
if ( !isset( $handlers[ $key ] ) or MFAHandler::accessToArea( 'core', 'SecurityQuestions', Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) ) )
|
|
{
|
|
Output::i()->error( 'node_error', '2C122/M', 404, '' );
|
|
}
|
|
|
|
/* Do it */
|
|
if ( Request::i()->act === 'enable' )
|
|
{
|
|
/* Include the CSS we'll need */
|
|
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
|
|
|
/* Did we just submit it? */
|
|
if ( isset( Request::i()->mfa_setup ) and $handlers[ $key ]->configurationScreenSubmit( Member::loggedIn() ) )
|
|
{
|
|
$_SESSION['MFAAuthenticated'] = time();
|
|
|
|
Member::loggedIn()->members_bitoptions['security_questions_opt_out'] = FALSE;
|
|
Member::loggedIn()->save();
|
|
|
|
/* Invalidate other sessions */
|
|
Member::loggedIn()->invalidateSessionsAndLogins( Session::i()->id );
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
|
|
}
|
|
|
|
/* Show the configuration modal */
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack('settings');
|
|
return $output . Theme::i()->getTemplate( 'system' )->settingsMfaSetup( $handlers[ $key ]->configurationScreen( Member::loggedIn(), FALSE, Url::internal( 'app=core&module=system&controller=settings&area=mfa&act=enable&type=' . $key, 'front', 'settings_mfa' ) ), Url::internal( 'app=core&module=system&controller=settings&area=mfa&act=enable&type=' . $key, 'front', 'settings_mfa' ) );
|
|
}
|
|
elseif ( Request::i()->act === 'disable' )
|
|
{
|
|
/* Disable it */
|
|
$handlers[ $key ]->disableHandlerForMember( Member::loggedIn() );
|
|
Member::loggedIn()->save();
|
|
|
|
/* If we have now disabled everything, save that we have opted out */
|
|
if ( SettingsClass::i()->mfa_required_groups != '*' and !Member::loggedIn()->inGroup( explode( ',', SettingsClass::i()->mfa_required_groups ) ) )
|
|
{
|
|
$enabledHandlers = FALSE;
|
|
foreach ( $handlers as $handler )
|
|
{
|
|
if ( $handler->memberHasConfiguredHandler( Member::loggedIn() ) )
|
|
{
|
|
$enabledHandlers = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if ( !$enabledHandlers )
|
|
{
|
|
Member::loggedIn()->members_bitoptions['security_questions_opt_out'] = TRUE;
|
|
Member::loggedIn()->save();
|
|
}
|
|
}
|
|
|
|
/* Redirect */
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front', 'settings_mfa' ) );
|
|
}
|
|
}
|
|
|
|
|
|
$output.= Theme::i()->getTemplate( 'system' )->settingsPrivacy();
|
|
|
|
/* If we're still here, just show the screen */
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Request personal identifiable information
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function requestPiiData() : void
|
|
{
|
|
if( ( $_SESSION['mfaValidationRequired'] AND !isset( $_SESSION['passwordValidatedForMfa'] ) ) OR !PrivacyAction::canRequestPiiData() OR SettingsClass::i()->pii_type !== 'on' )
|
|
{
|
|
Output::i()->error( 'node_error', '1C122/10', 403, '' );
|
|
}
|
|
PrivacyAction::requestPiiData();
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front')->setFragment('piiDataRequest'), 'pii_requested' );
|
|
}
|
|
|
|
/**
|
|
* Download personal identifiable information
|
|
*
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
protected function downloadPiiData() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
if( ( $_SESSION['mfaValidationRequired'] AND !isset( $_SESSION['passwordValidatedForMfa'] ) ) OR !PrivacyAction::canDownloadPiiData() OR SettingsClass::i()->pii_type !== 'on' )
|
|
{
|
|
Output::i()->error( 'node_error', '1C122/11', 403, '' );
|
|
}
|
|
|
|
$xml = Member::loggedIn()->getPiiData();
|
|
|
|
Db::i()->delete( 'core_member_privacy_actions', array( 'member_id=? AND action=?', Member::loggedIn()->member_id, PrivacyAction::TYPE_REQUEST_PII ) );
|
|
Db::i()->delete( 'core_notifications', array( 'member=? AND notification_key=?', Member::loggedIn()->member_id, 'pii_data' ) );
|
|
Member::loggedIn()->logHistory( 'core', 'privacy', array( 'type' => 'pii_download' ) );
|
|
Output::i()->sendOutput( $xml->asXML(), 200, 'application/xml', [ 'Content-Disposition' => Output::getContentDisposition( 'attachment', Member::loggedIn()->name.'_personal_information.xml' ) ], FALSE, FALSE, FALSE );
|
|
}
|
|
|
|
/**
|
|
* Request account deletion
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function requestAccountDeletion() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
if( ( $_SESSION['mfaValidationRequired'] AND !isset( $_SESSION['passwordValidatedForMfa'] ) ) OR !Member::loggedIn()->canUseAccountDeletion() OR SettingsClass::i()->right_to_be_forgotten_type !== 'on' )
|
|
{
|
|
Output::i()->error( 'node_error', '2C122/13', 403, '' );
|
|
}
|
|
|
|
if( !PrivacyAction::canDeleteAccount() )
|
|
{
|
|
Output::i()->error( 'node_error', '1C122/12', 403, '' );
|
|
}
|
|
|
|
PrivacyAction::requestAccountDeletion();
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front')->setFragment('requestAccountDeletion'), 'account_deletion_requested' );
|
|
}
|
|
|
|
/**
|
|
* Cancel account deletion
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function cancelAccountDeletion() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
try
|
|
{
|
|
$where = [];
|
|
$where[] = ['member_id=?', Member::loggedIn()->member_id];
|
|
$where[] = [ Db::i()->in( 'action',[PrivacyAction::TYPE_REQUEST_DELETE, PrivacyAction::TYPE_REQUEST_DELETE_VALIDATION ] ) ];
|
|
$row = Db::i()->select( '*', PrivacyAction::$databaseTable, $where )->first();
|
|
PrivacyAction::constructFromData( $row )->delete();
|
|
Member::loggedIn()->logHistory( 'core', 'privacy', [ 'type' => 'account_deletion_cancelled' ] );
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=mfa', 'front'), 'account_deletion_cancelled' );
|
|
}
|
|
catch( UnderflowException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2C122/Y', 404, '' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirm account deletion
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function confirmAccountDeletion() : void
|
|
{
|
|
$key = Request::i()->vid;
|
|
try
|
|
{
|
|
$request = PrivacyAction::getDeletionRequestByMemberAndKey( Member::loggedIn(), $key );
|
|
|
|
$request->confirmAccountDeletion();
|
|
Output::i()->redirect( $this->url->setQueryString( 'area','mfa'), 'account_deletion_confirmed' );
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2C122/Z', 404, '' );
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Initial MFA Setup
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function initialMfa() : void
|
|
{
|
|
$handlers = array();
|
|
foreach ( MFAHandler::handlers() as $key => $handler )
|
|
{
|
|
if ( $handler->isEnabled() and $handler->memberCanUseHandler( Member::loggedIn() ) )
|
|
{
|
|
$handlers[ $key ] = $handler;
|
|
}
|
|
}
|
|
|
|
if ( isset( Request::i()->mfa_setup ) )
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
foreach ( $handlers as $key => $handler )
|
|
{
|
|
if ( ( count( $handlers ) == 1 ) or $key == Request::i()->mfa_method )
|
|
{
|
|
if ( $handler->configurationScreenSubmit( Member::loggedIn() ) )
|
|
{
|
|
$_SESSION['MFAAuthenticated'] = time();
|
|
$this->_performRedirect( Url::internal('') );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ( $handlers as $key => $handler )
|
|
{
|
|
if ( $handler->memberHasConfiguredHandler( Member::loggedIn() ) )
|
|
{
|
|
$this->_performRedirect( Url::internal('') );
|
|
}
|
|
}
|
|
|
|
if ( isset( Request::i()->_mfa ) and Request::i()->_mfa == 'optout' )
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
Member::loggedIn()->members_bitoptions['security_questions_opt_out'] = TRUE;
|
|
Member::loggedIn()->save();
|
|
Member::loggedIn()->logHistory( 'core', 'mfa', array( 'handler' => 'questions', 'enable' => FALSE, 'optout' => TRUE ) );
|
|
$this->_performRedirect( Url::internal('') );
|
|
}
|
|
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack('reg_complete_2fa_title');
|
|
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
|
Output::i()->output = Theme::i()->getTemplate( 'login', 'core', 'global' )->mfaSetup( $handlers, Member::loggedIn(), Url::internal( 'app=core&module=system&controller=settings&do=initialMfa', 'front', 'settings' )->addRef( $this->_performRedirect( Url::internal(''), '', TRUE ) ) );
|
|
}
|
|
|
|
/**
|
|
* Security Questions
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _securityquestions() : string
|
|
{
|
|
$handler = new \IPS\MFA\SecurityQuestions\Handler();
|
|
|
|
if ( !$handler->isEnabled() )
|
|
{
|
|
Output::i()->error( 'requested_route_404', '2C122/J', 404, '' );
|
|
}
|
|
|
|
$url = Url::internal( 'app=core&module=system&controller=settings&area=securityquestions', 'front', 'settings_securityquestions' );
|
|
if ( isset( Request::i()->initial ) )
|
|
{
|
|
if ( isset( Request::i()->ref ) )
|
|
{
|
|
$url = $url->setQueryString( 'ref', Request::i()->ref );
|
|
}
|
|
|
|
if ( !$handler->memberCanUseHandler( Member::loggedIn() ) or $handler->memberHasConfiguredHandler( Member::loggedIn() ) )
|
|
{
|
|
$this->_performRedirect( Url::internal('') );
|
|
}
|
|
|
|
$url = $url->setQueryString( 'initial', 1 );
|
|
}
|
|
elseif ( $handler->memberHasConfiguredHandler( Member::loggedIn() ) )
|
|
{
|
|
if ( isset( Request::i()->_securityQuestionSetup ) )
|
|
{
|
|
return Theme::i()->getTemplate( 'system', 'core' )->securityQuestionsFinished();
|
|
}
|
|
elseif ( $output = MFAHandler::accessToArea( 'core', 'SecurityQuestions', $url ) )
|
|
{
|
|
return $output;
|
|
}
|
|
}
|
|
|
|
$output = $handler->configurationScreen( Member::loggedIn(), !isset( Request::i()->initial ), $url );
|
|
|
|
if ( isset( Request::i()->initial ) )
|
|
{
|
|
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
|
|
Output::i()->sidebar['enabled'] = FALSE;
|
|
Output::i()->output = $output;
|
|
}
|
|
else
|
|
{
|
|
return $output;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* MFA Email Recovery
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function mfarecovery() : void
|
|
{
|
|
/* Who are we */
|
|
if ( isset( $_SESSION['processing2FA'] ) )
|
|
{
|
|
$member = Member::load( $_SESSION['processing2FA']['memberId'] );
|
|
}
|
|
else
|
|
{
|
|
$member = Member::loggedIn();
|
|
}
|
|
|
|
/* Can we use this? */
|
|
if ( !$member->member_id or !( ( $member->failed_mfa_attempts >= SettingsClass::i()->security_questions_tries and SettingsClass::i()->mfa_lockout_behaviour == 'email' ) or in_array( 'email', explode( ',', SettingsClass::i()->mfa_forgot_behaviour ) ) ) )
|
|
{
|
|
Output::i()->error( 'no_module_permission', '2C122/L', 403, '' );
|
|
}
|
|
|
|
/* If we have an existing validation record, we can just reuse it */
|
|
$sendEmail = TRUE;
|
|
try
|
|
{
|
|
$existing = Db::i()->select( array( 'vid', 'email_sent' ), 'core_validating', array( 'member_id=? AND forgot_security=1', $member->member_id ) )->first();
|
|
$vid = $existing['vid'];
|
|
|
|
/* If we sent an email within the last 15 minutes, don't send another one otherwise someone could be a nuisence */
|
|
if ( $existing['email_sent'] and $existing['email_sent'] > ( time() - 900 ) )
|
|
{
|
|
$sendEmail = FALSE;
|
|
}
|
|
else
|
|
{
|
|
$plainSecurityKey = Login::generateRandomString();
|
|
Db::i()->update( 'core_validating', [ 'email_sent' => time(), 'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag() ], [ 'vid=?', $vid ] );
|
|
}
|
|
}
|
|
catch ( UnderflowException $e )
|
|
{
|
|
$vid = md5( $member->members_pass_hash . Login::generateRandomString() );
|
|
$plainSecurityKey = Login::generateRandomString();
|
|
|
|
Db::i()->insert( 'core_validating', [
|
|
'vid' => $vid,
|
|
'member_id' => $member->member_id,
|
|
'entry_date' => time(),
|
|
'forgot_security' => 1,
|
|
'ip_address' => Request::i()->ipAddress(),
|
|
'email_sent' => time(),
|
|
'security_key' => Encrypt::fromPlaintext( $plainSecurityKey )->tag()
|
|
] );
|
|
}
|
|
|
|
/* Send email */
|
|
if ( $sendEmail )
|
|
{
|
|
Email::buildFromTemplate( 'core', 'mfaRecovery', array( $member, $vid, $plainSecurityKey ), Email::TYPE_TRANSACTIONAL )->send( $member );
|
|
$message = "mfa_recovery_email_sent";
|
|
}
|
|
else
|
|
{
|
|
$message = "mfa_recovery_email_already_sent";
|
|
}
|
|
|
|
/* Show confirmation page with further instructions */
|
|
Output::i()->sidebar['enabled'] = FALSE;
|
|
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack('mfa_account_recovery');
|
|
Output::i()->output = Theme::i()->getTemplate( 'system' )->mfaAccountRecovery( $message );
|
|
}
|
|
|
|
/**
|
|
* Validate MFA Email Recovery
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function mfarecoveryvalidate() : void
|
|
{
|
|
/* Validate */
|
|
try
|
|
{
|
|
$record = Db::i()->select( '*', 'core_validating', array( 'vid=? AND member_id=? AND forgot_security=1', Request::i()->vid, Request::i()->mid ) )->first();
|
|
}
|
|
catch ( UnderflowException $e )
|
|
{
|
|
Output::i()->error( 'mfa_recovery_no_validation_key', '2C122/K', 410, '' );
|
|
}
|
|
|
|
/* Check security key is valid */
|
|
if( !Login::compareHashes( Encrypt::fromTag( $record['security_key'] )->decrypt(), Request::i()->security_key ) )
|
|
{
|
|
Output::i()->error( 'mfavalidate_invalid_security_key', '3C122/16', 403, '' );
|
|
}
|
|
|
|
/* Remove all MFA */
|
|
$member = Member::load( $record['member_id'] );
|
|
foreach ( MFAHandler::handlers() as $key => $handler )
|
|
{
|
|
$handler->disableHandlerForMember( $member );
|
|
}
|
|
$member->failed_mfa_attempts = 0;
|
|
$member->save();
|
|
|
|
/* Delete validating record */
|
|
Db::i()->delete( 'core_validating', array( 'member_id=? AND forgot_security=1', $member->member_id ) );
|
|
|
|
/* Log in if necessary */
|
|
if ( !Member::loggedIn()->member_id and isset( $_SESSION['processing2FA'] ) )
|
|
{
|
|
( new Success( $member, Handler::load( $_SESSION['processing2FA']['handler'] ), $_SESSION['processing2FA']['remember'], $_SESSION['processing2FA']['anonymous'] ) )->process();
|
|
}
|
|
|
|
/* Redirect */
|
|
Output::i()->redirect( Url::internal( '' ) );
|
|
}
|
|
|
|
/**
|
|
* Username
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _username() : string
|
|
{
|
|
/* Check they have permission to change their username */
|
|
if( !Member::loggedIn()->group['g_dname_changes'] )
|
|
{
|
|
Output::i()->error( 'username_err_nochange', '1C122/4', 403, '' );
|
|
}
|
|
|
|
/* SSO Overrides */
|
|
foreach( Application::allExtensions( 'core', 'SSO', FALSE ) as $ext )
|
|
{
|
|
/* @var SSOAbstract $ext */
|
|
if( $ext->isEnabled() AND $url = $ext->displayNameChange() )
|
|
{
|
|
Output::i()->redirect( $url );
|
|
}
|
|
}
|
|
|
|
if ( Member::loggedIn()->group['g_displayname_unit'] )
|
|
{
|
|
if ( Member::loggedIn()->group['gbw_displayname_unit_type'] )
|
|
{
|
|
if ( Member::loggedIn()->joined->diff( DateTime::create() )->days < Member::loggedIn()->group['g_displayname_unit'] )
|
|
{
|
|
Output::i()->error(
|
|
Member::loggedIn()->language()->addToStack( 'username_err_days', FALSE, array( 'sprintf' => array(
|
|
Member::loggedIn()->joined->add(
|
|
new DateInterval( 'P' . Member::loggedIn()->group['g_displayname_unit'] . 'D' )
|
|
)->localeDate()
|
|
), 'pluralize' => array( Member::loggedIn()->group['g_displayname_unit'] ) ) ),
|
|
'1C122/5', 403, '' );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( Member::loggedIn()->member_posts < Member::loggedIn()->group['g_displayname_unit'] )
|
|
{
|
|
Output::i()->error(
|
|
Member::loggedIn()->language()->addToStack( 'username_err_posts' , FALSE, array( 'sprintf' => array(
|
|
( Member::loggedIn()->group['g_displayname_unit'] - Member::loggedIn()->member_posts )
|
|
), 'pluralize' => array( Member::loggedIn()->group['g_displayname_unit'] ) ) ),
|
|
'1C122/6', 403, '' );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* How many changes */
|
|
$nameCount = Db::i()->select( 'COUNT(*) as count, MIN(log_date) as min_date', 'core_member_history', array(
|
|
'log_member=? AND log_app=? AND log_type=? AND log_date>?',
|
|
Member::loggedIn()->member_id,
|
|
'core',
|
|
'display_name',
|
|
DateTime::create()->sub( new DateInterval( 'P' . Member::loggedIn()->group['g_dname_date'] . 'D' ) )->getTimestamp()
|
|
) )->first();
|
|
|
|
if ( Member::loggedIn()->group['g_dname_changes'] != -1 and $nameCount['count'] >= Member::loggedIn()->group['g_dname_changes'] )
|
|
{
|
|
return Theme::i()->getTemplate( 'system' )->settingsUsernameLimitReached( Member::loggedIn()->language()->addToStack('username_err_limit', FALSE, array( 'sprintf' => array( Member::loggedIn()->group['g_dname_date'] ), 'pluralize' => array( Member::loggedIn()->group['g_dname_changes'] ) ) ) );
|
|
}
|
|
else
|
|
{
|
|
/* Build form */
|
|
$form = new Form;
|
|
$form->class = 'ipsForm_collapseTablet';
|
|
$form->add( new Text( 'new_username', '', TRUE, array( 'accountUsername' => Member::loggedIn(), 'htmlAutocomplete' => "username" ) ) );
|
|
|
|
/* Handle submissions */
|
|
if ( $values = $form->values() )
|
|
{
|
|
/* Disable syncing */
|
|
$profileSync = Member::loggedIn()->profilesync;
|
|
if ( isset( $profileSync['name'] ) )
|
|
{
|
|
unset( $profileSync['name'] );
|
|
Member::loggedIn()->profilesync = $profileSync;
|
|
Member::loggedIn()->save();
|
|
}
|
|
|
|
/* Save */
|
|
$oldName = Member::loggedIn()->name;
|
|
Member::loggedIn()->name = $values['new_username'];
|
|
Member::loggedIn()->save();
|
|
Member::loggedIn()->logHistory( 'core', 'display_name', array( 'old' => $oldName, 'new' => $values['new_username'], 'by' => 'manual' ) );
|
|
|
|
/* Sync with login handlers */
|
|
foreach ( Login::methods() as $method )
|
|
{
|
|
try
|
|
{
|
|
$method->changeUsername( Member::loggedIn(), $oldName, $values['new_username'] );
|
|
}
|
|
catch( BadMethodCallException $e ){}
|
|
}
|
|
|
|
/* Redirect */
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=username', 'front', 'settings_username' ), 'username_changed' );
|
|
}
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsUsername( $form, $nameCount['count'], Member::loggedIn()->group['g_dname_changes'], $nameCount['min_date'] ? DateTime::ts( $nameCount['min_date'] ) : Member::loggedIn()->joined, Member::loggedIn()->group['g_dname_date'] );
|
|
}
|
|
|
|
/**
|
|
* Link Preference
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
protected function _links() : string
|
|
{
|
|
/* Build form */
|
|
$form = new Form;
|
|
$form->class = 'ipsForm_collapseTablet';
|
|
$form->add( new Radio( 'link_pref', Member::loggedIn()->linkPref() ?: SettingsClass::i()->link_default, FALSE, array( 'options' => array(
|
|
'unread' => 'profile_settings_cvb_unread',
|
|
'first' => 'profile_settings_cvb_first',
|
|
'last' => 'profile_settings_cvb_last'
|
|
) ) ) );
|
|
|
|
if ( Member::loggedIn()->group['gbw_change_layouts'] )
|
|
{
|
|
if ( Member::loggedIn()->isEditingTheme() )
|
|
{
|
|
$form->addMessage( 'member_change_theme_layouts_editing_message' );
|
|
}
|
|
else
|
|
{
|
|
$form->add( new YesNo( 'member_change_theme_layouts', ( Member::loggedIn()->layouts and $memberOptions = json_decode( Member::loggedIn()->layouts, true ) and count( $memberOptions ) ), false, ['togglesOn' => ['forum_view', 'forum_list_view', 'forum_topic_view', 'downloads_categories_view', 'blog_view', 'courses_default_view', 'cm_store_view']] ) );
|
|
|
|
if( Application::appIsEnabled( 'forums' ) )
|
|
{
|
|
$form->add( new Radio( 'forum_view', Member::loggedIn()->getLayoutValue( 'forums_forum' ), false, ['options' => [
|
|
'table' => 'forums_default_view_table',
|
|
'grid' => 'forums_default_view_grid',
|
|
'fluid' => 'forums_default_view_fluid',
|
|
'modern' => 'forums_default_view_modern'
|
|
]], null, null, null, 'forum_view' ) );
|
|
|
|
$form->add( new Radio( 'forum_list_view', Member::loggedIn()->getLayoutValue( 'forums_topic' ), false, ['options' => [
|
|
'list' => 'forums_topic_list_list',
|
|
'snippet' => 'forums_topic_list_snippet',
|
|
]], null, null, null, 'forum_list_view' ) );
|
|
|
|
$form->add( new Radio( 'forum_topic_view', Member::loggedIn()->getLayoutValue( 'forums_post' ), false, ['options' => [
|
|
'classic' => 'forums_topic_view_classic',
|
|
'modern' => 'forums_topic_view_modern',
|
|
]], null, null, null, 'forum_topic_view' ) );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'nexus' ) )
|
|
{
|
|
$form->add( new Radio( 'cm_store_view', Member::loggedIn()->getLayoutValue( 'store_view' ), FALSE, array( 'options' => array(
|
|
'grid' => 'cm_store_view_grid',
|
|
'list' => 'cm_store_view_list'
|
|
) ), null, null, null, 'cm_store_view' ) );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'downloads' ) )
|
|
{
|
|
$form->add( new Radio( 'downloads_categories_view', Member::loggedIn()->getLayoutValue( 'downloads_categories' ), false, [ 'options' => [
|
|
'table' => 'downloads_default_view_table',
|
|
'grid' => 'downloads_default_view_grid'
|
|
]], null, null, null, 'downloads_categories_view' ) );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'blog' ) )
|
|
{
|
|
$form->add( new Radio( 'blog_view', Member::loggedIn()->getLayoutValue( 'blog_view' ), false, [ 'options' => [
|
|
'table' => 'blog_view_mode_table',
|
|
'grid' => 'blog_view_mode_grid'
|
|
]], null, null, null, 'blog_view' ) );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'courses' ) )
|
|
{
|
|
$form->add( new Radio( 'courses_default_view', Member::loggedIn()->getLayoutValue( 'courses_view' ), false, [ 'options' => [
|
|
'list' => 'course_view_list',
|
|
'grid' => 'course_view_grid'
|
|
]], null, null, null, 'courses_default_view' ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Handle submissions */
|
|
if ( $values = $form->values() )
|
|
{
|
|
switch( $values['link_pref'] )
|
|
{
|
|
case 'last':
|
|
Member::loggedIn()->members_bitoptions['link_pref_unread'] = FALSE;
|
|
Member::loggedIn()->members_bitoptions['link_pref_last'] = TRUE;
|
|
Member::loggedIn()->members_bitoptions['link_pref_first'] = FALSE;
|
|
break;
|
|
case 'unread':
|
|
Member::loggedIn()->members_bitoptions['link_pref_unread'] = TRUE;
|
|
Member::loggedIn()->members_bitoptions['link_pref_last'] = FALSE;
|
|
Member::loggedIn()->members_bitoptions['link_pref_first'] = FALSE;
|
|
break;
|
|
default:
|
|
Member::loggedIn()->members_bitoptions['link_pref_unread'] = FALSE;
|
|
Member::loggedIn()->members_bitoptions['link_pref_last'] = FALSE;
|
|
Member::loggedIn()->members_bitoptions['link_pref_first'] = TRUE;
|
|
break;
|
|
}
|
|
|
|
/* Update the view preferences */
|
|
if ( ! Member::loggedIn()->isEditingTheme() )
|
|
{
|
|
if ( ! empty( $values['member_change_theme_layouts'] ) )
|
|
{
|
|
if( Application::appIsEnabled( 'forums' ) )
|
|
{
|
|
Member::loggedIn()->setLayoutValue( 'forums_forum', $values['forum_view'] );
|
|
Member::loggedIn()->setLayoutValue( 'forums_topic', $values['forum_list_view'] );
|
|
Member::loggedIn()->setLayoutValue( 'forums_post', $values['forum_topic_view'] );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'downloads' ) )
|
|
{
|
|
Member::loggedIn()->setLayoutValue( 'downloads_categories', $values['downloads_categories_view'] );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'blog' ) )
|
|
{
|
|
Member::loggedIn()->setLayoutValue( 'blog_view', $values['blog_view'] );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'courses' ) )
|
|
{
|
|
Member::loggedIn()->setLayoutValue( 'courses_view', $values['courses_default_view' ] );
|
|
}
|
|
|
|
if( Application::appIsEnabled( 'nexus' ) )
|
|
{
|
|
Member::loggedIn()->setLayoutValue( 'store_view', $values['cm_store_view'] );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Member::loggedIn()->setLayoutValue( null, null );
|
|
}
|
|
}
|
|
|
|
Member::loggedIn()->save();
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=links', 'front', 'settings_links' ), 'saved' );
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsLinks( $form );
|
|
}
|
|
|
|
/**
|
|
* Signature
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _signature() : string
|
|
{
|
|
/* Check they have permission to change their signature */
|
|
$sigLimits = explode( ":", Member::loggedIn()->group['g_signature_limits']);
|
|
|
|
if( !Member::loggedIn()->canEditSignature() )
|
|
{
|
|
Output::i()->error( 'signatures_disabled', '2C122/C', 403, '' );
|
|
}
|
|
|
|
/* Check limits */
|
|
if ( Member::loggedIn()->group['g_sig_unit'] )
|
|
{
|
|
/* Days */
|
|
if ( Member::loggedIn()->group['gbw_sig_unit_type'] )
|
|
{
|
|
if ( Member::loggedIn()->joined->diff( DateTime::create() )->days < Member::loggedIn()->group['g_sig_unit'] )
|
|
{
|
|
Output::i()->error( Member::loggedIn()->language()->pluralize(
|
|
sprintf(
|
|
Member::loggedIn()->language()->get('sig_err_days'),
|
|
Member::loggedIn()->joined->add(
|
|
new DateInterval( 'P' . Member::loggedIn()->group['g_sig_unit'] . 'D' )
|
|
)->localeDate()
|
|
), array( Member::loggedIn()->group['g_sig_unit'] ) ),
|
|
'1C122/D', 403, '' );
|
|
}
|
|
}
|
|
/* Posts */
|
|
else
|
|
{
|
|
if ( Member::loggedIn()->member_posts < Member::loggedIn()->group['g_sig_unit'] )
|
|
{
|
|
Output::i()->error( Member::loggedIn()->language()->pluralize(
|
|
sprintf(
|
|
Member::loggedIn()->language()->get('sig_err_posts'),
|
|
( Member::loggedIn()->group['g_sig_unit'] - Member::loggedIn()->member_posts )
|
|
), array( Member::loggedIn()->group['g_sig_unit'] ) ),
|
|
'1C122/E', 403, '' );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Build form */
|
|
$form = new Form;
|
|
$form->class = 'ipsForm_collapseTablet';
|
|
$form->add( new YesNo( 'view_sigs', Member::loggedIn()->members_bitoptions['view_sigs'], FALSE ) );
|
|
$form->add( new Editor( 'signature', Member::loggedIn()->signature, FALSE, array( 'app' => 'core', 'key' => 'Signatures', 'autoSaveKey' => "frontsig-" . Member::loggedIn()->member_id, 'attachIds' => array( Member::loggedIn()->member_id ) ) ) );
|
|
|
|
/* Handle submissions */
|
|
if ( $values = $form->values() )
|
|
{
|
|
if( $values['signature'] )
|
|
{
|
|
/* Check Limits */
|
|
$signature = new DOMDocument( '1.0', 'UTF-8' );
|
|
$signature->loadHTML( DOMDocument::wrapHtml( $values['signature'] ) );
|
|
|
|
$errors = array();
|
|
|
|
/* Links */
|
|
if ( is_numeric( $sigLimits[4] ) and ( $signature->getElementsByTagName('a')->length + $signature->getElementsByTagName('iframe')->length ) > $sigLimits[4] )
|
|
{
|
|
$errors[] = Member::loggedIn()->language()->addToStack('sig_num_links_exceeded');
|
|
}
|
|
|
|
/* Number of Images */
|
|
if ( is_numeric( $sigLimits[1] ) )
|
|
{
|
|
$imageCount = 0;
|
|
foreach ( $signature->getElementsByTagName('img') as $img )
|
|
{
|
|
if( !$img->hasAttribute("data-emoticon") )
|
|
{
|
|
$imageCount++;
|
|
}
|
|
}
|
|
|
|
/* Look for background-image URLs too */
|
|
$xpath = new DOMXpath( $signature );
|
|
|
|
foreach ( $xpath->query("//*[contains(@style, 'url') and contains(@style, 'background')]") as $styleUrl )
|
|
{
|
|
$imageCount++;
|
|
}
|
|
|
|
if( $imageCount > $sigLimits[1] )
|
|
{
|
|
$errors[] = Member::loggedIn()->language()->addToStack('sig_num_images_exceeded');
|
|
}
|
|
}
|
|
|
|
/* Size of images */
|
|
if ( ( is_numeric( $sigLimits[2] ) and $sigLimits[2] ) or ( is_numeric( $sigLimits[3] ) and $sigLimits[3] ) )
|
|
{
|
|
foreach ( $signature->getElementsByTagName('img') as $image )
|
|
{
|
|
$attachId = $image->getAttribute('data-fileid');
|
|
$imageProperties = NULL;
|
|
|
|
if( $attachId )
|
|
{
|
|
try
|
|
{
|
|
$attachment = Db::i()->select( 'attach_location, attach_thumb_location', 'core_attachments', array( 'attach_id=?', $attachId ) )->first();
|
|
$imageProperties = File::get( 'core_Attachment', $attachment['attach_thumb_location'] ?: $attachment['attach_location'] )->getImageDimensions();
|
|
$src = (string) File::get( 'core_Attachment', $attachment['attach_location'] )->url;
|
|
}
|
|
catch( UnderflowException $e ){}
|
|
}
|
|
|
|
if( is_array( $imageProperties ) AND count( $imageProperties ) )
|
|
{
|
|
if( $imageProperties[0] > $sigLimits[2] OR $imageProperties[1] > $sigLimits[3] )
|
|
{
|
|
$errors[] = Member::loggedIn()->language()->addToStack( 'sig_imagetoobig', FALSE, array( 'sprintf' => array( $src, $sigLimits[2], $sigLimits[3] ) ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Lines */
|
|
$preBreaks = 0;
|
|
|
|
/* Make sure we are not trying to bypass the limit by using <pre> tags, which will not have <p> or <br> tags in its content */
|
|
foreach( $signature->getElementsByTagName('pre') AS $pre )
|
|
{
|
|
$content = nl2br( trim( $pre->nodeValue ) );
|
|
$preBreaks += count( explode( "<br />", $content ) );
|
|
}
|
|
|
|
if ( ( is_numeric( $sigLimits[5] ) and ( $signature->getElementsByTagName('p')->length + $signature->getElementsByTagName('br')->length + $preBreaks ) > $sigLimits[5] ) or strlen( $values['signature'] ) > 20000 )
|
|
{
|
|
$errors[] = Member::loggedIn()->language()->addToStack('sig_num_lines_exceeded');
|
|
}
|
|
}
|
|
|
|
if( !empty( $errors ) )
|
|
{
|
|
$form->error = Member::loggedIn()->language()->addToStack('sig_restrictions_exceeded');
|
|
$form->elements['']['signature']->error = Member::loggedIn()->language()->formatList( $errors );
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsSignature( $form, $sigLimits );
|
|
}
|
|
|
|
Member::loggedIn()->signature = $values['signature'];
|
|
Member::loggedIn()->members_bitoptions['view_sigs'] = $values['view_sigs'];
|
|
|
|
Member::loggedIn()->save();
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=signature', 'front', 'settings_signature' ), 'signature_changed' );
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsSignature( $form, $sigLimits );
|
|
}
|
|
|
|
/**
|
|
* Login Method
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _login() : string
|
|
{
|
|
/* Load method */
|
|
try
|
|
{
|
|
$method = Handler::load( Request::i()->service );
|
|
if ( !$method->showInUcp( Member::loggedIn() ) )
|
|
{
|
|
throw new OutOfRangeException;
|
|
}
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'page_doesnt_exist', '2C122/B', 404, '' );
|
|
}
|
|
|
|
/* Are we connected? */
|
|
$blurb = 'profilesync_blurb';
|
|
$canDisassociate = FALSE;
|
|
|
|
try
|
|
{
|
|
$connected = $method->canProcess( Member::loggedIn() );
|
|
if ( $connected )
|
|
{
|
|
$photoUrl = $method->userProfilePhoto( Member::loggedIn() );
|
|
$profileName = $method->userProfileName( Member::loggedIn() );
|
|
|
|
/* Can we disassociate? */
|
|
foreach ( Login::methods() as $_method )
|
|
{
|
|
if ( $_method->id != $method->id and $_method->canProcess( Member::loggedIn() ) )
|
|
{
|
|
$canDisassociate = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch ( LoginException $e )
|
|
{
|
|
$connected = FALSE;
|
|
$blurb = 'profilesync_expire_blurb';
|
|
|
|
/* If we previously associated an account but that link has expired, we should still allow you to disassociate */
|
|
if( $method->canProcess( Member::loggedIn() ) )
|
|
{
|
|
$canDisassociate = TRUE;
|
|
}
|
|
}
|
|
|
|
if ( $canDisassociate and isset( Request::i()->disassociate ) )
|
|
{
|
|
Session::i()->csrfCheck();
|
|
$method->disassociate();
|
|
|
|
if ( $method->showInUcp( Member::loggedIn() ) )
|
|
{
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id, 'front', 'settings_login' ) );
|
|
}
|
|
else
|
|
{
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings', 'front', 'settings' ) );
|
|
}
|
|
}
|
|
|
|
/* Are we connected? */
|
|
if ( $connected )
|
|
{
|
|
/* Are we forcing syncing of anything? */
|
|
$syncOptions = $method->syncOptions( Member::loggedIn() );
|
|
$forceSync = array();
|
|
foreach ( $method->forceSync() as $type )
|
|
{
|
|
$forceSync[ $type ] = array(
|
|
'label' => Member::loggedIn()->language()->addToStack( "profilesync_{$type}_force", FALSE, array( 'sprintf' => array( Member::loggedIn()->language()->addToStack( $method->_title ) ) ) ),
|
|
'error' => isset( Member::loggedIn()->profilesync[ $type ]['error'] ) ? Member::loggedIn()->profilesync[ $type ]['error'] : NULL
|
|
);
|
|
}
|
|
|
|
/* Show sync options */
|
|
$form = NULL;
|
|
if ( $syncOptions )
|
|
{
|
|
$form = new Form( 'sync', 'profilesync_save' );
|
|
$form->class = 'ipsForm--vertical ipsForm--profile-sync';
|
|
foreach ( $syncOptions as $option )
|
|
{
|
|
if ( !Handler::handlerHasForceSync( $option, NULL, Member::loggedIn() ) )
|
|
{
|
|
if ( $option == 'photo' and !Member::loggedIn()->group['g_edit_profile'] )
|
|
{
|
|
continue;
|
|
}
|
|
if ( $option == 'cover' and ( !Member::loggedIn()->group['g_edit_profile'] or !Member::loggedIn()->group['gbw_allow_upload_bgimage'] ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( $option == 'status' )
|
|
{
|
|
$checked = ( isset( Member::loggedIn()->profilesync[ $option ] ) and array_key_exists( $method->id, Member::loggedIn()->profilesync[ $option ]) );
|
|
}
|
|
else
|
|
{
|
|
$checked = ( isset( Member::loggedIn()->profilesync[ $option ] ) and Member::loggedIn()->profilesync[ $option ]['handler'] == $method->id );
|
|
}
|
|
$field = new Checkbox( "profilesync_{$option}", $checked, FALSE, array( 'labelSprintf' => array( Member::loggedIn()->language()->addToStack( $method->_title ) ) ), NULL, NULL, NULL, "profilesync_{$option}_{$method->id}" );
|
|
if ( $checked and ( ( $option == 'status' and $error = Member::loggedIn()->profilesync[ $option ][ $method->id ]['error'] ) or ( $option != 'status' and $error = Member::loggedIn()->profilesync[ $option ]['error'] ) ) )
|
|
{
|
|
$field->description = Theme::i()->getTemplate( 'system' )->settingsLoginMethodSynError( $error );
|
|
}
|
|
$form->add( $field );
|
|
}
|
|
}
|
|
if ( $values = $form->values() )
|
|
{
|
|
$profileSync = Member::loggedIn()->profilesync;
|
|
$changes = array();
|
|
|
|
foreach ( $values as $k => $v )
|
|
{
|
|
$option = mb_substr( $k, 12 );
|
|
if ( $option === 'status' )
|
|
{
|
|
if ( isset( Member::loggedIn()->profilesync[ $option ][ $method->id ] ) )
|
|
{
|
|
if ( !$v )
|
|
{
|
|
unset( $profileSync[ $option ][ $method->id ] );
|
|
$changes[ $option ] = FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( $v )
|
|
{
|
|
$profileSync[ $option ][ $method->id ] = array( 'lastsynced' => NULL, 'error' => NULL );
|
|
$changes[ $option ] = TRUE;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( isset( Member::loggedIn()->profilesync[ $option ] ) and Member::loggedIn()->profilesync[ $option ]['handler'] == $method->id )
|
|
{
|
|
if ( !$v )
|
|
{
|
|
unset( $profileSync[ $option ] );
|
|
$changes[ $option ] = FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( $v )
|
|
{
|
|
$profileSync[ $option ] = array( 'handler' => $method->id, 'ref' => NULL, 'error' => NULL );
|
|
$changes[ $option ] = TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( count( $changes ) )
|
|
{
|
|
Member::loggedIn()->logHistory( 'core', 'social_account', array( 'changed' => $changes, 'handler' => $method->id, 'service' => $method::getTitle() ) );
|
|
}
|
|
|
|
Member::loggedIn()->profilesync = $profileSync;
|
|
Member::loggedIn()->save();
|
|
Member::loggedIn()->profileSync();
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id, 'front', 'settings_login' ) );
|
|
}
|
|
}
|
|
|
|
$extraPermissions = NULL;
|
|
$login = NULL;
|
|
if ( isset( Request::i()->scopes ) )
|
|
{
|
|
$method = Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
|
$extraPermissions = Request::i()->scopes;
|
|
$login = new Login( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id . '&scopes=' . Request::i()->scopes, 'front', 'settings_login' ), Login::LOGIN_UCP );
|
|
$login->reauthenticateAs = Member::loggedIn();
|
|
|
|
try
|
|
{
|
|
if ( $success = $login->authenticate( $method ) )
|
|
{
|
|
if ( $success->member->member_id === Member::loggedIn()->member_id )
|
|
{
|
|
$method->completeLink( Member::loggedIn(), NULL );
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id, 'front', 'settings_login' ) );
|
|
}
|
|
}
|
|
}
|
|
catch( Exception $ex ) { }
|
|
}
|
|
|
|
/* Display */
|
|
return Theme::i()->getTemplate( 'system' )->settingsLoginMethodOn( $method, $form, $canDisassociate, $photoUrl, $profileName, $extraPermissions, $login, $forceSync );
|
|
}
|
|
|
|
/* No - show option to connect */
|
|
else
|
|
{
|
|
$login = new Login( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id, 'front', 'settings_login' ), Login::LOGIN_UCP );
|
|
$login->reauthenticateAs = Member::loggedIn();
|
|
$error = NULL;
|
|
try
|
|
{
|
|
if ( $success = $login->authenticate( $method ) )
|
|
{
|
|
if ( $success->member->member_id === Member::loggedIn()->member_id )
|
|
{
|
|
$method->completeLink( Member::loggedIn(), NULL );
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id, 'front', 'settings_login' ) );
|
|
}
|
|
else
|
|
{
|
|
$error = Member::loggedIn()->language()->addToStack( 'profilesync_already_associated', FALSE, array( 'sprintf' => array( $method->_title ) ) );
|
|
}
|
|
}
|
|
}
|
|
catch ( LoginException $e )
|
|
{
|
|
if ( $e->getCode() === LoginException::MERGE_SOCIAL_ACCOUNT )
|
|
{
|
|
if ( $e->member->member_id === Member::loggedIn()->member_id )
|
|
{
|
|
$method->completeLink( Member::loggedIn(), NULL );
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=login&service=' . $method->id, 'front', 'settings_login' ) );
|
|
}
|
|
else
|
|
{
|
|
$error = Member::loggedIn()->language()->addToStack( 'profilesync_email_exists', FALSE, array( 'sprintf' => array( $method->_title ) ) );
|
|
}
|
|
}
|
|
elseif( $e->getCode() === LoginException::LOCAL_ACCOUNT_ALREADY_MERGED )
|
|
{
|
|
$error = Member::loggedIn()->language()->addToStack( 'profilesync_already_merged', FALSE, array( 'sprintf' => array( $method->_title, $method->_title, $method->_title ) ) );
|
|
}
|
|
else
|
|
{
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsLoginMethodOff( $method, $login, $error, $blurb, $canDisassociate );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apps
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function _apps() : string
|
|
{
|
|
$apps = array();
|
|
|
|
foreach ( Db::i()->select( '*', 'core_oauth_server_access_tokens', array( 'member_id=?', Member::loggedIn()->member_id ), 'issued DESC' ) as $accessToken )
|
|
{
|
|
if ( $accessToken['status'] == 'revoked' )
|
|
{
|
|
continue;
|
|
}
|
|
try
|
|
{
|
|
$client = OAuthClient::load( $accessToken['client_id'] );
|
|
if ( !$client->enabled )
|
|
{
|
|
throw new OutOfRangeException;
|
|
}
|
|
if ( !$client->ucp )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !isset( $apps[ $client->client_id ] ) )
|
|
{
|
|
$apps[ $client->client_id ] = array(
|
|
'issued' => $accessToken['issued'],
|
|
'client' => $client,
|
|
'scopes' => array()
|
|
);
|
|
}
|
|
else
|
|
{
|
|
if ( $accessToken['issued'] < $apps[ $client->client_id ]['issued'] )
|
|
{
|
|
$apps[ $client->client_id ]['issued'] = $accessToken['issued'];
|
|
}
|
|
}
|
|
|
|
$scopes = array();
|
|
if ( $accessToken['scope'] and $authorizedScopes = json_decode( $accessToken['scope'] ) )
|
|
{
|
|
$availableScopes = json_decode( $client->scopes, TRUE );
|
|
foreach ( $authorizedScopes as $scope )
|
|
{
|
|
if ( isset( $availableScopes[ $scope ] ) and !isset( $apps[ $client->client_id ]['scopes'][ $scope ] ) )
|
|
{
|
|
$apps[ $client->client_id ]['scopes'][ $scope ] = $availableScopes[ $scope ]['description'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch ( OutOfRangeException $e ) { }
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'system' )->settingsApps( $apps );
|
|
}
|
|
|
|
/**
|
|
* Change App Permissions
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function revokeApp() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
try
|
|
{
|
|
$client = OAuthClient::load( Request::i()->client_id );
|
|
Db::i()->update( 'core_oauth_server_access_tokens', array( 'status' => 'revoked' ), array( 'client_id=? AND member_id=?', $client->client_id, Member::loggedIn()->member_id ) );
|
|
Member::loggedIn()->logHistory( 'core', 'oauth', array( 'type' => 'revoked_access_token', 'client' => $client->client_id ) );
|
|
}
|
|
catch ( Exception $e ) { }
|
|
|
|
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=settings&area=apps', 'front', 'settings_apps' ) );
|
|
}
|
|
|
|
/**
|
|
* Disable All Signatures
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function toggleSigs() : void
|
|
{
|
|
if ( ! SettingsClass::i()->signatures_enabled )
|
|
{
|
|
Output::i()->error( 'signatures_disabled', '2C122/F', 403, '' );
|
|
}
|
|
|
|
Session::i()->csrfCheck();
|
|
|
|
if ( Member::loggedIn()->members_bitoptions['view_sigs'] )
|
|
{
|
|
Member::loggedIn()->members_bitoptions['view_sigs'] = 0;
|
|
}
|
|
else
|
|
{
|
|
Member::loggedIn()->members_bitoptions['view_sigs'] = 1;
|
|
}
|
|
|
|
Member::loggedIn()->save();
|
|
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( 'OK' );
|
|
}
|
|
|
|
$redirectUrl = Request::i()->referrer() ?: Url::internal( "app=core&module=system&controller=settings", 'front', 'settings' );
|
|
Output::i()->redirect( $redirectUrl, 'signature_pref_toggled' );
|
|
}
|
|
|
|
/**
|
|
* Dismiss Profile Completion
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function dismissProfile() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
Member::loggedIn()->members_bitoptions['profile_completion_dismissed'] = TRUE;
|
|
Member::loggedIn()->save();
|
|
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( 'OK' );
|
|
}
|
|
else
|
|
{
|
|
$redirectUrl = Request::i()->referrer() ?: Url::internal( "app=core&module=system&controller=settings", 'front', 'settings' );
|
|
Output::i()->redirect( $redirectUrl );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Completion Wizard
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function completion() : void
|
|
{
|
|
$steps = array();
|
|
$url = Url::internal( 'app=core&module=system&controller=settings&do=completion', 'front', 'settings' )->setQueryString( 'ref', Request::i()->ref );
|
|
|
|
foreach( Application::allExtensions( 'core', 'ProfileSteps' ) AS $extension )
|
|
{
|
|
if ( is_array( $extension::wizard() ) AND count( $extension::wizard() ) )
|
|
{
|
|
$steps = array_merge( $steps, $extension::wizard() );
|
|
}
|
|
if ( method_exists( $extension, 'extraStep') AND count( $extension::extraStep() ) )
|
|
{
|
|
$steps = array_merge( $steps, $extension::extraStep() );
|
|
}
|
|
}
|
|
|
|
$steps = ProfileStep::setOrder( $steps );
|
|
|
|
$steps = array_merge( $steps, array( 'profile_done' => function( $data ) use ( $url ) {
|
|
|
|
unset( $_SESSION[ 'wizard-' . md5( $url ) . '-step' ] );
|
|
unset( $_SESSION[ 'wizard-' . md5( $url ) . '-data' ] );
|
|
|
|
if( isset( $_SESSION['profileCompletionData'] ) )
|
|
{
|
|
unset( $_SESSION['profileCompletionData'] );
|
|
}
|
|
|
|
$this->_performRedirect( Url::internal( "app=core&module=system&controller=settings", 'front', 'settings' ), 'saved' );
|
|
} ) );
|
|
|
|
$wizard = new Wizard( $steps, $url, FALSE, NULL, TRUE );
|
|
$wizard->template = array( Theme::i()->getTemplate( 'system', 'core', 'front' ), 'completeWizardTemplate' );
|
|
|
|
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( '2fa.css', 'core', 'global' ) );
|
|
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/settings.css' ) );
|
|
|
|
Output::i()->bodyClasses[] = 'ipsLayout_minimal';
|
|
Output::i()->sidebar['enabled'] = FALSE;
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( 'complete_your_profile' );
|
|
Output::i()->output = (string) $wizard;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to newsletter
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function newsletterSubscribe() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
|
|
Member::loggedIn()->allow_admin_mails = TRUE;
|
|
Member::loggedIn()->save();
|
|
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( 'OK' );
|
|
}
|
|
|
|
$this->_performRedirect( Url::internal( "app=core&module=system&controller=settings", 'front', 'settings' ), 'block_newsletter_subscribed' );
|
|
}
|
|
|
|
/**
|
|
* Toggle Anonymously Online
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function updateAnon() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
/* Check validation */
|
|
if( $_SESSION['mfaValidationRequired'] AND !isset( $_SESSION['passwordValidatedForMfa'] ) )
|
|
{
|
|
Output::i()->error( 'node_error', '1C122/14', 403, '' );
|
|
}
|
|
|
|
/* Check this value can be toggled */
|
|
if( Member::loggedIn()->group['g_hide_online_list'] >= 1 )
|
|
{
|
|
Output::i()->error( 'online_status_cannot_change', '2C122/X', 403, '' );
|
|
}
|
|
|
|
if ( $output = MFAHandler::accessToArea( 'core', 'DeviceManagement', $this->url->setQuerystring('do','updateDeviceEmail')->csrf()) )
|
|
{
|
|
Output::i()->output = $output;
|
|
return;
|
|
}
|
|
|
|
/* Update the bitwise flag */
|
|
Member::loggedIn()->members_bitoptions['is_anon'] = (bool) Request::i()->value;
|
|
Member::loggedIn()->save();
|
|
|
|
/* Update users devices */
|
|
foreach( new ActiveRecordIterator( Db::i()->select( '*', 'core_members_known_devices', array( "member_id=?", Member::loggedIn()->member_id ) ), 'IPS\Member\Device' ) AS $device )
|
|
{
|
|
$device->anonymous = ( Request::i()->value ) ? 1 : 0;
|
|
$device->save();
|
|
}
|
|
|
|
/* Update the session */
|
|
Session::i()->setType( ( Request::i()->value ) ? Front::LOGIN_TYPE_ANONYMOUS : Front::LOGIN_TYPE_MEMBER );
|
|
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( 'OK' );
|
|
}
|
|
|
|
$this->_performRedirect( Url::internal( "app=core&module=system&controller=settings&area=mfa", 'front', 'settings_mfa' ), 'saved' );
|
|
}
|
|
|
|
/**
|
|
* Toggle Whether PII is included in the data layer
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function togglePii() : void
|
|
{
|
|
if ( ! SettingsClass::i()->core_datalayer_member_pii_choice OR ( $_SESSION['mfaValidationRequired'] AND !isset( $_SESSION['passwordValidatedForMfa'] ) ) OR SettingsClass::i()->pii_type !== 'on' )
|
|
{
|
|
Output::i()->error( 'page_not_found', '3T251/7', 404 );
|
|
}
|
|
Session::i()->csrfCheck();
|
|
|
|
/* Update the bitwise flag */
|
|
Member::loggedIn()->members_bitoptions['datalayer_pii_optout'] = ! Member::loggedIn()->members_bitoptions['datalayer_pii_optout'];
|
|
Member::loggedIn()->save();
|
|
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( 'OK' );
|
|
}
|
|
|
|
$this->_performRedirect( Url::internal( "app=core&module=system&controller=settings&area=mfa", 'front', 'settings_mfa' ), 'saved' );
|
|
}
|
|
|
|
/**
|
|
* Invite
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function invite() : void
|
|
{
|
|
$url = Url::internal( "" );
|
|
|
|
if( SettingsClass::i()->ref_on and Member::loggedIn()->member_id )
|
|
{
|
|
$url = $url->setQueryString( array( '_rid' => Member::loggedIn()->member_id ) );
|
|
}
|
|
|
|
$links = Service::getAllServices( $url, SettingsClass::i()->board_name );
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( 'block_invite' );
|
|
Output::i()->output = Theme::i()->getTemplate( 'system' )->invite( $links, $url );
|
|
}
|
|
|
|
/**
|
|
* Toggle New Device Email
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function updateDeviceEmail() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
if ( $output = MFAHandler::accessToArea( 'core', 'DeviceManagement', $this->url->setQuerystring('do','updateDeviceEmail')->csrf()) )
|
|
{
|
|
Output::i()->output = $output;
|
|
return;
|
|
}
|
|
|
|
/* Update the bitwise flag */
|
|
Member::loggedIn()->members_bitoptions['new_device_email'] = (bool) Request::i()->value;
|
|
Member::loggedIn()->save();
|
|
|
|
if ( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( 'OK' );
|
|
}
|
|
|
|
$this->_performRedirect( Url::internal( "app=core&module=system&controller=settings&area=devices", 'front', 'settings_devices' ), 'saved' );
|
|
}
|
|
|
|
/**
|
|
* Redirect the user
|
|
* -consolidated to reduce duplicate code
|
|
*
|
|
* @param Url $fallbackUrl URL to send user to if no referrer was passed
|
|
* @param string $message (Optional) message to show during redirect
|
|
* @param bool $return Return URL instead of redirecting
|
|
* @return string|null
|
|
*/
|
|
protected function _performRedirect( Url $fallbackUrl, string $message='', bool $return=FALSE ) : ?string
|
|
{
|
|
/* Redirect */
|
|
$ref = Request::i()->referrer();
|
|
if ( $ref === NULL )
|
|
{
|
|
$ref = $fallbackUrl;
|
|
}
|
|
|
|
if( $return === TRUE )
|
|
{
|
|
return $ref;
|
|
}
|
|
|
|
Output::i()->redirect( $ref, $message );
|
|
}
|
|
} |