Version 4.3.0

This commit is contained in:
Neo committed 2025-12-19 05:47:12 -08:00
1 parent 96997ddd8e
commit fe1acf984a
1349 files changed
+116159 -67711

No files matched your search

+491
View File
@@ -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;
}
}
+32
View File
@@ -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;
}
}
+632
View File
@@ -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;
}
}
+475
View File
@@ -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 { }
+474
View File
@@ -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' );
}
}
+346
View File
@@ -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 ];
}
}
+318
View File
@@ -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 ];
}
}
+756
View File
@@ -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 );
}
}
+390
View File
@@ -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" );
}
}