Version 4.3.0
This commit is contained in:
1 parent
96997ddd8e
commit
fe1acf984a
1349 files changed
+116159
-67711
No files matched your search
@@ -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" );
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user