Version 4.3.0
This commit is contained in:
1 parent
96997ddd8e
commit
fe1acf984a
1349 files changed
+116159
-67711
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -22,12 +22,16 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Exception extends \DomainException
|
||||
{
|
||||
const INTERNAL_ERROR = 1;
|
||||
const REGISTRATION_DENIED_BY_SPAM_SERVICE = 2;
|
||||
const REGISTRATION_DISABLED = 3;
|
||||
const BAD_PASSWORD = 4;
|
||||
const NO_ACCOUNT = 5;
|
||||
// If two login handlers produce different exceptions, the one with the higher code is shown
|
||||
// For example: if internal database returns "NO_ACCOUNT", but external returns "BAD_PASSWORD", the
|
||||
// message the user needs to see is of course "Password incorrect" (rather than "Account does not exist")
|
||||
const NO_ACCOUNT = 1;
|
||||
const INTERNAL_ERROR = 2;
|
||||
const REGISTRATION_DENIED_BY_SPAM_SERVICE = 3;
|
||||
const REGISTRATION_DISABLED = 4;
|
||||
const BAD_PASSWORD = 5;
|
||||
const MERGE_SOCIAL_ACCOUNT = 6;
|
||||
const ACCOUNT_LOCKED = 7;
|
||||
|
||||
/**
|
||||
* @brief Member
|
||||
@@ -55,15 +59,6 @@ class _Exception extends \DomainException
|
||||
*/
|
||||
public function __construct( $message, $code=NULL, $previous=NULL, $member=NULL )
|
||||
{
|
||||
if ( $code === static::BAD_PASSWORD and $member !== NULL )
|
||||
{
|
||||
/* $member->failed_logins may not be an array explicitly when upgrading due to past bugs */
|
||||
$failedLogins = is_array( $member->failed_logins ) ? $member->failed_logins : array();
|
||||
$failedLogins[ \IPS\Request::i()->ipAddress() ][] = time();
|
||||
$member->failed_logins = $failedLogins;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
parent::__construct( $message, $code, $previous );
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief External Database Login Handler
|
||||
* @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 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* External Database Login Handler
|
||||
*/
|
||||
class _External extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Authentication types
|
||||
*/
|
||||
public $authTypes;
|
||||
|
||||
/**
|
||||
* Initiate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->authTypes = $this->settings['auth_types'] ?: \IPS\Login::AUTH_TYPE_USERNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from from
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $values )
|
||||
{
|
||||
/* Build where clause */
|
||||
switch ( $this->authTypes )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$where = array( "{$this->settings['db_col_user']}=?", $values['auth'] );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_email']}=?", $values['auth'] );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_user']}=? OR {$this->settings['db_col_email']}=?", $values['auth'], $values['auth'] );
|
||||
break;
|
||||
|
||||
}
|
||||
if ( $this->settings['db_extra'] )
|
||||
{
|
||||
$where[] = array( $this->settings['db_extra'] );
|
||||
}
|
||||
|
||||
/* Fetch result */
|
||||
try
|
||||
{
|
||||
$result = $this->externalDb()->select( '*', $this->settings['db_table'], $where )->first();
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
switch ( $this->authTypes )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$type = 'username_or_email';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$type = 'username';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$type = 'email_address';
|
||||
break;
|
||||
}
|
||||
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $type ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Get a local account if one exists */
|
||||
$member = NULL;
|
||||
if ( $this->settings['db_col_user'] )
|
||||
{
|
||||
$_member = \IPS\Member::load( $result[ $this->settings['db_col_user'] ], 'name' );
|
||||
if ( $_member->member_id )
|
||||
{
|
||||
$member = $_member;
|
||||
}
|
||||
}
|
||||
if ( $this->settings['db_col_email'] )
|
||||
{
|
||||
$_member = \IPS\Member::load( $result[ $this->settings['db_col_email'] ], 'email' );
|
||||
if ( $_member->member_id )
|
||||
{
|
||||
$member = $_member;
|
||||
}
|
||||
}
|
||||
|
||||
/* If the password doesn't match, throw an exception */
|
||||
if ( !\IPS\Login::compareHashes( $this->encryptedPassword( $values['password'] ), $result[ $this->settings['db_col_pass'] ] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
|
||||
/* Or, create a member */
|
||||
if ( $member === NULL )
|
||||
{
|
||||
$member = $this->createOrUpdateAccount( NULL, array(), $this->settings['db_col_user'] ? $result[ $this->settings['db_col_user'] ] : NULL, $this->settings['db_col_email'] ? $result[ $this->settings['db_col_email'] ] : NULL );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt Password
|
||||
*
|
||||
* @param string $password The password
|
||||
* @return bool
|
||||
*/
|
||||
protected function encryptedPassword( $password )
|
||||
{
|
||||
switch ( $this->settings['db_encryption'] )
|
||||
{
|
||||
case 'md5':
|
||||
return md5( $password );
|
||||
|
||||
case 'sha1':
|
||||
return sha1( $password );
|
||||
|
||||
default:
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
return array(
|
||||
'login_external_conn',
|
||||
'sql_host' => new \IPS\Helpers\Form\Text( 'login_external_host', $this->settings['sql_host'] ?: 'localhost', TRUE ),
|
||||
'sql_user' => new \IPS\Helpers\Form\Text( 'login_external_user', $this->settings['sql_user'], TRUE ),
|
||||
'sql_pass' => new \IPS\Helpers\Form\Text( 'login_external_pass', $this->settings['sql_pass'], TRUE ),
|
||||
'sql_database' => new \IPS\Helpers\Form\Text( 'login_external_database', $this->settings['sql_database'], TRUE ),
|
||||
'sql_port' => new \IPS\Helpers\Form\Number( 'login_external_port', $this->settings['sql_port'], FALSE ),
|
||||
'sql_socket' => new \IPS\Helpers\Form\Text( 'login_external_socket', $this->settings['sql_socket'], FALSE ),
|
||||
'login_external_schema',
|
||||
'db_table' => new \IPS\Helpers\Form\Text( 'login_external_table', $this->settings['db_table'], TRUE ),
|
||||
'db_col_user' => new \IPS\Helpers\Form\Text( 'login_external_username', $this->settings['db_col_user'], FALSE, array(), function( $val )
|
||||
{
|
||||
if ( !$val and \IPS\Request::i()->login_auth_types & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
throw new \DomainException('login_external_username_err');
|
||||
}
|
||||
} ),
|
||||
'db_col_email' => new \IPS\Helpers\Form\Text( 'login_external_email', $this->settings['db_col_email'], FALSE, array(), function( $val )
|
||||
{
|
||||
if ( !$val and \IPS\Request::i()->login_auth_types & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
throw new \DomainException('login_external_email_err');
|
||||
}
|
||||
} ),
|
||||
'db_col_pass' => new \IPS\Helpers\Form\Text( 'login_external_password', $this->settings['db_col_pass'], TRUE ),
|
||||
'db_encryption' => new \IPS\Helpers\Form\Select( 'login_external_encryption', $this->settings['db_encryption'], TRUE, array( 'options' => array(
|
||||
'md5' => 'MD5',
|
||||
'sha1' => 'SHA1',
|
||||
'plaintext' => 'login_external_encryption_plain',
|
||||
) ) ),
|
||||
'db_extra' => new \IPS\Helpers\Form\Text( 'login_external_extra', isset( $this->settings['db_extra'] ) ? $this->settings['db_extra'] : '' ),
|
||||
'login_settings',
|
||||
'auth_types' => new \IPS\Helpers\Form\Select( 'login_auth_types', $this->settings['auth_types'], TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
) ) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
$select = array( $this->settings['db_col_pass'] );
|
||||
|
||||
if ( $this->settings['db_col_user'] )
|
||||
{
|
||||
$select[] = $this->settings['db_col_user'];
|
||||
}
|
||||
|
||||
if ( $this->settings['db_col_email'] )
|
||||
{
|
||||
$select[] = $this->settings['db_col_email'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->externalDb()->select( implode( ',', $select ), $this->settings['db_table'], ( isset( $this->settings['db_extra'] ) AND $this->settings['db_extra'] != '' ) ? array( $this->settings['db_extra'] ) : NULL )->first();
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
// It's possible that no users exist, which is fine
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB Connection
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
protected function externalDb()
|
||||
{
|
||||
return \IPS\Db::i( 'external_login', $this->settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_USERNAME and $member->name and $this->usernameIsInUse( $member->name ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_EMAIL and $member->email and $this->emailIsInUse( $member->email ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'username' or 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return $this->canProcess( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
if ( $exclude )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->externalDb()->select( $this->settings['db_col_email'], $this->settings['db_table'], array( "{$this->settings['db_col_email']}=?", $email ) )->first();
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username )
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = $this->externalDb()->select( $this->settings['db_col_user'], $this->settings['db_table'], array( "{$this->settings['db_col_user']}=?", $username ) )->first();
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @return void
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
if ( $this->settings['db_col_email'] )
|
||||
{
|
||||
$this->externalDb()->update( $this->settings['db_table'], array( $this->settings['db_col_email'] => $newEmail ), array( $this->settings['db_col_email'] . '=?', $oldEmail ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
$where = '1=0';
|
||||
switch ( $this->authTypes )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$where = array( "{$this->settings['db_col_user']}=?", $member->name );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_email']}=?", $member->email );
|
||||
break;
|
||||
|
||||
|
||||
}
|
||||
|
||||
$this->externalDb()->update( $this->settings['db_table'], array( $this->settings['db_col_pass'] => $this->encryptedPassword( $newPassword ) ), $where );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
if ( $this->settings['db_col_user'] )
|
||||
{
|
||||
$this->externalDb()->update( $this->settings['db_table'], array( $this->settings['db_col_user'] => $newUsername ), array( $this->settings['db_col_user'] . '=?', $oldUsername ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Facebook Login Handler
|
||||
* @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 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Login Handler
|
||||
*/
|
||||
class _Facebook extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'facebook-square';
|
||||
|
||||
/**
|
||||
* @brief Logo
|
||||
*/
|
||||
public static $logo = 'Facebook';
|
||||
|
||||
/**
|
||||
* @brief Share Service
|
||||
*/
|
||||
public static $shareService = 'facebook';
|
||||
|
||||
/**
|
||||
* Get Form
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL for the login page
|
||||
* @param bool $ucp Is UCP? (as opposed to login form)
|
||||
* @param \IPS\Http\Url $destination The URL to redirect to after a successful login
|
||||
* @return string
|
||||
*/
|
||||
public function loginForm( \IPS\Http\Url $url, $ucp = FALSE, \IPS\Http\Url $destination = NULL )
|
||||
{
|
||||
$scope = 'email';
|
||||
$facebookUrl = "https://www.facebook.com/dialog/oauth";
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments )
|
||||
{
|
||||
if ( isset( $this->settings['allow_status_import'] ) and $this->settings['allow_status_import'] )
|
||||
{
|
||||
$scope .= ',user_posts';
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['autoshare'] ) and $this->settings['autoshare'] )
|
||||
{
|
||||
$scope .= ',publish_actions';
|
||||
}
|
||||
|
||||
if ( isset( \IPS\Request::i()->permissionRequest_Facebook ) )
|
||||
{
|
||||
$allowedExtra = array( 'pages_show_list', 'manage_pages', 'publish_pages' );
|
||||
|
||||
/* We need at least version 2.5 for this to work, 2.8 is latest */
|
||||
$facebookUrl = "https://www.facebook.com/v2.8/dialog/oauth";
|
||||
|
||||
foreach( explode( ',', \IPS\Request::i()->permissionRequest_Facebook ) as $new )
|
||||
{
|
||||
if ( in_array( trim( $new ), $allowedExtra ) )
|
||||
{
|
||||
$scope .= "," . $new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$url = \IPS\Http\Url::external( $facebookUrl )->setQueryString( array(
|
||||
'client_id' => $this->settings['app_id'],
|
||||
'scope' => $scope,
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/facebook/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'state' => ( $ucp ? 'ucp' : \IPS\Dispatcher::i()->controllerLocation ) . '-' . \IPS\Session::i()->csrfKey . '-' . ( $destination ? base64_encode( $destination ) : '' )
|
||||
) );
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->facebook( (string) $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param string $url The URL for the login page
|
||||
* @param \IPS\Member $member If we want to integrate this login method with an existing member, provide the member object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $url, $member=NULL )
|
||||
{
|
||||
$url = $url->setQueryString( 'loginProcess', 'facebook' );
|
||||
|
||||
try
|
||||
{
|
||||
/* CSRF Check */
|
||||
if ( \IPS\Request::i()->state !== \IPS\Session::i()->csrfKey )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'CSRF_FAIL', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Check user approved */
|
||||
if( !isset( \IPS\Request::i()->code ) OR !\IPS\Request::i()->code )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'denied_oauth', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get a token */
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/v2.8/oauth/access_token" )->request()->post( array(
|
||||
'client_id' => $this->settings['app_id'],
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/facebook/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'client_secret' => $this->settings['app_secret'],
|
||||
'code' => \IPS\Request::i()->code
|
||||
) )->decodeJson();
|
||||
}
|
||||
catch( \RuntimeException $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Now exchange it for a one that will last a bit longer in case the user wants to use syncing */
|
||||
$response['access_token'] = $this->exchangeToken( $response['access_token'] );
|
||||
|
||||
if ( ! $response['access_token'] )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Set up appsecret_proof https://developers.facebook.com/docs/graph-api/securing-requests */
|
||||
$appSecretProof = hash_hmac( 'sha256', $response['access_token'], $this->settings['app_secret'] );
|
||||
|
||||
/* Get the user data */
|
||||
$userData = \IPS\Http\Url::external( "https://graph.facebook.com/me?fields=email,id,name&access_token={$response['access_token']}&appsecret_proof={$appSecretProof}" )->request()->get()->decodeJson();
|
||||
|
||||
/* Find or create member */
|
||||
$member = $this->createOrUpdateAccount( $member ?: \IPS\Member::load( $userData['id'], 'fb_uid' ), array(
|
||||
'fb_uid' => $userData['id'],
|
||||
'fb_token' => $response['access_token']
|
||||
), $this->settings['real_name'] ? $userData['name'] : NULL, ( isset( $userData['email'] ) AND $userData['email'] ) ? $userData['email'] : NULL, $response['access_token'], array( 'photo' => TRUE, 'cover' => TRUE, 'status' => '' ) );
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exchange a short lived token for a longer lived token
|
||||
*
|
||||
* @param string $shortLivedToken The short lived token to exchange for a long lived token
|
||||
* @return string
|
||||
*/
|
||||
public function exchangeToken( $shortLivedToken )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/v2.8/oauth/access_token" )->request()->post( array(
|
||||
'grant_type' => 'fb_exchange_token',
|
||||
'client_id' => $this->settings['app_id'],
|
||||
'client_secret' => $this->settings['app_secret'],
|
||||
'fb_exchange_token' => $shortLivedToken
|
||||
) )->decodeJson();
|
||||
|
||||
return $response['access_token'];
|
||||
}
|
||||
catch( \RuntimeException $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param mixed $details Details as they were passed to the exception thrown in authenticate()
|
||||
* @return void
|
||||
*/
|
||||
public static function link( \IPS\Member $member, $details )
|
||||
{
|
||||
$userData = \IPS\Http\Url::external( "https://graph.facebook.com/me?access_token={$details}" )->request()->get()->decodeJson();
|
||||
$member->fb_uid = $userData['id'];
|
||||
$member->fb_token = $details;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Output::i()->sidebar['actions'] = array(
|
||||
'help' => array(
|
||||
'title' => 'help',
|
||||
'icon' => 'question-circle',
|
||||
'link' => \IPS\Http\Url::ips( 'docs/login_facebook' ),
|
||||
'target' => '_blank',
|
||||
'class' => ''
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
$return = array(
|
||||
'app_id' => new \IPS\Helpers\Form\Text( 'login_facebook_app', ( isset( $this->settings['app_id'] ) ) ? $this->settings['app_id'] : '', TRUE ),
|
||||
'app_secret' => new \IPS\Helpers\Form\Text( 'login_facebook_secret', ( isset( $this->settings['app_secret'] ) ) ? $this->settings['app_secret'] : '', TRUE ),
|
||||
'real_name' => new \IPS\Helpers\Form\YesNo( 'login_real_name', ( isset( $this->settings['real_name'] ) ) ? $this->settings['real_name'] : FALSE, TRUE )
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments )
|
||||
{
|
||||
$return['allow_status_import'] = new \IPS\Helpers\Form\YesNo( 'login_facebook_allow_status_import', ( isset( $this->settings['allow_status_import'] ) ) ? $this->settings['allow_status_import'] : FALSE, FALSE );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return ( $member->fb_uid and $member->fb_token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Google Login Handler
|
||||
* @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 20 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Login Handler
|
||||
*/
|
||||
class _Google extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'google-plus';
|
||||
|
||||
/**
|
||||
* @brief Logo
|
||||
*/
|
||||
public static $logo = 'Google';
|
||||
|
||||
/**
|
||||
* Get Form
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL for the login page
|
||||
* @param bool $ucp Is UCP? (as opposed to login form)
|
||||
* @param \IPS\Http\Url $destination The URL to redirect to after a successful login
|
||||
* @return string
|
||||
*/
|
||||
public function loginForm( \IPS\Http\Url $url, $ucp = FALSE, \IPS\Http\Url $destination = NULL )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->google( (string) $this->_googleSignInUrl( ( $ucp ? 'ucp' : \IPS\Dispatcher::i()->controllerLocation ), $ucp, $destination ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sign in URL
|
||||
*
|
||||
* @param string $base Controls where the user is taken back to
|
||||
* @param bool $forcePrompt Force prompt? Will mean user is shown the Google site even if previously authenticated. Only to be used if a new refresh token is needed.
|
||||
* @param \IPS\Http\Url $destination The URL to redirect to after a successful login
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function _googleSignInUrl( $base, $forcePrompt, \IPS\Http\Url $destination = NULL )
|
||||
{
|
||||
$params = array(
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->settings['client_id'],
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/google/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'scope' => 'https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
|
||||
'access_type' => 'offline',
|
||||
'state' => $base . '-' . \IPS\Session::i()->csrfKey . '-' . ( $destination ? base64_encode( $destination ) : '' )
|
||||
);
|
||||
|
||||
if ( $forcePrompt )
|
||||
{
|
||||
$params['approval_prompt'] = 'force';
|
||||
}
|
||||
|
||||
return \IPS\Http\Url::external( "https://accounts.google.com/o/oauth2/auth" )->setQueryString( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param string $url The URL for the login page
|
||||
* @param \IPS\Member $member If we want to integrate this login method with an existing member, provide the member object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $url, $member=NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* CSRF Check */
|
||||
if ( \IPS\Request::i()->state !== \IPS\Session::i()->csrfKey )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'CSRF_FAIL', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if( !isset( \IPS\Request::i()->code ) OR !\IPS\Request::i()->code )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'denied_oauth', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get access token so we can get user data */
|
||||
$response = \IPS\Http\Url::external( "https://accounts.google.com/o/oauth2/token" )->request()->post( array(
|
||||
'code' => \IPS\Request::i()->code,
|
||||
'client_id' => $this->settings['client_id'],
|
||||
'client_secret' => $this->settings['client_secret'],
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/google/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'grant_type' => 'authorization_code',
|
||||
) )->decodeJson();
|
||||
if ( isset( $response['error'] ) or !isset( $response['access_token'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get user data */
|
||||
$userData = \IPS\Http\Url::external( "https://www.googleapis.com/oauth2/v1/userinfo?access_token={$response['access_token']}" )->request()->get()->decodeJson();
|
||||
|
||||
/* Set member properties */
|
||||
$memberProperties = array( 'google_id' => $userData['id'] );
|
||||
if ( isset( $response['refresh_token'] ) )
|
||||
{
|
||||
$memberProperties['google_token'] = $response['refresh_token'];
|
||||
}
|
||||
|
||||
/* Find or create member */
|
||||
$member = $this->createOrUpdateAccount( $member ?: \IPS\Member::load( $userData['id'], 'google_id' ), $memberProperties, $this->settings['real_name'] ? $userData['name'] : NULL, $userData['email'], $response['access_token'], array( 'photo' => TRUE, 'cover' => TRUE, 'status' => '' ) );
|
||||
|
||||
/* We need a refresh token so we can get their user data again later.
|
||||
This is only provided if they actually clicked through the Google auth screen
|
||||
and not if they were authenticated automatically. So if we don't have one,
|
||||
send them back, forcing the login screen, so we get one */
|
||||
if ( !$member->google_token )
|
||||
{
|
||||
\IPS\Output::i()->redirect( $this->_googleSignInUrl( \IPS\Request::i()->base, TRUE ) );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param mixed $details Details as they were passed to the exception thrown in authenticate()
|
||||
* @return void
|
||||
*/
|
||||
public static function link( \IPS\Member $member, $details )
|
||||
{
|
||||
$userData = \IPS\Http\Url::external( "https://www.googleapis.com/plus/v1/people/me?access_token={$details}" )->request()->get()->decodeJson('id');
|
||||
$member->google_id = $userData['id'];
|
||||
$member->google_token = $details;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Output::i()->sidebar['actions'] = array(
|
||||
'help' => array(
|
||||
'title' => 'help',
|
||||
'icon' => 'question-circle',
|
||||
'link' => \IPS\Http\Url::ips( 'docs/login_google' ),
|
||||
'target' => '_blank',
|
||||
'class' => ''
|
||||
),
|
||||
);
|
||||
|
||||
return array(
|
||||
'client_id' => new \IPS\Helpers\Form\Text( 'login_google_id', ( isset( $this->settings['client_id'] ) ) ? $this->settings['client_id'] : '', TRUE ),
|
||||
'client_secret' => new \IPS\Helpers\Form\Text( 'login_google_secret', ( isset( $this->settings['client_secret'] ) ) ? $this->settings['client_secret'] : '', TRUE ),
|
||||
'real_name' => new \IPS\Helpers\Form\YesNo( 'login_real_name', ( isset( $this->settings['real_name'] ) ) ? $this->settings['real_name'] : FALSE, TRUE )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
* @throws \UnexpectedValueException If response code is not 302
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://accounts.google.com/o/oauth2/auth" )->setQueryString( array(
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->settings['client_id'],
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/google/auth.php', 'none' ),
|
||||
'scope' => 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
|
||||
'state' => 'admin-' . \IPS\Session::i()->csrfKey
|
||||
) )->request()->get();
|
||||
|
||||
if ( $response->httpResponseCode != 200 )
|
||||
{
|
||||
\IPS\Log::log( "Google oauth error: " . json_encode( $response ), 'google_oauth' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( 'login_handler_Google' ) ) ) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( 'login_handler_Google' ) ) ) ) );
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return ( $member->google_id and $member->google_token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Trait for login handlers which redirect the user after clicking a button
|
||||
* @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 15 May 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Login Handler for handlers which redirect the user after clicking a button
|
||||
*/
|
||||
trait ButtonHandler
|
||||
{
|
||||
/**
|
||||
* Get type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function type()
|
||||
{
|
||||
return \IPS\Login::TYPE_BUTTON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function button()
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->loginButton( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
abstract public function authenticateButton( \IPS\Login $login );
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function buttonColor();
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function buttonIcon();
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function buttonText();
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in user cp sidebar
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForUcp()
|
||||
{
|
||||
return $this->buttonIcon();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,960 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief External Database Login Handler
|
||||
* @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 1 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Internal Database Login Handler
|
||||
*/
|
||||
class _ExternalDatabase extends \IPS\Login\Handler
|
||||
{
|
||||
/**
|
||||
* @brief Can we have multiple instances of this handler?
|
||||
*/
|
||||
public static $allowMultiple = TRUE;
|
||||
|
||||
use UsernamePasswordHandler;
|
||||
|
||||
/* !ACP Form */
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_External';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
$return = array(
|
||||
'login_external_conn',
|
||||
'sql_host' => new \IPS\Helpers\Form\Text( 'login_external_host', ( isset( $this->settings['sql_host'] ) and $this->settings['sql_host'] ) ? $this->settings['sql_host'] : 'localhost', TRUE ),
|
||||
'sql_user' => new \IPS\Helpers\Form\Text( 'login_external_user', isset( $this->settings['sql_user'] ) ? $this->settings['sql_user'] : NULL, TRUE ),
|
||||
'sql_pass' => new \IPS\Helpers\Form\Text( 'login_external_pass', isset( $this->settings['sql_pass'] ) ? $this->settings['sql_pass'] : NULL, TRUE ),
|
||||
'sql_database' => new \IPS\Helpers\Form\Text( 'login_external_database', isset( $this->settings['sql_database'] ) ? $this->settings['sql_database'] : NULL, TRUE ),
|
||||
'sql_port' => new \IPS\Helpers\Form\Number( 'login_external_port', isset( $this->settings['sql_port'] ) ? $this->settings['sql_port'] : 3306, FALSE ),
|
||||
'sql_socket' => new \IPS\Helpers\Form\Text( 'login_external_socket', isset( $this->settings['sql_socket'] ) ? $this->settings['sql_socket'] : NULL, FALSE ),
|
||||
'login_external_schema',
|
||||
'db_table' => new \IPS\Helpers\Form\Text( 'login_external_table', isset( $this->settings['db_table'] ) ? $this->settings['db_table'] : NULL, TRUE ),
|
||||
'db_col_id' => new \IPS\Helpers\Form\Text( 'login_external_id', isset( $this->settings['db_col_id'] ) ? $this->settings['db_col_id'] : NULL, FALSE ),
|
||||
'db_col_user' => new \IPS\Helpers\Form\Text( 'login_external_username', isset( $this->settings['db_col_user'] ) ? $this->settings['db_col_user'] : NULL, FALSE, array(), function( $val )
|
||||
{
|
||||
if ( !$val and \IPS\Request::i()->login_auth_types & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
throw new \DomainException('login_external_username_err');
|
||||
}
|
||||
} ),
|
||||
'db_col_email' => new \IPS\Helpers\Form\Text( 'login_external_email', isset( $this->settings['db_col_email'] ) ? $this->settings['db_col_email'] : NULL, FALSE, array(), function( $val )
|
||||
{
|
||||
if ( !$val and \IPS\Request::i()->login_auth_types & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
throw new \DomainException('login_external_email_err');
|
||||
}
|
||||
} ),
|
||||
'db_col_pass' => new \IPS\Helpers\Form\Text( 'login_external_password', isset( $this->settings['db_col_pass'] ) ? $this->settings['db_col_pass'] : NULL, TRUE ),
|
||||
'db_encryption' => new \IPS\Helpers\Form\Radio( 'login_external_encryption', ( isset( $this->settings['db_encryption'] ) and $this->settings['db_encryption'] ) ? $this->settings['db_encryption'] : NULL, TRUE, array(
|
||||
'options' => array(
|
||||
'password_hash' => 'login_external_encryption_password_hash',
|
||||
'md5' => 'MD5',
|
||||
'sha1' => 'SHA1',
|
||||
'plaintext' => 'login_external_encryption_plain',
|
||||
'other' => 'login_external_encryption_other',
|
||||
),
|
||||
'toggles' => array(
|
||||
'other' => array( 'db_encryption_hash', 'db_encryption_validate' )
|
||||
)
|
||||
) ),
|
||||
'db_encryption_hash' => new \IPS\Helpers\Form\Codemirror( 'login_external_encryption_hash', isset( $this->settings['db_encryption_hash'] ) ? $this->settings['db_encryption_hash'] : 'return password_hash( $providedPassword );', NULL, array(
|
||||
'mode' => 'php',
|
||||
'tags' => array( '$providedPassword' => \IPS\Member::loggedIn()->language()->addToStack('login_external_encryption_custom_password') )
|
||||
), function( $val )
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = eval( 'function _' . md5( mt_rand() ) . '() { ' . $val . ' }' );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
}
|
||||
}, NULL, NULL, 'db_encryption_hash' ),
|
||||
'db_encryption_validate' => new \IPS\Helpers\Form\Codemirror( 'login_external_encryption_validate', isset( $this->settings['db_encryption_validate'] ) ? $this->settings['db_encryption_validate'] : 'return password_verify( $providedPassword, $row[\'password\'] );', NULL, array(
|
||||
'mode' => 'php',
|
||||
'tags' => array( '$row' => \IPS\Member::loggedIn()->language()->addToStack('login_external_encryption_custom_row'), '$providedPassword' => \IPS\Member::loggedIn()->language()->addToStack('login_external_encryption_custom_password') )
|
||||
), function( $val )
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = eval( 'function _' . md5( mt_rand() ) . '() { ' . $val . ' }' );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
}
|
||||
}, NULL, NULL, 'db_encryption_validate' ),
|
||||
'db_extra' => new \IPS\Helpers\Form\Text( 'login_external_extra', isset( $this->settings['db_extra'] ) ? $this->settings['db_extra'] : NULL ),
|
||||
'login_settings',
|
||||
'auth_types' => new \IPS\Helpers\Form\Select( 'login_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : ( \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ), TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
) ) ),
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->allow_forgot_password == 'normal' or \IPS\Settings::i()->allow_forgot_password == 'handler' )
|
||||
{
|
||||
$return['forgot_password_url'] = new \IPS\Helpers\Form\Url( 'handler_forgot_password_url', isset( $this->settings['forgot_password_url'] ) ? $this->settings['forgot_password_url'] : NULL );
|
||||
\IPS\Member::loggedIn()->language()->words['handler_forgot_password_url_desc'] = \IPS\Member::loggedIn()->language()->addToStack( \IPS\Settings::i()->allow_forgot_password == 'normal' ? 'handler_forgot_password_url_desc_normal' : 'handler_forgot_password_url_deschandler' );
|
||||
}
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
$return['sync_name_changes'] = new \IPS\Helpers\Form\Radio( 'login_sync_name_changes', isset( $this->settings['sync_name_changes'] ) ? $this->settings['sync_name_changes'] : 1, FALSE, array( 'options' => array(
|
||||
1 => 'login_sync_changes_yes',
|
||||
0 => 'login_sync_changes_no',
|
||||
) ) );
|
||||
if ( \IPS\Settings::i()->allow_email_changes == 'normal' )
|
||||
{
|
||||
$return['sync_email_changes'] = new \IPS\Helpers\Form\Radio( 'login_sync_email_changes', isset( $this->settings['sync_email_changes'] ) ? $this->settings['sync_email_changes'] : 1, FALSE, array( 'options' => array(
|
||||
1 => 'login_sync_changes_yes',
|
||||
0 => 'login_sync_changes_no',
|
||||
) ) );
|
||||
}
|
||||
if ( \IPS\Settings::i()->allow_password_changes == 'normal' )
|
||||
{
|
||||
$return['sync_password_changes'] = new \IPS\Helpers\Form\Radio( 'login_sync_password_changes', isset( $this->settings['sync_password_changes'] ) ? $this->settings['sync_password_changes'] : 1, FALSE, array( 'options' => array(
|
||||
1 => 'login_sync_changes_yes',
|
||||
0 => 'login_sync_password_changes_no',
|
||||
) ) );
|
||||
}
|
||||
|
||||
$return['show_in_ucp'] = new \IPS\Helpers\Form\Radio( 'login_handler_show_in_ucp', isset( $this->settings['show_in_ucp'] ) ? $this->settings['show_in_ucp'] : 'disabled', FALSE, array(
|
||||
'options' => array(
|
||||
'always' => 'login_handler_show_in_ucp_always',
|
||||
'loggedin' => 'login_handler_show_in_ucp_loggedin',
|
||||
'disabled' => 'login_handler_show_in_ucp_disabled'
|
||||
),
|
||||
'toggles' => array(
|
||||
'always' => array( 'login_update_name_changes_inc_optional', 'login_update_email_changes_inc_optional' ),
|
||||
'loggedin' => array( 'login_update_name_changes_inc_optional', 'login_update_email_changes_inc_optional' ),
|
||||
'disabled' => array( 'login_update_name_changes_no_optional', 'login_update_email_changes_no_optional' ),
|
||||
)
|
||||
) );
|
||||
|
||||
$return['update_name_changes_inc_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes_inc_optional', isset( $this->settings['update_name_changes'] ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_name_changes_inc_optional' );
|
||||
$return['update_name_changes_no_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes_no_optional', ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] != 'optional' ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_name_changes_no_optional' );
|
||||
$return['update_email_changes_inc_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes_inc_optional', isset( $this->settings['update_email_changes'] ) ? $this->settings['update_email_changes'] : 'force', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_email_changes_inc_optional' );
|
||||
$return['update_email_changes_no_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes_no_optional', ( isset( $this->settings['update_email_changes'] ) and $this->settings['update_email_changes'] != 'optional' ) ? $this->settings['update_email_changes'] : 'force', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_email_changes_no_optional' );
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_name_changes_inc_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_name_changes');
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_name_changes_no_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_name_changes');
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_email_changes_inc_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_email_changes');
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_email_changes_no_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_email_changes');
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Handler Settings
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function acpFormSave( &$values )
|
||||
{
|
||||
$_values = $values;
|
||||
|
||||
$settings = parent::acpFormSave( $values );
|
||||
|
||||
if ( $_values['login_handler_show_in_ucp'] == 'never' )
|
||||
{
|
||||
$settings['update_name_changes'] = $_values['login_update_name_changes_no_optional'];
|
||||
$settings['update_email_changes'] = $_values['login_update_email_changes_no_optional'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$settings['update_name_changes'] = $_values['login_update_name_changes_inc_optional'];
|
||||
$settings['update_email_changes'] = $_values['login_update_email_changes_inc_optional'];
|
||||
}
|
||||
|
||||
unset( $settings['update_name_changes_inc_optional'] );
|
||||
unset( $settings['update_name_changes_no_optional'] );
|
||||
unset( $settings['update_email_changes_inc_optional'] );
|
||||
unset( $settings['update_email_changes_no_optional'] );
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
$select = array( $this->settings['db_col_pass'] );
|
||||
|
||||
if ( $this->settings['db_col_user'] )
|
||||
{
|
||||
$select[] = $this->settings['db_col_user'];
|
||||
}
|
||||
|
||||
if ( $this->settings['db_col_email'] )
|
||||
{
|
||||
$select[] = $this->settings['db_col_email'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->_externalDb()->select( implode( ',', $select ), $this->settings['db_table'], ( isset( $this->settings['db_extra'] ) AND $this->settings['db_extra'] != '' ) ? array( $this->settings['db_extra'] ) : NULL )->first();
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
// It's possible that no users exist, which is fine
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* !Authentication */
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
|
||||
{
|
||||
/* Fetch result */
|
||||
try
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $usernameOrEmail );
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
switch ( $this->authType() )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$type = 'username_or_email';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$type = 'username';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$type = 'email_address';
|
||||
break;
|
||||
}
|
||||
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $type ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Get a local account if one exists */
|
||||
$name = $this->settings['db_col_user'] ? $result[ $this->settings['db_col_user'] ] : NULL;
|
||||
$email = $this->settings['db_col_email'] ? $result[ $this->settings['db_col_email'] ] : NULL;
|
||||
$member = NULL;
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$link = \IPS\Db::i()->select( '*', 'core_login_links', array( 'token_login_method=? AND token_identifier=?', $this->id, $result[ $this->settings['db_col_id'] ] ) )->first();
|
||||
$member = \IPS\Member::load( $link['token_member'] );
|
||||
|
||||
/* If the user never finished the linking process, or the account has been deleted, discard this access token */
|
||||
if ( !$link['token_linked'] or !$member->member_id )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $link['token_member'] ) );
|
||||
$member = NULL;
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $name )
|
||||
{
|
||||
$_member = \IPS\Member::load( $name, 'name' );
|
||||
if ( $_member->member_id )
|
||||
{
|
||||
$member = $_member;
|
||||
}
|
||||
}
|
||||
if ( $email )
|
||||
{
|
||||
$_member = \IPS\Member::load( $email, 'email' );
|
||||
if ( $_member->member_id )
|
||||
{
|
||||
$member = $_member;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Verify password */
|
||||
if( !$this->_passwordIsValid( $result, $password ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
|
||||
/* Create account if we don't have one */
|
||||
if ( $member )
|
||||
{
|
||||
return $member;
|
||||
}
|
||||
elseif ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $login->type === \IPS\Login::LOGIN_UCP )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
|
||||
$exception->handler = $this;
|
||||
$exception->member = $login->reauthenticateAs;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$member = $this->createAccount( $name, $email );
|
||||
|
||||
\IPS\Db::i()->insert( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $member->member_id,
|
||||
'token_identifier' => $result[ $this->settings['db_col_id'] ],
|
||||
'token_linked' => 1,
|
||||
) );
|
||||
|
||||
$member->logHistory( 'core', 'social_account', array(
|
||||
'service' => static::getTitle(),
|
||||
'handler' => $this->id,
|
||||
'account_id' => $this->userId( $member ),
|
||||
'account_name' => $this->userProfileName( $member ),
|
||||
'linked' => TRUE,
|
||||
'registered' => TRUE
|
||||
) );
|
||||
|
||||
if ( $syncOptions = $this->syncOptions( $member, TRUE ) )
|
||||
{
|
||||
$profileSync = array();
|
||||
foreach ( $syncOptions as $option )
|
||||
{
|
||||
$profileSync[ $option ] = array( 'handler' => $this->id, 'ref' => NULL, 'error' => NULL );
|
||||
}
|
||||
$member->profilesync = $profileSync;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Login\Exception $exception )
|
||||
{
|
||||
if ( $exception->getCode() === \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $exception->member->member_id,
|
||||
'token_identifier' => $result[ $this->settings['db_col_id'] ],
|
||||
'token_linked' => 0,
|
||||
) );
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->createAccount( $name, $email );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticatePasswordForMember( \IPS\Member $member, $password )
|
||||
{
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $member->email );
|
||||
|
||||
if( $this->_passwordIsValid( $result, $password ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $member->name );
|
||||
|
||||
if( $this->_passwordIsValid( $result, $password ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get row from external database
|
||||
*
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @return array
|
||||
* @throws \UnderflowException
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function _getRowFromExternalDb( $usernameOrEmail )
|
||||
{
|
||||
/* Build where clause */
|
||||
switch ( $this->authType() )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$where = array( "{$this->settings['db_col_user']}=?", $usernameOrEmail );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_email']}=?", $usernameOrEmail );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_user']}=? OR {$this->settings['db_col_email']}=?", $usernameOrEmail, $usernameOrEmail );
|
||||
break;
|
||||
|
||||
}
|
||||
if ( $this->settings['db_extra'] )
|
||||
{
|
||||
$where[] = array( $this->settings['db_extra'] );
|
||||
}
|
||||
|
||||
/* Fetch */
|
||||
return $this->_externalDb()->select( '*', $this->settings['db_table'], $where )->first();
|
||||
}
|
||||
|
||||
/* !Other Login Handler Methods */
|
||||
|
||||
/**
|
||||
* Can this handler process a login for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
return parent::canProcess( $member );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_USERNAME and $member->name and $this->usernameIsInUse( $member->name ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_EMAIL and $member->email and $this->emailIsInUse( $member->email ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this handler process a password change for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canChangePassword( \IPS\Member $member )
|
||||
{
|
||||
if ( !isset( $this->settings['sync_password_changes'] ) or $this->settings['sync_password_changes'] )
|
||||
{
|
||||
return $this->canProcess( $member );
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
$where = array();
|
||||
$where[] = array( "{$this->settings['db_col_email']}=?", $email );
|
||||
|
||||
if ( $exclude )
|
||||
{
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $exclude->member_id ) )->first();
|
||||
$where[] = array( "{$this->settings['db_col_id']}<>?", $linkedId );
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->_externalDb()->select( $this->settings['db_col_email'], $this->settings['db_table'], $where )->first();
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
$where = array();
|
||||
$where[] = array( "{$this->settings['db_col_user']}=?", $username );
|
||||
|
||||
if ( $exclude )
|
||||
{
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $exclude->member_id ) )->first();
|
||||
$where[] = array( "{$this->settings['db_col_id']}<>?", $linkedId );
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->_externalDb()->select( $this->settings['db_col_user'], $this->settings['db_table'], $where )->first();
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @return void
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
if ( $this->settings['db_col_email'] and ( !isset( $this->settings['sync_email_changes'] ) or $this->settings['sync_email_changes'] ) )
|
||||
{
|
||||
$where = array( $this->settings['db_col_email'] . '=?', $oldEmail );
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
$where = array( "{$this->settings['db_col_id']}=?", $linkedId );
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
$this->_externalDb()->update( $this->settings['db_table'], array( $this->settings['db_col_email'] => $newEmail ), $where );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
if ( !isset( $this->settings['sync_password_changes'] ) or $this->settings['sync_password_changes'] )
|
||||
{
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
$where = array( "{$this->settings['db_col_id']}=?", $linkedId );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$where = '1=0';
|
||||
switch ( $this->authTypes )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$where = array( "{$this->settings['db_col_user']}=?", $member->name );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_email']}=?", $member->email );
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$where = array( "{$this->settings['db_col_email']}=? OR {$this->settings['db_col_user']}=?", $member->email, $member->name );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->_externalDb()->update( $this->settings['db_table'], array( $this->settings['db_col_pass'] => $this->_encryptedPassword( $newPassword ) ), $where );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
if ( $this->settings['db_col_user'] and ( !isset( $this->settings['sync_name_changes'] ) or $this->settings['sync_name_changes'] ) )
|
||||
{
|
||||
$where = array( $this->settings['db_col_user'] . '=?', $oldUsername );
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
$where = array( "{$this->settings['db_col_id']}=?", $linkedId );
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
$this->_externalDb()->update( $this->settings['db_table'], array( $this->settings['db_col_user'] => $newUsername ), $where );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot Password URL
|
||||
*
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function forgotPasswordUrl()
|
||||
{
|
||||
return ( isset( $this->settings['forgot_password_url'] ) and $this->settings['forgot_password_url'] ) ? \IPS\Http\Url::external( $this->settings['forgot_password_url'] ) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( isset( $this->settings['db_col_user'] ) and $this->settings['db_col_user'] )
|
||||
{
|
||||
$result = NULL;
|
||||
|
||||
try
|
||||
{
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
$result = $this->_externalDb()->select( '*', $this->settings['db_table'], array( "{$this->settings['db_col_id']}=?", $linkedId ) )->first();;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $member->email );
|
||||
}
|
||||
|
||||
if ( !$result and $this->authType() & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $member->name );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( $result )
|
||||
{
|
||||
return $result[ $this->settings['db_col_user'] ];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userEmail( \IPS\Member $member )
|
||||
{
|
||||
if ( isset( $this->settings['db_col_email'] ) and $this->settings['db_col_email'] )
|
||||
{
|
||||
$result = NULL;
|
||||
|
||||
try
|
||||
{
|
||||
if ( $this->settings['db_col_id'] )
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
$result = $this->_externalDb()->select( '*', $this->settings['db_table'], array( "{$this->settings['db_col_id']}=?", $linkedId ) )->first();
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $member->email );
|
||||
}
|
||||
|
||||
if ( !$result and $this->authType() & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
$result = $this->_getRowFromExternalDb( $member->name );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( $result )
|
||||
{
|
||||
return $result[ $this->settings['db_col_email'] ];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( isset( $this->settings['db_col_email'] ) and $this->settings['db_col_email'] and isset( $this->settings['update_email_changes'] ) and $this->settings['update_email_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['db_col_user'] ) and $this->settings['db_col_user'] and isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/* !Utility Methods */
|
||||
|
||||
/**
|
||||
* Get DB Connection
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
protected function _externalDb()
|
||||
{
|
||||
return \IPS\Db::i( 'external_login', $this->settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Password is valid
|
||||
*
|
||||
* @param array $row The member row
|
||||
* @param string $providedPassword The provided password
|
||||
* @return bool
|
||||
*/
|
||||
protected function _passwordIsValid( $row, $providedPassword )
|
||||
{
|
||||
switch ( $this->settings['db_encryption'] )
|
||||
{
|
||||
case 'password_hash':
|
||||
return password_verify( $providedPassword, $row[ $this->settings['db_col_pass'] ] );
|
||||
|
||||
case 'other':
|
||||
try
|
||||
{
|
||||
return @eval( $this->settings['db_encryption_validate'] );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'external_login' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'external_login' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
default:
|
||||
return \IPS\Login::compareHashes( $row[ $this->settings['db_col_pass'] ], $this->_encryptedPassword( $providedPassword ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypted password
|
||||
*
|
||||
* @param string $providedPassword The plaintext password
|
||||
* @return bool
|
||||
*/
|
||||
protected function _encryptedPassword( $providedPassword )
|
||||
{
|
||||
switch ( $this->settings['db_encryption'] )
|
||||
{
|
||||
case 'md5':
|
||||
return md5( $providedPassword );
|
||||
|
||||
case 'sha1':
|
||||
return sha1( $providedPassword );
|
||||
|
||||
case 'password_hash':
|
||||
return password_hash( $providedPassword );
|
||||
|
||||
case 'other':
|
||||
try
|
||||
{
|
||||
return @eval( $this->settings['db_encryption_hash'] );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'external_login' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'external_login' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
default:
|
||||
return $providedPassword;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,836 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract Login Handler
|
||||
* @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 12 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Login Handler
|
||||
*/
|
||||
abstract class _Handler extends \IPS\Node\Model
|
||||
{
|
||||
/**
|
||||
* Get all handler classes
|
||||
*/
|
||||
public static function handlerClasses()
|
||||
{
|
||||
return array(
|
||||
'IPS\Login\Handler\Standard',
|
||||
'IPS\Login\Handler\OAuth2\Facebook',
|
||||
'IPS\Login\Handler\OAuth2\Google',
|
||||
'IPS\Login\Handler\OAuth2\LinkedIn',
|
||||
'IPS\Login\Handler\OAuth2\Microsoft',
|
||||
'IPS\Login\Handler\OAuth1\Twitter',
|
||||
'IPS\Login\Handler\OAuth2\Invision',
|
||||
'IPS\Login\Handler\OAuth2\Wordpress',
|
||||
'IPS\Login\Handler\OAuth2\Custom',
|
||||
'IPS\Login\Handler\ExternalDatabase',
|
||||
'IPS\Login\Handler\LDAP',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a particular handler
|
||||
*
|
||||
* @param string $classname Classname
|
||||
* @return \IPS\Login\Hander|NULL
|
||||
*/
|
||||
public static function findMethod( $classname )
|
||||
{
|
||||
foreach ( \IPS\Login::methods() as $method )
|
||||
{
|
||||
if ( $method instanceof $classname )
|
||||
{
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* !Login Handler */
|
||||
|
||||
/**
|
||||
* @brief Can we have multiple instances of this handler?
|
||||
*/
|
||||
public static $allowMultiple = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Share Service
|
||||
*/
|
||||
public static $shareService = NULL;
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Handler Settings
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function acpFormSave( &$values )
|
||||
{
|
||||
$settings = array();
|
||||
foreach ( $this->acpForm() as $key => $field )
|
||||
{
|
||||
if ( is_object( $field ) )
|
||||
{
|
||||
$settings[ $key ] = $values[ $field->name ];
|
||||
unset( $values[ $field->name ] );
|
||||
}
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract public function type();
|
||||
|
||||
/**
|
||||
* Can this handler process a login for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return (bool) $this->_link( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached links
|
||||
*/
|
||||
protected $_cachedLinks = array();
|
||||
|
||||
/**
|
||||
* Get link
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return array
|
||||
*/
|
||||
protected function _link( \IPS\Member $member )
|
||||
{
|
||||
if ( !isset( $this->_cachedLinks[ $member->member_id ] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->_cachedLinks[ $member->member_id ] = \IPS\Db::i()->select( '*', 'core_login_links', array( 'token_login_method=? AND token_member=? AND token_linked=1', $this->id, $member->member_id ) )->first();
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
$this->_cachedLinks[ $member->member_id ] = NULL;
|
||||
}
|
||||
}
|
||||
return $this->_cachedLinks[ $member->member_id ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this handler process a password change for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canChangePassword( \IPS\Member $member )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $eclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @param \IPS\Member|NULL $eclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email
|
||||
* @param string $newEmail New Email
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot Password URL
|
||||
*
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function forgotPasswordUrl()
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an account from login - checks registration is enabled, the name/email doesn't already exists and calls the spam service
|
||||
*
|
||||
* @param string $name The desired username. If not provided, not allowed, or another existing user has this name, it will be left blank and the user prompted to provide it.
|
||||
* @param string $email The user's email address. If it matches an existing account, an \IPS\Login\Exception object will be thrown so the user can be prompted to link those accounts. If not provided, it will be left blank and the user prompted to provide it.
|
||||
* @param bool $allowCreateAccount If an account can be created
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception If email address matches (\IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT), registration is disabled (IPS\Login\Exception::REGISTRATION_DISABLED) or the spam service denies registration (\IPS\Login\Exception::REGISTRATION_DENIED_BY_SPAM_SERVICE)
|
||||
*/
|
||||
protected function createAccount( $name=NULL, $email=NULL, $allowCreateAccount=TRUE )
|
||||
{
|
||||
/* Is there an existing user with the same email address? */
|
||||
if ( $email )
|
||||
{
|
||||
$existingAccount = \IPS\Member::load( $email, 'email' );
|
||||
if ( $existingAccount->member_id )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'link_your_accounts_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
|
||||
$exception->handler = $this;
|
||||
$exception->member = $existingAccount;
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nope - we need to register one - can we do that? */
|
||||
if( !$this->register or !$allowCreateAccount )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( \IPS\Login::registrationType() == 'disabled' ? 'reg_disabled' : 'reg_not_allowed_by_login', \IPS\Login\Exception::REGISTRATION_DISABLED );
|
||||
$exception->handler = $this;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/* Create the account */
|
||||
$member = new \IPS\Member;
|
||||
$member->member_group_id = \IPS\Settings::i()->member_group;
|
||||
$member->members_bitoptions['view_sigs'] = TRUE;
|
||||
$member->members_bitoptions['must_reaccept_terms'] = (bool) \IPS\Settings::i()->force_reg_terms;
|
||||
if ( $name and ( !\IPS\Settings::i()->username_characters or preg_match( '/^[' . str_replace( '\-', '-', preg_quote( \IPS\Settings::i()->username_characters, '/' ) ) . ']*$/i', $name ) ) )
|
||||
{
|
||||
$existingUsername = \IPS\Member::load( $name, 'name' );
|
||||
if ( !$existingUsername->member_id )
|
||||
{
|
||||
$member->name = $name;
|
||||
}
|
||||
}
|
||||
$spamCode = NULL;
|
||||
$spamAction = NULL;
|
||||
if ( $email )
|
||||
{
|
||||
/* Check it's an allowed domain */
|
||||
$allowed = TRUE;
|
||||
if ( \IPS\Settings::i()->allowed_reg_email and $allowedEmailDomains = explode( ',', \IPS\Settings::i()->allowed_reg_email ) )
|
||||
{
|
||||
$allowed = FALSE;
|
||||
foreach ( $allowedEmailDomains AS $domain )
|
||||
{
|
||||
if( \mb_stripos( $this->value, "@" . $domain ) !== FALSE )
|
||||
{
|
||||
$allowed = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $allowed )
|
||||
{
|
||||
$member->email = $email;
|
||||
}
|
||||
|
||||
/* Check the spam service is okay with it */
|
||||
if( \IPS\Settings::i()->spam_service_enabled )
|
||||
{
|
||||
$spamAction = $member->spamService( 'register', NULL, $spamCode );
|
||||
if( $spamAction == 4 )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'spam_denied_account', \IPS\Login\Exception::REGISTRATION_DENIED_BY_SPAM_SERVICE );
|
||||
$exception->handler = $this;
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
$member->save();
|
||||
$member->logHistory( 'core', 'account', array( 'type' => 'register_handler', 'service' => static::getTitle(), 'handler' => $this->id, 'spamCode' => $spamCode, 'spamAction' => $spamAction, 'complete' => (bool) ( $member->real_name and $member->email ) ), FALSE );
|
||||
|
||||
/* Create a device setting $sendNewDeviceEmail to false so that when we hand back to the login
|
||||
handler is doesn't send the new device email */
|
||||
\IPS\Member\Device::loadOrCreate( $member, FALSE )->save();
|
||||
|
||||
/* If registration is complete, do post-registration stuff */
|
||||
if ( $member->real_name and $member->email and !$member->members_bitoptions['bw_is_spammer'] )
|
||||
{
|
||||
$member->postRegistration( TRUE );
|
||||
}
|
||||
|
||||
/* Return our new member */
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param mixed $details Details as they were passed to the exception
|
||||
* @return void
|
||||
*/
|
||||
public function completeLink( \IPS\Member $member, $details )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_login_links', array( 'token_linked' => 1 ), array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) );
|
||||
|
||||
$member->logHistory( 'core', 'social_account', array(
|
||||
'service' => static::getTitle(),
|
||||
'handler' => $this->id,
|
||||
'account_id' => $this->userId( $member ),
|
||||
'account_name' => $this->userProfileName( $member ),
|
||||
'linked' => TRUE,
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink Account
|
||||
*
|
||||
* @param \IPS\Member $member The member or NULL for currently logged in member
|
||||
* @return void
|
||||
*/
|
||||
public function disassociate( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
$member->logHistory( 'core', 'social_account', array(
|
||||
'service' => static::getTitle(),
|
||||
'handler' => $this->id,
|
||||
'account_id' => $this->userId( $member ),
|
||||
'account_name' => $this->userProfileName( $member ),
|
||||
'linked' => FALSE,
|
||||
) );
|
||||
|
||||
\IPS\Db::i()->delete( 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in user cp sidebar
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForUcp()
|
||||
{
|
||||
return $this->logoForDeviceInformation() ?: 'database';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show in Account Settings?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member, or NULL for if it should show generally
|
||||
* @return bool
|
||||
*/
|
||||
public function showInUcp( \IPS\Member $member = NULL )
|
||||
{
|
||||
if ( isset( $this->settings['show_in_ucp'] ) )
|
||||
{
|
||||
switch ( $this->settings['show_in_ucp'] )
|
||||
{
|
||||
case 'always':
|
||||
return TRUE;
|
||||
|
||||
case 'loggedin':
|
||||
return ( $member and $this->canProcess( $member ) );
|
||||
|
||||
case 'disabled':
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's identifier (may not be a number)
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userId( \IPS\Member $member )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userEmail( \IPS\Member $member )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's cover photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userCoverPhoto( \IPS\Member $member )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's statuses since a particular date
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @param \IPS\DateTime|NULL $since Date/Time to get statuses since then, or NULL to get the latest one
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userStatuses( \IPS\Member $member, \IPS\DateTime $since = NULL )
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link to user's remote profile
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( $identifier, $username )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse status text - ensures valid and safe HTML, filters profanity, etc.
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
protected function _parseStatusText( \IPS\Member $member, $value )
|
||||
{
|
||||
/* Make sure utf8mb4 characters won't cause us issues */
|
||||
$value = \IPS\Text\Parser::utf8mb4SafeDecode( $value );
|
||||
|
||||
/* Parse */
|
||||
$value = \IPS\Text\Parser::parseStatic( $value, FALSE, NULL, $member, 'core_Members' );
|
||||
|
||||
/* Return */
|
||||
return $value;
|
||||
}
|
||||
|
||||
/* !ActiveRecord & Node */
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_login_methods';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'login_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'login_handlers';
|
||||
|
||||
/**
|
||||
* @brief [Node] Order Database Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'order';
|
||||
|
||||
/**
|
||||
* @brief [Node] Enabled/Disabled Column
|
||||
*/
|
||||
public static $databaseColumnEnabledDisabled = 'enabled';
|
||||
|
||||
/**
|
||||
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
||||
*/
|
||||
public static $titleLangPrefix = 'login_method_';
|
||||
|
||||
/**
|
||||
* Construct ActiveRecord from database row
|
||||
*
|
||||
* @param array $data Row from database table
|
||||
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
|
||||
* @return static
|
||||
*/
|
||||
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
|
||||
{
|
||||
$classname = $data['login_classname'];
|
||||
if ( !class_exists( $classname ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
/* Initiate an object */
|
||||
$obj = new $classname;
|
||||
$obj->_new = FALSE;
|
||||
$obj->_data = array();
|
||||
|
||||
/* Import data */
|
||||
$databasePrefixLength = \strlen( static::$databasePrefix );
|
||||
foreach ( $data as $k => $v )
|
||||
{
|
||||
if( static::$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
|
||||
{
|
||||
$k = \substr( $k, $databasePrefixLength );
|
||||
}
|
||||
|
||||
$obj->_data[ $k ] = $v;
|
||||
}
|
||||
$obj->changed = array();
|
||||
|
||||
/* Init */
|
||||
if ( method_exists( $obj, 'init' ) )
|
||||
{
|
||||
$obj->init();
|
||||
}
|
||||
|
||||
/* If it doesn't exist in the multiton store, set it */
|
||||
if( !isset( static::$multitons[ $data['login_id'] ] ) )
|
||||
{
|
||||
static::$multitons[ $data['login_id'] ] = $obj;
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_settings()
|
||||
{
|
||||
return ( isset( $this->_data['settings'] ) and $this->_data['settings'] ) ? json_decode( $this->_data['settings'], TRUE ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set settings
|
||||
*
|
||||
* @param array $values Values
|
||||
* @return void
|
||||
*/
|
||||
public function set_settings( $values )
|
||||
{
|
||||
$this->_data['settings'] = json_encode( $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to copy this node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canCopy()
|
||||
{
|
||||
if ( !static::$allowMultiple )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
return parent::canCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to delete this node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canDelete()
|
||||
{
|
||||
if ( parent::canDelete() )
|
||||
{
|
||||
return count( static::roots() ) > 1;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* !AdminCP Management */
|
||||
|
||||
protected static $enableAcpLoginByDefault = TRUE;
|
||||
|
||||
/**
|
||||
* [Node] Add/Edit Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
$form->addHeader('login_method_basic_settings');
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'login_method_name', $this->id ? NULL : \IPS\Member::loggedIn()->language()->addToStack( static::getTitle() ), TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'login_method_' . $this->id : NULL ) ) ) );
|
||||
if ( !( $this instanceof \IPS\Login\Handler\Standard ) )
|
||||
{
|
||||
$self = $this;
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'login_acp', $this->id ? $this->acp : static::$enableAcpLoginByDefault, FALSE, array(), function( $val ) use ( $self )
|
||||
{
|
||||
if ( !$val )
|
||||
{
|
||||
foreach ( \IPS\Login::methods() as $method )
|
||||
{
|
||||
if ( $method != $self and $method->canProcess( \IPS\Member::loggedIn() ) and $method->acp )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
throw new \DomainException( 'login_handler_cannot_disable_acp' );
|
||||
}
|
||||
} ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'login_register', $this->id ? $this->register : TRUE, FALSE, array(
|
||||
'options' => array(
|
||||
1 => 'login_register_enabled',
|
||||
0 => 'login_register_disabled'
|
||||
),
|
||||
'toggles' => array(
|
||||
1 => array( 'login_real_name', 'login_real_email' )
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
foreach ( $this->acpForm() as $key => $field )
|
||||
{
|
||||
if ( is_string( $field ) )
|
||||
{
|
||||
$form->addHeader( $field );
|
||||
}
|
||||
elseif ( is_array( $field ) )
|
||||
{
|
||||
$form->addHeader( $field[0] );
|
||||
$form->addMessage( $field[1] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$form->add( $field );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( static::$shareService ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$shareService = \IPS\core\ShareLinks\Service::load( static::$shareService, 'share_key' );
|
||||
$form->addHeader( 'sharelinks' );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'share_autoshare_' . static::$shareService, $shareService->autoshare ) );
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Save Add/Edit Form
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return void
|
||||
*/
|
||||
public function saveForm( $values )
|
||||
{
|
||||
if ( isset( static::$shareService ) and isset( $values[ 'share_autoshare_' . static::$shareService ] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$shareService = \IPS\core\ShareLinks\Service::load( static::$shareService, 'share_key' );
|
||||
$shareService->autoshare = $values[ 'share_autoshare_' . static::$shareService ];
|
||||
$shareService->save();
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
unset( $values[ 'share_autoshare_' . static::$shareService ] );
|
||||
}
|
||||
|
||||
parent::saveForm( $values );
|
||||
}
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
$settings = $this->acpFormSave( $values );
|
||||
$values['login_settings'] = $settings;
|
||||
$this->settings = $settings;
|
||||
$this->testSettings();
|
||||
|
||||
if( isset( $values['login_method_name'] ) )
|
||||
{
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
\IPS\Lang::saveCustom( 'core', "login_method_{$this->id}", $values['login_method_name'] );
|
||||
unset( $values['login_method_name'] );
|
||||
}
|
||||
|
||||
return parent::formatFormValues( $values );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save Changed Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
parent::save();
|
||||
unset( \IPS\Data\Store::i()->loginMethods );
|
||||
\IPS\Data\Cache::i()->clearAll();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief LDAP Exception
|
||||
* @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 6 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\LDAP;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP Exception
|
||||
*/
|
||||
class _Exception extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief LDAP Login Handler
|
||||
* @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 2 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP Database Login Handler
|
||||
*/
|
||||
class _LDAP extends \IPS\Login\Handler
|
||||
{
|
||||
/**
|
||||
* @brief Can we have multiple instances of this handler?
|
||||
*/
|
||||
public static $allowMultiple = TRUE;
|
||||
|
||||
use UsernamePasswordHandler;
|
||||
|
||||
/* !ACP Form */
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Ldap';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
$return = array(
|
||||
'ldap_header',
|
||||
'server_protocol' => new \IPS\Helpers\Form\Radio( 'ldap_server_protocol', isset( $this->settings['server_protocol'] ) ? $this->settings['server_protocol'] : 3, TRUE, array( 'options' => array( 3 => 'V3', 2 => 'V2' ) ) ),
|
||||
'server_host' => new \IPS\Helpers\Form\Text( 'ldap_server_host', isset( $this->settings['server_host'] ) ? $this->settings['server_host'] : NULL, TRUE, array( 'placeholder' => 'ldap.example.com' ) ),
|
||||
'server_port' => new \IPS\Helpers\Form\Number( 'ldap_server_port', isset( $this->settings['server_port'] ) ? $this->settings['server_port'] : 389, TRUE ),
|
||||
'server_user' => new \IPS\Helpers\Form\Text( 'ldap_server_user', isset( $this->settings['server_user'] ) ? $this->settings['server_user'] : NULL ),
|
||||
'server_pass' => new \IPS\Helpers\Form\Text( 'ldap_server_pass', isset( $this->settings['server_pass'] ) ? $this->settings['server_pass'] : NULL ),
|
||||
'opt_referrals' => new \IPS\Helpers\Form\YesNo( 'ldap_opt_referrals', isset( $this->settings['opt_referrals'] ) ? $this->settings['opt_referrals'] : TRUE, TRUE ),
|
||||
'ldap_directory',
|
||||
'base_dn' => new \IPS\Helpers\Form\Text( 'ldap_base_dn', isset( $this->settings['base_dn'] ) ? $this->settings['base_dn'] : NULL, TRUE, array( 'placeholder' => 'dc=example,dc=com' ) ),
|
||||
'uid_field' => new \IPS\Helpers\Form\Text( 'ldap_uid_field', isset( $this->settings['uid_field'] ) ? $this->settings['uid_field'] : 'uid', TRUE ),
|
||||
'un_suffix' => new \IPS\Helpers\Form\Text( 'ldap_un_suffix', isset( $this->settings['un_suffix'] ) ? $this->settings['un_suffix'] : NULL, FALSE, array( 'placeholder' => '@example.com' ) ),
|
||||
'name_field' => new \IPS\Helpers\Form\Text( 'ldap_name_field', $this->_nameField() ?: 'cn' ),
|
||||
'email_field' => new \IPS\Helpers\Form\Text( 'ldap_email_field', isset( $this->settings['email_field'] ) ? $this->settings['email_field'] : 'mail' ),
|
||||
'filter' => new \IPS\Helpers\Form\Text( 'ldap_filter', isset( $this->settings['filter'] ) ? $this->settings['filter'] : NULL, FALSE, array( 'placeholder' => 'ou=your_department' ) ),
|
||||
'login_settings',
|
||||
'auth_types' => new \IPS\Helpers\Form\Select( 'login_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : ( \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ), TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
) ) ),
|
||||
'pw_required' => new \IPS\Helpers\Form\YesNo( 'ldap_pw_required', isset( $this->settings['pw_required'] ) ? $this->settings['pw_required'] : TRUE ),
|
||||
);
|
||||
if ( \IPS\Settings::i()->allow_forgot_password == 'normal' or \IPS\Settings::i()->allow_forgot_password == 'handler' )
|
||||
{
|
||||
$return['forgot_password_url'] = new \IPS\Helpers\Form\Url( 'handler_forgot_password_url', isset( $this->settings['forgot_password_url'] ) ? $this->settings['forgot_password_url'] : NULL );
|
||||
\IPS\Member::loggedIn()->language()->words['handler_forgot_password_url_desc'] = \IPS\Member::loggedIn()->language()->addToStack( \IPS\Settings::i()->allow_forgot_password == 'normal' ? 'handler_forgot_password_url_desc_normal' : 'handler_forgot_password_url_deschandler' );
|
||||
}
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
$return['sync_name_changes'] = new \IPS\Helpers\Form\Radio( 'login_sync_name_changes', isset( $this->settings['sync_name_changes'] ) ? $this->settings['sync_name_changes'] : 1, FALSE, array( 'options' => array(
|
||||
1 => 'login_sync_changes_yes',
|
||||
0 => 'login_sync_changes_no',
|
||||
) ) );
|
||||
if ( \IPS\Settings::i()->allow_email_changes == 'normal' )
|
||||
{
|
||||
$return['sync_email_changes'] = new \IPS\Helpers\Form\Radio( 'login_sync_email_changes', isset( $this->settings['sync_email_changes'] ) ? $this->settings['sync_email_changes'] : 1, FALSE, array( 'options' => array(
|
||||
1 => 'login_sync_changes_yes',
|
||||
0 => 'login_sync_changes_no',
|
||||
) ) );
|
||||
}
|
||||
if ( \IPS\Settings::i()->allow_password_changes == 'normal' )
|
||||
{
|
||||
$return['sync_password_changes'] = new \IPS\Helpers\Form\Radio( 'login_sync_password_changes', isset( $this->settings['sync_password_changes'] ) ? $this->settings['sync_password_changes'] : 1, FALSE, array( 'options' => array(
|
||||
1 => 'login_sync_changes_yes',
|
||||
0 => 'login_sync_password_changes_no',
|
||||
) ) );
|
||||
}
|
||||
|
||||
$return['show_in_ucp'] = new \IPS\Helpers\Form\Radio( 'login_handler_show_in_ucp', isset( $this->settings['show_in_ucp'] ) ? $this->settings['show_in_ucp'] : 'disabled', FALSE, array(
|
||||
'options' => array(
|
||||
'always' => 'login_handler_show_in_ucp_always',
|
||||
'loggedin' => 'login_handler_show_in_ucp_loggedin',
|
||||
'disabled' => 'login_handler_show_in_ucp_disabled'
|
||||
),
|
||||
'toggles' => array(
|
||||
'always' => array( 'login_update_name_changes_inc_optional', 'login_update_email_changes_inc_optional' ),
|
||||
'loggedin' => array( 'login_update_name_changes_inc_optional', 'login_update_email_changes_inc_optional' ),
|
||||
'disabled' => array( 'login_update_name_changes_no_optional', 'login_update_email_changes_no_optional' ),
|
||||
)
|
||||
) );
|
||||
|
||||
$return['update_name_changes_inc_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes_inc_optional', isset( $this->settings['update_name_changes'] ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_name_changes_inc_optional' );
|
||||
$return['update_name_changes_no_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes_no_optional', ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] != 'optional' ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_name_changes_no_optional' );
|
||||
$return['update_email_changes_inc_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes_inc_optional', isset( $this->settings['update_email_changes'] ) ? $this->settings['update_email_changes'] : 'force', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_email_changes_inc_optional' );
|
||||
$return['update_email_changes_no_optional'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes_no_optional', ( isset( $this->settings['update_email_changes'] ) and $this->settings['update_email_changes'] != 'optional' ) ? $this->settings['update_email_changes'] : 'force', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_email_changes_no_optional' );
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_name_changes_inc_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_name_changes');
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_name_changes_no_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_name_changes');
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_email_changes_inc_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_email_changes');
|
||||
\IPS\Member::loggedIn()->language()->words['login_update_email_changes_no_optional'] = \IPS\Member::loggedIn()->language()->addToStack('login_update_email_changes');
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Handler Settings
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function acpFormSave( &$values )
|
||||
{
|
||||
$_values = $values;
|
||||
|
||||
$settings = parent::acpFormSave( $values );
|
||||
|
||||
if ( $_values['login_handler_show_in_ucp'] == 'never' )
|
||||
{
|
||||
$settings['update_name_changes'] = $_values['login_update_name_changes_no_optional'];
|
||||
$settings['update_email_changes'] = $_values['login_update_email_changes_no_optional'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$settings['update_name_changes'] = $_values['login_update_name_changes_inc_optional'];
|
||||
$settings['update_email_changes'] = $_values['login_update_email_changes_inc_optional'];
|
||||
}
|
||||
|
||||
unset( $settings['update_name_changes_inc_optional'] );
|
||||
unset( $settings['update_name_changes_no_optional'] );
|
||||
unset( $settings['update_email_changes_inc_optional'] );
|
||||
unset( $settings['update_email_changes_no_optional'] );
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
if ( !extension_loaded('ldap') )
|
||||
{
|
||||
throw new \InvalidArgumentException( 'login_ldap_err' );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->_ldap();
|
||||
}
|
||||
catch ( LDAP\Exception $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( $e->getMessage() ?: 'login_ldap_err_connect' );
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* !Authentication */
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = NULL;
|
||||
|
||||
/* Try email address */
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$result = $this->_getUserWithFilter( $this->settings['email_field'] . '=' . ldap_escape( $usernameOrEmail, NULL, LDAP_ESCAPE_FILTER ) );
|
||||
}
|
||||
|
||||
/* Try username */
|
||||
if ( !$result and $this->authType() & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
$result = $this->_getUserWithFilter( $this->_nameField() . '=' . ldap_escape( $usernameOrEmail . $this->settings['un_suffix'], NULL, LDAP_ESCAPE_FILTER ) );
|
||||
}
|
||||
|
||||
/* Don't have anything? */
|
||||
if ( !$result )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('username') ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Get a local account if one exists */
|
||||
$attrs = @ldap_get_attributes( $this->_ldap(), $result );
|
||||
if ( !$attrs )
|
||||
{
|
||||
throw new LDAP\Exception( ldap_error( $this->_ldap() ), ldap_errno( $this->_ldap() ) );
|
||||
}
|
||||
$nameField = $this->_nameField();
|
||||
$name = ( $nameField and isset( $attrs[ $nameField ] ) ) ? $attrs[ $nameField ][0] : NULL;
|
||||
$email = ( $this->settings['email_field'] and isset( $attrs[ $this->settings['email_field'] ] ) ) ? $attrs[ $this->settings['email_field'] ][0] : NULL;
|
||||
$member = NULL;
|
||||
try
|
||||
{
|
||||
$link = \IPS\Db::i()->select( '*', 'core_login_links', array( 'token_login_method=? AND token_identifier=?', $this->id, $attrs[ $this->settings['uid_field'] ][0] ) )->first();
|
||||
$member = \IPS\Member::load( $link['token_member'] );
|
||||
|
||||
/* If the user never finished the linking process, or the account has been deleted, discard this access token */
|
||||
if ( !$link['token_linked'] or !$member->member_id )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $link['token_member'] ) );
|
||||
$member = NULL;
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
|
||||
/* Verify password */
|
||||
if ( !$this->_passwordIsValid( $result, $password ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
|
||||
/* Create account if we don't have one */
|
||||
if ( $member )
|
||||
{
|
||||
return $member;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $login->type === \IPS\Login::LOGIN_UCP )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
|
||||
$exception->handler = $this;
|
||||
$exception->member = $login->reauthenticateAs;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$member = $this->createAccount( $name, $email );
|
||||
|
||||
\IPS\Db::i()->insert( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $member->member_id,
|
||||
'token_identifier' => $attrs[ $this->settings['uid_field'] ][0],
|
||||
'token_linked' => 1,
|
||||
) );
|
||||
|
||||
$member->logHistory( 'core', 'social_account', array(
|
||||
'service' => static::getTitle(),
|
||||
'handler' => $this->id,
|
||||
'account_id' => $this->userId( $member ),
|
||||
'account_name' => $this->userProfileName( $member ),
|
||||
'linked' => TRUE,
|
||||
'registered' => TRUE
|
||||
) );
|
||||
|
||||
if ( $syncOptions = $this->syncOptions( $member, TRUE ) )
|
||||
{
|
||||
$profileSync = array();
|
||||
foreach ( $syncOptions as $option )
|
||||
{
|
||||
$profileSync[ $option ] = array( 'handler' => $this->id, 'ref' => NULL, 'error' => NULL );
|
||||
}
|
||||
$member->profilesync = $profileSync;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Login\Exception $exception )
|
||||
{
|
||||
if ( $exception->getCode() === \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $exception->member->member_id,
|
||||
'token_identifier' => $attrs[ $this->settings['uid_field'] ][0],
|
||||
'token_linked' => 0,
|
||||
) );
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( LDAP\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'ldap' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticatePasswordForMember( \IPS\Member $member, $password )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['uid_field'] . '=' . ldap_escape( $linkedId, NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
return $this->_passwordIsValid( $result, $password );
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* !Utility Methods */
|
||||
|
||||
protected $_ldap;
|
||||
|
||||
/**
|
||||
* Get LDAP Connection
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Login\Handler\LDAP\Exception
|
||||
*/
|
||||
protected function _ldap()
|
||||
{
|
||||
if ( !$this->_ldap )
|
||||
{
|
||||
$this->_ldap = ldap_connect( $this->settings['server_host'], ( isset( $this->settings['server_port'] ) and $this->settings['server_port'] ) ? intval( $this->settings['server_port'] ) : 389 );
|
||||
if ( !$this->_ldap )
|
||||
{
|
||||
throw new LDAP\Exception;
|
||||
}
|
||||
|
||||
@ldap_set_option( $this->_ldap, LDAP_OPT_PROTOCOL_VERSION, $this->settings['server_protocol'] );
|
||||
@ldap_set_option( $this->_ldap, LDAP_OPT_REFERRALS, (bool) $this->settings['opt_referrals'] );
|
||||
|
||||
if ( !@ldap_bind( $this->_ldap, ( isset( $this->settings['server_user'] ) and $this->settings['server_user'] ) ? $this->settings['server_user'] : NULL, ( isset( $this->settings['server_pass'] ) and $this->settings['server_pass'] ) ? $this->settings['server_pass'] : NULL ) )
|
||||
{
|
||||
throw new LDAP\Exception( ldap_error( $this->_ldap ), ldap_errno( $this->_ldap ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_ldap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function _nameField()
|
||||
{
|
||||
return isset( $this->settings['name_field'] ) ? $this->settings['name_field'] : ( isset( $this->settings['uid_field'] ) ? $this->settings['uid_field'] : NULL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user
|
||||
*
|
||||
* @param string $filter Filter
|
||||
* @return resource|NULL
|
||||
*/
|
||||
protected function _getUserWithFilter( $filter )
|
||||
{
|
||||
/* Add any additional filter */
|
||||
if ( $this->settings['filter'] )
|
||||
{
|
||||
$filter = ( mb_substr( $this->settings['filter'], 0, 1 ) === '(' ) ? "(&({$filter}){$this->settings['filter']})" : "(&({$filter})({$this->settings['filter']}))";
|
||||
}
|
||||
|
||||
/* Search */
|
||||
$search = @ldap_search( $this->_ldap(), $this->settings['base_dn'], $filter );
|
||||
if ( !$search )
|
||||
{
|
||||
throw new LDAP\Exception( ldap_error( $this->_ldap() ), ldap_errno( $this->_ldap() ) );
|
||||
}
|
||||
|
||||
/* Get result */
|
||||
$result = @ldap_first_entry( $this->_ldap(), $search );
|
||||
if ( !$result )
|
||||
{
|
||||
if ( $errno = ldap_errno( $this->_ldap ) )
|
||||
{
|
||||
throw new LDAP\Exception( ldap_error( $this->_ldap() ), $errno );
|
||||
}
|
||||
else
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Password is valid
|
||||
*
|
||||
* @param resource $result The resource from LDAP
|
||||
* @param string $providedPassword The provided password
|
||||
* @return bool
|
||||
*/
|
||||
protected function _passwordIsValid( $result, $providedPassword )
|
||||
{
|
||||
return (bool) @ldap_bind( $this->_ldap(), ldap_get_dn( $this->_ldap(), $result ), ( $this->settings['pw_required'] ? $providedPassword : '' ) );
|
||||
}
|
||||
|
||||
/* !Other Login Handler Methods */
|
||||
|
||||
/**
|
||||
* Can this handler process a password change for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canChangePassword( \IPS\Member $member )
|
||||
{
|
||||
if ( !isset( $this->settings['sync_password_changes'] ) or $this->settings['sync_password_changes'] )
|
||||
{
|
||||
return $this->canProcess( $member );
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
if ( $this->settings['email_field'] )
|
||||
{
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['email_field'] . '=' . ldap_escape( $email, NULL, LDAP_ESCAPE_FILTER )) )
|
||||
{
|
||||
if ( $exclude )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $exclude->member_id ) )->first();
|
||||
|
||||
if ( $attrs = @ldap_get_attributes( $this->_ldap(), $result ) and $attrs[ $this->settings['uid_field'] ][0] == $linkedId )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
if ( $this->_nameField() )
|
||||
{
|
||||
if ( $result = $this->_getUserWithFilter( $this->_nameField() . '=' . ldap_escape( $username, NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
if ( $exclude )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $exclude->member_id ) )->first();
|
||||
|
||||
if ( $attrs = @ldap_get_attributes( $this->_ldap(), $result ) and $attrs[ $this->settings['uid_field'] ][0] == $linkedId )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @return void
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
if ( !isset( $this->settings['sync_email_changes'] ) or $this->settings['sync_email_changes'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['uid_field'] . '=' . ldap_escape( $linkedId, NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
if ( !@ldap_modify( $this->_ldap(), ldap_get_dn( $this->_ldap(), $result ), array( $this->settings['email_field'] => $newEmail ) ) )
|
||||
{
|
||||
$e = new LDAP\Exception( ldap_error( $this->_ldap() ), ldap_errno( $this->_ldap() ) );
|
||||
\IPS\Log::log( $e, 'ldap' );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
if ( !isset( $this->settings['sync_password_changes'] ) or $this->settings['sync_password_changes'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['uid_field'] . '=' . ldap_escape( $linkedId, NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
if ( !@ldap_modify( $this->_ldap(), ldap_get_dn( $this->_ldap(), $result ), array( 'userPassword' => "{SHA}" . base64_encode( pack( "H*", sha1( $newPassword ) ) ) ) ) )
|
||||
{
|
||||
$e = new LDAP\Exception( ldap_error( $this->_ldap() ), ldap_errno( $this->_ldap() ) );
|
||||
\IPS\Log::log( $e, 'ldap' );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
if ( !isset( $this->settings['sync_name_changes'] ) or $this->settings['sync_name_changes'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$linkedId = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) )->first();
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['uid_field'] . '=' . ldap_escape( $linkedId, NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
if ( !@ldap_modify( $this->_ldap(), ldap_get_dn( $this->_ldap(), $result ), array( $this->_nameField() => $newUsername ) ) )
|
||||
{
|
||||
$e = new LDAP\Exception( ldap_error( $this->_ldap() ), ldap_errno( $this->_ldap() ) );
|
||||
\IPS\Log::log( $e, 'ldap' );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot Password URL
|
||||
*
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function forgotPasswordUrl()
|
||||
{
|
||||
return ( isset( $this->settings['forgot_password_url'] ) and $this->settings['forgot_password_url'] ) ? \IPS\Http\Url::external( $this->settings['forgot_password_url'] ) : NULL;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( $nameField = $this->_nameField() )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['uid_field'] . '=' . ldap_escape( $link['token_identifier'], NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
if ( $attrs = @ldap_get_attributes( $this->_ldap(), $result ) and isset( $attrs[ $nameField ] ) )
|
||||
{
|
||||
return $attrs[ $nameField ][0];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userEmail( \IPS\Member $member )
|
||||
{
|
||||
if ( $this->settings['email_field'] )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( $result = $this->_getUserWithFilter( $this->settings['uid_field'] . '=' . ldap_escape( $link['token_identifier'], NULL, LDAP_ESCAPE_FILTER ) ) )
|
||||
{
|
||||
if ( $attrs = @ldap_get_attributes( $this->_ldap(), $result ) and isset( $attrs[ $this->settings['email_field'] ] ) )
|
||||
{
|
||||
return $attrs[ $this->settings['email_field'] ][0];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( isset( $this->settings['email_field'] ) and $this->settings['email_field'] and isset( $this->settings['update_email_changes'] ) and $this->settings['update_email_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( $this->_nameField() and isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract OAuth1 Login Handler
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 31 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract OAuth1 Login Handler
|
||||
*/
|
||||
abstract class _OAuth1 extends \IPS\Login\Handler
|
||||
{
|
||||
/* !Login Handler: Basics */
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
$return = array(
|
||||
array( 'login_handler_oauth_settings', static::getTitle() . '_info' ),
|
||||
'consumer_key' => new \IPS\Helpers\Form\Text( 'oauth_consumer_key', ( isset( $this->settings['consumer_key'] ) ) ? $this->settings['consumer_key'] : '', TRUE ),
|
||||
'consumer_secret' => new \IPS\Helpers\Form\Text( 'oauth_consumer_secret', ( isset( $this->settings['consumer_secret'] ) ) ? $this->settings['consumer_secret'] : '', TRUE ),
|
||||
);
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
$return['show_in_ucp'] = new \IPS\Helpers\Form\Radio( 'login_handler_show_in_ucp', isset( $this->settings['show_in_ucp'] ) ? $this->settings['show_in_ucp'] : 'always', FALSE, array(
|
||||
'options' => array(
|
||||
'always' => 'login_handler_show_in_ucp_always',
|
||||
'loggedin' => 'login_handler_show_in_ucp_loggedin',
|
||||
),
|
||||
) );
|
||||
$return['update_name_changes'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes', isset( $this->settings['update_name_changes'] ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_name_changes_inc_optional' );
|
||||
$return['update_email_changes'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes', isset( $this->settings['update_email_changes'] ) ? $this->settings['update_email_changes'] : 'optional', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_email_changes_inc_optional' );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', $this->tokenRequestEndpoint() );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack( 'oauth1_setup_error', FALSE, array( 'sprintf' => array( $e->getMessage() ) ) ) );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response->decodeQueryString('oauth_token');
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack( 'oauth1_setup_error', FALSE, array( 'sprintf' => array( (string) $response ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* !Authentication */
|
||||
|
||||
use \IPS\Login\Handler\ButtonHandler;
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateButton( \IPS\Login $login )
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->denied ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
elseif ( isset( \IPS\Request::i()->oauth_token ) )
|
||||
{
|
||||
return $this->_handleAuthorizationResponse( $login );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_redirectToAuthorizationEndpoint( $login );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to Resource Owner Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return void
|
||||
*/
|
||||
protected function _redirectToAuthorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
$callback = $login->url->setQueryString( '_processLogin', $this->id )->csrf();
|
||||
if ( isset( \IPS\Request::i()->ref ) )
|
||||
{
|
||||
$callback = $callback->setQueryString( 'ref', \IPS\Request::i()->ref );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', $this->tokenRequestEndpoint(), array( 'oauth_callback' => (string) $callback ) )->decodeQueryString('oauth_token');
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::i( $e, 'twitter' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $this->authorizationEndpoint( $login )->setQueryString( 'oauth_token', $response['oauth_token'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle authorization response
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
protected function _handleAuthorizationResponse( \IPS\Login $login )
|
||||
{
|
||||
/* Authenticate */
|
||||
try
|
||||
{
|
||||
$response = $this->_sendRequest( 'post', $this->accessTokenEndpoint(), array( 'oauth_verifier' => \IPS\Request::i()->oauth_verifier ), \IPS\Request::i()->oauth_token )->decodeQueryString('oauth_token');
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::i( $e, 'twitter' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get user id */
|
||||
try
|
||||
{
|
||||
$userId = $this->authenticatedUserId( $response['oauth_token'], $response['oauth_token_secret'] );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'oauth' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Has this user signed in with this service before? */
|
||||
try
|
||||
{
|
||||
$oauthAccess = \IPS\Db::i()->select( '*', 'core_login_links', array( 'token_login_method=? AND token_identifier=?', $this->id, $userId ) )->first();
|
||||
$member = \IPS\Member::load( $oauthAccess['token_member'] );
|
||||
|
||||
/* If the user never finished the linking process, or the account has been deleted, discard this access token */
|
||||
if ( !$oauthAccess['token_linked'] or !$member->member_id )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $oauthAccess['token_member'] ) );
|
||||
throw new \UnderflowException;
|
||||
}
|
||||
|
||||
/* Otherwise, update our token... */
|
||||
\IPS\Db::i()->update( 'core_login_links', array(
|
||||
'token_access_token' => $response['oauth_token'],
|
||||
'token_secret' => $response['oauth_token_secret'],
|
||||
), array( 'token_login_method=? AND token_member=?', $this->id, $oauthAccess['token_member'] ) );
|
||||
|
||||
/* ... and return the member object */
|
||||
return $member;
|
||||
}
|
||||
/* No, create or link the account */
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
/* Get the username + email */
|
||||
$name = NULL;
|
||||
try
|
||||
{
|
||||
$name = $this->authenticatedUserName( $response['oauth_token'], $response['oauth_token_secret'] );
|
||||
}
|
||||
catch ( \Exception $e ) {}
|
||||
$email = NULL;
|
||||
try
|
||||
{
|
||||
$email = $this->authenticatedEmail( $response['oauth_token'], $response['oauth_token_secret'] );
|
||||
}
|
||||
catch ( \Exception $e ) {}
|
||||
|
||||
try
|
||||
{
|
||||
if ( $login->type === \IPS\Login::LOGIN_UCP )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
|
||||
$exception->handler = $this;
|
||||
$exception->member = $login->reauthenticateAs;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$member = $this->createAccount( $name, $email );
|
||||
|
||||
\IPS\Db::i()->insert( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $member->member_id,
|
||||
'token_identifier' => $userId,
|
||||
'token_linked' => 1,
|
||||
'token_access_token' => $response['oauth_token'],
|
||||
'token_secret' => $response['oauth_token_secret'],
|
||||
) );
|
||||
|
||||
$member->logHistory( 'core', 'social_account', array(
|
||||
'service' => static::getTitle(),
|
||||
'handler' => $this->id,
|
||||
'account_id' => $this->userId( $member ),
|
||||
'account_name' => $this->userProfileName( $member ),
|
||||
'linked' => TRUE,
|
||||
'registered' => TRUE
|
||||
) );
|
||||
|
||||
if ( $syncOptions = $this->syncOptions( $member, TRUE ) )
|
||||
{
|
||||
$profileSync = array();
|
||||
foreach ( $syncOptions as $option )
|
||||
{
|
||||
$profileSync[ $option ] = array( 'handler' => $this->id, 'ref' => NULL, 'error' => NULL );
|
||||
}
|
||||
$member->profilesync = $profileSync;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Login\Exception $exception )
|
||||
{
|
||||
if ( $exception->getCode() === \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $exception->member->member_id,
|
||||
'token_identifier' => $userId,
|
||||
'token_linked' => 0,
|
||||
'token_access_token' => $response['oauth_token'],
|
||||
'token_secret' => $response['oauth_token_secret'],
|
||||
) );
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
abstract protected function authorizationEndpoint( \IPS\Login $login );
|
||||
|
||||
/**
|
||||
* Token Request Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
abstract protected function tokenRequestEndpoint();
|
||||
|
||||
/**
|
||||
* Access Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
abstract protected function accessTokenEndpoint();
|
||||
|
||||
/**
|
||||
* Send Request
|
||||
*
|
||||
* @param string $method HTTP Method
|
||||
* @param \IPS\Http\Url $url URL
|
||||
* @param array|string|NULL $params Parameters
|
||||
* @param string $token OAuth Token
|
||||
* @param array $otherParams Other params to send obvs
|
||||
* @param string $mimeBoundary Mime data to send (boundary => data )
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
*/
|
||||
protected function _sendRequest( $method, $url, $params=array(), $token='', $secret='', $otherParams=array(), $mimeBoundary=array() )
|
||||
{
|
||||
/* Generate the OAUTH Authorization Header */
|
||||
$OAuthAuthorization = array_merge( array(
|
||||
'oauth_consumer_key' => $this->settings['consumer_key'],
|
||||
'oauth_nonce' => md5( \IPS\Login::generateRandomString() ),
|
||||
'oauth_signature_method'=> 'HMAC-SHA1',
|
||||
'oauth_timestamp' => time(),
|
||||
'oauth_token' => $token,
|
||||
'oauth_version' => '1.0'
|
||||
) );
|
||||
|
||||
$queryStringParams = array();
|
||||
foreach ( $params as $k => $v )
|
||||
{
|
||||
if ( mb_substr( $k, 0, 6 ) === 'oauth_' )
|
||||
{
|
||||
$OAuthAuthorization = array_merge( array( $k => $v ), $OAuthAuthorization );
|
||||
unset( $params[ $k ] );
|
||||
}
|
||||
elseif ( $method === 'get' )
|
||||
{
|
||||
$queryStringParams[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
/* All keys sent in the signature must be in alphabetical order, that includes oAuth keys and user sent params */
|
||||
$allKeys = array_merge( $OAuthAuthorization, $params );
|
||||
ksort( $allKeys );
|
||||
|
||||
$signatureBaseString = mb_strtoupper( $method ) . '&' . rawurlencode( (string) $url ) . '&' . rawurlencode( http_build_query( $allKeys, NULL, '&', PHP_QUERY_RFC3986 ) );
|
||||
$signingKey = rawurlencode( $this->settings['consumer_secret'] ) . '&' . rawurlencode( $secret ?: $token );
|
||||
$OAuthAuthorizationEncoded = array();
|
||||
foreach ( $OAuthAuthorization as $k => $v )
|
||||
{
|
||||
$OAuthAuthorizationEncoded[] = rawurlencode( $k ) . '="' . rawurlencode( $v ) . '"';
|
||||
|
||||
if ( $k === 'oauth_nonce' )
|
||||
{
|
||||
$signature = base64_encode( hash_hmac( 'sha1', $signatureBaseString, $signingKey, TRUE ) );
|
||||
$OAuthAuthorizationEncoded[] = rawurlencode( 'oauth_signature' ) . '="' . rawurlencode( $signature ) . '"';
|
||||
}
|
||||
}
|
||||
$OAuthAuthorizationHeader = 'OAuth ' . implode( ', ', $OAuthAuthorizationEncoded );
|
||||
|
||||
$headers = array( 'Authorization' => $OAuthAuthorizationHeader );
|
||||
|
||||
/* Send the request */
|
||||
if ( ! count( $mimeBoundary ) )
|
||||
{
|
||||
if ( $method === 'get' )
|
||||
{
|
||||
return $url->setQueryString( $queryStringParams )->request()->setHeaders( $headers )->get();
|
||||
}
|
||||
else
|
||||
{
|
||||
return $url->setQueryString( $queryStringParams )->request()->setHeaders( $headers )->$method( $params );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$headers['Content-Type'] = 'multipart/form-data; boundary=' . $mimeBoundary[0];
|
||||
|
||||
return $url->setQueryString( $queryStringParams )->request()->setHeaders( $headers )->$method( $mimeBoundary[1] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's identifier (may not be a number)
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userId( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $error['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedUserId( $link['token_access_token'], $link['token_secret'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedUserName( $link['token_access_token'], $link['token_secret'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userEmail( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $error['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedEmail( $link['token_access_token'], $link['token_secret'] );
|
||||
}
|
||||
|
||||
/* !UCP */
|
||||
|
||||
/**
|
||||
* Show in Account Settings?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member, or NULL for if it should show generally
|
||||
* @return bool
|
||||
*/
|
||||
public function showInUcp( \IPS\Member $member = NULL )
|
||||
{
|
||||
if ( !isset( $this->settings['show_in_ucp'] ) )
|
||||
{
|
||||
return TRUE; // Default to showing
|
||||
}
|
||||
return parent::showInUcp( $member );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Twitter Login Handler
|
||||
* @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 1 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth1;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter Login Handler
|
||||
*/
|
||||
class _Twitter extends \IPS\Login\Handler\OAuth1
|
||||
{
|
||||
/**
|
||||
* @brief Share Service
|
||||
*/
|
||||
public static $shareService = 'Twitter';
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Twitter';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['login_acp_desc'] = \IPS\Member::loggedIn()->language()->addToStack('login_acp_will_reauth');
|
||||
|
||||
return array_merge(
|
||||
array(
|
||||
'name' => new \IPS\Helpers\Form\Radio( 'login_real_name', ( isset( $this->settings['name'] ) ) ? $this->settings['name'] : 'any', TRUE, array(
|
||||
'options' => array(
|
||||
'real' => 'login_twitter_name_real',
|
||||
'screen' => 'login_twitter_name_screen',
|
||||
'any' => 'login_real_name_disabled',
|
||||
),
|
||||
'toggles' => array(
|
||||
'real' => array( 'login_update_name_changes_inc_optional' ),
|
||||
'screen' => array( 'login_update_name_changes_inc_optional' ),
|
||||
)
|
||||
), NULL, NULL, NULL, 'login_real_name' ),
|
||||
),
|
||||
parent::acpForm(),
|
||||
array(
|
||||
'allow_status_import' => new \IPS\Helpers\Form\YesNo( 'login_generic_allow_status_import', ( isset( $this->settings['allow_status_import'] ) ) ? $this->settings['allow_status_import'] : FALSE, FALSE )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return '#00abf0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return 'twitter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return 'login_twitter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return 'ipsSocial_twitter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return \IPS\Theme::i()->resource( 'logos/login/Twitter.png', 'core', 'interface' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
$return = \IPS\Http\Url::external('https://api.twitter.com/oauth/authenticate');
|
||||
|
||||
if ( $login->type === \IPS\Login::LOGIN_ACP or $login->type === \IPS\Login::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
$return = $return->setQueryString( 'force_login', 'true' );
|
||||
}
|
||||
|
||||
if ( $login->type === \IPS\Login::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
try
|
||||
{
|
||||
$token = \IPS\Db::i()->select( array( 'token_access_token', 'token_secret' ), 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $login->reauthenticateAs->member_id ) )->first();
|
||||
$userDetails = $this->_userData( $token['token_access_token'], $token['token_secret'] );
|
||||
if ( isset( $userDetails['screen_name'] ) )
|
||||
{
|
||||
$return = $return->setQueryString( 'screen_name', $userDetails['screen_name'] );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Request Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenRequestEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external('https://api.twitter.com/oauth/request_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function accessTokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external('https://api.twitter.com/oauth/access_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken, $accessTokenSecret )
|
||||
{
|
||||
return $this->_userData( $accessToken, $accessTokenSecret )['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken, $accessTokenSecret )
|
||||
{
|
||||
if ( $this->settings['name'] == 'screen' )
|
||||
{
|
||||
return $this->_userData( $accessToken, $accessTokenSecret )['screen_name'];
|
||||
}
|
||||
elseif ( $this->settings['name'] == 'real' )
|
||||
{
|
||||
return $this->_userData( $accessToken, $accessTokenSecret )['name'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken, $accessTokenSecret )
|
||||
{
|
||||
return $this->_userData( $accessToken, $accessTokenSecret )['email'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'], $link['token_secret'] );
|
||||
if ( !$userData['default_profile_image'] )
|
||||
{
|
||||
return \IPS\Http\Url::external( str_replace( '_normal', '', $userData['profile_image_url_https'] ?: $userData['profile_image_url'] ) );
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's cover photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userCoverPhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'], $link['token_secret'] );
|
||||
if ( isset( $userData['profile_banner_url'] ) and $userData['profile_banner_url'] )
|
||||
{
|
||||
return \IPS\Http\Url::external( $userData['profile_banner_url'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's statuses since a particular date
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @param \IPS\DateTime|NULL $since Date/Time to get statuses since then, or NULL to get the latest one
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userStatuses( \IPS\Member $member, \IPS\DateTime $since = NULL )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$return = array();
|
||||
$response = $this->_sendRequest( 'get', \IPS\Http\Url::external('https://api.twitter.com/1.1/statuses/user_timeline.json'), $since ? array() : array( 'count' => 1 ), $link['token_access_token'], $link['token_secret'] )->decodeJson();
|
||||
foreach ( $response as $statusData )
|
||||
{
|
||||
if ( $since and strtotime( $statusData['created_at'] ) < $since->getTimestamp() )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$status = \IPS\core\Statuses\Status::createItem( $member, $member->ip_address, new \IPS\DateTime( $statusData['created_at'] ) );
|
||||
$status->content = $this->_parseStatusText( $member, nl2br( $statusData['text'], FALSE ) );
|
||||
|
||||
$return[] = $status;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link to user's remote profile
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( $identifier, $username )
|
||||
{
|
||||
return \IPS\Http\Url::external( "https://twitter.com/" )->setPath( $username );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' and isset( $this->settings['name'] ) and $this->settings['name'] != 'any' )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
$return[] = 'photo';
|
||||
$return[] = 'cover';
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments and isset( $this->settings['allow_status_import'] ) and $this->settings['allow_status_import'] )
|
||||
{
|
||||
$return[] = 'status';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken, $accessTokenSecret )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', \IPS\Http\Url::external('https://api.twitter.com/1.1/account/verify_credentials.json'), array( 'include_email' => 'true' ), $accessToken, $accessTokenSecret )->decodeJson();
|
||||
if ( isset( $response['errors'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['errors'][0]['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Post something to Twitter
|
||||
*
|
||||
* @param \IPS\Member $member Member posting
|
||||
* @param string $content Content to post
|
||||
* @param \IPS\Http\Url|NULL $url Optional link
|
||||
* @return void
|
||||
*/
|
||||
public function postToTwitter( \IPS\Member $member, $content, \IPS\Http\Url $url = NULL )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$data = array( 'status' => $content );
|
||||
|
||||
if ( $url !== NULL )
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->twitter_config ) or \IPS\Data\Store::i()->twitter_config['time'] > time() - 86400 )
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', \IPS\Http\Url::external('https://api.twitter.com/1.1/help/configuration.json'), array(), $link['token_access_token'], $link['token_secret'] )->decodeJson();
|
||||
\IPS\Data\Store::i()->twitter_config = array_merge( $response, array( 'time' => time() ) );
|
||||
}
|
||||
$maxUrlLen = \IPS\Data\Store::i()->twitter_config['short_url_length'];
|
||||
$data['status'] = mb_substr( $data['status'], 0, ( 140 - ( $maxUrlLen + 1 ) ) ) . ' ' . $url;
|
||||
}
|
||||
|
||||
$response = $this->_sendRequest( 'post', \IPS\Http\Url::external('https://api.twitter.com/1.1/statuses/update.json'), $data, $link['token_access_token'], $link['token_secret'] )->decodeJson();
|
||||
|
||||
return isset( $response['id_str'] );
|
||||
}
|
||||
|
||||
/* ! Social Promotes */
|
||||
|
||||
/**
|
||||
* Request a token
|
||||
*
|
||||
* @param \IPS\Http\Url $callback Callback URK
|
||||
* @return string
|
||||
*/
|
||||
public function requestToken( $callback )
|
||||
{
|
||||
return $this->_sendRequest( 'get', $this->tokenRequestEndpoint(), array( 'oauth_callback' => (string) $callback ) )->decodeQueryString('oauth_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $verifier Verifier
|
||||
* @param string $accessTokenSecret Access Token
|
||||
* @return array
|
||||
*/
|
||||
public function exchangeToken( $verifier, $accessToken )
|
||||
{
|
||||
return $this->_sendRequest( 'post', $this->accessTokenEndpoint(), array( 'oauth_verifier' => $verifier ), $accessToken )->decodeQueryString('user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Can we publish to this twitter account?
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasWritePermissions( $accessToken, $accessTokenSecret )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', \IPS\Http\Url::external('https://api.twitter.com/1.1/account/verify_credentials.json'), array(), $accessToken, $accessTokenSecret );
|
||||
|
||||
if ( $response->httpHeaders['x-access-level'] == 'read' )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$response->decodeJson();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e ) { }
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a status
|
||||
*
|
||||
* @param array $data Post data to send
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return boolean
|
||||
*/
|
||||
public function sendStatus( $data, $accessToken, $accessTokenSecret )
|
||||
{
|
||||
return $this->_sendRequest( 'post', \IPS\Http\Url::external("https://api.twitter.com/1.1/statuses/update.json"), $data, $accessToken, $accessTokenSecret )->decodeJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send media
|
||||
*
|
||||
* @param array $contents Photo contents
|
||||
* @param string $accessToken Access Token
|
||||
* @param string $accessTokenSecret Access Token Secret
|
||||
* @return boolean
|
||||
*/
|
||||
public function sendMedia( $contents, $accessToken, $accessTokenSecret )
|
||||
{
|
||||
$mimeBoundary = sha1( microtime() );
|
||||
|
||||
$data = '--' . $mimeBoundary . "\r\n";
|
||||
$data .= 'Content-Disposition: form-data; name="media";' . "\r\n";
|
||||
$data .= 'Content-Type: application/octet-stream' . "\r\n" . "\r\n";
|
||||
$data .= $contents . "\r\n";
|
||||
$data .= '--' . $mimeBoundary . '--' . "\r\n" . "\r\n";
|
||||
|
||||
return $this->_sendRequest( 'post', \IPS\Http\Url::external('https://upload.twitter.com/1.1/media/upload.json'), array(), $accessToken, $accessTokenSecret, array(), array( $mimeBoundary, $data ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Custom OAuth 2 Login Handler
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 31 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom OAuth 2 Login Handler
|
||||
*/
|
||||
class _Custom extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* @brief Can we have multiple instances of this handler?
|
||||
*/
|
||||
public static $allowMultiple = TRUE;
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_custom_oauth';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
$return = array();
|
||||
|
||||
$return[] = array( 'login_handler_oauth_settings', 'login_handler_custom_oauth_info' );
|
||||
|
||||
$return['grant_type'] = new \IPS\Helpers\Form\Radio( 'oauth_custom_grant_type', isset( $this->settings['grant_type'] ) ? $this->settings['grant_type'] : 'authorization_code', TRUE, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'client_grant_type_authorization_code',
|
||||
'implicit' => 'client_grant_type_implicit',
|
||||
'password' => 'client_grant_type_password',
|
||||
),
|
||||
'toggles' => array(
|
||||
'authorization_code' => array( 'authorization_endpoint', 'authorization_endpoint_secure', 'button_color', 'button_text', 'client_secret' ),
|
||||
'implicit' => array( 'authorization_endpoint', 'authorization_endpoint_secure', 'button_color', 'button_text' ),
|
||||
'password' => array( 'client_secret', 'oauth_custom_auth_types', 'forgot_password_url' )
|
||||
)
|
||||
) );
|
||||
|
||||
$accountManagementSettings = array();
|
||||
$active = 'return';
|
||||
foreach ( parent::acpForm() as $k => $v )
|
||||
{
|
||||
if ( $v === 'account_management_settings' )
|
||||
{
|
||||
$active = 'accountManagementSettings';
|
||||
}
|
||||
if ( !is_string( $v ) and !is_array( $v ) )
|
||||
{
|
||||
${$active}[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$return['authentication_type'] = new \IPS\Helpers\Form\Radio( 'oauth_custom_authentication_type', isset( $this->settings['authentication_type'] ) ? $this->settings['authentication_type'] : static::AUTHENTICATE_HEADER, TRUE, array(
|
||||
'options' => array(
|
||||
static::AUTHENTICATE_HEADER => 'oauth_custom_authentication_type_header',
|
||||
static::AUTHENTICATE_POST => 'oauth_custom_authentication_type_post',
|
||||
)
|
||||
) );
|
||||
|
||||
$return['scopes'] = new \IPS\Helpers\Form\Stack( 'oauth_scopes_to_request', isset( $this->settings['scopes'] ) ? $this->settings['scopes'] : array(), FALSE, array() );
|
||||
|
||||
$authorizationEndpointValidation = function( $val )
|
||||
{
|
||||
if ( \IPS\OAUTH_REQUIRES_HTTPS and $val and $val instanceof \IPS\Http\Url )
|
||||
{
|
||||
if ( $val->data[ \IPS\Http\Url::COMPONENT_SCHEME ] !== 'https' )
|
||||
{
|
||||
throw new \DomainException('authorization_endpoint_https');
|
||||
}
|
||||
if ( $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] )
|
||||
{
|
||||
throw new \DomainException('authorization_endpoint_fragment');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$return['authorization_endpoint'] = new \IPS\Helpers\Form\Url( 'oauth_authorization_endpoint', isset( $this->settings['authorization_endpoint'] ) ? $this->settings['authorization_endpoint'] : NULL, NULL, array( 'placeholder' => 'https://example.com/oauth/authorize' ), $authorizationEndpointValidation, NULL, NULL, 'authorization_endpoint' );
|
||||
$return['authorization_endpoint_secure'] = new \IPS\Helpers\Form\Url( 'oauth_authorization_endpoint_secure', isset( $this->settings['authorization_endpoint_secure'] ) ? $this->settings['authorization_endpoint_secure'] : NULL, NULL, array( 'nullLang' => 'oauth_authorization_endpoint_same', 'placeholder' => 'https://example.com/oauth/authorize/?prompt=login' ), $authorizationEndpointValidation, NULL, NULL, 'authorization_endpoint_secure' );
|
||||
$return['token_endpoint'] = new \IPS\Helpers\Form\Url( 'oauth_token_endpoint', isset( $this->settings['token_endpoint'] ) ? $this->settings['token_endpoint'] : NULL, TRUE, array( 'placeholder' => 'https://www.example.com/oauth/token' ) );
|
||||
$return['user_endpoint'] = new \IPS\Helpers\Form\Url( 'oauth_user_endpoint', isset( $this->settings['user_endpoint'] ) ? $this->settings['user_endpoint'] : NULL, TRUE, array( 'placeholder' => 'https://www.example.com/oauth/me' ) );
|
||||
$return['uid_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_uid_field', isset( $this->settings['uid_field'] ) ? $this->settings['uid_field'] : NULL, TRUE, array() );
|
||||
$return['name_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_name_field', isset( $this->settings['name_field'] ) ? $this->settings['name_field'] : NULL, FALSE, array(), NULL, NULL, NULL, 'login_real_name' );
|
||||
$return['email_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_email_field', isset( $this->settings['email_field'] ) ? $this->settings['email_field'] : NULL, FALSE, array(), NULL, NULL, NULL, 'login_real_email' );
|
||||
$return['photo_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_photo_field', isset( $this->settings['photo_field'] ) ? $this->settings['photo_field'] : NULL );
|
||||
if ( \IPS\Settings::i()->allow_forgot_password == 'normal' or \IPS\Settings::i()->allow_forgot_password == 'handler' )
|
||||
{
|
||||
$return['forgot_password_url'] = new \IPS\Helpers\Form\Url( 'handler_forgot_password_url', isset( $this->settings['forgot_password_url'] ) ? $this->settings['forgot_password_url'] : NULL, FALSE, array(), NULL, NULL, NULL, 'forgot_password_url' );
|
||||
\IPS\Member::loggedIn()->language()->words['handler_forgot_password_url_desc'] = \IPS\Member::loggedIn()->language()->addToStack( \IPS\Settings::i()->allow_forgot_password == 'normal' ? 'handler_forgot_password_url_desc_normal' : 'handler_forgot_password_url_deschandler' );
|
||||
}
|
||||
|
||||
$return[] = 'login_handler_oauth_ui';
|
||||
$return['auth_types'] = new \IPS\Helpers\Form\Select( 'oauth_custom_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : ( \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ), TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
) ), NULL, NULL, NULL, 'oauth_custom_auth_types' );
|
||||
$return['button_color'] = new \IPS\Helpers\Form\Color( 'oauth_custom_button_color', isset( $this->settings['button_color'] ) ? $this->settings['button_color'] : '#478F79', NULL, array(), NULL, NULL, NULL, 'button_color' );
|
||||
$return['button_text'] = new \IPS\Helpers\Form\Translatable( 'oauth_custom_button_text', NULL, NULL, array( 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('oauth_custom_button_text_custom_placeholder'), 'app' => 'core', 'key' => ( $this->id ? "core_custom_oauth_{$this->id}" : NULL ) ), NULL, NULL, NULL, 'button_text' );
|
||||
$return['button_icon'] = new \IPS\Helpers\Form\Upload( 'oauth_custom_button_icon', ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL, FALSE, array( 'storageExtension' => 'core_Login' ), NULL, NULL, NULL, 'button_icon' );
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
foreach ( $accountManagementSettings as $k => $v )
|
||||
{
|
||||
$return[ $k ] = $v;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Handler Settings
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function acpFormSave( &$values )
|
||||
{
|
||||
$return = parent::acpFormSave( $values );
|
||||
$return['button_icon'] = (string) $return['button_icon'];
|
||||
$return['authorization_endpoint'] = (string) $return['authorization_endpoint'];
|
||||
$return['token_endpoint'] = (string) $return['token_endpoint'];
|
||||
$return['user_endpoint'] = (string) $return['user_endpoint'];
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
$parent = parent::formatFormValues( $values );
|
||||
|
||||
if( isset( $values['oauth_custom_button_text'] ) )
|
||||
{
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
\IPS\Lang::saveCustom( 'core', "core_custom_oauth_{$this->id}", $values['oauth_custom_button_text'] );
|
||||
unset( $values['button_text'] );
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return $this->settings['button_color'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] )->url : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in user cp sidebar
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForUcp()
|
||||
{
|
||||
return $this->logoForDeviceInformation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return "core_custom_oauth_{$this->id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return isset( $this->settings['grant_type'] ) ? $this->settings['grant_type'] : 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Should client credentials be sent as an "Authoriation" header, or as POST data?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _authenticationType()
|
||||
{
|
||||
return isset( $this->settings['authentication_type'] ) ? $this->settings['authentication_type'] : static::AUTHENTICATE_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
return $this->settings['scopes'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
if ( isset( $this->settings['authorization_endpoint_secure'] ) and $this->settings['authorization_endpoint_secure'] and ( $login->type === \IPS\Login::LOGIN_ACP or $login->type === \IPS\Login::LOGIN_REAUTHENTICATE ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( $this->settings['authorization_endpoint_secure'] );
|
||||
}
|
||||
|
||||
return \IPS\Http\Url::external( $this->settings['authorization_endpoint'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external( $this->settings['token_endpoint'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
if ( $userId = static::getValueFromArray( $this->_userData( $accessToken, $this->settings['uid_field'] ), $this->settings['uid_field'] ) )
|
||||
{
|
||||
return $userId;
|
||||
}
|
||||
throw new \Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
if ( isset( $this->settings['name_field'] ) and $this->settings['name_field'] and $username = static::getValueFromArray( $this->_userData( $accessToken, $this->settings['name_field'] ), $this->settings['name_field'] ) )
|
||||
{
|
||||
return $username;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
if ( isset( $this->settings['email_field'] ) and $this->settings['email_field'] and $email = static::getValueFromArray( $this->_userData( $accessToken, $this->settings['email_field'] ), $this->settings['email_field'] ) )
|
||||
{
|
||||
return $email;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( isset( $this->settings['photo_field'] ) and $this->settings['photo_field'] )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( $photo = static::getValueFromArray( $this->_userData( $link['token_access_token'], $this->settings['photo_field'] ), $this->settings['photo_field'] ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( $photo );
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedUserName( $link['token_access_token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( isset( $this->settings['email_field'] ) and $this->settings['email_field'] and ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' ) )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['name_field'] ) and $this->settings['name_field'] and isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['photo_field'] ) and $this->settings['photo_field'] )
|
||||
{
|
||||
$return[] = 'photo';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken, $expectedParam = NULL )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
/* Try the most sensible way first */
|
||||
$response = \IPS\Http\Url::external( $this->settings['user_endpoint'] )->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$accessToken}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
/* Check if we got what we were expecting. If we didn't, try sending the access token in the query string.
|
||||
While the spec discourages this usage, it is still valid and some providers may require it */
|
||||
if ( $expectedParam !== NULL )
|
||||
{
|
||||
if ( static::getValueFromArray( $response, $expectedParam ) === NULL )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['user_endpoint'] )->setQueryString( 'access_token', $accessToken )->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
}
|
||||
|
||||
/* Set */
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from an array
|
||||
*
|
||||
* @param array $array The array with the data
|
||||
* @param string $key The key using[square][brackets]
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function getValueFromArray( $array, $key )
|
||||
{
|
||||
while ( $pos = mb_strpos( $key, '[' ) )
|
||||
{
|
||||
preg_match( '/^(.+?)\[([^\]]+?)?\](.*)?$/', $key, $matches );
|
||||
|
||||
if ( !array_key_exists( $matches[1], $array ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$array = $array[ $matches[1] ];
|
||||
$key = $matches[2] . $matches[3];
|
||||
}
|
||||
|
||||
if ( !isset( $array[ $key ] ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $array[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot Password URL
|
||||
*
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function forgotPasswordUrl()
|
||||
{
|
||||
return ( isset( $this->settings['forgot_password_url'] ) and $this->settings['forgot_password_url'] ) ? \IPS\Http\Url::external( $this->settings['forgot_password_url'] ) : NULL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief OAuth Exception Class
|
||||
* @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 29 Apr 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
class _Exception extends \Exception
|
||||
{
|
||||
/**
|
||||
* @brief Description of the error
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $message Exception Message
|
||||
* @param string|NULL $description Error Description or NULL.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $message, $description = null )
|
||||
{
|
||||
parent::__construct( $message );
|
||||
$this->description = $description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Facebook Login Handler
|
||||
* @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 31 May 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Login Handler
|
||||
*/
|
||||
class _Facebook extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* @brief Share Service
|
||||
*/
|
||||
public static $shareService = 'Facebook';
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Facebook';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['login_acp_desc'] = \IPS\Member::loggedIn()->language()->addToStack('login_acp_will_reauth');
|
||||
\IPS\Member::loggedIn()->language()->words['oauth_client_id'] = \IPS\Member::loggedIn()->language()->addToStack('login_facebook_app');
|
||||
\IPS\Member::loggedIn()->language()->words['oauth_client_client_secret'] = \IPS\Member::loggedIn()->language()->addToStack('login_facebook_secret');
|
||||
|
||||
return array_merge(
|
||||
array(
|
||||
'real_name' => new \IPS\Helpers\Form\Radio( 'login_real_name', isset( $this->settings['real_name'] ) ? $this->settings['real_name'] : 1, FALSE, array(
|
||||
'options' => array(
|
||||
1 => 'login_real_name_facebook',
|
||||
0 => 'login_real_name_disabled',
|
||||
),
|
||||
'toggles' => array(
|
||||
1 => array( 'login_update_name_changes_inc_optional' ),
|
||||
)
|
||||
), NULL, NULL, NULL, 'login_real_name' ),
|
||||
),
|
||||
parent::acpForm(),
|
||||
array(
|
||||
'allow_status_import' => new \IPS\Helpers\Form\YesNo( 'login_facebook_allow_status_import', ( isset( $this->settings['allow_status_import'] ) ) ? $this->settings['allow_status_import'] : FALSE, FALSE )
|
||||
)
|
||||
);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return '#3a579a';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return 'facebook-official';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return 'login_facebook';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return 'ipsSocial_facebook';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return \IPS\Theme::i()->resource( 'logos/login/Facebook.png', 'core', 'interface' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
$return = array('email');
|
||||
|
||||
if ( isset( static::$shareService ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$shareService = \IPS\core\ShareLinks\Service::load( static::$shareService, 'share_key' );
|
||||
if ( $shareService->autoshare )
|
||||
{
|
||||
$return[] = 'publish_actions';
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments and isset( $this->settings['allow_status_import'] ) and $this->settings['allow_status_import'] )
|
||||
{
|
||||
$return[] = 'user_posts';
|
||||
}
|
||||
|
||||
$additionalPermitted = array( 'manage_pages', 'publish_pages', 'user_managed_groups', 'publish_actions' );
|
||||
if ( $additional !== NULL )
|
||||
{
|
||||
foreach( $additional as $scope )
|
||||
{
|
||||
if ( in_array( $scope, $additionalPermitted ) )
|
||||
{
|
||||
$return[] = $scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes Issued
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function scopesIssued( $accessToken )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->_authorizedRequest( 'me/permissions', $accessToken );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$return = array();
|
||||
if ( isset( $response['data'] ) )
|
||||
{
|
||||
foreach ( $response['data'] as $perm )
|
||||
{
|
||||
if ( $perm['status'] === 'granted' )
|
||||
{
|
||||
$return[] = $perm['permission'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
$return = \IPS\Http\Url::external('https://www.facebook.com/dialog/oauth');
|
||||
|
||||
if ( $login->type === \IPS\Login::LOGIN_ACP or $login->type === \IPS\Login::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
$return = $return->setQueryString( 'auth_type', 'reauthenticate' );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external('https://graph.facebook.com/v2.9/oauth/access_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirection Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function redirectionEndpoint()
|
||||
{
|
||||
if ( isset( $this->settings['legacy_redirect'] ) and $this->settings['legacy_redirect'] )
|
||||
{
|
||||
return \IPS\Http\Url::internal( 'applications/core/interface/facebook/auth.php', 'none' );
|
||||
}
|
||||
return parent::redirectionEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
if ( isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
return $this->_userData( $accessToken )['name'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['email'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $error['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$photoVars = explode( ':', $member->group['g_photo_max_vars'] );
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$link['token_identifier']}/picture?width={$photoVars[1]}&redirect=false" )->request()->get()->decodeJson();
|
||||
if ( !$response['data']['is_silhouette'] )
|
||||
{
|
||||
return \IPS\Http\Url::external( $response['data']['url'] );
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $error['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->_userData( $link['token_access_token'] )['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's cover photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userCoverPhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'] );
|
||||
|
||||
if ( isset( $userData['cover'] ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( $userData['cover']['source'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's statuses since a particular date
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @param \IPS\DateTime|NULL $since Date/Time to get statuses since then, or NULL to get the latest one
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userStatuses( \IPS\Member $member, \IPS\DateTime $since = NULL )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$data = array();
|
||||
if ( $since )
|
||||
{
|
||||
$data['since'] = $since->getTimestamp();
|
||||
}
|
||||
|
||||
$return = array();
|
||||
$response = $this->_authorizedRequest( 'me/posts', $link['token_access_token'], $data, 'get' );
|
||||
foreach ( $response['data'] as $statusData )
|
||||
{
|
||||
if( isset( $statusData['message'] ) and !isset( $statusData['story'] ) )
|
||||
{
|
||||
$status = \IPS\core\Statuses\Status::createItem( $member, $member->ip_address, new \IPS\DateTime( $statusData['created_time'] ) );
|
||||
$status->content = $this->_parseStatusText( $member, nl2br( $statusData['message'], FALSE ) );
|
||||
|
||||
$return[] = $status;
|
||||
|
||||
if ( !$since )
|
||||
{
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link to user's remote profile
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( $identifier, $username )
|
||||
{
|
||||
return \IPS\Http\Url::external( "https://www.facebook.com/" . $identifier );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' ) and in_array( 'email', $this->authorizedScopes( $member ) ) )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' and isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
$return[] = 'photo';
|
||||
$return[] = 'cover';
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments and isset( $this->settings['allow_status_import'] ) and $this->settings['allow_status_import'] and in_array( 'user_posts', $this->authorizedScopes( $member ) ) )
|
||||
{
|
||||
$return[] = 'status';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$response = $this->_authorizedRequest( 'me', $accessToken, array(
|
||||
'fields' => 'email,id,name,picture,cover',
|
||||
'appsecret_proof' => hash_hmac( 'sha256', $accessToken, $this->settings['client_secret'] )
|
||||
), 'get' );
|
||||
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['error']['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make authorized request
|
||||
*
|
||||
* @param string $endpoint Endpoint
|
||||
* @param string $accessToken Access Token
|
||||
* @param array|NULL $postData Data to post or query string]
|
||||
* @param string|NULL $method 'get' or 'post'
|
||||
* @return array
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
*/
|
||||
protected function _authorizedRequest( $endpoint, $accessToken, $data = NULL, $method = NULL )
|
||||
{
|
||||
$url = \IPS\Http\Url::external( "https://graph.facebook.com/{$endpoint}" );
|
||||
if ( $method === 'get' and $data )
|
||||
{
|
||||
$url = $url->setQueryString( $data );
|
||||
}
|
||||
|
||||
$request = $url->request()->setHeaders( array( 'Authorization' => "Bearer {$accessToken}" ) );
|
||||
if ( $method === 'get' or !$data )
|
||||
{
|
||||
$response = $request->get();
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = $request->post( $data );
|
||||
}
|
||||
|
||||
return $response->decodeJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post something to Facebook
|
||||
*
|
||||
* @param \IPS\Member $member Member posting
|
||||
* @param string $content Content to post
|
||||
* @param \IPS\Http\Url|NULL $url Optional link
|
||||
* @return void
|
||||
*/
|
||||
public function postToFacebook( \IPS\Member $member, $content, \IPS\Http\Url $url = NULL )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$data = array( 'message' => $content );
|
||||
if ( $url !== NULL )
|
||||
{
|
||||
$data['link'] = (string) $url;
|
||||
}
|
||||
|
||||
$response = $this->_authorizedRequest( 'me/feed', $link['token_access_token'], $data );
|
||||
|
||||
return isset( $response['id'] );
|
||||
}
|
||||
|
||||
/* ! Social Promotion */
|
||||
|
||||
/**
|
||||
* Exchange a short lived token for a longer lived token
|
||||
*
|
||||
* @param string $shortLivedToken The short lived token to exchange for a long lived token
|
||||
* @return string
|
||||
*/
|
||||
public function exchangeToken( $shortLivedToken )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'fb_exchange_token',
|
||||
'fb_exchange_token' => $shortLivedToken
|
||||
) )->decodeJson();
|
||||
|
||||
return isset( $response['access_token'] ) ? $response['access_token'] : NULL;
|
||||
}
|
||||
catch( \RuntimeException $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pages this user manages
|
||||
*
|
||||
* @param \IPS\Member $member Member requesting pages
|
||||
* @return array
|
||||
*/
|
||||
public function getPages( $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $error['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
$pages = array();
|
||||
|
||||
$response = $this->_authorizedRequest( $link['token_identifier'] . '/accounts', $link['token_access_token'], array(
|
||||
'appsecret_proof' => hash_hmac( 'sha256', $link['token_access_token'], $this->settings['client_secret'] )
|
||||
), 'get' );
|
||||
|
||||
if ( !empty( $response['data'] ) )
|
||||
{
|
||||
foreach( $response['data'] as $page )
|
||||
{
|
||||
$pages[ $page['id'] ] = array( $page['name'], $page['access_token'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups this user manages
|
||||
*
|
||||
* @param \IPS\Member $member Member requesting pages
|
||||
* @return array
|
||||
*/
|
||||
public function getGroups( $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $error['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
$groups = array();
|
||||
|
||||
$response = $this->_authorizedRequest( $link['token_identifier'] . '/groups', $link['token_access_token'], array(
|
||||
'appsecret_proof' => hash_hmac( 'sha256', $link['token_access_token'], $this->settings['client_secret'] )
|
||||
), 'get' );
|
||||
|
||||
if ( !empty( $response['data'] ) )
|
||||
{
|
||||
foreach( $response['data'] as $group )
|
||||
{
|
||||
$groups[ $group['id'] ] = array( $group['name'], $group['privacy'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Google Login Handler
|
||||
* @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 1 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Login Handler
|
||||
*/
|
||||
class _Google extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Google';
|
||||
}
|
||||
|
||||
protected static $enableAcpLoginByDefault = FALSE;
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['login_acp_desc'] = \IPS\Member::loggedIn()->language()->addToStack('login_acp_cannot_reauth');
|
||||
\IPS\Member::loggedIn()->language()->words['oauth_client_id'] = \IPS\Member::loggedIn()->language()->addToStack('login_google_id');
|
||||
|
||||
return array_merge(
|
||||
array(
|
||||
'real_name' => new \IPS\Helpers\Form\Radio( 'login_real_name', isset( $this->settings['real_name'] ) ? $this->settings['real_name'] : 1, FALSE, array(
|
||||
'options' => array(
|
||||
1 => 'login_real_name_google',
|
||||
0 => 'login_real_name_disabled',
|
||||
),
|
||||
'toggles' => array(
|
||||
1 => array( 'login_update_name_changes_inc_optional' ),
|
||||
)
|
||||
), NULL, NULL, NULL, 'login_real_name' )
|
||||
),
|
||||
parent::acpForm(),
|
||||
array(
|
||||
'allow_status_import' => new \IPS\Helpers\Form\YesNo( 'login_generic_allow_status_import', ( isset( $this->settings['allow_status_import'] ) ) ? $this->settings['allow_status_import'] : FALSE, FALSE )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return '#4285F4';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return 'google';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return 'login_google';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return 'ipsSocial_google';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return \IPS\Theme::i()->resource( 'logos/login/Google.png', 'core', 'interface' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
$return = array(
|
||||
'profile',
|
||||
'email',
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments and isset( $this->settings['allow_status_import'] ) and $this->settings['allow_status_import'] )
|
||||
{
|
||||
$return[] = 'https://www.googleapis.com/auth/plus.me';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes Issued
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function scopesIssued( $accessToken )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://www.googleapis.com/oauth2/v2/tokeninfo" )
|
||||
->setQueryString( 'access_token', $accessToken )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return isset( $response['scope'] ) ? explode( ' ', $response['scope'] ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
$return = \IPS\Http\Url::external('https://accounts.google.com/o/oauth2/v2/auth?access_type=offline');
|
||||
|
||||
if ( $login->type === \IPS\Login::LOGIN_ACP or $login->type === \IPS\Login::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
$return = $return->setQueryString( 'prompt', 'consent' );
|
||||
}
|
||||
|
||||
if ( $login->type === \IPS\Login::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
try
|
||||
{
|
||||
$return = $return->setQueryString( 'login_hint', $this->authenticatedEmail( \IPS\Db::i()->select( 'token_access_token', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $login->reauthenticateAs->member_id ) )->first() ) );
|
||||
}
|
||||
catch ( \UnderflowException $e ) {}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external('https://www.googleapis.com/oauth2/v4/token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirection Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function redirectionEndpoint()
|
||||
{
|
||||
if ( isset( $this->settings['legacy_redirect'] ) and $this->settings['legacy_redirect'] )
|
||||
{
|
||||
return \IPS\Http\Url::internal( 'applications/core/interface/google/auth.php', 'none' );
|
||||
}
|
||||
return parent::redirectionEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
if ( isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
return $this->_userData( $accessToken )['displayName'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
foreach ( $this->_userData( $accessToken )['emails'] as $email )
|
||||
{
|
||||
return $email['value'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'] );
|
||||
if ( isset( $userData['image'] ) and !$userData['image']['isDefault'] )
|
||||
{
|
||||
return \IPS\Http\Url::external( $userData['image']['url'] )->setQueryString( 'sz', NULL );
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->_userData( $link['token_access_token'] )['displayName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's cover photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userCoverPhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'] );
|
||||
|
||||
if ( isset( $userData['cover'] ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( $userData['cover']['coverPhoto']['url'] )->setQueryString( 'sz', NULL );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's statuses since a particular date
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @param \IPS\DateTime|NULL $since Date/Time to get statuses since then, or NULL to get the latest one
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userStatuses( \IPS\Member $member, \IPS\DateTime $since = NULL )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://www.googleapis.com/plus/v1/people/me/activities/public" )
|
||||
->setQueryString( $since ? array() : array( 'maxResults' => 1 ) )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$link['token_access_token']}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
$return = array();
|
||||
foreach ( $response['items'] as $statusData )
|
||||
{
|
||||
if ( $since and strtotime( $statusData['published'] ) < $since->getTimestamp() )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ( isset( $statusData['object']['content'] ) )
|
||||
{
|
||||
$status = \IPS\core\Statuses\Status::createItem( $member, $member->ip_address, new \IPS\DateTime( $statusData['published'] ) );
|
||||
$status->content = $this->_parseStatusText( $member, nl2br( $statusData['object']['content'], FALSE ) );
|
||||
|
||||
$return[] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link to user's remote profile
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( $identifier, $username )
|
||||
{
|
||||
return \IPS\Http\Url::external( "https://plus.google.com/" . $identifier );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$authorizedScopes = $this->authorizedScopes( $member );
|
||||
|
||||
if( $authorizedScopes === NULL )
|
||||
{
|
||||
$authorizedScopes = array();
|
||||
}
|
||||
|
||||
$return = array();
|
||||
|
||||
if ( ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' ) and ( in_array( 'email', $authorizedScopes ) or in_array( 'https://www.googleapis.com/auth/userinfo.email', $authorizedScopes ) ) )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' and isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
$return[] = 'photo';
|
||||
$return[] = 'cover';
|
||||
|
||||
if ( \IPS\Settings::i()->profile_comments and isset( $this->settings['allow_status_import'] ) and $this->settings['allow_status_import'] and in_array( 'https://www.googleapis.com/auth/plus.me', $authorizedScopes ) )
|
||||
{
|
||||
$return[] = 'status';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://www.googleapis.com/plus/v1/people/me" )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$accessToken}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['error']['errors'][0]['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief OAuth Exception Class
|
||||
* @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 29 Apr 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
class _InitException extends \Exception { }
|
||||
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Invision Community Login Handler
|
||||
* @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 2 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invision Community Login Handler
|
||||
*/
|
||||
class _Invision extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* @brief Can we have multiple instances of this handler?
|
||||
*/
|
||||
public static $allowMultiple = TRUE;
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_InvisionCommunity';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['login_acp_desc'] = \IPS\Member::loggedIn()->language()->addToStack('login_acp_will_reauth');
|
||||
|
||||
$return = array();
|
||||
|
||||
$return[] = array( 'login_handler_InvisionCommunity_info_title', 'login_handler_InvisionCommunity_info' );
|
||||
$return['url'] = new \IPS\Helpers\Form\Url( 'oauth_invision_endpoint', isset( $this->settings['url'] ) ? $this->settings['url'] : NULL, TRUE, array( 'placeholder' => 'https://othercommunity.example.com' ), function( $val )
|
||||
{
|
||||
if ( rtrim( (string) $val, '/' ) === rtrim( \IPS\Settings::i()->base_url, '/' ) )
|
||||
{
|
||||
throw new \DomainException('oauth_invision_endpoint_internal');
|
||||
}
|
||||
} );
|
||||
$return['grant_type'] = new \IPS\Helpers\Form\Radio( 'oauth_invision_grant_type', isset( $this->settings['grant_type'] ) ? $this->settings['grant_type'] : 'authorization_code', TRUE, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'invision_grant_type_authorization_code',
|
||||
'password' => 'invision_grant_type_password',
|
||||
),
|
||||
'toggles' => array(
|
||||
'authorization_code' => array( 'button_color', 'button_text' ),
|
||||
'password' => array( 'oauth_custom_auth_types' )
|
||||
)
|
||||
) );
|
||||
$accountManagementSettings = array();
|
||||
$active = 'return';
|
||||
foreach ( parent::acpForm() as $k => $v )
|
||||
{
|
||||
if ( $v === 'account_management_settings' )
|
||||
{
|
||||
$active = 'accountManagementSettings';
|
||||
}
|
||||
if ( !is_string( $v ) and !is_array( $v ) )
|
||||
{
|
||||
$$active[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$return[] = 'login_handler_oauth_ui';
|
||||
$return['auth_types'] = new \IPS\Helpers\Form\Select( 'oauth_custom_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : ( \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ), TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
) ), NULL, NULL, NULL, 'oauth_custom_auth_types' );
|
||||
$return['button_color'] = new \IPS\Helpers\Form\Color( 'oauth_custom_button_color', isset( $this->settings['button_color'] ) ? $this->settings['button_color'] : '#3E4148', NULL, array(), NULL, NULL, NULL, 'button_color' );
|
||||
$return['button_text'] = new \IPS\Helpers\Form\Translatable( 'oauth_custom_button_text', NULL, NULL, array( 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('oauth_custom_button_text_invision_placeholder'), 'app' => 'core', 'key' => ( $this->id ? "core_custom_oauth_{$this->id}" : NULL ) ), NULL, NULL, NULL, 'button_text' );
|
||||
$return['button_icon'] = new \IPS\Helpers\Form\Upload( 'oauth_custom_button_icon', ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL, FALSE, array( 'storageExtension' => 'core_Login' ), NULL, NULL, NULL, 'button_icon' );
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
foreach ( $accountManagementSettings as $k => $v )
|
||||
{
|
||||
$return[ $k ] = $v;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Handler Settings
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function acpFormSave( &$values )
|
||||
{
|
||||
$return = parent::acpFormSave( $values );
|
||||
$return['url'] = (string) $return['url'];
|
||||
$return['button_icon'] = (string) $return['button_icon'];
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
if( isset( $values['oauth_custom_button_text'] ) )
|
||||
{
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
\IPS\Lang::saveCustom( 'core', "core_custom_oauth_{$this->id}", $values['oauth_custom_button_text'] );
|
||||
unset( $values['button_text'] );
|
||||
}
|
||||
|
||||
return parent::formatFormValues( $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return $this->settings['button_color'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return 'ipsSocial_ips';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] )->url : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in user cp sidebar
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForUcp()
|
||||
{
|
||||
return $this->logoForDeviceInformation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return "core_custom_oauth_{$this->id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Should client credentials be sent as an "Authoriation" header, or as POST data?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _authenticationType()
|
||||
{
|
||||
return static::AUTHENTICATE_POST; // Just because it's possible their server isn't configured to accept HTTP Authorization whereas we know this will always work
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return isset( $this->settings['grant_type'] ) ? $this->settings['grant_type'] : 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
return array( 'profile', 'email' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
$return = \IPS\Http\Url::external( $this->settings['url'] . '/oauth/authorize/' );
|
||||
|
||||
if ( $login->type === \IPS\Login::LOGIN_ACP or $login->type === \IPS\Login::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
$return = $return->setQueryString( 'prompt', 'login' );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external( $this->settings['url'] . '/oauth/token/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
$userData = $this->_userData( $accessToken );
|
||||
if ( isset( $userData['email'] ) )
|
||||
{
|
||||
return $userData['email'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'] );
|
||||
if ( !isset( $userData['photoUrlIsDefault'] ) or !$userData['photoUrlIsDefault'] )
|
||||
{
|
||||
return \IPS\Http\Url::external( $userData['photoUrl'] );
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->_userData( $link['token_access_token'] )['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's cover photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userCoverPhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'] );
|
||||
if ( isset( $userData['coverPhotoUrl'] ) and $userData['coverPhotoUrl'] )
|
||||
{
|
||||
return \IPS\Http\Url::external( $userData['coverPhotoUrl'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link to user's remote profile
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( $identifier, $username )
|
||||
{
|
||||
return \IPS\Http\Url::external( $this->settings['url'] )->setQueryString( 'showuser', $identifier );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' or ( $defaultOnly and $this->settings['update_email_changes'] === 'force' ) )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' and isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
$return[] = 'photo';
|
||||
$return[] = 'cover';
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] . '/api/index.php?/core/me' )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$accessToken}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( isset( $response['errorCode'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['errorMessage'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$email = \IPS\Http\Url::external( $this->settings['url'] . '/api/index.php?/core/me/email' )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$accessToken}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( isset( $email['email'] ) )
|
||||
{
|
||||
$response['email'] = $email['email'];
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot Password URL
|
||||
*
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function forgotPasswordUrl()
|
||||
{
|
||||
return \IPS\Http\Url::external( $this->settings['url'] . '/index.php?app=core&module=system&controller=lostpass' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief LinkedIn Login Handler
|
||||
* @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 1 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkedIn Login Handler
|
||||
*/
|
||||
class _LinkedIn extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Linkedin';
|
||||
}
|
||||
|
||||
protected static $enableAcpLoginByDefault = FALSE;
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['login_acp_desc'] = \IPS\Member::loggedIn()->language()->addToStack('login_acp_cannot_reauth');
|
||||
\IPS\Member::loggedIn()->language()->words['oauth_client_id'] = \IPS\Member::loggedIn()->language()->addToStack('login_linkedin_key');
|
||||
|
||||
return array_merge( array(
|
||||
'real_name' => new \IPS\Helpers\Form\Radio( 'login_real_name', isset( $this->settings['real_name'] ) ? $this->settings['real_name'] : 1, FALSE, array(
|
||||
'options' => array(
|
||||
1 => 'login_real_name_linkedin',
|
||||
0 => 'login_real_name_disabled',
|
||||
),
|
||||
'toggles' => array(
|
||||
1 => array( 'login_update_name_changes_inc_optional' ),
|
||||
)
|
||||
), NULL, NULL, NULL, 'login_real_name'
|
||||
) ), parent::acpForm() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return '#007eb3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return 'linkedin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return 'login_linkedin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return 'ipsSocial_linkedin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return \IPS\Theme::i()->resource( 'logos/login/Linkedin.png', 'core', 'interface' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Should client credentials be sent as an "Authoriation" header, or as POST data?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _authenticationType()
|
||||
{
|
||||
return static::AUTHENTICATE_POST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
return array(
|
||||
'r_basicprofile',
|
||||
'r_emailaddress',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
return \IPS\Http\Url::external('https://www.linkedin.com/oauth/v2/authorization');
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external('https://www.linkedin.com/oauth/v2/accessToken');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirection Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function redirectionEndpoint()
|
||||
{
|
||||
if ( isset( $this->settings['legacy_redirect'] ) and $this->settings['legacy_redirect'] )
|
||||
{
|
||||
return \IPS\Http\Url::internal( 'applications/core/interface/linkedin/auth.php', 'none' );
|
||||
}
|
||||
return parent::redirectionEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
if ( isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
return (string) $this->_userData( $accessToken )['formattedName'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['emailAddress'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$userData = $this->_userData( $link['token_access_token'] );
|
||||
|
||||
if ( isset( $userData['pictureUrls'] ) AND isset( $userData['pictureUrls']['_total'] ) AND $userData['pictureUrls']['_total'] )
|
||||
{
|
||||
foreach ( $this->_userData( $link['token_access_token'] )['pictureUrls']['values'] as $picture )
|
||||
{
|
||||
return \IPS\Http\Url::external( $picture );
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->_userData( $link['token_access_token'] )['formattedName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link to user's remote profile
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( $identifier, $username )
|
||||
{
|
||||
return \IPS\Http\Url::external( "https://www.linkedin.com/in/" )->setQueryString( 'id', $identifier );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' and isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
$return[] = 'photo';
|
||||
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://api.linkedin.com/v1/people/~:(id,formattedName,emailAddress,picture-urls::(original),member-url-resources)" )
|
||||
->setQueryString( array( 'format' => 'json' ) )
|
||||
->request()
|
||||
->setHeaders( array( 'Authorization' => "Bearer {$accessToken}" ) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( isset( $response['errorCode'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Microsoft Login Handler
|
||||
* @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 1 June 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Login Handler
|
||||
*/
|
||||
class _Microsoft extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Live';
|
||||
}
|
||||
|
||||
protected static $enableAcpLoginByDefault = FALSE;
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['login_acp_desc'] = \IPS\Member::loggedIn()->language()->addToStack('login_acp_cannot_reauth');
|
||||
\IPS\Member::loggedIn()->language()->words['oauth_client_id'] = \IPS\Member::loggedIn()->language()->addToStack('login_live_client');
|
||||
\IPS\Member::loggedIn()->language()->words['oauth_client_client_secret'] = \IPS\Member::loggedIn()->language()->addToStack('login_live_secret');
|
||||
|
||||
return array_merge( array(
|
||||
'real_name' => new \IPS\Helpers\Form\Radio( 'login_real_name', isset( $this->settings['real_name'] ) ? $this->settings['real_name'] : 1, FALSE, array(
|
||||
'options' => array(
|
||||
1 => 'login_real_name_microsoft',
|
||||
0 => 'login_real_name_disabled',
|
||||
),
|
||||
'toggles' => array(
|
||||
1 => array( 'login_update_name_changes_inc_optional' ),
|
||||
)
|
||||
), NULL, NULL, NULL, 'login_real_name'
|
||||
) ), parent::acpForm() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return '#008b00';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return 'windows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return 'login_live';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonClass()
|
||||
{
|
||||
return 'ipsSocial_microsoft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return \IPS\Theme::i()->resource( 'logos/login/Microsoft.png', 'core', 'interface' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
return array(
|
||||
'wl.signin',
|
||||
'wl.emails',
|
||||
'wl.offline_access'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
return \IPS\Http\Url::external('https://login.live.com/oauth20_authorize.srf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external('https://login.live.com/oauth20_token.srf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirection Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function redirectionEndpoint()
|
||||
{
|
||||
if ( isset( $this->settings['legacy_redirect'] ) and $this->settings['legacy_redirect'] )
|
||||
{
|
||||
return \IPS\Http\Url::internal( 'applications/core/interface/microsoft/auth.php', 'none' );
|
||||
}
|
||||
return parent::redirectionEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
if ( isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
return $this->_userData( $accessToken )['name'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
return $this->_userData( $accessToken )['emails']['preferred'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$url = \IPS\Http\Url::external( "https://apis.live.net/v5.0/me/picture" )->setQueryString( 'access_token', $link['token_access_token'] );
|
||||
if ( $url->request()->head()->httpResponseCode == 404 )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->_userData( $link['token_access_token'] )['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' and isset( $this->settings['real_name'] ) and $this->settings['real_name'] )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
$return[] = 'photo';
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://apis.live.net/v5.0/me" )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$accessToken}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['error']['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract OAuth2 Login Handler
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 31 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract OAuth2 Login Handler
|
||||
*/
|
||||
abstract class _OAuth2 extends \IPS\Login\Handler
|
||||
{
|
||||
/* !Login Handler: Basics */
|
||||
|
||||
/**
|
||||
* @brief Any additional scopes to authenticate with
|
||||
*/
|
||||
public $additionalScopes = NULL;
|
||||
|
||||
/**
|
||||
* Get type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function type()
|
||||
{
|
||||
if ( $this->grantType() === 'password' )
|
||||
{
|
||||
return \IPS\Login::TYPE_USERNAME_PASSWORD;
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Login::TYPE_BUTTON;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
$return = array(
|
||||
array( 'login_handler_oauth_settings', static::getTitle() . '_info' ),
|
||||
'client_id' => new \IPS\Helpers\Form\Text( 'oauth_client_id', isset( $this->settings['client_id'] ) ? $this->settings['client_id'] : NULL, TRUE ),
|
||||
'client_secret' => new \IPS\Helpers\Form\Text( 'oauth_client_client_secret', isset( $this->settings['client_secret'] ) ? $this->settings['client_secret'] : NULL, NULL, array(), NULL, NULL, NULL, 'client_secret' ),
|
||||
);
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
$return['show_in_ucp'] = new \IPS\Helpers\Form\Radio( 'login_handler_show_in_ucp', isset( $this->settings['show_in_ucp'] ) ? $this->settings['show_in_ucp'] : 'always', FALSE, array(
|
||||
'options' => array(
|
||||
'always' => 'login_handler_show_in_ucp_always',
|
||||
'loggedin' => 'login_handler_show_in_ucp_loggedin',
|
||||
),
|
||||
) );
|
||||
$return['update_name_changes'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes', isset( $this->settings['update_name_changes'] ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_name_changes_inc_optional' );
|
||||
$return['update_email_changes'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes', isset( $this->settings['update_email_changes'] ) ? $this->settings['update_email_changes'] : 'optional', FALSE, array( 'options' => array(
|
||||
'force' => 'login_update_changes_yes',
|
||||
'optional' => 'login_update_changes_optional',
|
||||
'disabled' => 'login_update_changes_no',
|
||||
) ), NULL, NULL, NULL, 'login_update_email_changes_inc_optional' );
|
||||
|
||||
return array_merge( $return, parent::acpForm() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Authorization Code / Implicit */
|
||||
if ( $this->grantType() === 'authorization_code' )
|
||||
{
|
||||
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => 'xxx',
|
||||
'redirect_uri' => (string) $this->redirectionEndpoint(),
|
||||
) )->decodeJson();
|
||||
|
||||
if ( isset( $response['error'] ) and $response['error'] === 'invalid_client' )
|
||||
{
|
||||
throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack( 'oauth_setup_error_secret' ) );
|
||||
}
|
||||
}
|
||||
/* Password */
|
||||
elseif ( $this->grantType() === 'password' )
|
||||
{
|
||||
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'password',
|
||||
'username' => 'username',
|
||||
'password' => 'password',
|
||||
) )->decodeJson();
|
||||
|
||||
if ( !isset( $response['error'] ) or $response['error'] !== 'invalid_grant' )
|
||||
{
|
||||
throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack( 'oauth_setup_error_generic', FALSE, array( 'sprintf' => array( isset( $response['error_description'] ) ? $response['error_description'] : NULL ) ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack( 'oauth_setup_error_generic', FALSE, array( 'sprintf' => array( $e->getMessage() ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* !Button Authentication */
|
||||
|
||||
use ButtonHandler;
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateButton( \IPS\Login $login )
|
||||
{
|
||||
/* If we have a code, process it */
|
||||
if ( $this->grantType() === 'authorization_code' and ( isset( \IPS\Request::i()->code ) or isset( \IPS\Request::i()->error ) ) )
|
||||
{
|
||||
return $this->_handleAuthorizationResponse( $login );
|
||||
}
|
||||
|
||||
/* If we have a token, process that */
|
||||
elseif ( $this->grantType() === 'implicit' and ( isset( \IPS\Request::i()->access_token ) or isset( \IPS\Request::i()->error ) ) )
|
||||
{
|
||||
return $this->_handleAuthorizationResponse( $login );
|
||||
}
|
||||
|
||||
/* Otherwise send them to the Authorization Endpoint */
|
||||
else
|
||||
{
|
||||
$target = $this->authorizationEndpoint( $login )->setQueryString( array(
|
||||
'client_id' => $this->settings['client_id'],
|
||||
'response_type' => $this->grantType() === 'authorization_code' ? 'code' : 'implicit',
|
||||
'redirect_uri' => (string) $this->redirectionEndpoint(),
|
||||
'state' => $this->id . '-' . base64_encode( $login->url ) . '-' . \IPS\Session::i()->csrfKey . '-' . \IPS\Request::i()->ref,
|
||||
) );
|
||||
|
||||
if ( $scopes = $this->scopesToRequest( isset( \IPS\Request::i()->scopes ) ? explode( ',', \IPS\Request::i()->scopes ) : NULL ) )
|
||||
{
|
||||
$target = $target->setQueryString( 'scope', implode( ' ', $scopes ) );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $target );
|
||||
}
|
||||
}
|
||||
|
||||
/* !Username/Password Authentication */
|
||||
|
||||
use UsernamePasswordHandler;
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
|
||||
{
|
||||
$data = array(
|
||||
'grant_type' => 'password',
|
||||
'username' => $usernameOrEmail,
|
||||
'password' => $password,
|
||||
);
|
||||
if ( $scopes = $this->scopesToRequest() )
|
||||
{
|
||||
$data['scope'] = implode( ' ', $scopes );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$accessToken = $this->_authenticatedRequest( $this->tokenEndpoint(), $data )->decodeJson();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'oauth' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( isset( $accessToken['access_token'] ) )
|
||||
{
|
||||
return $this->_processAccessToken( $login, $accessToken );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( $accessToken['error'] ) and $accessToken['error'] === 'invalid_grant' )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_bad_username_or_password', \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
\IPS\Log::log( print_r( $accessToken, TRUE ), 'oauth' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticatePasswordForMember( \IPS\Member $member, $password )
|
||||
{
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'password',
|
||||
'username' => $member->name,
|
||||
'password' => $password,
|
||||
) )->decodeJson();
|
||||
if ( isset( $response['access_token'] ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'password',
|
||||
'username' => $member->email,
|
||||
'password' => $password,
|
||||
) )->decodeJson();
|
||||
if ( isset( $response['access_token'] ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* !OAuth Authentication */
|
||||
|
||||
const AUTHENTICATE_HEADER = 'header';
|
||||
const AUTHENTICATE_POST = 'post';
|
||||
|
||||
/**
|
||||
* Should client credentials be sent as an "Authoriation" header, or as POST data?
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _authenticationType()
|
||||
{
|
||||
return static::AUTHENTICATE_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send request authenticated with client credentials
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL
|
||||
* @return \IPS\Http\Response
|
||||
*/
|
||||
protected function _authenticatedRequest( \IPS\Http\Url $url, $data )
|
||||
{
|
||||
$request = $url->request();
|
||||
|
||||
if ( $this->_authenticationType() === static::AUTHENTICATE_HEADER )
|
||||
{
|
||||
$request = $request->login( $this->settings['client_id'], $this->settings['client_secret'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['client_id'] = $this->settings['client_id'];
|
||||
$data['client_secret'] = $this->settings['client_secret'];
|
||||
}
|
||||
|
||||
return $request->post( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle authorization response
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
protected function _handleAuthorizationResponse( \IPS\Login $login )
|
||||
{
|
||||
/* Did we get an error? */
|
||||
if ( isset( \IPS\Request::i()->error ) )
|
||||
{
|
||||
if ( \IPS\Request::i()->error === 'access_denied' )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Log::log( print_r( $_GET, TRUE ), 'oauth' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/* If we have a code, swap it for an access token, otherwise, decode what we have */
|
||||
if ( isset( \IPS\Request::i()->code ) )
|
||||
{
|
||||
$accessToken = $this->_exchangeAuthorizationCodeForAccessToken( \IPS\Request::i()->code );
|
||||
}
|
||||
else
|
||||
{
|
||||
$accessToken = array(
|
||||
'access_token' => \IPS\Request::i()->access_token,
|
||||
'token_type' => isset( \IPS\Request::i()->token_type ) ? \IPS\Request::i()->token_type : 'bearer'
|
||||
);
|
||||
if ( isset( \IPS\Request::i()->expires_in ) )
|
||||
{
|
||||
$accessToken['expires_in'] = \IPS\Request::i()->expires_in;
|
||||
}
|
||||
}
|
||||
|
||||
/* Process */
|
||||
return $this->_processAccessToken( $login, $accessToken );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an Access Token
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param array $accessToken Access Token
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
protected function _processAccessToken( \IPS\Login $login, $accessToken )
|
||||
{
|
||||
/* Get user id */
|
||||
try
|
||||
{
|
||||
$userId = $this->authenticatedUserId( $accessToken['access_token'] );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'oauth' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* What scopes did we get? */
|
||||
if ( isset( $accessToken['scope'] ) )
|
||||
{
|
||||
$scope = explode( ' ', $accessToken['scope'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$scope = $this->scopesIssued( $accessToken['access_token'] );
|
||||
}
|
||||
|
||||
/* Has this user signed in with this service before? */
|
||||
try
|
||||
{
|
||||
$oauthAccess = \IPS\Db::i()->select( '*', 'core_login_links', array( 'token_login_method=? AND token_identifier=?', $this->id, $userId ) )->first();
|
||||
$member = \IPS\Member::load( $oauthAccess['token_member'] );
|
||||
|
||||
/* If the user never finished the linking process, or the account has been deleted, discard this access token */
|
||||
if ( !$oauthAccess['token_linked'] or !$member->member_id )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $oauthAccess['token_member'] ) );
|
||||
throw new \UnderflowException;
|
||||
}
|
||||
|
||||
/* Otherwise, update our token... */
|
||||
\IPS\Db::i()->update( 'core_login_links', array(
|
||||
'token_access_token' => $accessToken['access_token'],
|
||||
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + intval( $accessToken['expires_in'] ) ) : NULL,
|
||||
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
|
||||
'token_scope' => $scope ? json_encode( $scope ) : NULL,
|
||||
), array( 'token_login_method=? AND token_member=?', $this->id, $oauthAccess['token_member'] ) );
|
||||
|
||||
/* ... and return the member object */
|
||||
return $member;
|
||||
}
|
||||
/* No, create or link the account */
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
/* Get the username + email */
|
||||
$name = NULL;
|
||||
try
|
||||
{
|
||||
$name = $this->authenticatedUserName( $accessToken['access_token'] );
|
||||
}
|
||||
catch ( \Exception $e ) {}
|
||||
|
||||
$email = NULL;
|
||||
try
|
||||
{
|
||||
$email = $this->authenticatedEmail( $accessToken['access_token'] );
|
||||
}
|
||||
catch ( \Exception $e ) {}
|
||||
|
||||
try
|
||||
{
|
||||
if ( $login->type === \IPS\Login::LOGIN_UCP )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
|
||||
$exception->handler = $this;
|
||||
$exception->member = $login->reauthenticateAs;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$member = $this->createAccount( $name, $email );
|
||||
|
||||
\IPS\Db::i()->replace( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $member->member_id,
|
||||
'token_identifier' => $userId,
|
||||
'token_linked' => 1,
|
||||
'token_access_token' => $accessToken['access_token'],
|
||||
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + intval( $accessToken['expires_in'] ) ) : NULL,
|
||||
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
|
||||
'token_scope' => $scope ? json_encode( $scope ) : NULL,
|
||||
) );
|
||||
|
||||
$member->logHistory( 'core', 'social_account', array(
|
||||
'service' => static::getTitle(),
|
||||
'handler' => $this->id,
|
||||
'account_id' => $userId,
|
||||
'account_name' => $name,
|
||||
'linked' => TRUE,
|
||||
'registered' => TRUE
|
||||
) );
|
||||
|
||||
if ( $syncOptions = $this->syncOptions( $member, TRUE ) )
|
||||
{
|
||||
$profileSync = array();
|
||||
foreach ( $syncOptions as $option )
|
||||
{
|
||||
$profileSync[ $option ] = array( 'handler' => $this->id, 'ref' => NULL, 'error' => NULL );
|
||||
}
|
||||
$member->profilesync = $profileSync;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Login\Exception $exception )
|
||||
{
|
||||
if ( $exception->getCode() === \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT )
|
||||
{
|
||||
\IPS\Db::i()->replace( 'core_login_links', array(
|
||||
'token_login_method' => $this->id,
|
||||
'token_member' => $exception->member->member_id,
|
||||
'token_identifier' => $userId,
|
||||
'token_linked' => 0,
|
||||
'token_access_token' => $accessToken['access_token'],
|
||||
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + intval( $accessToken['expires_in'] ) ) : NULL,
|
||||
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
|
||||
'token_scope' => $scope ? json_encode( $scope ) : NULL,
|
||||
) );
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for access token
|
||||
*
|
||||
* @param string $code Authorization code
|
||||
* @return array
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
protected function _exchangeAuthorizationCodeForAccessToken( $code )
|
||||
{
|
||||
/* Make the request */
|
||||
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => (string) $this->redirectionEndpoint(),
|
||||
) )->decodeJson();
|
||||
|
||||
/* Check for any errors */
|
||||
if ( isset( $response['error'] ) or !isset( $response['access_token'] ) or ( isset( $response['token_type'] ) and mb_strtolower( $response['token_type'] ) !== 'bearer' ) )
|
||||
{
|
||||
\IPS\Log::log( print_r( $response, TRUE ), 'oauth' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return array
|
||||
*/
|
||||
protected function _link( \IPS\Member $member )
|
||||
{
|
||||
$link = parent::_link( $member );
|
||||
|
||||
if ( $link and $link['token_expires'] and $link['token_expires'] < time() and $link['token_refresh_token'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
$newAccessToken = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $link['token_refresh_token'],
|
||||
) )->decodeJson();
|
||||
|
||||
if ( isset( $newAccessToken['error'] ) or !isset( $newAccessToken['access_token'] ) or ( isset( $newAccessToken['token_type'] ) and mb_strtolower( $newAccessToken['token_type'] ) !== 'bearer' ) )
|
||||
{
|
||||
\IPS\Log::log( print_r( $newAccessToken, TRUE ), 'oauth' );
|
||||
\IPS\Db::i()->update( 'core_login_links', array( 'token_refresh_token' => NULL ), array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) );
|
||||
return $link;
|
||||
}
|
||||
|
||||
$update = array();
|
||||
if ( isset( $newAccessToken['access_token'] ) )
|
||||
{
|
||||
$update['token_access_token'] = $newAccessToken['access_token'];
|
||||
}
|
||||
if ( isset( $newAccessToken['expires_in'] ) )
|
||||
{
|
||||
$update['token_expires'] = $newAccessToken['expires_in'];
|
||||
}
|
||||
if ( isset( $newAccessToken['refresh_token'] ) )
|
||||
{
|
||||
$update['token_refresh_token'] = $newAccessToken['refresh_token'];
|
||||
}
|
||||
|
||||
foreach ( $update as $k => $v )
|
||||
{
|
||||
$link[ $k ] = $v;
|
||||
$this->_cachedLinks[ $member->member_id ][ $k ] = $v;
|
||||
}
|
||||
\IPS\Db::i()->update( 'core_login_links', $update, array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'oauth' );
|
||||
}
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/* !OAuth Abstract */
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function grantType();
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes Issued
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function scopesIssued( $accessToken )
|
||||
{
|
||||
return $this->scopesToRequest(); // Unless the individual handler overrides this, we'll just assume it's given us what we asked for (which is how the OAuth spec says you're supposed to do it anyway)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized scopes
|
||||
*
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function authorizedScopes( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $link['token_scope'] ? json_decode( $link['token_scope'] ) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
abstract protected function authorizationEndpoint( \IPS\Login $login );
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
abstract protected function tokenEndpoint();
|
||||
|
||||
/**
|
||||
* Redirection Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function redirectionEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::internal( 'oauth/callback/', 'none' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function authenticatedUserId( $accessToken );
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's identifier (may not be a number)
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userId( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedUserId( $link['token_access_token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedUserName( $link['token_access_token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userEmail( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedEmail( $link['token_access_token'] );
|
||||
}
|
||||
|
||||
/* !UCP */
|
||||
|
||||
/**
|
||||
* Show in Account Settings?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member, or NULL for if it should show generally
|
||||
* @return bool
|
||||
*/
|
||||
public function showInUcp( \IPS\Member $member = NULL )
|
||||
{
|
||||
if ( !isset( $this->settings['show_in_ucp'] ) ) // Default to showing
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return parent::showInUcp( $member );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Wordpress Login Handler
|
||||
* @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 11 Oct 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler\OAuth2;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wordpress oAuth Login Handler
|
||||
*/
|
||||
class _Wordpress extends \IPS\Login\Handler\OAuth2
|
||||
{
|
||||
/**
|
||||
* @brief Can we have multiple instances of this handler?
|
||||
*/
|
||||
public static $allowMultiple = TRUE;
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_wordpress';
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
$return = array();
|
||||
$return[] = array( 'login_handler_wordpress_settings', 'login_handler_wordpress_oauth_info' );
|
||||
|
||||
$accountManagementSettings = array();
|
||||
$active = 'return';
|
||||
foreach ( parent::acpForm() as $k => $v )
|
||||
{
|
||||
if ( $v === 'account_management_settings' )
|
||||
{
|
||||
$active = 'accountManagementSettings';
|
||||
}
|
||||
if ( !is_string( $v ) and !is_array( $v ) )
|
||||
{
|
||||
${$active}[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$endpointValidation = function( $val )
|
||||
{
|
||||
if ( \IPS\OAUTH_REQUIRES_HTTPS and $val and $val instanceof \IPS\Http\Url )
|
||||
{
|
||||
if ( $val->data[ \IPS\Http\Url::COMPONENT_SCHEME ] !== 'https' )
|
||||
{
|
||||
throw new \DomainException('authorization_endpoint_https');
|
||||
}
|
||||
if ( $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] )
|
||||
{
|
||||
throw new \DomainException('authorization_endpoint_fragment');
|
||||
}
|
||||
}
|
||||
};
|
||||
$return['wordpress_url'] = new \IPS\Helpers\Form\Url( 'wordpress_url', isset( $this->settings['wordpress_url'] ) ? $this->settings['wordpress_url'] : NULL, NULL, array( 'placeholder' => 'https://example.com/' ), $endpointValidation, NULL, NULL, 'wordpress_url' );
|
||||
|
||||
$return[] = 'login_handler_oauth_ui';
|
||||
$return['button_color'] = new \IPS\Helpers\Form\Color( 'oauth_custom_button_color', isset( $this->settings['button_color'] ) ? $this->settings['button_color'] : '#23282d', NULL, array(), NULL, NULL, NULL, 'button_color' );
|
||||
$return['button_text'] = new \IPS\Helpers\Form\Translatable( 'oauth_custom_button_text', NULL, NULL, array( 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('oauth_custom_button_text_custom_placeholder'), 'app' => 'core', 'key' => ( $this->id ? "core_custom_oauth_{$this->id}" : NULL ) ), NULL, NULL, NULL, 'button_text' );
|
||||
$return['button_icon'] = new \IPS\Helpers\Form\Upload( 'oauth_custom_button_icon', ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL, FALSE, array( 'storageExtension' => 'core_Login' ), NULL, NULL, NULL, 'button_icon' );
|
||||
|
||||
$return[] = 'account_management_settings';
|
||||
foreach ( $accountManagementSettings as $k => $v )
|
||||
{
|
||||
$return[ $k ] = $v;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Handler Settings
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function acpFormSave( &$values )
|
||||
{
|
||||
$return = parent::acpFormSave( $values );
|
||||
$return['button_icon'] = (string) $return['button_icon'];
|
||||
$return['wordpress_url'] = (string) $return['wordpress_url'];
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
$parent = parent::formatFormValues( $values );
|
||||
|
||||
if( isset( $values['oauth_custom_button_text'] ) )
|
||||
{
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
\IPS\Lang::saveCustom( 'core', "core_custom_oauth_{$this->id}", $values['oauth_custom_button_text'] );
|
||||
unset( $values['button_text'] );
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonColor()
|
||||
{
|
||||
return $this->settings['button_color'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonIcon()
|
||||
{
|
||||
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : 'wordpress';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in information about logins with this method
|
||||
* Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForDeviceInformation()
|
||||
{
|
||||
return ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] )->url : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo to display in user cp sidebar
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function logoForUcp()
|
||||
{
|
||||
return $this->logoForDeviceInformation() ?: 'wordpress';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buttonText()
|
||||
{
|
||||
return "core_custom_oauth_{$this->id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function grantType()
|
||||
{
|
||||
return 'authorization_code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scopes to request
|
||||
*
|
||||
* @param array|NULL $additional Any additional scopes to request
|
||||
* @return array
|
||||
*/
|
||||
protected function scopesToRequest( $additional=NULL )
|
||||
{
|
||||
return array( "profile" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Endpoint
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function authorizationEndpoint( \IPS\Login $login )
|
||||
{
|
||||
return \IPS\Http\Url::external( rtrim( $this->settings['wordpress_url'], "/" ) . "/wp-content/plugins/miniorange-oauth-20-server/web/moserver/authorize" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function tokenEndpoint()
|
||||
{
|
||||
return \IPS\Http\Url::external( rtrim( $this->settings['wordpress_url'], "/" ) . "/wp-content/plugins/miniorange-oauth-20-server/web/moserver/token" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's identifier (may not be a number)
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string
|
||||
*/
|
||||
protected function authenticatedUserId( $accessToken )
|
||||
{
|
||||
if ( isset( $this->_userData( $accessToken )[ 'username' ] ) )
|
||||
{
|
||||
return $this->_userData( $accessToken )[ 'username' ];
|
||||
}
|
||||
throw new \Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's username
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedUserName( $accessToken )
|
||||
{
|
||||
if ( isset( $this->_userData( $accessToken )[ 'display_name' ] ) )
|
||||
{
|
||||
return $this->_userData( $accessToken )[ 'display_name' ];
|
||||
}
|
||||
throw new \Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user's email address
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function authenticatedEmail( $accessToken )
|
||||
{
|
||||
if ( isset( $this->_userData( $accessToken )[ 'email' ] ) )
|
||||
{
|
||||
return $this->_userData( $accessToken )[ 'email' ];
|
||||
}
|
||||
throw new \Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile photo
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return \IPS\Http\Url|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfilePhoto( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if ( isset( $this->_userData( $link['token_access_token'] )[ 'avatar' ] ) )
|
||||
{
|
||||
return $this->_userData( $link['token_access_token'] )[ 'avatar' ];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's profile name
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param \IPS\Member $member Member
|
||||
* @return string|NULL
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \DomainException General error where it is safe to show a message to the user
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userProfileName( \IPS\Member $member )
|
||||
{
|
||||
if ( !( $link = $this->_link( $member ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
return $this->authenticatedUserName( $link['token_access_token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing Options
|
||||
*
|
||||
* @param \IPS\Member $member The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
|
||||
* @param bool $defaultOnly If TRUE, only returns which options should be enabled by default for a new account
|
||||
* @return array
|
||||
*/
|
||||
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
|
||||
if ( ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' ) )
|
||||
{
|
||||
$return[] = 'email';
|
||||
}
|
||||
|
||||
if ( isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' )
|
||||
{
|
||||
$return[] = 'name';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached user data
|
||||
*/
|
||||
protected $_cachedUserData = array();
|
||||
|
||||
/**
|
||||
* Get user data
|
||||
*
|
||||
* @param string $accessToken Access Token
|
||||
* @throws \IPS\Login\Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws \RuntimeException Unexpected error from service
|
||||
*/
|
||||
protected function _userData( $accessToken )
|
||||
{
|
||||
if ( !isset( $this->_cachedUserData[ $accessToken ] ) )
|
||||
{
|
||||
$data = \IPS\Http\Url::external( rtrim( $this->settings['wordpress_url'], "/" ) . "/wp-content/plugins/miniorange-oauth-20-server/web/moserver/resource" )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => "Bearer {$accessToken}"
|
||||
) )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
/* As with the Custom handler - if we did not get expected data, try using the query string method. */
|
||||
if ( !count( $data ) )
|
||||
{
|
||||
$data = \IPS\Http\Url::external( rtrim( $this->settings['wordpress_url'], "/" ) . "/wp-content/plugins/miniorange-oauth-20-server/web/moserver/resource" )
|
||||
->setQueryString( 'access_token', $accessToken )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $data;
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot Password URL
|
||||
*
|
||||
* @return \IPS\Http\Url|NULL
|
||||
*/
|
||||
public function forgotPasswordUrl()
|
||||
{
|
||||
return \IPS\Http\Url::external( rtrim( $this->settings['wordpress_url'], "/" ) . "/wp-login.php?action=lostpassword" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Standard Internal Database Login Handler
|
||||
* @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 12 May 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Internal Database Login Handler
|
||||
*/
|
||||
class _Standard extends \IPS\Login\Handler
|
||||
{
|
||||
use UsernamePasswordHandler;
|
||||
|
||||
/**
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTitle()
|
||||
{
|
||||
return 'login_handler_Internal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
|
||||
{
|
||||
/* Get member(s) */
|
||||
$where = array();
|
||||
$params = array();
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
$where[] = 'name=?';
|
||||
$params[] = $usernameOrEmail;
|
||||
|
||||
if ( $usernameOrEmail !== \IPS\Request::legacyEscape( $usernameOrEmail ) )
|
||||
{
|
||||
$where[] = 'name=?';
|
||||
$params[] = \IPS\Request::legacyEscape( $usernameOrEmail );
|
||||
}
|
||||
}
|
||||
if ( $this->authType() & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$where[] = 'email=?';
|
||||
$params[] = $usernameOrEmail;
|
||||
|
||||
if ( $usernameOrEmail !== \IPS\Request::legacyEscape( $usernameOrEmail ) )
|
||||
{
|
||||
$where[] = 'email=?';
|
||||
$params[] = \IPS\Request::legacyEscape( $usernameOrEmail );
|
||||
}
|
||||
}
|
||||
$members = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array_merge( array( implode( ' OR ', $where ) ), $params ) ), 'IPS\Member' );
|
||||
|
||||
/* If we didn't match any, throw an exception */
|
||||
if ( !count( $members ) )
|
||||
{
|
||||
$type = 'username_or_email';
|
||||
switch ( $this->authType() )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$type = 'username_or_email';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
$type = 'username';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
$type = 'email_address';
|
||||
break;
|
||||
}
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $type ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Check the password for each possible account */
|
||||
foreach ( $members as $member )
|
||||
{
|
||||
if ( $this->authenticatePasswordForMember( $member, $password ) )
|
||||
{
|
||||
/* If it's the old style, convert it to the new */
|
||||
if ( $member->members_pass_salt )
|
||||
{
|
||||
$member->setLocalPassword( $password );
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? Throw a password incorrect exception */
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticatePasswordForMember( \IPS\Member $member, $password )
|
||||
{
|
||||
if ( password_verify( $password, $member->members_pass_hash ) === TRUE )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
elseif ( $member->members_pass_salt and mb_strlen( $member->members_pass_hash ) === 32 )
|
||||
{
|
||||
return $member->verifyLegacyPassword( $password );
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this handler process a login for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return (bool) $member->members_pass_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this handler process a password change for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canChangePassword( \IPS\Member $member )
|
||||
{
|
||||
return $this->canProcess( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
$member->setLocalPassword( $newPassword );
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show in Account Settings?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member, or NULL for if it should show generally
|
||||
* @return bool
|
||||
*/
|
||||
public function showInUcp( \IPS\Member $member = NULL )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Trait for Login Handlers which accept username/email address and password
|
||||
* @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 12 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login\Handler;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Login Handler for handlers which accept username/email address and password
|
||||
*/
|
||||
trait UsernamePasswordHandler
|
||||
{
|
||||
/**
|
||||
* Get type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function type()
|
||||
{
|
||||
return \IPS\Login::TYPE_USERNAME_PASSWORD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auth type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function authType()
|
||||
{
|
||||
return $this->settings['auth_types'];
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @return array List of settings to save - settings will be stored to core_login_methods.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
return array(
|
||||
'auth_types' => new \IPS\Helpers\Form\Select( 'login_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : ( \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ), TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
abstract public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password );
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function authenticatePasswordForMember( \IPS\Member $member, $password );
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Internal Login Handler
|
||||
* @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 13 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal Login Handler
|
||||
*/
|
||||
class _Internal extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Authentication types
|
||||
*/
|
||||
public $authTypes;
|
||||
|
||||
/**
|
||||
* Initiate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->authTypes = $this->settings['auth_types'] ?: \IPS\Login::AUTH_TYPE_USERNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from from
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $values )
|
||||
{
|
||||
/* Get member(s) */
|
||||
$members = array();
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
$_member = \IPS\Member::load( $values['auth'], 'name', NULL );
|
||||
if ( $_member->member_id )
|
||||
{
|
||||
$members[] = $_member;
|
||||
}
|
||||
|
||||
$_legacyMember = \IPS\Member::load( \IPS\Request::legacyEscape( $values['auth'] ), 'name', NULL );
|
||||
if ( $_legacyMember->member_id )
|
||||
{
|
||||
$members[] = $_legacyMember;
|
||||
}
|
||||
}
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$_member = \IPS\Member::load( $values['auth'], 'email' );
|
||||
if ( $_member->member_id )
|
||||
{
|
||||
$members[] = $_member;
|
||||
}
|
||||
|
||||
$_legacyMember = \IPS\Member::load( \IPS\Request::legacyEscape( $values['auth'] ), 'email' );
|
||||
if ( $_legacyMember->member_id )
|
||||
{
|
||||
$members[] = $_legacyMember;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we didn't match any, throw an exception */
|
||||
if ( empty( $members ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $this->getLoginType( $this->authTypes ) ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Check the password for each possible account */
|
||||
foreach ( $members as $member )
|
||||
{
|
||||
if ( \IPS\Login::compareHashes( $member->members_pass_hash, $member->encryptedPassword( $values['password'] ) ) )
|
||||
{
|
||||
/* If it's the old style, convert it to the new */
|
||||
if ( mb_strlen( $member->members_pass_salt ) !== 22 )
|
||||
{
|
||||
$member->members_pass_salt = $member->generateSalt();
|
||||
$member->members_pass_hash = $member->encryptedPassword( $values['password'] );
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? Throw a password incorrect exception */
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
return array(
|
||||
'auth_types' => new \IPS\Helpers\Form\Select( 'login_auth_types', $this->settings['auth_types'], TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is enabled
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__enabled()
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is locked to current enabled/disabled status
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__locked()
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
if ( !$member->members_pass_hash )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_USERNAME and $member->name )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_EMAIL and $member->email )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'username' or 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
/* We can only change the internal password if we haave one (i.e. we didn't sign in with a social service) because we need the existing password */
|
||||
if ( $type === 'password' )
|
||||
{
|
||||
return (bool) $member->members_pass_hash;
|
||||
}
|
||||
|
||||
/* But we can always change username and email */
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
$member = \IPS\Member::load( $email, 'email' );
|
||||
return (bool) ( $member->member_id and $member != $exclude );
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username )
|
||||
{
|
||||
$member = \IPS\Member::load( $username, 'name' );
|
||||
return (bool) $member->member_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
$member->email = $newEmail;
|
||||
$member->save();
|
||||
$member->memberSync( 'onEmailChange', array( $newEmail, $oldEmail ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
/* Change the password */
|
||||
$member->members_pass_salt = $member->generateSalt();
|
||||
$member->members_pass_hash = $member->encryptedPassword( $newPassword );
|
||||
$member->save();
|
||||
|
||||
/* Resync */
|
||||
$member->memberSync( 'onPassChange', array( $newPassword ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
$member->name = $newUsername;
|
||||
$member->save();
|
||||
}
|
||||
}
|
||||
@@ -1,740 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief IPS Connect Login Handler
|
||||
* @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 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* IPS Connect Login Handler
|
||||
*/
|
||||
class _Ipsconnect extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Authentication types
|
||||
*/
|
||||
public $authTypes;
|
||||
|
||||
/**
|
||||
* Initiate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->authTypes = ( !empty( $this->settings['auth_types'] ) ) ? $this->settings['auth_types'] : \IPS\Login::AUTH_TYPE_USERNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief We are coming from a login request, so do not attempt to register us on the master if a local account is created
|
||||
*/
|
||||
protected $skipMasterRegistration = FALSE;
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $values )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Are we being returned from a cross-domain login? */
|
||||
if( \IPS\Request::i()->loginProcess == 'ipsconnect' AND \IPS\Request::i()->ipsconnectId AND \IPS\Request::i()->ipsconnectKey )
|
||||
{
|
||||
$member = \IPS\Member::load( \IPS\Request::i()->ipsconnectId, 'ipsconnect_id' );
|
||||
|
||||
if( $member->member_id AND \IPS\Login::compareHashes( \IPS\Request::i()->ipsconnectKey, md5( $this->settings['key'] . $member->ipsconnect_id ) ) )
|
||||
{
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
/* First, we need to fetch the user's salt to prevent sending password in plaintext */
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'fetchSalt', 'key' => md5( $this->settings['key'] . $values['auth'] ), 'idType' => $this->authTypes, 'id' => $values['auth'] ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
/* No response or status is not 'SUCCESS' */
|
||||
if ( empty( $response ) OR empty( $response['status'] ) OR $response['status'] != 'SUCCESS' OR !$response['pass_salt'] )
|
||||
{
|
||||
if( isset( $response['status'] ) AND $response['status'] == 'ACCOUNT_NOT_FOUND' )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $this->getLoginType( $this->authTypes ) ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/* Now create a dummy user to get password hash */
|
||||
$testUser = new \IPS\Member;
|
||||
$testUser->members_pass_salt = $response['pass_salt'];
|
||||
$password = $testUser->encryptedPassword( $values['password'] );
|
||||
|
||||
/* Now try to login */
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'login', 'key' => md5( $this->settings['key'] . $values['auth'] ), 'idType' => $this->authTypes, 'id' => $values['auth'], 'password' => $password ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
/* Are we validating? */
|
||||
if( $response['connect_status'] == 'VALIDATING' )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_remote_validate', FALSE, array( 'sprintf' => array( $response['connect_revalidate_url'] ) ) ) );
|
||||
}
|
||||
|
||||
/* No response or status is not 'SUCCESS' */
|
||||
if ( empty( $response ) OR empty( $response['status'] ) OR $response['status'] != 'SUCCESS' )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* No connect status means account not found */
|
||||
if( empty( $response['connect_status'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $this->getLoginType( $this->authTypes ) ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Load the local account now, or create it if it does not exist */
|
||||
$member = \IPS\Member::load( $response['connect_id'], 'ipsconnect_id' );
|
||||
if( !$member->member_id )
|
||||
{
|
||||
$member = \IPS\Member::load( $response['email'], 'email' );
|
||||
}
|
||||
|
||||
/* Update any changes on existing accounts */
|
||||
if ( $member->member_id )
|
||||
{
|
||||
if( $response['connect_id'] != $member->ipsconnect_id )
|
||||
{
|
||||
$member->ipsconnect_id = $response['connect_id'];
|
||||
}
|
||||
|
||||
if( $response['email'] != $member->email )
|
||||
{
|
||||
$member->email = $response['email'];
|
||||
}
|
||||
|
||||
if( $response['name'] != $member->name AND \IPS\CONNECT_NOSYNC_NAMES === FALSE )
|
||||
{
|
||||
$member->name = $response['name'];
|
||||
}
|
||||
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* Otherwise create the account if it does not exist yet */
|
||||
else
|
||||
{
|
||||
$this->skipMasterRegistration = TRUE;
|
||||
|
||||
$member = $this->createOrUpdateAccount( $member, array(
|
||||
'ipsconnect_id' => $response['connect_id'],
|
||||
'members_pass_salt' => $testUser->members_pass_salt,
|
||||
'members_pass_hash' => $password
|
||||
), $response['name'] ?: NULL, $response['email'] ?: NULL );
|
||||
|
||||
$this->skipMasterRegistration = FALSE;
|
||||
}
|
||||
|
||||
/* Do we have any custom callbacks we need to call? */
|
||||
$this->connectSuccessful( $member );
|
||||
|
||||
/* If we are still here and successful, redirect to master to log us in to each installation */
|
||||
\IPS\Login::$forcedRedirectUrl = \IPS\Http\Url::external( $this->settings['url'] )->setQueryString(
|
||||
array(
|
||||
'do' => 'crossLogin',
|
||||
'key' => md5( $this->settings['key'] . $member->ipsconnect_id ),
|
||||
'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php',
|
||||
'id' => $member->ipsconnect_id,
|
||||
'returnTo' => (string) \IPS\Login::getDestination()->setQueryString(
|
||||
array(
|
||||
'loginProcess' => 'ipsconnect',
|
||||
'ipsconnectKey' => md5( $this->settings['key'] . $member->ipsconnect_id ),
|
||||
'ipsconnectId' => $member->ipsconnect_id
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return $member;
|
||||
}
|
||||
/* Have to catch \RuntimeException to catch the BAD_JSON throw from Response.php */
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* If we are still here, throw a generic bad password exception */
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method called upon a successful login - useful for plugins to do "something" when login is successful
|
||||
*
|
||||
* @param \IPS\Member $member Member that is logged in
|
||||
* @return void
|
||||
* @note By default this method does nothing, but is abstracted to allow easy extending
|
||||
*/
|
||||
public function connectSuccessful( \IPS\Member $member )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the URL to redirect to following registration, if any
|
||||
*
|
||||
* @param \IPS\Member $member The member that just registered
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function getRegistrationDestination( $member )
|
||||
{
|
||||
return \IPS\Http\Url::external( $this->settings['url'] )->setQueryString( array( 'do' => 'crossLogin', 'key' => md5( $this->settings['key'] . $member->ipsconnect_id ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'id' => $member->ipsconnect_id, 'returnTo' => (string) \IPS\Http\Url::internal( '', 'front' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
return array(
|
||||
'url' => new \IPS\Helpers\Form\Text( 'login_ipsconnect_url', $this->settings['url'], TRUE ),
|
||||
'key' => new \IPS\Helpers\Form\Text( 'login_ipsconnect_key', $this->settings['key'], TRUE ),
|
||||
'auth_types' => new \IPS\Helpers\Form\Select( 'login_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : \IPS\Login::AUTH_TYPE_USERNAME, TRUE, array( 'options' => array(
|
||||
\IPS\Login::AUTH_TYPE_USERNAME => 'username',
|
||||
\IPS\Login::AUTH_TYPE_EMAIL => 'email_address',
|
||||
\IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return (bool) $this->_getConnectId( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is enabled
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__enabled()
|
||||
{
|
||||
$result = parent::get__enabled();
|
||||
|
||||
/* If we are not enabled, determine if we are a master installation already - if so, we can't be enabled */
|
||||
if( $result === FALSE )
|
||||
{
|
||||
if( \IPS\Db::i()->select( 'COUNT(*)', 'core_ipsconnect_slaves', array( 'slave_enabled=?', 1 ) )->first() > 0 )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'username' or 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return (bool) $this->_getConnectId( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
if( !$this->settings['url'] or $this->settings['url'] == (string) \IPS\Http\Url::internal( 'applications/core/interface/ipsconnect/ipsconnect.php', 'none' ) )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_error_ipsconnect_self') );
|
||||
}
|
||||
|
||||
/* If we are already a master, we can't be a slave too */
|
||||
if( \IPS\Db::i()->select( 'COUNT(*)', 'core_ipsconnect_slaves' )->first() > 0 )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_error_ipsconnect_master') );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'verifySettings', 'key' => $this->settings['key'], 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'ourKey' => \IPS\CONNECT_MASTER_KEY ?: md5( md5( \IPS\Settings::i()->sql_user . \IPS\Settings::i()->sql_pass ) . \IPS\Settings::i()->board_start ) ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( empty( $response ) OR empty( $response['status'] ) OR $response['status'] != 'SUCCESS' )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Ipsconnect') ) ) ) );
|
||||
}
|
||||
}
|
||||
/* Have to catch \RuntimeException to catch the BAD_JSON throw from Response.php */
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Ipsconnect') ) ) ) );
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the user's associated IPS Connect ID
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return int|NULL The IPS Connect ID or NULL if none found
|
||||
*/
|
||||
protected function _getConnectId( \IPS\Member $member )
|
||||
{
|
||||
/* Already have it? */
|
||||
if( $member->ipsconnect_id )
|
||||
{
|
||||
return $member->ipsconnect_id;
|
||||
}
|
||||
|
||||
/* An incomplete member may not have an email address, which won't return an ID */
|
||||
if( !$member->email )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Fetch it from the master */
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'fetchId', 'key' => md5( $this->settings['key'] . $member->email ), 'id' => $member->email, 'idType' => 'email' ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( !empty( $response ) AND !empty( $response['status'] ) AND $response['status'] == 'SUCCESS' AND $response['connect_id'] )
|
||||
{
|
||||
$member->ipsconnect_id = $response['connect_id'];
|
||||
$member->save();
|
||||
}
|
||||
}
|
||||
/* Have to catch \RuntimeException to catch the BAD_JSON throw from Response.php */
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
if( $exclude !== NULL AND $exclude instanceof \IPS\Member AND $email == $exclude->email )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'checkEmail', 'key' => $this->settings['key'], 'email' => $email ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( !empty( $response ) AND !empty( $response['status'] ) AND $response['status'] == 'SUCCESS' AND $response['used'] != 0 )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
/* Have to catch \RuntimeException to catch the BAD_JSON throw from Response.php */
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username )
|
||||
{
|
||||
if( \IPS\CONNECT_NOSYNC_NAMES === TRUE )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'checkName', 'key' => $this->settings['key'], 'name' => $username ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if ( !empty( $response ) AND !empty( $response['status'] ) AND $response['status'] == 'SUCCESS' AND $response['used'] != 0 )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
/* Have to catch \RuntimeException to catch the BAD_JSON throw from Response.php */
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an account off
|
||||
*
|
||||
* @param \IPS\Member $member The member that was just logged out
|
||||
* @param \IPS\Http\Url $redirectUrl The URL to send the user back to
|
||||
* @return void
|
||||
* @note This is NOT called if you force log out all users from the ACP on an individual site
|
||||
*/
|
||||
public function logoutAccount( \IPS\Member $member, \IPS\Http\Url $redirectUrl )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if ( $connectId )
|
||||
{
|
||||
\IPS\Output::i()->redirect(
|
||||
\IPS\Http\Url::external( $this->settings['url'] )->setQueryString( array(
|
||||
'do' => 'logout',
|
||||
'key' => md5( $this->settings['key'] . $connectId ),
|
||||
'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php',
|
||||
'id' => $connectId,
|
||||
'returnTo' => (string) $redirectUrl,
|
||||
) ),
|
||||
'',
|
||||
303
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @return void
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if( $connectId )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'changeEmail', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'email' => $newEmail, 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
catch( \RuntimeException $e ){}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if( $connectId )
|
||||
{
|
||||
/* Now create a dummy user to get password hash */
|
||||
$testUser = new \IPS\Member;
|
||||
$testUser->members_pass_salt = $testUser->generateSalt();
|
||||
$password = $testUser->encryptedPassword( $newPassword );
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'changePassword', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'pass_hash' => $password, 'pass_salt' => $testUser->members_pass_salt, 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
catch( \RuntimeException $e ){}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
if( \IPS\CONNECT_NOSYNC_NAMES === TRUE )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if( $connectId )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'changeName', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'name' => $newUsername, 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
catch( \RuntimeException $e ){}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an account
|
||||
*
|
||||
* @param \IPS\Member $member The member that was just created
|
||||
* @return void
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function createAccount( \IPS\Member $member )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* If account creation is occurring because the user just successfully logged in, do not try to then create an account on the master install */
|
||||
if( $this->skipMasterRegistration === TRUE )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array(
|
||||
'do' => 'register',
|
||||
'key' => $this->settings['key'],
|
||||
'name' => $member->name,
|
||||
'email' => $member->email,
|
||||
'pass_hash' => $member->members_pass_hash,
|
||||
'pass_salt' => $member->members_pass_salt,
|
||||
'revalidateUrl' => ( $member->members_bitoptions['validating'] == TRUE ) ? (string) \IPS\Http\Url::internal( "", 'front' ) : '',
|
||||
'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php'
|
||||
) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if( !empty( $response ) AND !empty( $response['status'] ) AND $response['status'] == 'SUCCESS' AND !empty( $response['connect_id'] ) )
|
||||
{
|
||||
$member->ipsconnect_id = $response['connect_id'];
|
||||
$member->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return void
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function validateAccount( \IPS\Member $member )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if( $connectId )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'validate', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return void
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function deleteAccount( \IPS\Member $member )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if( $connectId )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'delete', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban or unban account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param bool TRUE means member is being banned, FALSE means they are being unbanned
|
||||
* @return void
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function banAccount( \IPS\Member $member, $ban=TRUE )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
|
||||
if( $connectId )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'ban', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'status' => (int) $ban, 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two accounts
|
||||
*
|
||||
* @param \IPS\Member $member The member to keep with original data
|
||||
* @param \IPS\Member $member2 The member that will be deleted
|
||||
* @return void
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function mergeAccounts( \IPS\Member $member, \IPS\Member $member2 )
|
||||
{
|
||||
if( \IPS\Request::i()->slaveCall )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$connectId = $this->_getConnectId( $member );
|
||||
$connectId2 = $this->_getConnectId( $member2 );
|
||||
|
||||
if( $connectId AND $connectId2 )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'merge', 'key' => md5( $this->settings['key'] . $connectId ), 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php', 'remove' => $connectId2, 'id' => $connectId ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an action when the login handler is disabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onDisable()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( $this->settings['url'] )
|
||||
->setQueryString( array( 'do' => 'disableSlave', 'key' => $this->settings['key'], 'url' => \IPS\Settings::i()->base_url . '/applications/core/interface/ipsconnect/ipsconnect.php' ) )
|
||||
->request()
|
||||
->get()
|
||||
->decodeJson();
|
||||
}
|
||||
catch( \RuntimeException $e ){}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief LDAP Login Handler
|
||||
* @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 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP Login Handler
|
||||
*/
|
||||
class _Ldap extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Authentication types
|
||||
*/
|
||||
public $authTypes = 0;
|
||||
|
||||
/**
|
||||
* @brief LDAP Resource
|
||||
*/
|
||||
protected $ldap;
|
||||
|
||||
/**
|
||||
* Initiate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
if ( $this->settings['uid_field'] )
|
||||
{
|
||||
$this->authTypes += \IPS\Login::AUTH_TYPE_USERNAME;
|
||||
}
|
||||
if ( $this->settings['email_field'] )
|
||||
{
|
||||
$this->authTypes += \IPS\Login::AUTH_TYPE_EMAIL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from from
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $values )
|
||||
{
|
||||
/* Get user */
|
||||
$result = $this->getUser( $values['auth'], $values['auth'] );
|
||||
if ( !$result )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('username') ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Find or create member */
|
||||
$member = \IPS\Member::load( $values['auth'], $result['type'] );
|
||||
|
||||
/* Check Password */
|
||||
if ( !@ldap_bind( $this->ldap, ldap_get_dn( $this->ldap, $result['resource'] ), ( $this->settings['pw_required'] ? $values['password'] : '' ) ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
|
||||
/* Return or create member */
|
||||
if ( !$member->member_id )
|
||||
{
|
||||
$userData = ldap_get_attributes( $this->ldap, $result['resource'] );
|
||||
|
||||
$member = $this->createOrUpdateAccount(
|
||||
NULL,
|
||||
array(),
|
||||
( $this->settings['uid_field'] and isset( $userData[ $this->settings['uid_field'] ][0] ) ) ? $userData[ $this->settings['uid_field'] ][0] : NULL,
|
||||
( $this->settings['email_field'] and isset( $userData[ $this->settings['email_field'] ][0] ) ) ? $userData[ $this->settings['email_field'] ][0] : NULL
|
||||
);
|
||||
}
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to LDAP server
|
||||
*
|
||||
* @return resource
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function ldap()
|
||||
{
|
||||
/* Connect to server */
|
||||
if ( !$this->settings['server_port'] )
|
||||
{
|
||||
$this->ldap = ldap_connect( $this->settings['server_host'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->ldap = ldap_connect( $this->settings['server_host'], $this->settings['server_port'] );
|
||||
}
|
||||
|
||||
/* Specify Protocol Version */
|
||||
ldap_set_option( $this->ldap, LDAP_OPT_PROTOCOL_VERSION, $this->settings['server_protocol'] );
|
||||
|
||||
/* OPT Referrals */
|
||||
if ( $this->settings['opt_referrals'] )
|
||||
{
|
||||
ldap_set_option( $this->ldap, LDAP_OPT_REFERRALS, true );
|
||||
}
|
||||
else
|
||||
{
|
||||
ldap_set_option( $this->ldap, LDAP_OPT_REFERRALS, false );
|
||||
}
|
||||
|
||||
/* Bind to directory */
|
||||
if ( $this->settings['server_user'] or $this->settings['server_pass'] )
|
||||
{
|
||||
$bind = ldap_bind( $this->ldap, $this->settings['server_user'], $this->settings['server_pass'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$bind = ldap_bind( $this->ldap );
|
||||
}
|
||||
|
||||
if ( $bind === FALSE )
|
||||
{
|
||||
throw new \RuntimeException( 'ldap_err_bind' );
|
||||
}
|
||||
|
||||
return $this->ldap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user
|
||||
*
|
||||
* @param string $username The username
|
||||
* @paeam string $email The email address
|
||||
* @return array('resource' => resource, 'type' => 'name'|'email)|FALSE
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
protected function getUser( $username=NULL, $email=NULL )
|
||||
{
|
||||
$result = NULL;
|
||||
$type = NULL;
|
||||
|
||||
/* Connect */
|
||||
try
|
||||
{
|
||||
$this->ldap = $this->ldap();
|
||||
}
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Try email address */
|
||||
if ( $email and $this->authTypes & \IPS\Login::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$result = $this->getUserWithFilter("{$this->settings['email_field']}={$email}");
|
||||
if ( $result )
|
||||
{
|
||||
$type = 'email';
|
||||
}
|
||||
}
|
||||
|
||||
/* Try username */
|
||||
if ( !$result and $username and $this->authTypes & \IPS\Login::AUTH_TYPE_USERNAME )
|
||||
{
|
||||
$result = $this->getUserWithFilter("{$this->settings['uid_field']}={$username}{$this->settings['un_suffix']}");
|
||||
if ( $result )
|
||||
{
|
||||
$type = 'name';
|
||||
}
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $result ? array( 'resource' => $result, 'type' => $type ) : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user with filter
|
||||
*
|
||||
* @param string $filter Filter
|
||||
* @return resource|FALSE
|
||||
*/
|
||||
protected function getUserWithFilter( $filter )
|
||||
{
|
||||
/* Add any additional filter */
|
||||
if ( $this->settings['filter'] )
|
||||
{
|
||||
$filter = ( mb_substr( $this->settings['filter'], 0, 1 ) === '(' ) ? "(&({$filter}){$this->settings['filter']})" : "(&({$filter})({$this->settings['filter']}))";
|
||||
}
|
||||
|
||||
/* Get user */
|
||||
$search = ldap_search( $this->ldap, $this->settings['base_dn'], $filter );
|
||||
$result = ldap_first_entry( $this->ldap, $search );
|
||||
|
||||
/* Return */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
return array(
|
||||
'server_protocol' => new \IPS\Helpers\Form\Select( 'ldap_server_protocol', $this->settings['server_protocol'], TRUE, array( 'options' => array( 3 => 3, 2 => 2 ) ) ),
|
||||
'server_host' => new \IPS\Helpers\Form\Text( 'ldap_server_host', $this->settings['server_host'], TRUE ),
|
||||
'server_port' => new \IPS\Helpers\Form\Number( 'ldap_server_port', $this->settings['server_port'] ),
|
||||
'opt_referrals' => new \IPS\Helpers\Form\YesNo( 'ldap_opt_referrals', $this->settings['opt_referrals'] ?: FALSE, TRUE ),
|
||||
'server_user' => new \IPS\Helpers\Form\Text( 'ldap_server_user', $this->settings['server_user'] ),
|
||||
'server_pass' => new \IPS\Helpers\Form\Text( 'ldap_server_pass', $this->settings['server_pass'] ),
|
||||
'base_dn' => new \IPS\Helpers\Form\Text( 'ldap_base_dn', $this->settings['base_dn'], TRUE ),
|
||||
'uid_field' => new \IPS\Helpers\Form\Text( 'ldap_uid_field', $this->settings['uid_field'] ?: 'uid' ),
|
||||
'email_field' => new \IPS\Helpers\Form\Text( 'ldap_email_field', $this->settings['email_field'] ?: 'mail' ),
|
||||
'un_suffix' => new \IPS\Helpers\Form\Text( 'ldap_un_suffix', $this->settings['un_suffix'] ),
|
||||
'pw_required' => new \IPS\Helpers\Form\YesNo( 'ldap_pw_required', $this->settings['pw_required'] ?: TRUE, TRUE ),
|
||||
'filter' => new \IPS\Helpers\Form\Text( 'ldap_filter', $this->settings['filter'] ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
if ( !extension_loaded('ldap') )
|
||||
{
|
||||
throw new \InvalidArgumentException( 'login_ldap_err' );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->ldap();
|
||||
}
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( 'login_ldap_err_connect' );
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_USERNAME and $member->name and $this->usernameIsInUse( $member->name ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
if ( $this->authTypes & \IPS\Login::AUTH_TYPE_EMAIL and $member->email and $this->emailIsInUse( $member->email ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'username' or 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return $this->canProcess( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $exclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
if ( $exclude )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (bool) $this->getUser( NULL, $email );
|
||||
}
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username )
|
||||
{
|
||||
try
|
||||
{
|
||||
return (bool) $this->getUser( $username, NULL );
|
||||
}
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
{
|
||||
$user = $this->getUser( NULL, $member->email );
|
||||
if ( $user )
|
||||
{
|
||||
ldap_modify( $this->ldap, ldap_get_dn( $this->ldap, $user['resource'] ), array( $this->settings['email_field'] => $newEmail ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
$user = $this->getUser( $member->name, $member->email );
|
||||
if ( $user )
|
||||
{
|
||||
ldap_modify( $this->ldap, ldap_get_dn( $this->ldap, $user['resource'] ), array( 'userPassword' => "{SHA}" . base64_encode( pack( "H*", sha1( $newPassword ) ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
$user = $this->getUser( $member->name, $member->email );
|
||||
if ( $user )
|
||||
{
|
||||
ldap_modify( $this->ldap, ldap_get_dn( $this->ldap, $user['resource'] ), array( $this->settings['uid_field'] => $newUsername . $this->settings['un_suffix'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief LinkedIn Login Handler
|
||||
* @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 20 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkedIn Login Handler
|
||||
*/
|
||||
class _Linkedin extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'linkedin';
|
||||
|
||||
/**
|
||||
* @brief Logo
|
||||
*/
|
||||
public static $logo = 'Linkedin';
|
||||
|
||||
/**
|
||||
* Get Form
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL for the login page
|
||||
* @param bool $ucp Is UCP? (as opposed to login form)
|
||||
* @param \IPS\Http\Url $destination The URL to redirect to after a successful login
|
||||
* @return string
|
||||
*/
|
||||
public function loginForm( \IPS\Http\Url $url, $ucp = FALSE, \IPS\Http\Url $destination = NULL )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->linkedin( (string) \IPS\Http\Url::external( "https://www.linkedin.com/uas/oauth2/authorization" )->setQueryString( array(
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->settings['api_key'],
|
||||
'scope' => 'r_basicprofile r_emailaddress',
|
||||
'state' => ( $ucp ? 'ucp' : \IPS\Dispatcher::i()->controllerLocation ) . '-' . \IPS\Session::i()->csrfKey . '-' . ( $destination ? base64_encode( $destination ) : '' ),
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/linkedin/auth.php', 'none', \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param string $url The URL for the login page
|
||||
* @param \IPS\Member $member If we want to integrate this login method with an existing member, provide the member object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $url, $member=NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* CSRF Check */
|
||||
if ( \IPS\Request::i()->state !== \IPS\Session::i()->csrfKey )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'CSRF_FAIL', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if( !isset( \IPS\Request::i()->code ) OR !\IPS\Request::i()->code )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'denied_oauth', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get a token */
|
||||
$response = \IPS\Http\Url::external( "https://www.linkedin.com/uas/oauth2/accessToken?" . http_build_query( array(
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => \IPS\Request::i()->code,
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/linkedin/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'client_id' => $this->settings['api_key'],
|
||||
'client_secret' => $this->settings['secret_key'],
|
||||
) ) )->request()->post()->decodeJson();
|
||||
|
||||
if ( isset( $response['error'] ) or !isset( $response['access_token'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get user data */
|
||||
$userData = array();
|
||||
foreach ( \IPS\Http\Url::external( "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address)?oauth2_access_token={$response['access_token']}" )->request()->get()->decodeXml() as $k => $v )
|
||||
{
|
||||
$userData[ $k ] = (string) $v;
|
||||
}
|
||||
|
||||
/* Find or create member */
|
||||
$member = $this->createOrUpdateAccount( $member ?: \IPS\Member::load( $userData['id'], 'linkedin_id' ), array(
|
||||
'linkedin_id' => $userData['id'],
|
||||
'linkedin_token' => $response['access_token'],
|
||||
), $this->settings['real_name'] ? $userData['formatted-name'] : NULL, $userData['email-address'], $response['access_token'], array( 'photo' => TRUE ) );
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param mixed $details Details as they were passed to the exception thrown in authenticate()
|
||||
* @return void
|
||||
*/
|
||||
public static function link( \IPS\Member $member, $details )
|
||||
{
|
||||
$userData = array();
|
||||
foreach ( \IPS\Http\Url::external( "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address)?oauth2_access_token={$details}" )->request()->get()->decodeXml() as $k => $v )
|
||||
{
|
||||
$userData[ $k ] = (string) $v;
|
||||
}
|
||||
|
||||
$member->linkedin_id = $userData['id'];
|
||||
$member->linkedin_token = $details;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Output::i()->sidebar['actions'] = array(
|
||||
'help' => array(
|
||||
'title' => 'help',
|
||||
'icon' => 'question-circle',
|
||||
'link' => \IPS\Http\Url::ips( 'docs/login_linkedin' ),
|
||||
'target' => '_blank',
|
||||
'class' => ''
|
||||
),
|
||||
);
|
||||
|
||||
return array(
|
||||
'api_key' => new \IPS\Helpers\Form\Text( 'login_linkedin_key', ( isset( $this->settings['api_key'] ) ) ? $this->settings['api_key'] : '', TRUE ),
|
||||
'secret_key' => new \IPS\Helpers\Form\Text( 'login_linkedin_secret', ( isset( $this->settings['secret_key'] ) ) ? $this->settings['secret_key'] : '', TRUE ),
|
||||
'real_name' => new \IPS\Helpers\Form\YesNo( 'login_real_name', ( isset( $this->settings['real_name'] ) ) ? $this->settings['real_name'] : FALSE, TRUE )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \UnexpectedValueException If response code is not 200
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://www.linkedin.com/uas/oauth2/authorization" )->setQueryString( array(
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->settings['api_key'],
|
||||
'scope' => 'r_basicprofile r_emailaddress',
|
||||
'state' => \IPS\Session::i()->csrfKey,
|
||||
'redirect_uri' => \IPS\Settings::i()->base_url
|
||||
) )->request()->get();
|
||||
|
||||
if ( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Linkedin') ) ) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Linkedin') ) ) ) );
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return ( $member->linkedin_id and $member->linkedin_token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Windows Live Login Handler
|
||||
* @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 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows Live Login Handler
|
||||
*/
|
||||
class _Live extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'windows';
|
||||
|
||||
/**
|
||||
* @brief Logo
|
||||
*/
|
||||
public static $logo = 'Microsoft';
|
||||
|
||||
/**
|
||||
* Get Form
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL for the login page
|
||||
* @param bool $ucp Is UCP? (as opposed to login form)
|
||||
* @param \IPS\Http\Url $destination The URL to redirect to after a successful login
|
||||
* @return string
|
||||
*/
|
||||
public function loginForm( \IPS\Http\Url $url, $ucp = FALSE, \IPS\Http\Url $destination = NULL )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->live( (string) \IPS\Http\Url::external( 'https://login.live.com/oauth20_authorize.srf' )->setQueryString( array(
|
||||
'client_id' => $this->settings['client_id'],
|
||||
'scope' => 'wl.signin wl.emails wl.offline_access',
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/microsoft/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'state' => ( $ucp ? 'ucp' : \IPS\Dispatcher::i()->controllerLocation ) . '-' . \IPS\Session::i()->csrfKey . '-' . ( $destination ? base64_encode( $destination ) : '' )
|
||||
) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param string $url The URL for the login page
|
||||
* @param \IPS\Member $member If we want to integrate this login method with an existing member, provide the member object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $url, $member=NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* CSRF Check */
|
||||
if ( \IPS\Request::i()->state !== \IPS\Session::i()->csrfKey )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'CSRF_FAIL', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
if( !isset( \IPS\Request::i()->code ) OR !\IPS\Request::i()->code )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'denied_oauth', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Send HTTP request */
|
||||
$response = \IPS\Http\Url::external( 'https://login.live.com/oauth20_token.srf' )->request()->post( array(
|
||||
'client_id' => $this->settings['client_id'],
|
||||
'redirect_uri' => (string) \IPS\Http\Url::internal( 'applications/core/interface/microsoft/auth.php', 'none', NULL, array(), \IPS\Settings::i()->logins_over_https ? \IPS\Http\Url::PROTOCOL_HTTPS : 0 ),
|
||||
'client_secret' => $this->settings['client_secret'],
|
||||
'code' => \IPS\Request::i()->code,
|
||||
'grant_type' => 'authorization_code'
|
||||
) )->decodeJson();
|
||||
|
||||
/* Check the response */
|
||||
if ( isset( $response['error'] ) or !isset( $response['access_token'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Get user data */
|
||||
$userData = \IPS\Http\Url::external( "https://apis.live.net/v5.0/me?access_token={$response['access_token']}" )->request()->get()->decodeJson();
|
||||
|
||||
/* Find or create member */
|
||||
$member = $this->createOrUpdateAccount( $member ?: \IPS\Member::load( $userData['id'], 'live_id' ), array(
|
||||
'live_id' => $userData['id'],
|
||||
'live_token' => $response['refresh_token'],
|
||||
), $this->settings['real_name'] ? $userData['name'] : NULL, $userData['emails']['preferred'], $response['access_token'], array( 'photo' => TRUE, 'cover' => FALSE, 'status' => '' ), 'IPS\core\ProfileSync\Microsoft' );
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param mixed $details Details as they were passed to the exception thrown in authenticate()
|
||||
* @return void
|
||||
*/
|
||||
public static function link( \IPS\Member $member, $details )
|
||||
{
|
||||
$userData = \IPS\Http\Url::external( "https://apis.live.net/v5.0/me?access_token={$details}" )->request()->get()->decodeJson();
|
||||
$member->live_id = $userData['id'];
|
||||
$member->live_token = $details;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Output::i()->sidebar['actions'] = array(
|
||||
'help' => array(
|
||||
'title' => 'help',
|
||||
'icon' => 'question-circle',
|
||||
'link' => \IPS\Http\Url::ips( 'docs/login_live' ),
|
||||
'target' => '_blank',
|
||||
'class' => ''
|
||||
),
|
||||
);
|
||||
|
||||
return array(
|
||||
'client_id' => new \IPS\Helpers\Form\Text( 'login_live_client', ( isset( $this->settings['client_id'] ) ) ? $this->settings['client_id'] : '', TRUE ),
|
||||
'client_secret' => new \IPS\Helpers\Form\Text( 'login_live_secret', ( isset( $this->settings['client_secret'] ) ) ? $this->settings['client_secret'] : '', TRUE ),
|
||||
'real_name' => new \IPS\Helpers\Form\YesNo( 'login_real_name', ( isset( $this->settings['real_name'] ) ) ? $this->settings['real_name'] : FALSE, TRUE )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
* @throws \UnexpectedValueException If response code is not 200
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://login.live.com/oauth20_authorize.srf?client_id={$this->settings['client_id']}&scope=wl.signin%20wl.emails&response_type=code&redirect_uri=" )->request()->get();
|
||||
if ( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Live') ) ) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Live') ) ) ) );
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return ( $member->live_id and $member->live_token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
+370
-470
@@ -5,7 +5,8 @@
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 13 Mar 2013
|
||||
* @since 13 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS;
|
||||
@@ -22,512 +23,309 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Login
|
||||
{
|
||||
const LOGIN_FRONT = 1;
|
||||
const LOGIN_ACP = 2;
|
||||
const LOGIN_REGISTRATION_FORM = 3;
|
||||
const LOGIN_REAUTHENTICATE = 4;
|
||||
const LOGIN_UCP = 5;
|
||||
|
||||
const AUTH_TYPE_USERNAME = 1;
|
||||
const AUTH_TYPE_EMAIL = 2;
|
||||
|
||||
/**
|
||||
* @brief Handlers
|
||||
*/
|
||||
public static $handlers = NULL;
|
||||
|
||||
/**
|
||||
* @brief All handlers
|
||||
*/
|
||||
public static $allHandlers = NULL;
|
||||
const TYPE_USERNAME_PASSWORD = 1;
|
||||
const TYPE_BUTTON = 2;
|
||||
|
||||
/**
|
||||
* Get Handlers
|
||||
*
|
||||
* @param bool $all Force fetching of all enabled login handlers
|
||||
* @return array
|
||||
*/
|
||||
public static function handlers( $all=FALSE )
|
||||
{
|
||||
/* Do we need all handlers? */
|
||||
if( $all === TRUE )
|
||||
{
|
||||
if( static::$allHandlers !== NULL )
|
||||
{
|
||||
return static::$allHandlers;
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_login_handlers', 'login_enabled=1', 'login_order' ) as $row )
|
||||
{
|
||||
try
|
||||
{
|
||||
static::$allHandlers[ $row['login_key'] ] = \IPS\Login\LoginAbstract::constructFromData( $row );
|
||||
}
|
||||
catch ( \RuntimeException $e ) { /* Skip over any which error (may happen if they haven't been updated for 4.x for example */ }
|
||||
}
|
||||
|
||||
if ( \IPS\Dispatcher::hasInstance() === TRUE )
|
||||
{
|
||||
if( \IPS\Dispatcher::i()->controllerLocation == 'front' )
|
||||
{
|
||||
static::$handlers = static::$allHandlers;
|
||||
}
|
||||
}
|
||||
|
||||
return static::$allHandlers;
|
||||
}
|
||||
|
||||
/* Fetch the appropriate handlers */
|
||||
if ( static::$handlers === NULL )
|
||||
{
|
||||
if ( \IPS\Dispatcher::i()->controllerLocation === 'front' )
|
||||
{
|
||||
if ( isset( \IPS\Data\Store::i()->loginHandlers ) )
|
||||
{
|
||||
$rows = \IPS\Data\Store::i()->loginHandlers;
|
||||
}
|
||||
else
|
||||
{
|
||||
$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_login_handlers', 'login_enabled=1', 'login_order' ) );
|
||||
\IPS\Data\Store::i()->loginHandlers = $rows;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$rows = \IPS\Db::i()->select( '*', 'core_login_handlers', 'login_enabled=1 AND login_acp=1', 'login_order' );
|
||||
}
|
||||
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
try
|
||||
{
|
||||
static::$handlers[ $row['login_key'] ] = \IPS\Login\LoginAbstract::constructFromData( $row );
|
||||
}
|
||||
catch ( \RuntimeException $e ) { /* Skip over any which error (may happen if they haven't bee updated for 4.x for example */ }
|
||||
}
|
||||
|
||||
if( \IPS\Dispatcher::i()->controllerLocation == 'front' )
|
||||
{
|
||||
static::$allHandlers = static::$handlers;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we have no handlers, use the standard internal */
|
||||
if ( empty( static::$handlers ) )
|
||||
{
|
||||
static::$handlers['Internal'] = \IPS\Login\LoginAbstract::load('internal');
|
||||
}
|
||||
|
||||
return static::$handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single handler object
|
||||
*
|
||||
* @throws UnderflowException
|
||||
* @return \IPS\Login
|
||||
*/
|
||||
public static function getHandler( $handler )
|
||||
{
|
||||
return \IPS\Login\LoginAbstract::constructFromData( \IPS\Db::i()->select( '*', 'core_login_handlers', array( 'login_key=?', $handler ) )->first() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief URL
|
||||
*/
|
||||
protected $url = '';
|
||||
|
||||
/**
|
||||
* @brief Handlers which use the 'Standard' log in form
|
||||
*/
|
||||
public $standardHandlers = array();
|
||||
|
||||
/**
|
||||
* @brief Forms
|
||||
*/
|
||||
protected $forms = NULL;
|
||||
|
||||
/**
|
||||
* @brief Show flag options (remember me, anonymous) on form?
|
||||
*/
|
||||
public $flagOptions = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Which handler was used?
|
||||
*/
|
||||
public $usedHandler = NULL;
|
||||
public $url;
|
||||
|
||||
/**
|
||||
* @brief Login form type
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @brief Reauthenticating member
|
||||
*/
|
||||
public $reauthenticateAs;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL page for the login screen
|
||||
* @param \IPS\Http\Url $url The URL page for the login screen
|
||||
* @param \IPS\Http\Url $returnUrl The URL to send the user back to after login
|
||||
* @param int $type One of the LOGIN_* constants
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Http\Url $url )
|
||||
public function __construct( \IPS\Http\Url $url = NULL, $type=1 )
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Force a URL to send to post-login
|
||||
* @note Useful when you need to redirect the user to another URL that is not local to this installation
|
||||
*/
|
||||
public static $forcedRedirectUrl = NULL;
|
||||
|
||||
/**
|
||||
* Fetch the URL to redirect to
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public static function getDestination()
|
||||
{
|
||||
/* Try and get a referrer... */
|
||||
try
|
||||
$this->type = $type;
|
||||
|
||||
if ( $type === static::LOGIN_REAUTHENTICATE or $type === static::LOGIN_UCP )
|
||||
{
|
||||
/* Are we forcing the user to be sent to a specific URL? */
|
||||
if( static::$forcedRedirectUrl !== NULL )
|
||||
{
|
||||
return static::$forcedRedirectUrl;
|
||||
}
|
||||
|
||||
/* Get the URL we need to redirect to */
|
||||
if ( isset( \IPS\Request::i()->ref ) and $decoded = @base64_decode( \IPS\Request::i()->ref ) )
|
||||
{
|
||||
$ref = \IPS\Http\Url::createFromString( $decoded );
|
||||
}
|
||||
elseif ( isset( $_SERVER['HTTP_REFERER'] ) )
|
||||
{
|
||||
$ref = \IPS\Http\Url::createFromString( $_SERVER['HTTP_REFERER'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
/* Make sure it's internal and to the front-end */
|
||||
if ( !( $ref instanceof \IPS\Http\Url\Internal ) or $ref->base !== 'front' )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
/* Strip the csrf and return */
|
||||
return $ref->stripQueryString( 'csrfKey' );
|
||||
}
|
||||
/* And if anything goes wrong, just use the base URL */
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
return \IPS\Http\Url::internal('');
|
||||
$this->reauthenticateAs = \IPS\Member::loggedIn();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* !Methods */
|
||||
|
||||
/**
|
||||
* Fetch the URL to redirect to following registration, if any
|
||||
* Get methods
|
||||
*
|
||||
* @param \IPS\Member $member The member that just registered
|
||||
* @return \IPS\Http\Url
|
||||
* @return array
|
||||
*/
|
||||
public static function getRegistrationDestination( $member )
|
||||
public static function methods()
|
||||
{
|
||||
foreach ( static::handlers() as $key => $handler )
|
||||
if ( !isset( \IPS\Data\Store::i()->loginMethods ) )
|
||||
{
|
||||
if ( method_exists( $handler, 'getRegistrationDestination' ) )
|
||||
{
|
||||
return $handler->getRegistrationDestination( $member );
|
||||
}
|
||||
\IPS\Data\Store::i()->loginMethods = iterator_to_array( \IPS\Db::i()->select( '*', 'core_login_methods', array( 'login_enabled=1' ) )->setKeyField('login_id') );
|
||||
}
|
||||
|
||||
$ref = NULL;
|
||||
if ( isset( \IPS\Request::i()->ref ) )
|
||||
$return = array();
|
||||
foreach ( \IPS\Data\Store::i()->loginMethods as $row )
|
||||
{
|
||||
try
|
||||
{
|
||||
$ref = \IPS\Http\Url::createFromString( base64_decode( \IPS\Request::i()->ref ) );
|
||||
if ( !( $ref instanceof \IPS\Http\Url\Internal ) )
|
||||
{
|
||||
$ref = NULL;
|
||||
}
|
||||
$return[ $row['login_id'] ] = \IPS\Login\Handler::constructFromData( $row );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
if ( in_array( \IPS\Settings::i()->reg_auth_type, array( 'admin', 'admin_user' ) ) )
|
||||
{
|
||||
return \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=validating', 'front', 'register' );
|
||||
}
|
||||
|
||||
return $ref ?: \IPS\Http\Url::internal( '' );
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get Login Forms
|
||||
* Get methods
|
||||
*
|
||||
* @param bool $acp TRUE=ACP login form, FALSE=front end login form
|
||||
* @param bool $skipReferer If set to true we will skip adding the "referer" to the form, useful for the quick login popup where you want the same page to reload
|
||||
* @return array
|
||||
*/
|
||||
public function forms( $acp=FALSE, $skipReferer=FALSE )
|
||||
protected function _methods()
|
||||
{
|
||||
if ( $this->forms === NULL )
|
||||
$methods = array();
|
||||
foreach ( static::methods() as $k => $method )
|
||||
{
|
||||
$this->forms = array();
|
||||
$standardTypes = 0;
|
||||
|
||||
foreach ( static::handlers() as $key => $handler )
|
||||
{
|
||||
if ( method_exists( $handler, 'loginForm' ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->forms[ $key ] = $handler->loginForm( $this->url, FALSE, $skipReferer ? $this->url : static::getDestination() );
|
||||
}
|
||||
catch( \BadMethodCallException $e )
|
||||
{
|
||||
/* The user may have installed a custom login handler, but not provided a template for the upgrader.
|
||||
This results in a NO_TEMPLATE_FILE exception and blocks the upgrader completely. We should just skip
|
||||
that login handler in this case */
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->forms['_standard'] = NULL;
|
||||
$standardTypes = $standardTypes | $handler->authTypes;
|
||||
$this->standardHandlers[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $standardTypes !== 0 )
|
||||
{
|
||||
switch ( $standardTypes )
|
||||
{
|
||||
case static::AUTH_TYPE_USERNAME:
|
||||
\IPS\Member::loggedIn()->language()->words['auth'] = \IPS\Member::loggedIn()->language()->addToStack( 'username', FALSE );
|
||||
break;
|
||||
|
||||
case static::AUTH_TYPE_EMAIL:
|
||||
\IPS\Member::loggedIn()->language()->words['auth'] = \IPS\Member::loggedIn()->language()->addToStack( 'email_address', FALSE );
|
||||
break;
|
||||
|
||||
case static::AUTH_TYPE_USERNAME + static::AUTH_TYPE_EMAIL:
|
||||
\IPS\Member::loggedIn()->language()->words['auth'] = \IPS\Member::loggedIn()->language()->addToStack( 'username_or_email', FALSE );
|
||||
break;
|
||||
}
|
||||
|
||||
$standardForm = new \IPS\Helpers\Form( "login__standard", 'login', $this->url );
|
||||
|
||||
$classname = 'IPS\Helpers\Form\Text';
|
||||
if ( $standardTypes === static::AUTH_TYPE_EMAIL )
|
||||
{
|
||||
$classname = 'IPS\Helpers\Form\Email';
|
||||
}
|
||||
$standardForm->class = 'ipsForm_vertical';
|
||||
$standardForm->add( new $classname( 'auth', NULL, TRUE, array( 'placeholder' => \IPS\Member::loggedIn()->language()->words['auth'], '_loginType' => $standardTypes, 'bypassProfanity' => TRUE ), NULL, NULL, NULL, 'auth' ) );
|
||||
$standardForm->add( new \IPS\Helpers\Form\Password( 'password', NULL, TRUE, array( 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack( 'password', FALSE ), 'bypassProfanity' => TRUE ), NULL, NULL, NULL, 'password' ) );
|
||||
|
||||
/* Are we adding the referer value to the form? */
|
||||
if( !$skipReferer )
|
||||
{
|
||||
$standardForm->hiddenValues['ref'] = base64_encode( static::getDestination() );
|
||||
}
|
||||
|
||||
if ( $this->flagOptions )
|
||||
{
|
||||
$standardForm->add( new \IPS\Helpers\Form\Checkbox( 'remember_me', TRUE ) );
|
||||
if ( !\IPS\Settings::i()->disable_anonymous )
|
||||
{
|
||||
$standardForm->add( new \IPS\Helpers\Form\Checkbox( 'signin_anonymous' ) );
|
||||
}
|
||||
$standardForm->addButton( 'forgotten_password', 'link', \IPS\Http\Url::internal( 'app=core&module=system&controller=lostpass', 'front', 'lostpassword' ), 'ipsButton ipsButton_small ipsButton_fullWidth ipsButton_link' );
|
||||
}
|
||||
|
||||
$this->forms['_standard'] = $standardForm;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Login Flags
|
||||
*/
|
||||
public $flags = array( 'remember_me' => TRUE, 'signin_anonymous' => FALSE );
|
||||
|
||||
/**
|
||||
* Check for successful authentication
|
||||
*
|
||||
* @return \IPS\Member|null
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
if ( ( !isset( \IPS\Request::i()->cookie[ 'IPSSession' . ucfirst( \IPS\Dispatcher::i()->controllerLocation ) ] ) or \IPS\Request::i()->cookie[ 'IPSSession' . ucfirst( \IPS\Dispatcher::i()->controllerLocation ) ] != session_id() ) )
|
||||
{
|
||||
if ( !isset( \IPS\Request::i()->cookieCheck ) )
|
||||
{
|
||||
$_SESSION['_cookieCheck'] = TRUE; // Forces it to write the session so the above check will fail on the next load
|
||||
\IPS\Output::i()->redirect( $this->url->setQueryString( 'cookieCheck', 1 ), NULL, 307 ); // 307 instructs the browser to resubmit the form as a POST request maintaining all the values from before
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->error( 'login_err_no_cookies', '1S267/1', 403, '' );
|
||||
if (
|
||||
$this->type === static::LOGIN_FRONT
|
||||
or $this->type === static::LOGIN_UCP
|
||||
or ( $this->type === static::LOGIN_ACP and $method->acp )
|
||||
or ( $this->type === static::LOGIN_REGISTRATION_FORM and $method->register )
|
||||
or ( $this->type === static::LOGIN_REAUTHENTICATE and $method->canProcess( $this->reauthenticateAs ) and !( $method instanceof \IPS\Login\LoginAbstract ) )
|
||||
) {
|
||||
$methods[ $k ] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
$handlers = static::handlers();
|
||||
foreach ( $this->forms() as $handler => $form )
|
||||
{
|
||||
/* Pass to the handler */
|
||||
$values = NULL;
|
||||
if ( ( is_object( $form ) and $values = $form->values() ) or ( ucfirst( \IPS\Request::i()->loginProcess ) === ucfirst( $handler ) ) )
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get methods which use a username and password
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function usernamePasswordMethods()
|
||||
{
|
||||
$return = array();
|
||||
foreach ( $this->_methods() as $method )
|
||||
{
|
||||
if ( $method->type() === static::TYPE_USERNAME_PASSWORD )
|
||||
{
|
||||
/* Set any flags */
|
||||
foreach ( array_keys( $this->flags ) as $k )
|
||||
{
|
||||
if ( isset( $values[ $k ] ) )
|
||||
{
|
||||
$this->flags[ $k ] = $values[ $k ];
|
||||
}
|
||||
}
|
||||
$return[ $method->id ] = $method;
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the username/password form ask for username or email address?
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function authType()
|
||||
{
|
||||
$authType = 0;
|
||||
foreach ( $this->usernamePasswordMethods() as $method )
|
||||
{
|
||||
$authType = $authType | $method->authType();
|
||||
}
|
||||
return $authType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get methods which use a button
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function buttonMethods()
|
||||
{
|
||||
$return = array();
|
||||
foreach ( $this->_methods() as $method )
|
||||
{
|
||||
if ( $method->type() === static::TYPE_BUTTON )
|
||||
{
|
||||
$return[ $method->id ] = $method;
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/* !Authentication */
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param \IPS\Login\Handler\NULL $onlyCheck If provided, will only check the given method
|
||||
* @return \IPS\Login\Success|NULL
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( \IPS\Login\Handler $onlyCheck = NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->_processLogin ) )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
/* Authenticate */
|
||||
$member = NULL;
|
||||
try
|
||||
/* Username/Password */
|
||||
if ( \IPS\Request::i()->_processLogin === 'usernamepassword' )
|
||||
{
|
||||
if ( $handler === '_standard' )
|
||||
$leastOffensiveException = NULL;
|
||||
$success = NULL;
|
||||
$fails = array();
|
||||
|
||||
foreach ( $this->usernamePasswordMethods() as $method )
|
||||
{
|
||||
$values['auth'] = mb_strtolower( $values['auth'] );
|
||||
$member = $this->authenticateStandard( $values );
|
||||
if ( !$onlyCheck or $method->id == $onlyCheck->id )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $this->type === static::LOGIN_REAUTHENTICATE )
|
||||
{
|
||||
if ( $method->authenticatePasswordForMember( $this->reauthenticateAs, \IPS\Request::i()->password ) )
|
||||
{
|
||||
$member = $this->reauthenticateAs;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $this->reauthenticateAs );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$member = $method->authenticateUsernamePassword( $this, \IPS\Request::i()->auth, \IPS\Request::i()->password );
|
||||
if ( $member === TRUE )
|
||||
{
|
||||
$member = $this->reauthenticateAs;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $member )
|
||||
{
|
||||
static::checkIfAccountIsLocked( $member, TRUE );
|
||||
$success = new Login\Success( $member, $method, isset( \IPS\Request::i()->remember_me ), isset( \IPS\Request::i()->anonymous ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
if ( $e->getCode() === \IPS\Login\Exception::BAD_PASSWORD and $e->member )
|
||||
{
|
||||
$fails[ $e->member->member_id ] = $e->member;
|
||||
}
|
||||
|
||||
if ( $leastOffensiveException === NULL or $leastOffensiveException->getCode() < $e->getCode() )
|
||||
{
|
||||
$leastOffensiveException = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $fails as $failedMember )
|
||||
{
|
||||
if ( !$success or $success->member->member_id != $failedMember->member_id )
|
||||
{
|
||||
$failedLogins = is_array( $failedMember->failed_logins ) ? $failedMember->failed_logins : array();
|
||||
$failedLogins[ \IPS\Request::i()->ipAddress() ][] = time();
|
||||
$failedMember->failed_logins = $failedLogins;
|
||||
$failedMember->save();
|
||||
}
|
||||
}
|
||||
|
||||
if ( $success )
|
||||
{
|
||||
return $success;
|
||||
}
|
||||
elseif ( $leastOffensiveException )
|
||||
{
|
||||
throw $leastOffensiveException;
|
||||
}
|
||||
else
|
||||
{
|
||||
$member = $handlers[ $handler ]->authenticate( is_object( $form ) ? $values : $this->url );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
/* Buttons */
|
||||
elseif ( isset( $this->buttonMethods()[ \IPS\Request::i()->_processLogin ] ) and ( !$onlyCheck or $onlyCheck->id == $this->buttonMethods()[ \IPS\Request::i()->_processLogin ]->id ) )
|
||||
{
|
||||
/* Check if the account is locked and throw that error rather than a bad password error first */
|
||||
if ( $e->getCode() === \IPS\Login\Exception::BAD_PASSWORD )
|
||||
{
|
||||
$this->checkIfAccountIsLocked( $e->member );
|
||||
}
|
||||
$method = $this->_methods()[ \IPS\Request::i()->_processLogin ];
|
||||
|
||||
/* If we're still here, throw the error we got */
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/* If we passed, log in! */
|
||||
if ( $member->member_id )
|
||||
{
|
||||
/* Set which handler processed it */
|
||||
if ( $handler !== '_standard' ) // If _standard, is set in authenticateStandard()
|
||||
if ( $member = $method->authenticateButton( $this ) )
|
||||
{
|
||||
$this->usedHandler = $handler;
|
||||
}
|
||||
|
||||
//if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation != 'setup' )
|
||||
//{
|
||||
/* Check if the account is locked */
|
||||
$this->checkIfAccountIsLocked( $member );
|
||||
if ( $this->type === static::LOGIN_REAUTHENTICATE and $member !== $this->reauthenticateAs )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_wrong_account', \IPS\Login\Exception::BAD_PASSWORD );
|
||||
}
|
||||
|
||||
/* Remove old failed login attempts */
|
||||
if ( \IPS\Settings::i()->ipb_bruteforce_period and ( \IPS\Settings::i()->ipb_bruteforce_unlock or !isset( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ) or $member->failed_logins[ \IPS\Request::i()->ipAddress() ] < \IPS\Settings::i()->ipb_bruteforce_attempts ) )
|
||||
{
|
||||
$removeLoginsOlderThan = \IPS\DateTime::create()->sub( new \DateInterval( 'PT' . \IPS\Settings::i()->ipb_bruteforce_period . 'M' ) );
|
||||
$failedLogins = $member->failed_logins;
|
||||
|
||||
/* The failed login data could potentially not be an array (i.e. a float) but as this code executes during the first
|
||||
step of upgrading to 4.0 if we don't force it to be an array here we could end up with an error exception we can't
|
||||
get past when attempting to upgrade. */
|
||||
if( !is_array( $failedLogins ) )
|
||||
{
|
||||
$failedLogins = array();
|
||||
}
|
||||
|
||||
if ( is_array( $failedLogins ) )
|
||||
{
|
||||
foreach ( $failedLogins as $ipAddress => $times )
|
||||
{
|
||||
foreach ( $times as $k => $v )
|
||||
{
|
||||
if ( $v < $removeLoginsOlderThan->getTimestamp() )
|
||||
{
|
||||
unset( $failedLogins[ $ipAddress ][ $k ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
$member->failed_logins = $failedLogins;
|
||||
}
|
||||
else
|
||||
{
|
||||
$member->failed_logins = array();
|
||||
}
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* If we're still here, the login was fine, so we can reset the count and process login */
|
||||
if ( isset( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ) )
|
||||
{
|
||||
$failedLogins = $member->failed_logins;
|
||||
unset( $failedLogins[ \IPS\Request::i()->ipAddress() ] );
|
||||
$member->failed_logins = $failedLogins;
|
||||
}
|
||||
$member->last_visit = time();
|
||||
$member->save();
|
||||
//}
|
||||
|
||||
return $member;
|
||||
static::checkIfAccountIsLocked( $member, TRUE );
|
||||
return new Login\Success( $member, $method );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? Just throw an exception */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate all 'Standard' forms
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticateStandard( $values )
|
||||
{
|
||||
$member = NULL;
|
||||
$leastOffensiveException = NULL;
|
||||
|
||||
$handlers = static::handlers();
|
||||
foreach ( $this->standardHandlers as $key )
|
||||
{
|
||||
try
|
||||
/* Backwards Compatibility for login handlers created before 4.3 */
|
||||
elseif ( isset( \IPS\Request::i()->loginProcess ) )
|
||||
{
|
||||
$member = $handlers[ $key ]->authenticate( $values );
|
||||
$this->usedHandler = $key;
|
||||
break;
|
||||
}
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
if ( $leastOffensiveException === NULL or $leastOffensiveException->getCode() > $e->getCode() )
|
||||
foreach ( $this->buttonMethods() as $method )
|
||||
{
|
||||
$leastOffensiveException = $e;
|
||||
if ( $method instanceof \IPS\Login\LoginAbstract and get_class( $method ) === 'IPS\Login\\' . mb_ucfirst( \IPS\Request::i()->loginProcess ) and ( !$onlyCheck or $method->id == $onlyCheck->id ) )
|
||||
{
|
||||
if ( $member = $method->authenticateButton( $this ) )
|
||||
{
|
||||
static::checkIfAccountIsLocked( $member, TRUE );
|
||||
return new Login\Success( $member, $method, isset( \IPS\Request::i()->remember_me ), isset( \IPS\Request::i()->anonymous ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $member === NULL )
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
throw $leastOffensiveException;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $member;
|
||||
/* If we're about to say the password is incorrect, check if the account is locked and throw that error rather than a bad password error first */
|
||||
if ( $e->getCode() === \IPS\Login\Exception::BAD_PASSWORD and $e->member )
|
||||
{
|
||||
static::checkIfAccountIsLocked( $e->member );
|
||||
}
|
||||
|
||||
/* If we're still here, throw the error we got */
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/* !Account Management Utility Methods */
|
||||
|
||||
/**
|
||||
* Check if an account is locked
|
||||
* After authentication (successful or failed) but before
|
||||
* processing the login, check if the account is locked
|
||||
*
|
||||
* @param \IPS\Member $member The account
|
||||
* @param \IPS\Member $member The account
|
||||
* @param bool $succcess Boolean value indicating if the login was successfu. If TRUE, and the account is not locked, failed logins will be removed
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function checkIfAccountIsLocked( $member )
|
||||
public static function checkIfAccountIsLocked( $member, $success = FALSE )
|
||||
{
|
||||
$unlockTime = static::accountUnlockTime( $member );
|
||||
$unlockTime = $member->unlockTime();
|
||||
if ( $unlockTime !== FALSE )
|
||||
{
|
||||
/* Notify the member if they've been locked */
|
||||
@@ -543,57 +341,105 @@ class _Login
|
||||
$location = \IPS\Request::i()->ipAddress();
|
||||
}
|
||||
\IPS\Email::buildFromTemplate( 'core', 'account_locked', array( $member, $location, isset( $unlockTime ) ? $unlockTime : NULL ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
|
||||
$member->logHistory( 'core', 'login', array( 'type' => 'lock', 'count' => count( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ), 'unlockTime' => isset( $unlockTime ) ? $unlockTime->getTimestamp() : NULL ) );
|
||||
}
|
||||
|
||||
if ( \IPS\Settings::i()->ipb_bruteforce_period and \IPS\Settings::i()->ipb_bruteforce_unlock )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_locked_unlock', FALSE, array( 'pluralize' => array( $unlockTime->diff( new DateTime() )->format('%i') ) ) ) );
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'login_err_locked_unlock', FALSE, array( 'pluralize' => array( $unlockTime->diff( new DateTime() )->format('%i') ) ) ), \IPS\Login\Exception::ACCOUNT_LOCKED );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'login_err_locked_nounlock' );
|
||||
throw new \IPS\Login\Exception( 'login_err_locked_nounlock', \IPS\Login\Exception::ACCOUNT_LOCKED );
|
||||
}
|
||||
}
|
||||
elseif ( $success )
|
||||
{
|
||||
$failedLogins = is_array( $member->failed_logins ) ? $member->failed_logins : array();
|
||||
unset( $failedLogins[ \IPS\Request::i()->ipAddress() ] );
|
||||
$member->failed_logins = $failedLogins;
|
||||
$member->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an account is locked - returns FALSE if account is unlocked, an \IPS\DateTime object if the account is locked until a certain time, or TRUE if account is unlocked indefinitely
|
||||
* Check if a given username is in use
|
||||
* Returns string with error message or FALSE if not in use
|
||||
*
|
||||
* @param \IPS\Member $member The account
|
||||
* @return \IPS\DateTime|bool
|
||||
* @param string $username Desired username
|
||||
* @param \IPS\Member $exclude If provided, that member will be excluded from the check
|
||||
* @param \IPS\Member $admin Boolean value indicating if error message can include details about which login method has claimed it
|
||||
* @return string|false
|
||||
*/
|
||||
public static function accountUnlockTime( $member )
|
||||
public static function usernameIsInUse( $username, $exclude = NULL, $admin = FALSE )
|
||||
{
|
||||
if ( \IPS\Settings::i()->ipb_bruteforce_attempts and isset( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ) and count( $member->failed_logins[ \IPS\Request::i()->ipAddress() ] ) >= \IPS\Settings::i()->ipb_bruteforce_attempts )
|
||||
/* Check locally */
|
||||
$existingMember = \IPS\Member::load( $username, 'name' );
|
||||
if ( $existingMember->member_id and ( !$exclude or $exclude->member_id != $existingMember->member_id ) )
|
||||
{
|
||||
if ( \IPS\Settings::i()->ipb_bruteforce_period and \IPS\Settings::i()->ipb_bruteforce_unlock )
|
||||
{
|
||||
$failedLogins = $member->failed_logins[ \IPS\Request::i()->ipAddress() ];
|
||||
sort( $failedLogins );
|
||||
|
||||
while ( count( $failedLogins ) > \IPS\Settings::i()->ipb_bruteforce_attempts )
|
||||
{
|
||||
array_pop( $failedLogins );
|
||||
}
|
||||
$unlockTime = \IPS\DateTime::ts( array_pop( $failedLogins ) );
|
||||
$unlockTime->add( new \DateInterval( 'PT' . \IPS\Settings::i()->ipb_bruteforce_period . 'M' ) );
|
||||
|
||||
/* If Unlock Time is in the past, return FALSE to avoid the exception and allow login */
|
||||
if ( $unlockTime->getTimestamp() < time() )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Otherwise that is what we're returning */
|
||||
return $unlockTime;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('member_name_exists');
|
||||
}
|
||||
|
||||
/* Check each handler */
|
||||
foreach( static::methods() as $k => $handler )
|
||||
{
|
||||
if( $handler->usernameIsInUse( $username, $exclude ) === TRUE )
|
||||
{
|
||||
if( $admin )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'member_name_exists_admin', FALSE, array('sprintf' => array( $handler->_title ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('member_name_exists');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? We're good */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given email address is in use
|
||||
* Returns string with error message or FALSE if not in use
|
||||
*
|
||||
* @param string $email Desired email address
|
||||
* @param \IPS\Member $exclude If provided, that member will be excluded from the check
|
||||
* @param \IPS\Member $admin Boolean value indicating if error message can include details about which login method has claimed it
|
||||
* @return string|false
|
||||
*/
|
||||
public static function emailIsInUse( $email, $exclude = NULL, $admin = FALSE )
|
||||
{
|
||||
/* Check locally */
|
||||
$existingMember = \IPS\Member::load( $email, 'email' );
|
||||
if ( $existingMember->member_id and ( !$exclude or $exclude->member_id != $existingMember->member_id ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('member_email_exists');
|
||||
}
|
||||
|
||||
/* Check each handler */
|
||||
foreach( static::methods() as $k => $handler )
|
||||
{
|
||||
if( $handler->emailIsInUse( $email, $exclude ) === TRUE )
|
||||
{
|
||||
if( $admin )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'member_email_exists_admin', FALSE, array('sprintf' => array( $handler->_title ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('member_email_exists');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? We're good */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* !Misc Utility Methods */
|
||||
|
||||
/**
|
||||
* Compare hashes in fixed length, time constant manner.
|
||||
*
|
||||
@@ -622,7 +468,7 @@ class _Login
|
||||
|
||||
return $status === 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a random string
|
||||
*
|
||||
@@ -633,7 +479,11 @@ class _Login
|
||||
{
|
||||
$return = '';
|
||||
|
||||
if( function_exists( 'openssl_random_pseudo_bytes' ) )
|
||||
if ( function_exists( 'random_bytes' ) )
|
||||
{
|
||||
$return = \substr( bin2hex( random_bytes( $length ) ), 0, $length );
|
||||
}
|
||||
elseif( function_exists( 'openssl_random_pseudo_bytes' ) )
|
||||
{
|
||||
$return = \substr( bin2hex( openssl_random_pseudo_bytes( ceil( $length / 2 ) ) ), 0, $length );
|
||||
}
|
||||
@@ -641,9 +491,59 @@ class _Login
|
||||
/* Fallback JUST IN CASE */
|
||||
if( !$return OR \strlen( $return ) != $length )
|
||||
{
|
||||
$return = \substr( md5( uniqid( microtime(), true ) ) . md5( uniqid( microtime(), true ) ), 0, $length );
|
||||
$return = \substr( md5( uniqid( '', true ) ) . md5( uniqid( '', true ) ), 0, $length );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
protected static $_registrationType = NULL;
|
||||
|
||||
/**
|
||||
* Registration Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function registrationType()
|
||||
{
|
||||
if ( static::$_registrationType === NULL )
|
||||
{
|
||||
static::$_registrationType = \IPS\Settings::i()->allow_reg;
|
||||
|
||||
if ( static::$_registrationType == 1 )
|
||||
{
|
||||
static::$_registrationType = \IPS\Settings::i()->quick_register ? 'normal' : 'full';
|
||||
}
|
||||
|
||||
if ( in_array( static::$_registrationType, array( 'normal', 'full' ) ) and !\IPS\Login\Handler::findMethod( 'IPS\Login\Handler\Standard' ) )
|
||||
{
|
||||
static::$_registrationType = 'disabled';
|
||||
}
|
||||
}
|
||||
|
||||
return static::$_registrationType;
|
||||
}
|
||||
|
||||
/* !Backwards Compatibility for login handlers created before 4.3 */
|
||||
|
||||
/**
|
||||
* Get Legacy Handlers
|
||||
*
|
||||
* @deprecated
|
||||
* @return array
|
||||
*/
|
||||
public static function handlers()
|
||||
{
|
||||
$return = array();
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_login_methods', array( 'login_enabled=1' ) ) as $method )
|
||||
{
|
||||
$method = \IPS\Login\Handler::constructFromData( $method );
|
||||
if ( $method instanceof \IPS\Login\LoginAbstract )
|
||||
{
|
||||
$return[ mb_substr( get_class( $method ), 10 ) ] = $method;
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
||||
+78
-562
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract Login Handler
|
||||
* @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 15 Mar 2013
|
||||
* @brief 4.2 and below Backwards Compatibilty Login Handler
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 16 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
@@ -18,279 +19,97 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Login Handler
|
||||
* 4.2 and below Backwards Compatibilty Login Handler
|
||||
*
|
||||
* @deprecated This class exists only to provide backwards compatibility for login handlers created before 4.3
|
||||
*/
|
||||
abstract class _LoginAbstract extends \IPS\Node\Model
|
||||
{
|
||||
abstract class _LoginAbstract extends \IPS\Login\Handler
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_login_handlers';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'login_';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'key';
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'login_handlers';
|
||||
|
||||
/**
|
||||
* @brief [Node] Order Database Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'order';
|
||||
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'lock';
|
||||
|
||||
/**
|
||||
* @brief Logo
|
||||
*/
|
||||
public static $logo = NULL;
|
||||
|
||||
/**
|
||||
* @brief Disable the copy button - useful when the forms are very distinctly different
|
||||
*/
|
||||
public $noCopyButton = TRUE;
|
||||
|
||||
/**
|
||||
* Construct ActiveRecord from database row
|
||||
*
|
||||
* @param array $data Row from database table
|
||||
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
|
||||
* @return static
|
||||
*/
|
||||
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
|
||||
{
|
||||
/* Initiate an object */
|
||||
$classname = '\\IPS\Login\\' . ucfirst( $data['login_key'] );
|
||||
if ( !class_exists( $classname ) )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
|
||||
$obj = new $classname;
|
||||
$obj->_new = FALSE;
|
||||
|
||||
/* Import data */
|
||||
$databasePrefixLength = \strlen( static::$databasePrefix );
|
||||
foreach ( $data as $k => $v )
|
||||
{
|
||||
if( static::$databasePrefix )
|
||||
{
|
||||
$k = \substr( $k, $databasePrefixLength );
|
||||
}
|
||||
|
||||
$obj->$k = $v;
|
||||
}
|
||||
$obj->changed = array();
|
||||
$obj->init();
|
||||
|
||||
/* Return */
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get Node Title
|
||||
* Get title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get__title()
|
||||
public static function getTitle()
|
||||
{
|
||||
$key = "login_handler_" . ucfirst($this->key);
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get the title to store in the log
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function titleForLog()
|
||||
{
|
||||
return $this->_logKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get the log key
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function get__logKey()
|
||||
{
|
||||
return \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'login_handler_' . ucfirst( $this->key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is enabled
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__enabled()
|
||||
{
|
||||
return $this->enabled;
|
||||
return 'login_handler_' . mb_substr( get_called_class(), 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Set whether or not this node is enabled
|
||||
* Get type
|
||||
*
|
||||
* @param bool $value Enable Node?
|
||||
* @return void
|
||||
* @return int
|
||||
*/
|
||||
protected function set__enabled( $value )
|
||||
public function type()
|
||||
{
|
||||
$this->enabled = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to add a child node to this node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canAdd()
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to add aa root node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function canAddRoot()
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Add/Edit Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
foreach ( $this->acpForm() as $k => $v )
|
||||
if ( method_exists( $this, 'loginForm' ) )
|
||||
{
|
||||
if ( is_string( $v ) )
|
||||
{
|
||||
$form->addHeader( $v );
|
||||
}
|
||||
else
|
||||
{
|
||||
$form->add( $v );
|
||||
$names[ $v->name ] = $k;
|
||||
}
|
||||
return \IPS\Login::TYPE_BUTTON;
|
||||
}
|
||||
|
||||
if ( mb_strtolower( $this->key ) !== 'internal' )
|
||||
else
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'login_acp', $this->acp ) );
|
||||
return \IPS\Login::TYPE_USERNAME_PASSWORD;
|
||||
}
|
||||
|
||||
if ( isset( static::$shareService ) )
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auth type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function authType()
|
||||
{
|
||||
return $this->authTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function button()
|
||||
{
|
||||
$destination = NULL;
|
||||
if ( isset( \IPS\Request::i()->ref ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$shareService = \IPS\core\ShareLinks\Service::load( static::$shareService, 'share_key' );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'share_autoshare_' . $this->key, $shareService->autoshare ) );
|
||||
$url = \IPS\Http\Url::createFromString( base64_decode( \IPS\Request::i()->ref ) );
|
||||
if ( $url instanceof \IPS\Http\Url\Internal )
|
||||
{
|
||||
$destination = $url;
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
return $this->loginForm( \IPS\Request::i()->url(), FALSE, $destination );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
* @param \IPS\Login $login The login object
|
||||
* @param string $usernameOrEmail The username or email address provided by the user
|
||||
* @param string $password The plaintext password provided by the user
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
|
||||
{
|
||||
$names = array();
|
||||
foreach ( $this->acpForm() as $k => $v )
|
||||
{
|
||||
if ( !is_string( $v ) )
|
||||
{
|
||||
$names[ $v->name ] = $k;
|
||||
}
|
||||
}
|
||||
|
||||
$save = array();
|
||||
foreach ( $names as $formName => $saveName )
|
||||
{
|
||||
if( array_key_exists( $formName, $values ) )
|
||||
{
|
||||
$save[ $saveName ] = $values[ $formName ];
|
||||
unset( $values[ $formName ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $values[ 'share_autoshare_' . $this->key ] ) )
|
||||
{
|
||||
$save['autoshare'] = $values[ 'share_autoshare_' . $this->key ];
|
||||
}
|
||||
|
||||
$values['settings'] = json_encode( $save );
|
||||
$this->settings = $values['settings'];
|
||||
|
||||
$this->testSettings();
|
||||
|
||||
if ( mb_strtolower( $this->key ) == 'internal' )
|
||||
{
|
||||
$values['login_acp'] = 1;
|
||||
}
|
||||
|
||||
return $values;
|
||||
return $this->authenticate( array( 'auth' => $usernameOrEmail, 'password' => $password ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Save Add/Edit Form
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return void
|
||||
* @param \IPS\Login $login The login object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function saveForm( $values )
|
||||
public function authenticateButton( \IPS\Login $login )
|
||||
{
|
||||
if ( isset( $values[ 'share_autoshare_' . $this->key ] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$shareService = \IPS\core\ShareLinks\Service::load( static::$shareService, 'share_key' );
|
||||
$shareService->autoshare = $values[ 'share_autoshare_' . $this->key ];
|
||||
$shareService->save();
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
unset( $values[ 'share_autoshare_' . $this->key ] );
|
||||
}
|
||||
|
||||
parent::saveForm( $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save Changed Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
parent::save();
|
||||
unset( \IPS\Data\Store::i()->loginHandlers );
|
||||
return $this->authenticate( $login->url );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,67 +130,17 @@ abstract class _LoginAbstract extends \IPS\Node\Model
|
||||
/* Create an account */
|
||||
if ( !$member or !$member->member_id )
|
||||
{
|
||||
/* Is registration enabled? */
|
||||
if( !\IPS\Settings::i()->allow_reg )
|
||||
try
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'reg_disabled', \IPS\Login\Exception::REGISTRATION_DISABLED );
|
||||
$exception->handler = mb_substr( get_called_class(), 10 );
|
||||
$exception->member = $member;
|
||||
throw $exception;
|
||||
$member = $this->createAccount( $name, $email );
|
||||
}
|
||||
|
||||
/* Init */
|
||||
$member = new \IPS\Member;
|
||||
$member->member_group_id = \IPS\Settings::i()->member_group;
|
||||
$member->members_bitoptions['view_sigs'] = TRUE;
|
||||
|
||||
/* Set name - if it already exists, we'll leave it blank and they'll be prompted to fill it in */
|
||||
if ( $name !== NULL and ( !\IPS\Settings::i()->username_characters or preg_match( '/^[' . str_replace( '\-', '-', preg_quote( \IPS\Settings::i()->username_characters, '/' ) ) . ']*$/iu', $name ) ) )
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
$existingUsername = \IPS\Member::load( $name, 'name' );
|
||||
if ( !$existingUsername->member_id )
|
||||
if ( $details )
|
||||
{
|
||||
$member->name = $name;
|
||||
$e->details = $details;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set email */
|
||||
if ( $email !== NULL )
|
||||
{
|
||||
/* Check it doesn't already exist */
|
||||
$existingEmail = \IPS\Member::load( $email, 'email' );
|
||||
if ( $existingEmail->member_id )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT );
|
||||
$exception->handler = mb_substr( get_called_class(), 10 );
|
||||
$exception->member = $existingEmail;
|
||||
if ( $details )
|
||||
{
|
||||
$exception->details = $details;
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/* Set it */
|
||||
$member->email = $email;
|
||||
|
||||
/* Check the spam service is okay with it */
|
||||
if( \IPS\Settings::i()->spam_service_enabled )
|
||||
{
|
||||
if( $member->spamService() == 4 )
|
||||
{
|
||||
$exception = new \IPS\Login\Exception( 'spam_denied_account', \IPS\Login\Exception::REGISTRATION_DENIED_BY_SPAM_SERVICE );
|
||||
$exception->handler = mb_substr( get_called_class(), 10 );
|
||||
$exception->member = $member;
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Set profile sync */
|
||||
if ( $profileSync !== NULL )
|
||||
{
|
||||
$member->profilesync = json_encode( array( mb_substr( get_called_class(), 10 ) => $profileSync ) );
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/* Member properties */
|
||||
@@ -380,20 +149,6 @@ abstract class _LoginAbstract extends \IPS\Node\Model
|
||||
$member->$k = $v;
|
||||
}
|
||||
$member->save();
|
||||
|
||||
/* Sync photo, etc now */
|
||||
if ( $profileSync !== NULL )
|
||||
{
|
||||
$profileSyncClass = $profileSyncClass ?: 'IPS\core\ProfileSync\\' . mb_substr( get_called_class(), 10 );
|
||||
$sync = new $profileSyncClass( $member );
|
||||
$sync->sync();
|
||||
}
|
||||
|
||||
/* If registration is complete, do post-registration stuff */
|
||||
if ( $member->name and $member->email and !$member->members_bitoptions['bw_is_spammer'] )
|
||||
{
|
||||
$member->postRegistration( TRUE );
|
||||
}
|
||||
}
|
||||
|
||||
/* Or just update? */
|
||||
@@ -414,265 +169,26 @@ abstract class _LoginAbstract extends \IPS\Node\Model
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings()
|
||||
{
|
||||
return json_decode( $this->_data['settings'], TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an action when the login handler is disabled
|
||||
* Can this handler process a password change for a member?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onDisable()
|
||||
public function canChangePassword( \IPS\Member $member )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the type as a textual string
|
||||
*
|
||||
* @param int $type The valid configured types
|
||||
* @return string
|
||||
*/
|
||||
public function getLoginType( $type )
|
||||
{
|
||||
switch ( $type )
|
||||
{
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
return 'username_or_email';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_USERNAME:
|
||||
return 'username';
|
||||
break;
|
||||
|
||||
case \IPS\Login::AUTH_TYPE_EMAIL:
|
||||
return 'email_address';
|
||||
break;
|
||||
}
|
||||
|
||||
return 'username';
|
||||
return method_exists( $this, 'canChange' ) ? $this->canChange( 'password', $member ) : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email is in use?
|
||||
* Used when registering or changing an email address to check the new one is available
|
||||
*
|
||||
* @param string $email Email Address
|
||||
* @param \IPS\Member|NULL $eclude Member to exclude
|
||||
* @return bool|NULL Boolean indicates if email is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function emailIsInUse( $email, \IPS\Member $exclude=NULL )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username is in use?
|
||||
* Used when registering or changing an username to check the new one is available
|
||||
*
|
||||
* @param string $username Username
|
||||
* @return bool|NULL Boolean indicates if username is in use (TRUE means is in use and thus not registerable) or NULL if this handler does not support such an API
|
||||
*/
|
||||
public function usernameIsInUse( $username )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Email Address
|
||||
* Link Account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldEmail Old Email Address
|
||||
* @param string $newEmail New Email Address
|
||||
* @param mixed $details Details as they were passed to the exception
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeEmail( \IPS\Member $member, $oldEmail, $newEmail )
|
||||
public function completeLink( \IPS\Member $member, $details )
|
||||
{
|
||||
if ( !$this->canChange( 'email', $member ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $newPassword New Password
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changePassword( \IPS\Member $member, $newPassword )
|
||||
{
|
||||
if ( !$this->canChange( 'password', $member ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Username
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param string $oldUsername Old Username
|
||||
* @param string $newUsername New Username
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changeUsername( \IPS\Member $member, $oldUsername, $newUsername )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an account off
|
||||
*
|
||||
* @param \IPS\Member $member The member that was just logged out
|
||||
* @param \IPS\Http\Url $redirectUrl The URL to send the user back to
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @note This is NOT called if you force log out all users from the ACP on an individual site
|
||||
*/
|
||||
public function logoutAccount( \IPS\Member $member, \IPS\Http\Url $redirectUrl )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an account
|
||||
*
|
||||
* @param \IPS\Member $member The member that was just created
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createAccount( \IPS\Member $member )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function validateAccount( \IPS\Member $member )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deleteAccount( \IPS\Member $member )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban or unban account
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param bool TRUE means member is being banned, FALSE means they are being unbanned
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function banAccount( \IPS\Member $member, $ban=TRUE )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two accounts
|
||||
*
|
||||
* @param \IPS\Member $member The member to keep with original data
|
||||
* @param \IPS\Member $member2 The member that will be deleted
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function mergeAccounts( \IPS\Member $member, \IPS\Member $member2 )
|
||||
{
|
||||
// By default do nothing. Handlers can extend.
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @param string $column Column to search
|
||||
* @param string $query Search query
|
||||
* @param string|null $order Column to order by
|
||||
* @param mixed $where Where clause
|
||||
* @return array
|
||||
*/
|
||||
public static function search( $column, $query, $order, $where=array() )
|
||||
{
|
||||
if ( $column === '_title' )
|
||||
{
|
||||
$return = array();
|
||||
foreach ( \IPS\Member::loggedIn()->language()->searchCustom( 'login_handler_', $query, TRUE ) as $key => $value )
|
||||
{
|
||||
try
|
||||
{
|
||||
$return[ $key ] = static::load( $key );
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
abstract function canProcess( \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to delete this node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canDelete()
|
||||
{
|
||||
return FALSE;
|
||||
static::link( $member, $details );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Successful Login Result
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 15 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Successful Login Result
|
||||
*/
|
||||
class _Success
|
||||
{
|
||||
/**
|
||||
* @brief The member who has successfully logged in
|
||||
*/
|
||||
public $member;
|
||||
|
||||
/**
|
||||
* @brief The device
|
||||
*/
|
||||
public $device;
|
||||
|
||||
/**
|
||||
* @brief The handler that processed the login
|
||||
*/
|
||||
public $handler;
|
||||
|
||||
/**
|
||||
* @brief If the "remember me" box was checked
|
||||
*/
|
||||
public $rememberMe = TRUE;
|
||||
|
||||
/**
|
||||
* @brief If the "sign in anonymously" box was checked
|
||||
*/
|
||||
public $anonymous = FALSE;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Member $member The member who has successfully logged in
|
||||
* @param \IPS\Login\Handler $handler The handler that processed the login
|
||||
* @param bool $rememberMe If the "remember me" box was checked
|
||||
* @param bool $anonymous If the "sign in anonymously" box was checked
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Member $member, \IPS\Login\Handler $handler, $rememberMe = TRUE, $anonymous = FALSE )
|
||||
{
|
||||
$this->member = $member;
|
||||
$this->device = \IPS\Member\Device::loadOrCreate( $member );
|
||||
$this->handler = $handler;
|
||||
$this->rememberMe = $rememberMe;
|
||||
$this->anonymous = $anonymous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get two-factor authentication form required to process login
|
||||
*
|
||||
* @param string|NULL $area The area being accessed or NULL to automatically use AuthenticateFront(Known)
|
||||
* @return string|NULL
|
||||
*/
|
||||
public function mfa( $area = NULL )
|
||||
{
|
||||
if ( !$area )
|
||||
{
|
||||
$area = $this->device->known ? 'AuthenticateFrontKnown' : 'AuthenticateFront';
|
||||
}
|
||||
return \IPS\MFA\MFAHandler::accessToArea( 'core', $area, \IPS\Http\Url::internal( '' ), $this->member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the login - set the session data, and send required cookies
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
/* Log in */
|
||||
\IPS\Session::i()->setMember( $this->member );
|
||||
if ( $this->anonymous and !\IPS\Settings::i()->disable_anonymous )
|
||||
{
|
||||
\IPS\Session::i()->setAnon();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If we're not anonymous, update our last visit time */
|
||||
$this->member->last_visit = time();
|
||||
$this->member->save();
|
||||
}
|
||||
|
||||
/* Log device */
|
||||
$this->device->anonymous = $this->anonymous and !\IPS\Settings::i()->disable_anonymous;
|
||||
$this->device->updateAfterAuthentication( $this->rememberMe, $this->handler );
|
||||
|
||||
/* Member sync */
|
||||
$this->member->memberSync( 'onLogin' );
|
||||
$this->member->profileSync();
|
||||
}
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Twitter Login Handler
|
||||
* @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 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter Login Handler
|
||||
*/
|
||||
class _Twitter extends LoginAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'twitter';
|
||||
|
||||
/**
|
||||
* @brief Logo
|
||||
*/
|
||||
public static $logo = 'Twitter';
|
||||
|
||||
/**
|
||||
* @brief Share Service
|
||||
*/
|
||||
public static $shareService = 'twitter';
|
||||
|
||||
/**
|
||||
* Get Form
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL for the login page
|
||||
* @param bool $ucp Is UCP? (as opposed to login form)
|
||||
* @param \IPS\Http\Url $destination The URL to redirect to after a successful login
|
||||
* @return string
|
||||
*/
|
||||
public function loginForm( \IPS\Http\Url $url, $ucp = FALSE, \IPS\Http\Url $destination = NULL )
|
||||
{
|
||||
$url = $url->setQueryString( 'loginProcess', 'twitter' );
|
||||
if ( $destination )
|
||||
{
|
||||
$url = $url->setQueryString( 'ref', base64_encode( $destination ) );
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->twitter( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param string $url The URL for the login page
|
||||
* @param \IPS\Member $member If we want to integrate this login method with an existing member, provide the member object
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $url, $member=NULL )
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->denied ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'denied_oauth', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
/* Get a request token */
|
||||
if ( !isset( \IPS\Request::i()->oauth_token ) )
|
||||
{
|
||||
$callback = $url->setQueryString( 'loginProcess', 'twitter' )->setQueryString( 'csrf', \IPS\Session::i()->csrfKey );
|
||||
if ( isset( \IPS\Request::i()->ref ) )
|
||||
{
|
||||
$callback = $callback->setQueryString( 'ref', \IPS\Request::i()->ref );
|
||||
}
|
||||
|
||||
$response = $this->sendRequest( 'get', 'https://api.twitter.com/oauth/request_token', array( 'oauth_callback' => (string) $callback ) )->decodeQueryString('oauth_token');
|
||||
\IPS\Output::i()->redirect( "https://api.twitter.com/oauth/authenticate?oauth_token={$response['oauth_token']}" );
|
||||
}
|
||||
|
||||
/* CSRF Check */
|
||||
if ( \IPS\Request::i()->csrf !== \IPS\Session::i()->csrfKey )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'CSRF_FAIL', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
/* Authenticate */
|
||||
$response = $this->sendRequest( 'post', 'https://api.twitter.com/oauth/access_token', array( 'oauth_verifier' => \IPS\Request::i()->oauth_verifier ), \IPS\Request::i()->oauth_token )->decodeQueryString('user_id');
|
||||
|
||||
/* What name are we using? */
|
||||
$member = $member ?: \IPS\Member::load( $response['user_id'], 'twitter_id' );
|
||||
$name = NULL;
|
||||
if ( !$member or !$member->member_id )
|
||||
{
|
||||
if ( $this->settings['name'] == 'screen' )
|
||||
{
|
||||
$name = $response['screen_name'];
|
||||
}
|
||||
elseif ( $this->settings['name'] == 'real' )
|
||||
{
|
||||
try
|
||||
{
|
||||
$user = $this->sendRequest( 'get', 'https://api.twitter.com/1.1/account/verify_credentials.json', array(), $response['oauth_token'], $response['oauth_token_secret'] )->decodeJson();
|
||||
$name = $user['name'];
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/* Find or create member */
|
||||
$member = $this->createOrUpdateAccount( $member, array(
|
||||
'twitter_id' => $response['user_id'],
|
||||
'twitter_token' => $response['oauth_token'],
|
||||
'twitter_secret' => $response['oauth_token_secret']
|
||||
), $name, NULL, NULL, array( 'photo' => TRUE, 'cover' => TRUE, 'status' => '' ) );
|
||||
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
catch ( \IPS\Login\Exception $e )
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings Form
|
||||
*
|
||||
* @param string $url URL to redirect user to after successful submission
|
||||
* @return array List of settings to save - settings will be stored to core_login_handlers.login_settings DB field
|
||||
* @code
|
||||
return array( 'savekey' => new \IPS\Helpers\Form\[Type]( ... ), ... );
|
||||
* @endcode
|
||||
*/
|
||||
public function acpForm()
|
||||
{
|
||||
\IPS\Output::i()->sidebar['actions'] = array(
|
||||
'help' => array(
|
||||
'title' => 'help',
|
||||
'icon' => 'question-circle',
|
||||
'link' => \IPS\Http\Url::ips( 'docs/login_twitter' ),
|
||||
'target' => '_blank',
|
||||
'class' => ''
|
||||
),
|
||||
);
|
||||
|
||||
return array(
|
||||
'consumer_key' => new \IPS\Helpers\Form\Text( 'login_twitter_key', ( isset( $this->settings['consumer_key'] ) ) ? $this->settings['consumer_key'] : '', TRUE ),
|
||||
'consumer_secret' => new \IPS\Helpers\Form\Text( 'login_twitter_secret', ( isset( $this->settings['consumer_secret'] ) ) ? $this->settings['consumer_secret'] : '', TRUE ),
|
||||
'name' => new \IPS\Helpers\Form\Radio( 'login_twitter_name', ( isset( $this->settings['name'] ) ) ? $this->settings['name'] : 'any', TRUE, array( 'options' => array(
|
||||
'real' => 'login_twitter_name_real',
|
||||
'screen' => 'login_twitter_name_screen',
|
||||
'any' => 'login_twitter_name_any',
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Settings
|
||||
*
|
||||
* @return bool
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function testSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->sendRequest( 'get', 'https://api.twitter.com/oauth/request_token', array( 'oauth_callback' => (string) \IPS\Http\Url::internal( '', 'front' ) ) );
|
||||
|
||||
if( $response->httpResponseCode != 200 )
|
||||
{
|
||||
\IPS\Log::log( "Twitter oauth error " . json_encode( $response ), 'twitter_oauth_error');
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Twitter') ) ) ) );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'login_3p_bad', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('login_handler_Twitter') ) ) ) );
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Request
|
||||
*
|
||||
* @param string $method HTTP Method
|
||||
* @param string $url URL
|
||||
* @param array $params Parameters
|
||||
* @param string $token OAuth Token
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
*/
|
||||
public function sendRequest( $method, $url, $params=array(), $token='', $secret='' )
|
||||
{
|
||||
/* Generate the OAUTH Authorization Header */
|
||||
$OAuthAuthorization = array_merge( array(
|
||||
'oauth_consumer_key' => $this->settings['consumer_key'],
|
||||
'oauth_nonce' => md5( \IPS\Login::generateRandomString() ),
|
||||
'oauth_signature_method'=> 'HMAC-SHA1',
|
||||
'oauth_timestamp' => time(),
|
||||
'oauth_token' => $token,
|
||||
'oauth_version' => '1.0'
|
||||
) );
|
||||
|
||||
foreach ( $params as $k => $v )
|
||||
{
|
||||
if ( mb_substr( $k, 0, 6 ) === 'oauth_' )
|
||||
{
|
||||
$OAuthAuthorization = array_merge( array( $k => $v ), $OAuthAuthorization );
|
||||
unset( $params[ $k ] );
|
||||
}
|
||||
}
|
||||
|
||||
/* Media handling needs specific handling as a multipart form and not a signed param list */
|
||||
$media = NULL;
|
||||
if ( isset( $params['media'] ) )
|
||||
{
|
||||
$media = $params['media'];
|
||||
unset( $params['media'] );
|
||||
}
|
||||
|
||||
/* All keys sent in the signature must be in alphabetical order, that includes oAuth keys and user sent params */
|
||||
$allKeys = array_merge( $OAuthAuthorization, $params );
|
||||
ksort( $allKeys );
|
||||
|
||||
$signatureBaseString = mb_strtoupper( $method ) . '&' . rawurlencode( $url ) . '&' . rawurlencode( http_build_query( $allKeys, NULL, '&', PHP_QUERY_RFC3986 ) );
|
||||
$signingKey = rawurlencode( $this->settings['consumer_secret'] ) . '&' . rawurlencode( $secret ?: $token );
|
||||
$OAuthAuthorizationEncoded = array();
|
||||
foreach ( $OAuthAuthorization as $k => $v )
|
||||
{
|
||||
$OAuthAuthorizationEncoded[] = rawurlencode( $k ) . '="' . rawurlencode( $v ) . '"';
|
||||
|
||||
if ( $k === 'oauth_nonce' )
|
||||
{
|
||||
$signature = base64_encode( hash_hmac( 'sha1', $signatureBaseString, $signingKey, TRUE ) );
|
||||
$OAuthAuthorizationEncoded[] = rawurlencode( 'oauth_signature' ) . '="' . rawurlencode( $signature ) . '"';
|
||||
}
|
||||
}
|
||||
$OAuthAuthorizationHeader = 'OAuth ' . implode( ', ', $OAuthAuthorizationEncoded );
|
||||
|
||||
$headers = array( 'Authorization' => $OAuthAuthorizationHeader );
|
||||
|
||||
/* Build the multipart media request */
|
||||
if ( $media )
|
||||
{
|
||||
$mimeBoundary = sha1( microtime() );
|
||||
$headers['Content-Type'] = 'multipart/form-data; boundary=' . $mimeBoundary;
|
||||
|
||||
$data = '--' . $mimeBoundary . "\r\n";
|
||||
$data .= 'Content-Disposition: form-data; name="media";' . "\r\n";
|
||||
$data .= 'Content-Type: application/octet-stream' . "\r\n" . "\r\n";
|
||||
$data .= $media . "\r\n";
|
||||
$data .= '--' . $mimeBoundary . '--' . "\r\n" . "\r\n";
|
||||
|
||||
$params = $data;
|
||||
}
|
||||
|
||||
/* Send the request */
|
||||
return \IPS\Http\Url::external( $url )->request()->setHeaders( $headers )->$method( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member sign in with this login handler?
|
||||
* Used to ensure when a user disassociates a social login that they have some other way of logging in
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canProcess( \IPS\Member $member )
|
||||
{
|
||||
return ( $member->twitter_id and $member->twitter_token and $member->twitter_secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member change their email/password with this login handler?
|
||||
*
|
||||
* @param string $type 'email' or 'password'
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function canChange( $type, \IPS\Member $member )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Internal Login Handler
|
||||
* @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 13 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Login;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade Login Handler
|
||||
*/
|
||||
class _Upgrade extends \IPS\Login\Internal
|
||||
{
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param array $values Values from from
|
||||
* @return \IPS\Member
|
||||
* @throws \IPS\Login\Exception
|
||||
*/
|
||||
public function authenticate( $values )
|
||||
{
|
||||
/* Get member(s) */
|
||||
$members = array();
|
||||
|
||||
$table = 'core_members';
|
||||
|
||||
if ( \IPS\Db::i()->checkForTable( 'members' ) AND !\IPS\Db::i()->checkForTable( 'core_members' ) )
|
||||
{
|
||||
$table = 'members';
|
||||
}
|
||||
|
||||
foreach( \IPS\Db::i()->select('*', $table, array( 'name=? or email=?', \IPS\Request::legacyEscape( $values['auth'] ), \IPS\Request::legacyEscape( $values['auth'] ) ) ) as $_member )
|
||||
{
|
||||
$members[] = \IPS\Member::constructFromData( $_member );
|
||||
}
|
||||
|
||||
/* If we didn't match any, throw an exception */
|
||||
if ( empty( $members ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( \IPS\Member::loggedIn()->language()->addToStack('login_err_no_account', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $this->getLoginType( $this->authTypes ) ) ) ) ), \IPS\Login\Exception::NO_ACCOUNT );
|
||||
}
|
||||
|
||||
/* Check the password for each possible account */
|
||||
foreach ( $members as $member )
|
||||
{
|
||||
if ( \IPS\Login::compareHashes( $member->members_pass_hash, $member->encryptedPassword( $values['password'] ) ) OR
|
||||
\IPS\Login::compareHashes( $member->members_pass_hash, $member->encryptedPassword( \IPS\Request::legacyEscape( $values['password'] ) ) ) )
|
||||
{
|
||||
/* Return */
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? Throw a password incorrect exception */
|
||||
throw new \IPS\Login\Exception( 'login_err_bad_password', \IPS\Login\Exception::BAD_PASSWORD, NULL, $member );
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Reference in new issue
Block a user