diff --git a/www/application/third_party/hybridauth/Adapter/AbstractAdapter.php b/www/application/third_party/hybridauth/Adapter/AbstractAdapter.php new file mode 100644 index 00000000..4d828203 --- /dev/null +++ b/www/application/third_party/hybridauth/Adapter/AbstractAdapter.php @@ -0,0 +1,372 @@ +providerId = (new \ReflectionClass($this))->getShortName(); + + $this->config = new Data\Collection($config); + + $this->setHttpClient($httpClient); + + $this->setStorage($storage); + + $this->setLogger($logger); + + $this->configure(); + + $this->logger->debug(sprintf('Initialize %s, config: ', get_class($this)), $config); + + $this->initialize(); + } + + /** + * Load adapter's configuration + */ + abstract protected function configure(); + + /** + * Adapter initializer + */ + abstract protected function initialize(); + + /** + * {@inheritdoc} + */ + abstract public function isConnected(); + + /** + * {@inheritdoc} + */ + public function apiRequest($url, $method = 'GET', $parameters = [], $headers = [], $multipart = false) + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function maintainToken() + { + // Nothing needed for most providers + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function getUserContacts() + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function getUserPages() + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function getUserActivity($stream) + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function setUserStatus($status) + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function setPageStatus($status, $pageId) + { + throw new NotImplementedException('Provider does not support this feature.'); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + $this->clearStoredData(); + } + + /** + * {@inheritdoc} + */ + public function getAccessToken() + { + $tokenNames = [ + 'access_token', + 'access_token_secret', + 'token_type', + 'refresh_token', + 'expires_in', + 'expires_at', + ]; + + $tokens = []; + + foreach ($tokenNames as $name) { + if ($this->getStoredData($name)) { + $tokens[$name] = $this->getStoredData($name); + } + } + + return $tokens; + } + + /** + * {@inheritdoc} + */ + public function setAccessToken($tokens = []) + { + $this->clearStoredData(); + + foreach ($tokens as $token => $value) { + $this->storeData($token, $value); + } + + // Re-initialize token parameters. + $this->initialize(); + } + + /** + * {@inheritdoc} + */ + public function setHttpClient(HttpClientInterface $httpClient = null) + { + $this->httpClient = $httpClient ?: new HttpClient(); + + if ($this->config->exists('curl_options') && method_exists($this->httpClient, 'setCurlOptions')) { + $this->httpClient->setCurlOptions($this->config->get('curl_options')); + } + } + + /** + * {@inheritdoc} + */ + public function getHttpClient() + { + return $this->httpClient; + } + + /** + * {@inheritdoc} + */ + public function setStorage(StorageInterface $storage = null) + { + $this->storage = $storage ?: new Session(); + } + + /** + * {@inheritdoc} + */ + public function getStorage() + { + return $this->storage; + } + + /** + * {@inheritdoc} + */ + public function setLogger(LoggerInterface $logger = null) + { + $this->logger = $logger ?: new Logger( + $this->config->get('debug_mode'), + $this->config->get('debug_file') + ); + + if (method_exists($this->httpClient, 'setLogger')) { + $this->httpClient->setLogger($this->logger); + } + } + + /** + * {@inheritdoc} + */ + public function getLogger() + { + return $this->logger; + } + + /** + * Set Adapter's API callback url + * + * @param string $callback + * + * @throws InvalidArgumentException + */ + protected function setCallback($callback) + { + if (!filter_var($callback, FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException('A valid callback url is required.'); + } + + $this->callback = $callback; + } + + /** + * Overwrite Adapter's API endpoints + * + * @param array|Data\Collection $endpoints + */ + protected function setApiEndpoints($endpoints = null) + { + if (empty($endpoints)) { + return; + } + + $collection = is_array($endpoints) ? new Data\Collection($endpoints) : $endpoints; + + $this->apiBaseUrl = $collection->get('api_base_url') ?: $this->apiBaseUrl; + $this->authorizeUrl = $collection->get('authorize_url') ?: $this->authorizeUrl; + $this->accessTokenUrl = $collection->get('access_token_url') ?: $this->accessTokenUrl; + } + + + /** + * Validate signed API responses Http status code. + * + * Since the specifics of error responses is beyond the scope of RFC6749 and OAuth Core specifications, + * Hybridauth will consider any HTTP status code that is different than '200 OK' as an ERROR. + * + * @param string $error String to pre append to message thrown in exception + * + * @throws HttpClientFailureException + * @throws HttpRequestFailedException + */ + protected function validateApiResponse($error = '') + { + $error .= !empty($error) ? '. ' : ''; + + if ($this->httpClient->getResponseClientError()) { + throw new HttpClientFailureException( + $error . 'HTTP client error: ' . $this->httpClient->getResponseClientError() . '.' + ); + } + + // if validateApiResponseHttpCode is set to false, we by pass verification of http status code + if (!$this->validateApiResponseHttpCode) { + return; + } + + $status = $this->httpClient->getResponseHttpCode(); + + if ($status < 200 || $status > 299) { + throw new HttpRequestFailedException( + $error . 'HTTP error ' . $this->httpClient->getResponseHttpCode() . + '. Raw Provider API response: ' . $this->httpClient->getResponseBody() . '.' + ); + } + } +} diff --git a/www/application/third_party/hybridauth/Adapter/AdapterInterface.php b/www/application/third_party/hybridauth/Adapter/AdapterInterface.php new file mode 100644 index 00000000..537daaa3 --- /dev/null +++ b/www/application/third_party/hybridauth/Adapter/AdapterInterface.php @@ -0,0 +1,155 @@ +deleteStoredData($name); + } + + $this->getStorage()->set($this->providerId . '.' . $name, $value); + } + + /** + * Retrieve a piece of data from storage. + * + * This method is mainly used for OAuth tokens (access, secret, refresh, and whatnot), but it + * can be also used by providers to retrieve from store any other useful data (i.g., user_id, + * auth_nonce, etc.) + * + * @param string $name + * + * @return mixed + */ + protected function getStoredData($name) + { + return $this->getStorage()->get($this->providerId . '.' . $name); + } + + /** + * Delete a stored piece of data. + * + * @param string $name + */ + protected function deleteStoredData($name) + { + $this->getStorage()->delete($this->providerId . '.' . $name); + } + + /** + * Delete all stored data of the instantiated adapter + */ + protected function clearStoredData() + { + $this->getStorage()->deleteMatch($this->providerId . '.'); + } +} diff --git a/www/application/third_party/hybridauth/Adapter/OAuth1.php b/www/application/third_party/hybridauth/Adapter/OAuth1.php new file mode 100644 index 00000000..e500d1a2 --- /dev/null +++ b/www/application/third_party/hybridauth/Adapter/OAuth1.php @@ -0,0 +1,616 @@ +consumerKey = $this->config->filter('keys')->get('id') ?: $this->config->filter('keys')->get('key'); + $this->consumerSecret = $this->config->filter('keys')->get('secret'); + + if (!$this->consumerKey || !$this->consumerSecret) { + throw new InvalidApplicationCredentialsException( + 'Your application id is required in order to connect to ' . $this->providerId + ); + } + + if ($this->config->exists('tokens')) { + $this->setAccessToken($this->config->get('tokens')); + } + + $this->setCallback($this->config->get('callback')); + $this->setApiEndpoints($this->config->get('endpoints')); + } + + /** + * {@inheritdoc} + */ + protected function initialize() + { + /** + * Set up OAuth Signature and Consumer + * + * OAuth Core: All Token requests and Protected Resources requests MUST be signed + * by the Consumer and verified by the Service Provider. + * + * The protocol defines three signature methods: HMAC-SHA1, RSA-SHA1, and PLAINTEXT.. + * + * The Consumer declares a signature method in the oauth_signature_method parameter.. + * + * http://oauth.net/core/1.0a/#signing_process + */ + $this->sha1Method = new OAuthSignatureMethodHMACSHA1(); + + $this->OAuthConsumer = new OAuthConsumer( + $this->consumerKey, + $this->consumerSecret + ); + + if ($this->getStoredData('request_token')) { + $this->consumerToken = new OAuthConsumer( + $this->getStoredData('request_token'), + $this->getStoredData('request_token_secret') + ); + } + + if ($this->getStoredData('access_token')) { + $this->consumerToken = new OAuthConsumer( + $this->getStoredData('access_token'), + $this->getStoredData('access_token_secret') + ); + } + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $this->logger->info(sprintf('%s::authenticate()', get_class($this))); + + if ($this->isConnected()) { + return true; + } + + try { + if (!$this->getStoredData('request_token')) { + // Start a new flow. + $this->authenticateBegin(); + } elseif (empty($_GET['oauth_token']) && empty($_GET['denied'])) { + // A previous authentication was not finished, and this request is not finishing it. + $this->authenticateBegin(); + } else { + // Finish a flow. + $this->authenticateFinish(); + } + } catch (Exception $exception) { + $this->clearStoredData(); + + throw $exception; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return (bool)$this->getStoredData('access_token'); + } + + /** + * Initiate the authorization protocol + * + * 1. Obtaining an Unauthorized Request Token + * 2. Build Authorization URL for Authorization Request and redirect the user-agent to the + * Authorization Server. + */ + protected function authenticateBegin() + { + $response = $this->requestAuthToken(); + + $this->validateAuthTokenRequest($response); + + $authUrl = $this->getAuthorizeUrl(); + + $this->logger->debug(sprintf('%s::authenticateBegin(), redirecting user to:', get_class($this)), [$authUrl]); + + HttpClient\Util::redirect($authUrl); + } + + /** + * Finalize the authorization process + * + * @throws AuthorizationDeniedException + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws InvalidAccessTokenException + * @throws InvalidOauthTokenException + */ + protected function authenticateFinish() + { + $this->logger->debug( + sprintf('%s::authenticateFinish(), callback url:', get_class($this)), + [HttpClient\Util::getCurrentUrl(true)] + ); + + $denied = filter_input(INPUT_GET, 'denied'); + $oauth_problem = filter_input(INPUT_GET, 'oauth_problem'); + $oauth_token = filter_input(INPUT_GET, 'oauth_token'); + $oauth_verifier = filter_input(INPUT_GET, 'oauth_verifier'); + + if ($denied) { + throw new AuthorizationDeniedException( + 'User denied access request. Provider returned a denied token: ' . htmlentities($denied) + ); + } + + if ($oauth_problem) { + throw new InvalidOauthTokenException( + 'Provider returned an error. oauth_problem: ' . htmlentities($oauth_problem) + ); + } + + if (!$oauth_token) { + throw new InvalidOauthTokenException( + 'Expecting a non-null oauth_token to continue the authorization flow.' + ); + } + + $response = $this->exchangeAuthTokenForAccessToken($oauth_token, $oauth_verifier); + + $this->validateAccessTokenExchange($response); + + $this->initialize(); + } + + /** + * Build Authorization URL for Authorization Request + * + * @param array $parameters + * + * @return string + */ + protected function getAuthorizeUrl($parameters = []) + { + $this->AuthorizeUrlParameters = !empty($parameters) + ? $parameters + : array_replace( + (array)$this->AuthorizeUrlParameters, + (array)$this->config->get('authorize_url_parameters') + ); + + $this->AuthorizeUrlParameters['oauth_token'] = $this->getStoredData('request_token'); + + return $this->authorizeUrl . '?' . http_build_query($this->AuthorizeUrlParameters, '', '&'); + } + + /** + * Unauthorized Request Token + * + * OAuth Core: The Consumer obtains an unauthorized Request Token by asking the Service Provider + * to issue a Token. The Request Token's sole purpose is to receive User approval and can only + * be used to obtain an Access Token. + * + * http://oauth.net/core/1.0/#auth_step1 + * 6.1.1. Consumer Obtains a Request Token + * + * @return string Raw Provider API response + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + */ + protected function requestAuthToken() + { + /** + * OAuth Core 1.0 Revision A: oauth_callback: An absolute URL to which the Service Provider will redirect + * the User back when the Obtaining User Authorization step is completed. + * + * http://oauth.net/core/1.0a/#auth_step1 + */ + if ('1.0a' == $this->oauth1Version) { + $this->requestTokenParameters['oauth_callback'] = $this->callback; + } + + $response = $this->oauthRequest( + $this->requestTokenUrl, + $this->requestTokenMethod, + $this->requestTokenParameters, + $this->requestTokenHeaders + ); + + return $response; + } + + /** + * Validate Unauthorized Request Token Response + * + * OAuth Core: The Service Provider verifies the signature and Consumer Key. If successful, + * it generates a Request Token and Token Secret and returns them to the Consumer in the HTTP + * response body. + * + * http://oauth.net/core/1.0/#auth_step1 + * 6.1.2. Service Provider Issues an Unauthorized Request Token + * + * @param string $response + * + * @return \Hybridauth\Data\Collection + * @throws InvalidOauthTokenException + */ + protected function validateAuthTokenRequest($response) + { + /** + * The response contains the following parameters: + * + * - oauth_token The Request Token. + * - oauth_token_secret The Token Secret. + * - oauth_callback_confirmed MUST be present and set to true. + * + * http://oauth.net/core/1.0/#auth_step1 + * 6.1.2. Service Provider Issues an Unauthorized Request Token + * + * Example of a successful response: + * + * HTTP/1.1 200 OK + * Content-Type: text/html; charset=utf-8 + * Cache-Control: no-store + * Pragma: no-cache + * + * oauth_token=80359084-clg1DEtxQF3wstTcyUdHF3wsdHM&oauth_token_secret=OIF07hPmJB:P + * 6qiHTi1znz6qiH3tTcyUdHnz6qiH3tTcyUdH3xW3wsDvV08e&example_parameter=example_value + * + * OAuthUtil::parse_parameters will attempt to decode the raw response into an array. + */ + $tokens = OAuthUtil::parse_parameters($response); + + $collection = new Data\Collection($tokens); + + if (!$collection->exists('oauth_token')) { + throw new InvalidOauthTokenException( + 'Provider returned no oauth_token: ' . htmlentities($response) + ); + } + + $this->consumerToken = new OAuthConsumer( + $tokens['oauth_token'], + $tokens['oauth_token_secret'] + ); + + $this->storeData('request_token', $tokens['oauth_token']); + $this->storeData('request_token_secret', $tokens['oauth_token_secret']); + + return $collection; + } + + /** + * Requests an Access Token + * + * OAuth Core: The Request Token and Token Secret MUST be exchanged for an Access Token and Token Secret. + * + * http://oauth.net/core/1.0a/#auth_step3 + * 6.3.1. Consumer Requests an Access Token + * + * @param string $oauth_token + * @param string $oauth_verifier + * + * @return string Raw Provider API response + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + */ + protected function exchangeAuthTokenForAccessToken($oauth_token, $oauth_verifier = '') + { + $this->tokenExchangeParameters['oauth_token'] = $oauth_token; + + /** + * OAuth Core 1.0 Revision A: oauth_verifier: The verification code received from the Service Provider + * in the "Service Provider Directs the User Back to the Consumer" step. + * + * http://oauth.net/core/1.0a/#auth_step3 + */ + if ('1.0a' == $this->oauth1Version) { + $this->tokenExchangeParameters['oauth_verifier'] = $oauth_verifier; + } + + $response = $this->oauthRequest( + $this->accessTokenUrl, + $this->tokenExchangeMethod, + $this->tokenExchangeParameters, + $this->tokenExchangeHeaders + ); + + return $response; + } + + /** + * Validate Access Token Response + * + * OAuth Core: If successful, the Service Provider generates an Access Token and Token Secret and returns + * them in the HTTP response body. + * + * The Access Token and Token Secret are stored by the Consumer and used when signing Protected Resources requests. + * + * http://oauth.net/core/1.0a/#auth_step3 + * 6.3.2. Service Provider Grants an Access Token + * + * @param string $response + * + * @return \Hybridauth\Data\Collection + * @throws InvalidAccessTokenException + */ + protected function validateAccessTokenExchange($response) + { + /** + * The response contains the following parameters: + * + * - oauth_token The Access Token. + * - oauth_token_secret The Token Secret. + * + * http://oauth.net/core/1.0/#auth_step3 + * 6.3.2. Service Provider Grants an Access Token + * + * Example of a successful response: + * + * HTTP/1.1 200 OK + * Content-Type: text/html; charset=utf-8 + * Cache-Control: no-store + * Pragma: no-cache + * + * oauth_token=sHeLU7Far428zj8PzlWR75&oauth_token_secret=fXb30rzoG&oauth_callback_confirmed=true + * + * OAuthUtil::parse_parameters will attempt to decode the raw response into an array. + */ + $tokens = OAuthUtil::parse_parameters($response); + + $collection = new Data\Collection($tokens); + + if (!$collection->exists('oauth_token')) { + throw new InvalidAccessTokenException( + 'Provider returned no access_token: ' . htmlentities($response) + ); + } + + $this->consumerToken = new OAuthConsumer( + $collection->get('oauth_token'), + $collection->get('oauth_token_secret') + ); + + $this->storeData('access_token', $collection->get('oauth_token')); + $this->storeData('access_token_secret', $collection->get('oauth_token_secret')); + + $this->deleteStoredData('request_token'); + $this->deleteStoredData('request_token_secret'); + + return $collection; + } + + /** + * Send a signed request to provider API + * + * Note: Since the specifics of error responses is beyond the scope of RFC6749 and OAuth specifications, + * Hybridauth will consider any HTTP status code that is different than '200 OK' as an ERROR. + * + * @param string $url + * @param string $method + * @param array $parameters + * @param array $headers + * @param bool $multipart + * + * @return mixed + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + */ + public function apiRequest($url, $method = 'GET', $parameters = [], $headers = [], $multipart = false) + { + // refresh tokens if needed + $this->maintainToken(); + + if (strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0) { + $url = rtrim($this->apiBaseUrl, '/') . '/' . ltrim($url, '/'); + } + + $parameters = array_replace($this->apiRequestParameters, (array)$parameters); + + $headers = array_replace($this->apiRequestHeaders, (array)$headers); + + $response = $this->oauthRequest($url, $method, $parameters, $headers, $multipart); + + $response = (new Data\Parser())->parse($response); + + return $response; + } + + /** + * Setup and Send a Signed Oauth Request + * + * This method uses OAuth Library. + * + * @param string $uri + * @param string $method + * @param array $parameters + * @param array $headers + * @param bool $multipart + * + * @return string Raw Provider API response + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + */ + protected function oauthRequest($uri, $method = 'GET', $parameters = [], $headers = [], $multipart = false) + { + $signing_parameters = $parameters; + if ($multipart) { + $signing_parameters = []; + } + + $request = OAuthRequest::from_consumer_and_token( + $this->OAuthConsumer, + $this->consumerToken, + $method, + $uri, + $signing_parameters + ); + + $request->sign_request( + $this->sha1Method, + $this->OAuthConsumer, + $this->consumerToken + ); + + $uri = $request->get_normalized_http_url(); + $headers = array_replace($request->to_header(), (array)$headers); + + $response = $this->httpClient->request( + $uri, + $method, + $parameters, + $headers, + $multipart + ); + + $this->validateApiResponse('Signed API request to ' . $uri . ' has returned an error'); + + return $response; + } +} diff --git a/www/application/third_party/hybridauth/Adapter/OAuth2.php b/www/application/third_party/hybridauth/Adapter/OAuth2.php new file mode 100644 index 00000000..06d495d9 --- /dev/null +++ b/www/application/third_party/hybridauth/Adapter/OAuth2.php @@ -0,0 +1,739 @@ +clientId = $this->config->filter('keys')->get('id') ?: $this->config->filter('keys')->get('key'); + $this->clientSecret = $this->config->filter('keys')->get('secret'); + + if (!$this->clientId || !$this->clientSecret) { + throw new InvalidApplicationCredentialsException( + 'Your application id is required in order to connect to ' . $this->providerId + ); + } + + $this->scope = $this->config->exists('scope') ? $this->config->get('scope') : $this->scope; + + if ($this->config->exists('tokens')) { + $this->setAccessToken($this->config->get('tokens')); + } + + $this->setCallback($this->config->get('callback')); + $this->setApiEndpoints($this->config->get('endpoints')); + } + + /** + * {@inheritdoc} + */ + protected function initialize() + { + $this->AuthorizeUrlParameters = [ + 'response_type' => 'code', + 'client_id' => $this->clientId, + 'redirect_uri' => $this->callback, + 'scope' => $this->scope, + ]; + + $this->tokenExchangeParameters = [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->callback + ]; + + $refreshToken = $this->getStoredData('refresh_token'); + if (!empty($refreshToken)) { + $this->tokenRefreshParameters = [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + ]; + } + + $this->apiRequestHeaders = [ + 'Authorization' => 'Bearer ' . $this->getStoredData('access_token') + ]; + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $this->logger->info(sprintf('%s::authenticate()', get_class($this))); + + if ($this->isConnected()) { + return true; + } + + try { + $this->authenticateCheckError(); + + $code = filter_input($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, 'code'); + + if (empty($code)) { + $this->authenticateBegin(); + } else { + $this->authenticateFinish(); + } + } catch (Exception $e) { + $this->clearStoredData(); + + throw $e; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + if ((bool)$this->getStoredData('access_token')) { + return (!$this->hasAccessTokenExpired() || $this->isRefreshTokenAvailable()); + } + return false; + } + + /** + * If we can use a refresh token, then an expired token does not stop us being connected. + * + * @return bool + */ + public function isRefreshTokenAvailable() + { + return is_array($this->tokenRefreshParameters); + } + + /** + * Authorization Request Error Response + * + * RFC6749: If the request fails due to a missing, invalid, or mismatching + * redirection URI, or if the client identifier is missing or invalid, + * the authorization server SHOULD inform the resource owner of the error. + * + * http://tools.ietf.org/html/rfc6749#section-4.1.2.1 + * + * @throws \Hybridauth\Exception\InvalidAuthorizationCodeException + * @throws \Hybridauth\Exception\AuthorizationDeniedException + */ + protected function authenticateCheckError() + { + $error = filter_input(INPUT_GET, 'error', FILTER_SANITIZE_SPECIAL_CHARS); + + if (!empty($error)) { + $error_description = filter_input(INPUT_GET, 'error_description', FILTER_SANITIZE_SPECIAL_CHARS); + $error_uri = filter_input(INPUT_GET, 'error_uri', FILTER_SANITIZE_SPECIAL_CHARS); + + $collated_error = sprintf('Provider returned an error: %s %s %s', $error, $error_description, $error_uri); + + if ($error == 'access_denied') { + throw new AuthorizationDeniedException($collated_error); + } + + throw new InvalidAuthorizationCodeException($collated_error); + } + } + + /** + * Initiate the authorization protocol + * + * Build Authorization URL for Authorization Request and redirect the user-agent to the + * Authorization Server. + */ + protected function authenticateBegin() + { + $authUrl = $this->getAuthorizeUrl(); + + $this->logger->debug(sprintf('%s::authenticateBegin(), redirecting user to:', get_class($this)), [$authUrl]); + + HttpClient\Util::redirect($authUrl); + } + + /** + * Finalize the authorization process + * + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws InvalidAccessTokenException + * @throws InvalidAuthorizationStateException + */ + protected function authenticateFinish() + { + $this->logger->debug( + sprintf('%s::authenticateFinish(), callback url:', get_class($this)), + [HttpClient\Util::getCurrentUrl(true)] + ); + + $state = filter_input($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, 'state'); + $code = filter_input($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, 'code'); + + /** + * Authorization Request State + * + * RFC6749: state : RECOMMENDED. An opaque value used by the client to maintain + * state between the request and callback. The authorization server includes + * this value when redirecting the user-agent back to the client. + * + * http://tools.ietf.org/html/rfc6749#section-4.1.1 + */ + if ($this->supportRequestState + && $this->getStoredData('authorization_state') != $state + ) { + throw new InvalidAuthorizationStateException( + 'The authorization state [state=' . substr(htmlentities($state), 0, 100) . '] ' + . 'of this page is either invalid or has already been consumed.' + ); + } + + /** + * Authorization Request Code + * + * RFC6749: If the resource owner grants the access request, the authorization + * server issues an authorization code and delivers it to the client: + * + * http://tools.ietf.org/html/rfc6749#section-4.1.2 + */ + $response = $this->exchangeCodeForAccessToken($code); + + $this->validateAccessTokenExchange($response); + + $this->initialize(); + } + + /** + * Build Authorization URL for Authorization Request + * + * RFC6749: The client constructs the request URI by adding the following + * $parameters to the query component of the authorization endpoint URI: + * + * - response_type REQUIRED. Value MUST be set to "code". + * - client_id REQUIRED. + * - redirect_uri OPTIONAL. + * - scope OPTIONAL. + * - state RECOMMENDED. + * + * http://tools.ietf.org/html/rfc6749#section-4.1.1 + * + * Sub classes may redefine this method when necessary. + * + * @param array $parameters + * + * @return string Authorization URL + */ + protected function getAuthorizeUrl($parameters = []) + { + $this->AuthorizeUrlParameters = !empty($parameters) + ? $parameters + : array_replace( + (array)$this->AuthorizeUrlParameters, + (array)$this->config->get('authorize_url_parameters') + ); + + if ($this->supportRequestState) { + if (!isset($this->AuthorizeUrlParameters['state'])) { + $this->AuthorizeUrlParameters['state'] = 'HA-' . str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'); + } + + $this->storeData('authorization_state', $this->AuthorizeUrlParameters['state']); + } + + $queryParams = http_build_query($this->AuthorizeUrlParameters, '', '&', $this->AuthorizeUrlParametersEncType); + return $this->authorizeUrl . '?' . $queryParams; + } + + /** + * Access Token Request + * + * This method will exchange the received $code in loginFinish() with an Access Token. + * + * RFC6749: The client makes a request to the token endpoint by sending the + * following parameters using the "application/x-www-form-urlencoded" + * with a character encoding of UTF-8 in the HTTP request entity-body: + * + * - grant_type REQUIRED. Value MUST be set to "authorization_code". + * - code REQUIRED. The authorization code received from the authorization server. + * - redirect_uri REQUIRED. + * - client_id REQUIRED. + * + * http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @param string $code + * + * @return string Raw Provider API response + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + */ + protected function exchangeCodeForAccessToken($code) + { + $this->tokenExchangeParameters['code'] = $code; + + $response = $this->httpClient->request( + $this->accessTokenUrl, + $this->tokenExchangeMethod, + $this->tokenExchangeParameters, + $this->tokenExchangeHeaders + ); + + $this->validateApiResponse('Unable to exchange code for API access token'); + + return $response; + } + + /** + * Validate Access Token Response + * + * RFC6749: If the access token request is valid and authorized, the + * authorization server issues an access token and optional refresh token. + * If the request client authentication failed or is invalid, the authorization + * server returns an error response as described in Section 5.2. + * + * Example of a successful response: + * + * HTTP/1.1 200 OK + * Content-Type: application/json;charset=UTF-8 + * Cache-Control: no-store + * Pragma: no-cache + * + * { + * "access_token":"2YotnFZFEjr1zCsicMWpAA", + * "token_type":"example", + * "expires_in":3600, + * "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", + * "example_parameter":"example_value" + * } + * + * http://tools.ietf.org/html/rfc6749#section-4.1.4 + * + * This method uses Data_Parser to attempt to decodes the raw $response (usually JSON) + * into a data collection. + * + * @param string $response + * + * @return \Hybridauth\Data\Collection + * @throws InvalidAccessTokenException + */ + protected function validateAccessTokenExchange($response) + { + $data = (new Data\Parser())->parse($response); + + $collection = new Data\Collection($data); + + if (!$collection->exists('access_token')) { + throw new InvalidAccessTokenException( + 'Provider returned no access_token: ' . htmlentities($response) + ); + } + + $this->storeData('access_token', $collection->get('access_token')); + $this->storeData('token_type', $collection->get('token_type')); + + if ($collection->get('refresh_token')) { + $this->storeData('refresh_token', $collection->get('refresh_token')); + } + + // calculate when the access token expire + if ($collection->exists('expires_in')) { + $expires_at = time() + (int)$collection->get('expires_in'); + + $this->storeData('expires_in', $collection->get('expires_in')); + $this->storeData('expires_at', $expires_at); + } + + $this->deleteStoredData('authorization_state'); + + $this->initialize(); + + return $collection; + } + + /** + * Refreshing an Access Token + * + * RFC6749: If the authorization server issued a refresh token to the + * client, the client makes a refresh request to the token endpoint by + * adding the following parameters ... in the HTTP request entity-body: + * + * - grant_type REQUIRED. Value MUST be set to "refresh_token". + * - refresh_token REQUIRED. The refresh token issued to the client. + * - scope OPTIONAL. + * + * http://tools.ietf.org/html/rfc6749#section-6 + * + * This method is similar to exchangeCodeForAccessToken(). The only + * difference is here we exchange refresh_token for a new access_token. + * + * @param array $parameters + * + * @return string|null Raw Provider API response, or null if we cannot refresh + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws InvalidAccessTokenException + */ + public function refreshAccessToken($parameters = []) + { + $this->tokenRefreshParameters = !empty($parameters) + ? $parameters + : $this->tokenRefreshParameters; + + if (!$this->isRefreshTokenAvailable()) { + return null; + } + + $response = $this->httpClient->request( + $this->accessTokenUrl, + $this->tokenRefreshMethod, + $this->tokenRefreshParameters, + $this->tokenRefreshHeaders + ); + + $this->validateApiResponse('Unable to refresh the access token'); + + $this->validateRefreshAccessToken($response); + + return $response; + } + + /** + * Check whether access token has expired + * + * @param int|null $time + * @return bool|null + */ + public function hasAccessTokenExpired($time = null) + { + if ($time === null) { + $time = time(); + } + + $expires_at = $this->getStoredData('expires_at'); + if (!$expires_at) { + return null; + } + + return $expires_at <= $time; + } + + /** + * Validate Refresh Access Token Request + * + * RFC6749: If valid and authorized, the authorization server issues an + * access token as described in Section 5.1. If the request failed + * verification or is invalid, the authorization server returns an error + * response as described in Section 5.2. + * + * http://tools.ietf.org/html/rfc6749#section-6 + * http://tools.ietf.org/html/rfc6749#section-5.1 + * http://tools.ietf.org/html/rfc6749#section-5.2 + * + * This method simply use validateAccessTokenExchange(), however sub + * classes may redefine it when necessary. + * + * @param $response + * + * @return \Hybridauth\Data\Collection + * @throws InvalidAccessTokenException + */ + protected function validateRefreshAccessToken($response) + { + return $this->validateAccessTokenExchange($response); + } + + /** + * Send a signed request to provider API + * + * RFC6749: Accessing Protected Resources: The client accesses protected + * resources by presenting the access token to the resource server. The + * resource server MUST validate the access token and ensure that it has + * not expired and that its scope covers the requested resource. + * + * Note: Since the specifics of error responses is beyond the scope of + * RFC6749 and OAuth specifications, Hybridauth will consider any HTTP + * status code that is different than '200 OK' as an ERROR. + * + * http://tools.ietf.org/html/rfc6749#section-7 + * + * @param string $url + * @param string $method + * @param array $parameters + * @param array $headers + * @param bool $multipart + * + * @return mixed + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws InvalidAccessTokenException + */ + public function apiRequest($url, $method = 'GET', $parameters = [], $headers = [], $multipart = false) + { + // refresh tokens if needed + $this->maintainToken(); + if ($this->hasAccessTokenExpired() === true) { + $this->refreshAccessToken(); + } + + if (strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0) { + $url = rtrim($this->apiBaseUrl, '/') . '/' . ltrim($url, '/'); + } + + $parameters = array_replace($this->apiRequestParameters, (array)$parameters); + $headers = array_replace($this->apiRequestHeaders, (array)$headers); + + $response = $this->httpClient->request( + $url, + $method, // HTTP Request Method. Defaults to GET. + $parameters, // Request Parameters + $headers, // Request Headers + $multipart // Is request multipart + ); + + $this->validateApiResponse('Signed API request to ' . $url . ' has returned an error'); + + $response = (new Data\Parser())->parse($response); + + return $response; + } +} diff --git a/www/application/third_party/hybridauth/Adapter/OpenID.php b/www/application/third_party/hybridauth/Adapter/OpenID.php new file mode 100644 index 00000000..f1c0e9d0 --- /dev/null +++ b/www/application/third_party/hybridauth/Adapter/OpenID.php @@ -0,0 +1,283 @@ +config->exists('openid_identifier')) { + $this->openidIdentifier = $this->config->get('openid_identifier'); + } + + if (empty($this->openidIdentifier)) { + throw new InvalidOpenidIdentifierException('OpenID adapter requires an openid_identifier.', 4); + } + + $this->setCallback($this->config->get('callback')); + $this->setApiEndpoints($this->config->get('endpoints')); + } + + /** + * {@inheritdoc} + */ + protected function initialize() + { + $hostPort = parse_url($this->callback, PHP_URL_PORT); + $hostUrl = parse_url($this->callback, PHP_URL_HOST); + + if ($hostPort) { + $hostUrl .= ':' . $hostPort; + } + + // @fixme: add proxy + $this->openIdClient = new LightOpenID($hostUrl, null); + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $this->logger->info(sprintf('%s::authenticate()', get_class($this))); + + if ($this->isConnected()) { + return true; + } + + if (empty($_REQUEST['openid_mode'])) { + $this->authenticateBegin(); + } else { + return $this->authenticateFinish(); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return (bool)$this->storage->get($this->providerId . '.user'); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + $this->storage->delete($this->providerId . '.user'); + + return true; + } + + /** + * Initiate the authorization protocol + * + * Include and instantiate LightOpenID + */ + protected function authenticateBegin() + { + $this->openIdClient->identity = $this->openidIdentifier; + $this->openIdClient->returnUrl = $this->callback; + $this->openIdClient->required = [ + 'namePerson/first', + 'namePerson/last', + 'namePerson/friendly', + 'namePerson', + 'contact/email', + 'birthDate', + 'birthDate/birthDay', + 'birthDate/birthMonth', + 'birthDate/birthYear', + 'person/gender', + 'pref/language', + 'contact/postalCode/home', + 'contact/city/home', + 'contact/country/home', + + 'media/image/default', + ]; + + $authUrl = $this->openIdClient->authUrl(); + + $this->logger->debug(sprintf('%s::authenticateBegin(), redirecting user to:', get_class($this)), [$authUrl]); + + HttpClient\Util::redirect($authUrl); + } + + /** + * Finalize the authorization process. + * + * @throws AuthorizationDeniedException + * @throws UnexpectedApiResponseException + */ + protected function authenticateFinish() + { + $this->logger->debug( + sprintf('%s::authenticateFinish(), callback url:', get_class($this)), + [HttpClient\Util::getCurrentUrl(true)] + ); + + if ($this->openIdClient->mode == 'cancel') { + throw new AuthorizationDeniedException('User has cancelled the authentication.'); + } + + if (!$this->openIdClient->validate()) { + throw new UnexpectedApiResponseException('Invalid response received.'); + } + + $openidAttributes = $this->openIdClient->getAttributes(); + + if (!$this->openIdClient->identity) { + throw new UnexpectedApiResponseException('Provider returned an unexpected response.'); + } + + $userProfile = $this->fetchUserProfile($openidAttributes); + + /* with openid providers we only get user profiles once, so we store it */ + $this->storage->set($this->providerId . '.user', $userProfile); + } + + /** + * Fetch user profile from received openid attributes + * + * @param array $openidAttributes + * + * @return User\Profile + */ + protected function fetchUserProfile($openidAttributes) + { + $data = new Data\Collection($openidAttributes); + + $userProfile = new User\Profile(); + + $userProfile->identifier = $this->openIdClient->identity; + + $userProfile->firstName = $data->get('namePerson/first'); + $userProfile->lastName = $data->get('namePerson/last'); + $userProfile->email = $data->get('contact/email'); + $userProfile->language = $data->get('pref/language'); + $userProfile->country = $data->get('contact/country/home'); + $userProfile->zip = $data->get('contact/postalCode/home'); + $userProfile->gender = $data->get('person/gender'); + $userProfile->photoURL = $data->get('media/image/default'); + $userProfile->birthDay = $data->get('birthDate/birthDay'); + $userProfile->birthMonth = $data->get('birthDate/birthMonth'); + $userProfile->birthYear = $data->get('birthDate/birthDate'); + + $userProfile = $this->fetchUserGender($userProfile, $data->get('person/gender')); + + $userProfile = $this->fetchUserDisplayName($userProfile, $data); + + return $userProfile; + } + + /** + * Extract users display names + * + * @param User\Profile $userProfile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function fetchUserDisplayName(User\Profile $userProfile, Data\Collection $data) + { + $userProfile->displayName = $data->get('namePerson'); + + $userProfile->displayName = $userProfile->displayName + ? $userProfile->displayName + : $data->get('namePerson/friendly'); + + $userProfile->displayName = $userProfile->displayName + ? $userProfile->displayName + : trim($userProfile->firstName . ' ' . $userProfile->lastName); + + return $userProfile; + } + + /** + * Extract users gender + * + * @param User\Profile $userProfile + * @param string $gender + * + * @return User\Profile + */ + protected function fetchUserGender(User\Profile $userProfile, $gender) + { + $gender = strtolower($gender); + + if ('f' == $gender) { + $gender = 'female'; + } + + if ('m' == $gender) { + $gender = 'male'; + } + + $userProfile->gender = $gender; + + return $userProfile; + } + + /** + * OpenID only provide the user profile one. This method will attempt to retrieve the profile from storage. + */ + public function getUserProfile() + { + $userProfile = $this->storage->get($this->providerId . '.user'); + + if (!is_object($userProfile)) { + throw new UnexpectedApiResponseException('Provider returned an unexpected response.'); + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Data/Collection.php b/www/application/third_party/hybridauth/Data/Collection.php new file mode 100644 index 00000000..60f3eb57 --- /dev/null +++ b/www/application/third_party/hybridauth/Data/Collection.php @@ -0,0 +1,154 @@ +collection = (object)$data; + } + + /** + * Retrieves the whole collection as array + * + * @return mixed + */ + public function toArray() + { + return (array)$this->collection; + } + + /** + * Retrieves an item + * + * @param $property + * + * @return mixed + */ + public function get($property) + { + if ($this->exists($property)) { + return $this->collection->$property; + } + + return null; + } + + /** + * Add or update an item + * + * @param $property + * @param mixed $value + */ + public function set($property, $value) + { + if ($property) { + $this->collection->$property = $value; + } + } + + /** + * .. until I come with a better name.. + * + * @param $property + * + * @return Collection + */ + public function filter($property) + { + if ($this->exists($property)) { + $data = $this->get($property); + + if (!is_a($data, 'Collection')) { + $data = new Collection($data); + } + + return $data; + } + + return new Collection([]); + } + + /** + * Checks whether an item within the collection + * + * @param $property + * + * @return bool + */ + public function exists($property) + { + return property_exists($this->collection, $property); + } + + /** + * Finds whether the collection is empty + * + * @return bool + */ + public function isEmpty() + { + return !(bool)$this->count(); + } + + /** + * Count all items in collection + * + * @return int + */ + public function count() + { + return count($this->properties()); + } + + /** + * Returns all items properties names + * + * @return array + */ + public function properties() + { + $properties = []; + + foreach ($this->collection as $key => $value) { + $properties[] = $key; + } + + return $properties; + } + + /** + * Returns all items values + * + * @return array + */ + public function values() + { + $values = []; + + foreach ($this->collection as $value) { + $values[] = $value; + } + + return $values; + } +} diff --git a/www/application/third_party/hybridauth/Data/Parser.php b/www/application/third_party/hybridauth/Data/Parser.php new file mode 100644 index 00000000..4259780b --- /dev/null +++ b/www/application/third_party/hybridauth/Data/Parser.php @@ -0,0 +1,119 @@ +parseJson($raw); + + if (!$data) { + $data = $this->parseXml($raw); + + if (!$data) { + $data = $this->parseQueryString($raw); + } + } + + return $data; + } + + /** + * Decodes a JSON string + * + * @param $result + * + * @return mixed + */ + public function parseJson($result) + { + return json_decode($result); + } + + /** + * Decodes a XML string + * + * @param $result + * + * @return mixed + */ + public function parseXml($result) + { + libxml_use_internal_errors(true); + + $result = preg_replace('/([<\/])([a-z0-9-]+):/i', '$1', $result); + $xml = simplexml_load_string($result); + + libxml_use_internal_errors(false); + + if (!$xml) { + return []; + } + + $arr = json_decode(json_encode((array)$xml), true); + $arr = array($xml->getName() => $arr); + + return $arr; + } + + /** + * Parses a string into variables + * + * @param $result + * + * @return \StdClass + */ + public function parseQueryString($result) + { + parse_str($result, $output); + + if (!is_array($output)) { + return $result; + } + + $result = new \StdClass(); + + foreach ($output as $k => $v) { + $result->$k = $v; + } + + return $result; + } + + /** + * needs to be improved + * + * @param $birthday + * @param $seperator + * + * @return array + */ + public function parseBirthday($birthday, $seperator) + { + $birthday = date_parse($birthday); + + return [$birthday['year'], $birthday['month'], $birthday['day']]; + } +} diff --git a/www/application/third_party/hybridauth/Exception/AuthorizationDeniedException.php b/www/application/third_party/hybridauth/Exception/AuthorizationDeniedException.php new file mode 100644 index 00000000..16262f1e --- /dev/null +++ b/www/application/third_party/hybridauth/Exception/AuthorizationDeniedException.php @@ -0,0 +1,15 @@ +getCode(); + $message = $this->getMessage(); + $file = $this->getFile(); + $line = $this->getLine(); + $trace = $this->getTraceAsString(); + + $html = sprintf('

%s

', $title); + $html .= '

Hybridauth has encountered the following error:

'; + $html .= '

Details

'; + + $html .= sprintf('
Exception: %s
', get_class($this)); + + $html .= sprintf('
Message: %s
', $message); + + $html .= sprintf('
File: %s
', $file); + + $html .= sprintf('
Line: %s
', $line); + + $html .= sprintf('
Code: %s
', $code); + + $html .= '

Trace

'; + $html .= sprintf('
%s
', $trace); + + if ($object) { + $html .= '

Debug

'; + + $obj_dump = print_r($object, true); + + // phpcs:ignore + $html .= sprintf('' . get_class($object) . ' extends ' . get_parent_class($object) . '
%s
', $obj_dump); + } + + $html .= '

Session

'; + + $session_dump = print_r($_SESSION, true); + + $html .= sprintf('
%s
', $session_dump); + + // phpcs:ignore + echo sprintf("%s%s", $title, $html); + } +} diff --git a/www/application/third_party/hybridauth/Exception/ExceptionInterface.php b/www/application/third_party/hybridauth/Exception/ExceptionInterface.php new file mode 100644 index 00000000..bd03c2b8 --- /dev/null +++ b/www/application/third_party/hybridauth/Exception/ExceptionInterface.php @@ -0,0 +1,36 @@ + 30, + CURLOPT_CONNECTTIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLINFO_HEADER_OUT => true, + CURLOPT_ENCODING => 'identity', + // phpcs:ignore + CURLOPT_USERAGENT => 'Hybridauth, PHP Social Authentication Library (https://github.com/hybridauth/hybridauth)', + ]; + + /** + * Method request() arguments + * + * This is used for debugging. + * + * @var array + */ + protected $requestArguments = []; + + /** + * Default request headers + * + * @var array + */ + protected $requestHeader = [ + 'Accept' => '*/*', + 'Cache-Control' => 'max-age=0', + 'Connection' => 'keep-alive', + 'Expect' => '', + 'Pragma' => '', + ]; + + /** + * Raw response returned by server + * + * @var string + */ + protected $responseBody = ''; + + /** + * Headers returned in the response + * + * @var array + */ + protected $responseHeader = []; + + /** + * Response HTTP status code + * + * @var int + */ + protected $responseHttpCode = 0; + + /** + * Last curl error number + * + * @var mixed + */ + protected $responseClientError = null; + + /** + * Information about the last transfer + * + * @var mixed + */ + protected $responseClientInfo = []; + + /** + * Hybridauth logger instance + * + * @var object + */ + protected $logger = null; + + /** + * {@inheritdoc} + */ + public function request($uri, $method = 'GET', $parameters = [], $headers = [], $multipart = false) + { + $this->requestHeader = array_replace($this->requestHeader, (array)$headers); + + $this->requestArguments = [ + 'uri' => $uri, + 'method' => $method, + 'parameters' => $parameters, + 'headers' => $this->requestHeader, + ]; + + $curl = curl_init(); + + switch ($method) { + case 'GET': + case 'DELETE': + unset($this->curlOptions[CURLOPT_POST]); + unset($this->curlOptions[CURLOPT_POSTFIELDS]); + + $uri = $uri . (strpos($uri, '?') ? '&' : '?') . http_build_query($parameters); + if ($method === 'DELETE') { + $this->curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + } + break; + case 'PUT': + case 'POST': + case 'PATCH': + $body_content = $multipart ? $parameters : http_build_query($parameters); + if (isset($this->requestHeader['Content-Type']) + && $this->requestHeader['Content-Type'] == 'application/json' + ) { + $body_content = json_encode($parameters); + } + + if ($method === 'POST') { + $this->curlOptions[CURLOPT_POST] = true; + } else { + $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $method; + } + $this->curlOptions[CURLOPT_POSTFIELDS] = $body_content; + break; + } + + $this->curlOptions[CURLOPT_URL] = $uri; + $this->curlOptions[CURLOPT_HTTPHEADER] = $this->prepareRequestHeaders(); + $this->curlOptions[CURLOPT_HEADERFUNCTION] = [$this, 'fetchResponseHeader']; + + foreach ($this->curlOptions as $opt => $value) { + curl_setopt($curl, $opt, $value); + } + + $response = curl_exec($curl); + + $this->responseBody = $response; + $this->responseHttpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $this->responseClientError = curl_error($curl); + $this->responseClientInfo = curl_getinfo($curl); + + if ($this->logger) { + // phpcs:ignore + $this->logger->debug(sprintf('%s::request( %s, %s ), response:', get_class($this), $uri, $method), $this->getResponse()); + + if (false === $response) { + // phpcs:ignore + $this->logger->error(sprintf('%s::request( %s, %s ), error:', get_class($this), $uri, $method), [$this->responseClientError]); + } + } + + curl_close($curl); + + return $this->responseBody; + } + + /** + * Get response details + * + * @return array Map structure of details + */ + public function getResponse() + { + $curlOptions = $this->curlOptions; + + $curlOptions[CURLOPT_HEADERFUNCTION] = '*omitted'; + + return [ + 'request' => $this->getRequestArguments(), + 'response' => [ + 'code' => $this->getResponseHttpCode(), + 'headers' => $this->getResponseHeader(), + 'body' => $this->getResponseBody(), + ], + 'client' => [ + 'error' => $this->getResponseClientError(), + 'info' => $this->getResponseClientInfo(), + 'opts' => $curlOptions, + ], + ]; + } + + /** + * Reset curl options + * + * @param array $curlOptions + */ + public function setCurlOptions($curlOptions) + { + foreach ($curlOptions as $opt => $value) { + $this->curlOptions[$opt] = $value; + } + } + + /** + * Set logger instance + * + * @param object $logger + */ + public function setLogger($logger) + { + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function getResponseBody() + { + return $this->responseBody; + } + + /** + * {@inheritdoc} + */ + public function getResponseHeader() + { + return $this->responseHeader; + } + + /** + * {@inheritdoc} + */ + public function getResponseHttpCode() + { + return $this->responseHttpCode; + } + + /** + * {@inheritdoc} + */ + public function getResponseClientError() + { + return $this->responseClientError; + } + + /** + * @return array + */ + protected function getResponseClientInfo() + { + return $this->responseClientInfo; + } + + /** + * Returns method request() arguments + * + * This is used for debugging. + * + * @return array + */ + protected function getRequestArguments() + { + return $this->requestArguments; + } + + /** + * Fetch server response headers + * + * @param mixed $curl + * @param string $header + * + * @return int + */ + protected function fetchResponseHeader($curl, $header) + { + $pos = strpos($header, ':'); + + if (!empty($pos)) { + $key = str_replace('-', '_', strtolower(substr($header, 0, $pos))); + + $value = trim(substr($header, $pos + 2)); + + $this->responseHeader[$key] = $value; + } + + return strlen($header); + } + + /** + * Convert request headers to the expect curl format + * + * @return array + */ + protected function prepareRequestHeaders() + { + $headers = []; + + foreach ($this->requestHeader as $header => $value) { + $headers[] = trim($header) . ': ' . trim($value); + } + + return $headers; + } +} diff --git a/www/application/third_party/hybridauth/HttpClient/Guzzle.php b/www/application/third_party/hybridauth/HttpClient/Guzzle.php new file mode 100644 index 00000000..5f0183ac --- /dev/null +++ b/www/application/third_party/hybridauth/HttpClient/Guzzle.php @@ -0,0 +1,276 @@ + + * $guzzle = new Hybridauth\HttpClient\Guzzle(new GuzzleHttp\Client(), [ + * 'verify' => '/path/to/your/certificate.crt', + * 'headers' => ['User-Agent' => '..'] + * // 'proxy' => ... + * ]); + * + * $adapter = new Hybridauth\Provider\Github($config, $guzzle); + * + * $adapter->authenticate(); + * + */ +class Guzzle implements HttpClientInterface +{ + /** + * Method request() arguments + * + * This is used for debugging. + * + * @var array + */ + protected $requestArguments = []; + + /** + * Default request headers + * + * @var array + */ + protected $requestHeader = []; + + /** + * Raw response returned by server + * + * @var string + */ + protected $responseBody = ''; + + /** + * Headers returned in the response + * + * @var array + */ + protected $responseHeader = []; + + /** + * Response HTTP status code + * + * @var int + */ + protected $responseHttpCode = 0; + + /** + * Last curl error number + * + * @var mixed + */ + protected $responseClientError = null; + + /** + * Information about the last transfer + * + * @var mixed + */ + protected $responseClientInfo = []; + + /** + * Hybridauth logger instance + * + * @var object + */ + protected $logger = null; + + /** + * GuzzleHttp client + * + * @var \GuzzleHttp\Client + */ + protected $client = null; + + /** + * .. + * @param null $client + * @param array $config + */ + public function __construct($client = null, $config = []) + { + $this->client = $client ? $client : new Client($config); + } + + /** + * {@inheritdoc} + */ + public function request($uri, $method = 'GET', $parameters = [], $headers = [], $multipart = false) + { + $this->requestHeader = array_replace($this->requestHeader, (array)$headers); + + $this->requestArguments = [ + 'uri' => $uri, + 'method' => $method, + 'parameters' => $parameters, + 'headers' => $this->requestHeader, + ]; + + $response = null; + + try { + switch ($method) { + case 'GET': + case 'DELETE': + $response = $this->client->request($method, $uri, [ + 'query' => $parameters, + 'headers' => $this->requestHeader, + ]); + break; + case 'PUT': + case 'PATCH': + case 'POST': + $body_type = $multipart ? 'multipart' : 'form_params'; + + if (isset($this->requestHeader['Content-Type']) + && $this->requestHeader['Content-Type'] === 'application/json' + ) { + $body_type = 'json'; + } + + $body_content = $parameters; + if ($multipart) { + $body_content = []; + foreach ($parameters as $key => $val) { + if ($val instanceof \CURLFile) { + $val = fopen($val->getFilename(), 'r'); + } + + $body_content[] = [ + 'name' => $key, + 'contents' => $val, + ]; + } + } + + $response = $this->client->request($method, $uri, [ + $body_type => $body_content, + 'headers' => $this->requestHeader, + ]); + break; + } + } catch (\Exception $e) { + $response = $e->getResponse(); + + $this->responseClientError = $e->getMessage(); + } + + if (!$this->responseClientError) { + $this->responseBody = $response->getBody(); + $this->responseHttpCode = $response->getStatusCode(); + $this->responseHeader = $response->getHeaders(); + } + + if ($this->logger) { + // phpcs:ignore + $this->logger->debug(sprintf('%s::request( %s, %s ), response:', get_class($this), $uri, $method), $this->getResponse()); + + if ($this->responseClientError) { + // phpcs:ignore + $this->logger->error(sprintf('%s::request( %s, %s ), error:', get_class($this), $uri, $method), [$this->responseClientError]); + } + } + + return $this->responseBody; + } + + /** + * Get response details + * + * @return array Map structure of details + */ + public function getResponse() + { + return [ + 'request' => $this->getRequestArguments(), + 'response' => [ + 'code' => $this->getResponseHttpCode(), + 'headers' => $this->getResponseHeader(), + 'body' => $this->getResponseBody(), + ], + 'client' => [ + 'error' => $this->getResponseClientError(), + 'info' => $this->getResponseClientInfo(), + 'opts' => null, + ], + ]; + } + + /** + * Set logger instance + * + * @param object $logger + */ + public function setLogger($logger) + { + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function getResponseBody() + { + return $this->responseBody; + } + + /** + * {@inheritdoc} + */ + public function getResponseHeader() + { + return $this->responseHeader; + } + + /** + * {@inheritdoc} + */ + public function getResponseHttpCode() + { + return $this->responseHttpCode; + } + + /** + * {@inheritdoc} + */ + public function getResponseClientError() + { + return $this->responseClientError; + } + + /** + * @return array + */ + protected function getResponseClientInfo() + { + return $this->responseClientInfo; + } + + /** + * Returns method request() arguments + * + * This is used for debugging. + * + * @return array + */ + protected function getRequestArguments() + { + return $this->requestArguments; + } +} diff --git a/www/application/third_party/hybridauth/HttpClient/HttpClientInterface.php b/www/application/third_party/hybridauth/HttpClient/HttpClientInterface.php new file mode 100644 index 00000000..46598d1f --- /dev/null +++ b/www/application/third_party/hybridauth/HttpClient/HttpClientInterface.php @@ -0,0 +1,58 @@ +get('HTTPS') && $collection->get('HTTPS') !== 'off') || + $collection->get('HTTP_X_FORWARDED_PROTO') === 'https') { + $protocol = 'https://'; + } + + return $protocol . + $collection->get('HTTP_HOST') . + $collection->get($requestUri ? 'REQUEST_URI' : 'PHP_SELF'); + } +} diff --git a/www/application/third_party/hybridauth/Hybridauth.php b/www/application/third_party/hybridauth/Hybridauth.php new file mode 100644 index 00000000..0dc96c11 --- /dev/null +++ b/www/application/third_party/hybridauth/Hybridauth.php @@ -0,0 +1,268 @@ +config = $config + [ + 'debug_mode' => Logger::NONE, + 'debug_file' => '', + 'curl_options' => null, + 'providers' => [] + ]; + $this->storage = $storage; + $this->logger = $logger; + $this->httpClient = $httpClient; + } + + /** + * Instantiate the given provider and authentication or authorization protocol. + * + * If not authenticated yet, the user will be redirected to the provider's site for + * authentication/authorisation, otherwise it will simply return an instance of + * provider's adapter. + * + * @param string $name adapter's name (case insensitive) + * + * @return \Hybridauth\Adapter\AdapterInterface + * @throws InvalidArgumentException + * @throws UnexpectedValueException + */ + public function authenticate($name) + { + $adapter = $this->getAdapter($name); + + $adapter->authenticate(); + + return $adapter; + } + + /** + * Returns a new instance of a provider's adapter by name + * + * @param string $name adapter's name (case insensitive) + * + * @return \Hybridauth\Adapter\AdapterInterface + * @throws InvalidArgumentException + * @throws UnexpectedValueException + */ + public function getAdapter($name) + { + $config = $this->getProviderConfig($name); + + $adapter = isset($config['adapter']) ? $config['adapter'] : sprintf('Hybridauth\\Provider\\%s', $name); + + if (!class_exists($adapter)) { + $adapter = null; + $fs = new \FilesystemIterator(__DIR__ . '/Provider/'); + /** @var \SplFileInfo $file */ + foreach ($fs as $file) { + if (!$file->isDir()) { + $provider = strtok($file->getFilename(), '.'); + if ($name === mb_strtolower($provider)) { + $adapter = sprintf('Hybridauth\\Provider\\%s', $provider); + break; + } + } + } + if ($adapter === null) { + throw new InvalidArgumentException('Unknown Provider.'); + } + } + + return new $adapter($config, $this->httpClient, $this->storage, $this->logger); + } + + /** + * Get provider config by name. + * + * @param string $name adapter's name (case insensitive) + * + * @throws UnexpectedValueException + * @throws InvalidArgumentException + * + * @return array + */ + public function getProviderConfig($name) + { + $name = strtolower($name); + + $providersConfig = array_change_key_case($this->config['providers'], CASE_LOWER); + + if (!isset($providersConfig[$name])) { + throw new InvalidArgumentException('Unknown Provider.'); + } + + if (!$providersConfig[$name]['enabled']) { + throw new UnexpectedValueException('Disabled Provider.'); + } + + $config = $providersConfig[$name]; + $config += [ + 'debug_mode' => $this->config['debug_mode'], + 'debug_file' => $this->config['debug_file'], + ]; + + if (!isset($config['callback']) && isset($this->config['callback'])) { + $config['callback'] = $this->config['callback']; + } + + return $config; + } + + /** + * Returns a boolean of whether the user is connected with a provider + * + * @param string $name adapter's name (case insensitive) + * + * @return bool + * @throws InvalidArgumentException + * @throws UnexpectedValueException + */ + public function isConnectedWith($name) + { + return $this->getAdapter($name)->isConnected(); + } + + /** + * Returns a list of enabled adapters names + * + * @return array + */ + public function getProviders() + { + $providers = []; + + foreach ($this->config['providers'] as $name => $config) { + if ($config['enabled']) { + $providers[] = $name; + } + } + + return $providers; + } + + /** + * Returns a list of currently connected adapters names + * + * @return array + * @throws InvalidArgumentException + * @throws UnexpectedValueException + */ + public function getConnectedProviders() + { + $providers = []; + + foreach ($this->getProviders() as $name) { + if ($this->isConnectedWith($name)) { + $providers[] = $name; + } + } + + return $providers; + } + + /** + * Returns a list of new instances of currently connected adapters + * + * @return \Hybridauth\Adapter\AdapterInterface[] + * @throws InvalidArgumentException + * @throws UnexpectedValueException + */ + public function getConnectedAdapters() + { + $adapters = []; + + foreach ($this->getProviders() as $name) { + $adapter = $this->getAdapter($name); + + if ($adapter->isConnected()) { + $adapters[$name] = $adapter; + } + } + + return $adapters; + } + + /** + * Disconnect all currently connected adapters at once + */ + public function disconnectAllAdapters() + { + foreach ($this->getProviders() as $name) { + $adapter = $this->getAdapter($name); + + if ($adapter->isConnected()) { + $adapter->disconnect(); + } + } + } +} diff --git a/www/application/third_party/hybridauth/Logger/Logger.php b/www/application/third_party/hybridauth/Logger/Logger.php new file mode 100644 index 00000000..92d5d2fb --- /dev/null +++ b/www/application/third_party/hybridauth/Logger/Logger.php @@ -0,0 +1,129 @@ +level !== Logger::NONE. + * + * @var string + */ + protected $file; + + /** + * @param bool|string $level One of Logger::NONE, Logger::DEBUG, Logger::INFO, Logger::ERROR + * @param string $file File where to write messages + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + public function __construct($level, $file) + { + $this->level = self::NONE; + + if ($level && $level !== self::NONE) { + $this->initialize($file); + + $this->level = $level === true ? Logger::DEBUG : $level; + $this->file = $file; + } + } + + /** + * @param string $file + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + protected function initialize($file) + { + if (!$file) { + throw new InvalidArgumentException('Log file is not specified.'); + } + + if (!file_exists($file) && !touch($file)) { + throw new RuntimeException(sprintf('Log file %s can not be created.', $file)); + } + + if (!is_writable($file)) { + throw new RuntimeException(sprintf('Log file %s is not writeable.', $file)); + } + } + + /** + * @inheritdoc + */ + public function info($message, array $context = []) + { + if (!in_array($this->level, [self::DEBUG, self::INFO])) { + return; + } + + $this->log(self::INFO, $message, $context); + } + + /** + * @inheritdoc + */ + public function debug($message, array $context = []) + { + if (!in_array($this->level, [self::DEBUG])) { + return; + } + + $this->log(self::DEBUG, $message, $context); + } + + /** + * @inheritdoc + */ + public function error($message, array $context = []) + { + if (!in_array($this->level, [self::DEBUG, self::INFO, self::ERROR])) { + return; + } + + $this->log(self::ERROR, $message, $context); + } + + /** + * @inheritdoc + */ + public function log($level, $message, array $context = []) + { + $datetime = new \DateTime(); + $datetime = $datetime->format(DATE_ATOM); + + $content = sprintf('%s -- %s -- %s -- %s', $level, $_SERVER['REMOTE_ADDR'], $datetime, $message); + $content .= ($context ? "\n" . print_r($context, true) : ''); + $content .= "\n"; + + file_put_contents($this->file, $content, FILE_APPEND); + } +} diff --git a/www/application/third_party/hybridauth/Logger/LoggerInterface.php b/www/application/third_party/hybridauth/Logger/LoggerInterface.php new file mode 100644 index 00000000..84059fa6 --- /dev/null +++ b/www/application/third_party/hybridauth/Logger/LoggerInterface.php @@ -0,0 +1,50 @@ +logger->info($message, $context); + } + + /** + * @inheritdoc + */ + public function debug($message, array $context = []) + { + $this->logger->debug($message, $context); + } + + /** + * @inheritdoc + */ + public function error($message, array $context = []) + { + $this->logger->error($message, $context); + } + + /** + * @inheritdoc + */ + public function log($level, $message, array $context = []) + { + $this->logger->log($level, $message, $context); + } +} diff --git a/www/application/third_party/hybridauth/Provider/AOLOpenID.php b/www/application/third_party/hybridauth/Provider/AOLOpenID.php new file mode 100644 index 00000000..2af1785e --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/AOLOpenID.php @@ -0,0 +1,26 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('user/profile'); + + $data = new Data\Collection($response); + + if (!$data->exists('user_id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('user_id'); + $userProfile->displayName = $data->get('name'); + $userProfile->email = $data->get('email'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Apple.php b/www/application/third_party/hybridauth/Provider/Apple.php new file mode 100644 index 00000000..33d44704 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Apple.php @@ -0,0 +1,296 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['id' => '', 'team_id' => '', 'key_id' => '', 'key_file' => '', 'key_content' => ''], + * 'scope' => 'name email', + * + * // Apple's custom auth url params + * 'authorize_url_parameters' => [ + * 'response_mode' => 'form_post' + * ] + * ]; + * + * $adapter = new Hybridauth\Provider\Apple($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * $tokens = $adapter->getAccessToken(); + * $response = $adapter->setUserStatus("Hybridauth test message.."); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + * + * Requires: + * + * composer require codercat/jwk-to-pem + * composer require firebase/php-jwt + * + * @see https://github.com/sputnik73/hybridauth-sign-in-with-apple + * @see https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api + */ +class Apple extends OAuth2 +{ + /** + * {@inheritdoc} + */ + protected $scope = 'name email'; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://appleid.apple.com/auth/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://appleid.apple.com/auth/authorize'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://appleid.apple.com/auth/token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://developer.apple.com/documentation/sign_in_with_apple'; + + /** + * {@inheritdoc} + * The Sign in with Apple servers require percent encoding (or URL encoding) + * for its query parameters. If you are using the Sign in with Apple REST API, + * you must provide values with encoded spaces (`%20`) instead of plus (`+`) signs. + */ + protected $AuthorizeUrlParametersEncType = PHP_QUERY_RFC3986; + + /** + * {@inheritdoc} + */ + protected function initialize() + { + parent::initialize(); + $this->AuthorizeUrlParameters['response_mode'] = 'form_post'; + + if ($this->isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->config->get('keys'); + $keys['secret'] = $this->getSecret(); + $this->config->set('keys', $keys); + return parent::configure(); + } + + /** + * {@inheritdoc} + * + * include id_token $tokenNames + */ + public function getAccessToken() + { + $tokenNames = [ + 'access_token', + 'id_token', + 'access_token_secret', + 'token_type', + 'refresh_token', + 'expires_in', + 'expires_at', + ]; + + $tokens = []; + + foreach ($tokenNames as $name) { + if ($this->getStoredData($name)) { + $tokens[$name] = $this->getStoredData($name); + } + } + + return $tokens; + } + + /** + * {@inheritdoc} + */ + protected function validateAccessTokenExchange($response) + { + $collection = parent::validateAccessTokenExchange($response); + + $this->storeData('id_token', $collection->get('id_token')); + + return $collection; + } + + public function getUserProfile() + { + $id_token = $this->getStoredData('id_token'); + + $verifyTokenSignature = + $this->config->exists('verifyTokenSignature') ? $this->config->get('verifyTokenSignature') : true; + + if (!$verifyTokenSignature) { + // payload extraction by https://github.com/omidborjian + // https://github.com/hybridauth/hybridauth/issues/1095#issuecomment-626479263 + // JWT splits the string to 3 components 1) first is header 2) is payload 3) is signature + $payload = explode('.', $id_token)[1]; + $payload = json_decode(base64_decode($payload)); + } else { + // validate the token signature and get the payload + $publicKeys = $this->apiRequest('keys'); + + \Firebase\JWT\JWT::$leeway = 120; + + $error = false; + $payload = null; + + foreach ($publicKeys->keys as $publicKey) { + try { + $rsa = new RSA(); + $jwk = (array)$publicKey; + + $rsa->loadKey( + [ + 'e' => new BigInteger(base64_decode($jwk['e']), 256), + 'n' => new BigInteger(base64_decode(strtr($jwk['n'], '-_', '+/'), true), 256) + ] + ); + $pem = $rsa->getPublicKey(); + + $payload = JWT::decode($id_token, $pem, ['RS256']); + break; + } catch (\Exception $e) { + $error = $e->getMessage(); + if ($e instanceof \Firebase\JWT\ExpiredException) { + break; + } + } + } + + if ($error && !$payload) { + throw new \Exception($error); + } + } + + $data = new Data\Collection($payload); + + if (!$data->exists('sub')) { + throw new UnexpectedValueException('Missing token payload.'); + } + + $userProfile = new User\Profile(); + $userProfile->identifier = $data->get('sub'); + $userProfile->email = $data->get('email'); + $this->storeData('expires_at', $data->get('exp')); + + if (!empty($_REQUEST['user'])) { + $objUser = json_decode($_REQUEST['user']); + $user = new Data\Collection($objUser); + if (!$user->isEmpty()) { + $name = $user->get('name'); + $userProfile->firstName = $name->firstName; + $userProfile->lastName = $name->lastName; + $userProfile->displayName = join(' ', [$userProfile->firstName, $userProfile->lastName]); + } + } + + return $userProfile; + } + + /** + * @return string secret token + */ + private function getSecret() + { + // Your 10-character Team ID + if (!$team_id = $this->config->filter('keys')->get('team_id')) { + throw new InvalidApplicationCredentialsException( + 'Missing parameter team_id: your team id is required to generate the JWS token.' + ); + } + + // Your Services ID, e.g. com.aaronparecki.services + if (!$client_id = $this->config->filter('keys')->get('id') ?: $this->config->filter('keys')->get('key')) { + throw new InvalidApplicationCredentialsException( + 'Missing parameter id: your client id is required to generate the JWS token.' + ); + } + + // Find the 10-char Key ID value from the portal + if (!$key_id = $this->config->filter('keys')->get('key_id')) { + throw new InvalidApplicationCredentialsException( + 'Missing parameter key_id: your key id is required to generate the JWS token.' + ); + } + + // Find the 10-char Key ID value from the portal + $key_content = $this->config->filter('keys')->get('key_content'); + + // Save your private key from Apple in a file called `key.txt` + if (!$key_content) { + if (!$key_file = $this->config->filter('keys')->get('key_file')) { + throw new InvalidApplicationCredentialsException( + 'Missing parameter key_content or key_file: your key is required to generate the JWS token.' + ); + } + + if (!file_exists($key_file)) { + throw new InvalidApplicationCredentialsException( + "Your key file $key_file does not exist." + ); + } + + $key_content = file_get_contents($key_file); + } + + $data = [ + 'iat' => time(), + 'exp' => time() + 86400 * 180, + 'iss' => $team_id, + 'aud' => 'https://appleid.apple.com', + 'sub' => $client_id + ]; + + $secret = JWT::encode($data, $key_content, 'ES256', $key_id); + + return $secret; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Authentiq.php b/www/application/third_party/hybridauth/Provider/Authentiq.php new file mode 100644 index 00000000..68aeeb01 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Authentiq.php @@ -0,0 +1,111 @@ +AuthorizeUrlParameters += [ + 'prompt' => 'consent' + ]; + + $this->tokenExchangeHeaders = [ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret) + ]; + + $this->tokenRefreshHeaders = [ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret) + ]; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('userinfo'); + + $data = new Data\Collection($response); + + if (!$data->exists('sub')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('sub'); + + $userProfile->displayName = $data->get('name'); + $userProfile->firstName = $data->get('given_name'); + // $userProfile->middleName = $data->get('middle_name'); // not supported + $userProfile->lastName = $data->get('family_name'); + + if (!empty($userProfile->displayName)) { + $userProfile->displayName = join(' ', array($userProfile->firstName, + // $userProfile->middleName, + $userProfile->lastName)); + } + + $userProfile->email = $data->get('email'); + $userProfile->emailVerified = $data->get('email_verified') ? $userProfile->email : ''; + + $userProfile->phone = $data->get('phone'); + // $userProfile->phoneVerified = $data->get('phone_verified') ? $userProfile->phone : ''; // not supported + + $userProfile->profileURL = $data->get('profile'); + $userProfile->webSiteURL = $data->get('website'); + $userProfile->photoURL = $data->get('picture'); + $userProfile->gender = $data->get('gender'); + $userProfile->address = $data->filter('address')->get('street_address'); + $userProfile->city = $data->filter('address')->get('locality'); + $userProfile->country = $data->filter('address')->get('country'); + $userProfile->region = $data->filter('address')->get('region'); + $userProfile->zip = $data->filter('address')->get('postal_code'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/AutoDesk.php b/www/application/third_party/hybridauth/Provider/AutoDesk.php new file mode 100644 index 00000000..949a56b8 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/AutoDesk.php @@ -0,0 +1,96 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'refresh_token', + ]; + } + } + + /** + * {@inheritdoc} + * + * See: https://forge.autodesk.com/en/docs/oauth/v2/reference/http/users-@me-GET/ + */ + public function getUserProfile() + { + $response = $this->apiRequest('userprofile/v1/users/@me'); + + $collection = new Data\Collection($response); + + $userProfile = new User\Profile(); + + $userProfile->identifier = $collection->get('userId'); + $userProfile->displayName + = $collection->get('firstName') .' '. $collection->get('lastName'); + $userProfile->firstName = $collection->get('firstName'); + $userProfile->lastName = $collection->get('lastName'); + $userProfile->email = $collection->get('emailId'); + $userProfile->language = $collection->get('language'); + $userProfile->webSiteURL = $collection->get('websiteUrl'); + $userProfile->photoURL + = $collection->filter('profileImages')->get('sizeX360'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/BitBucket.php b/www/application/third_party/hybridauth/Provider/BitBucket.php new file mode 100644 index 00000000..7fbe094c --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/BitBucket.php @@ -0,0 +1,111 @@ +/workspace/settings/api + */ + +/** + * BitBucket OAuth2 provider adapter. + */ +class BitBucket extends OAuth2 +{ + /** + * {@inheritdoc} + */ + protected $scope = 'email'; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://api.bitbucket.org/2.0/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://bitbucket.org/site/oauth2/authorize'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://bitbucket.org/site/oauth2/access_token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://developer.atlassian.com/bitbucket/concepts/oauth2.html'; + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('user'); + + $data = new Data\Collection($response); + + if (!$data->exists('uuid')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('uuid'); + $userProfile->profileURL = 'https://bitbucket.org/' . $data->get('username') . '/'; + $userProfile->displayName = $data->get('display_name'); + $userProfile->email = $data->get('email'); + $userProfile->webSiteURL = $data->get('website'); + $userProfile->region = $data->get('location'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('username'); + + if (empty($userProfile->email) && strpos($this->scope, 'email') !== false) { + try { + // user email is not mandatory so keep it quiet + $userProfile = $this->requestUserEmail($userProfile); + } catch (\Exception $e) { + } + } + + return $userProfile; + } + + /** + * Request user email + * + * @param $userProfile + * + * @return User\Profile + * + * @throws \Exception + */ + protected function requestUserEmail($userProfile) + { + $response = $this->apiRequest('user/emails'); + + foreach ($response->values as $idx => $item) { + if (!empty($item->is_primary) && $item->is_primary == true) { + $userProfile->email = $item->email; + + if (!empty($item->is_confirmed) && $item->is_confirmed == true) { + $userProfile->emailVerified = $userProfile->email; + } + + break; + } + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Blizzard.php b/www/application/third_party/hybridauth/Provider/Blizzard.php new file mode 100644 index 00000000..7f57d95e --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Blizzard.php @@ -0,0 +1,65 @@ +apiRequest('oauth/userinfo'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('battletag') ?: $data->get('login'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/BlizzardAPAC.php b/www/application/third_party/hybridauth/Provider/BlizzardAPAC.php new file mode 100644 index 00000000..826b2c1e --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/BlizzardAPAC.php @@ -0,0 +1,34 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + } + + /** + * {@inheritdoc} + * + * See: https://www.deviantart.com/developers/http/v1/20200519/user_whoami/2413749853e66c5812c9beccc0ab3495 + */ + public function getUserProfile() + { + $response = $this->apiRequest('user/whoami'); + + $data = new Data\Collection($response); + + $userProfile = new User\Profile(); + + $full_name = explode(' ', $data->filter('profile')->get('real_name')); + if (count($full_name) < 2) { + $full_name[1] = ''; + } + + $userProfile->identifier = $data->get('userid'); + $userProfile->displayName = $data->get('username'); + $userProfile->profileURL = $data->get('usericon'); + $userProfile->webSiteURL = $data->filter('profile')->get('website'); + $userProfile->firstName = $full_name[0]; + $userProfile->lastName = $full_name[1]; + $userProfile->profileURL = $data->filter('profile')->filter('profile_pic')->get('url'); + $userProfile->gender = $data->filter('details')->get('sex'); + $userProfile->age = $data->filter('details')->get('age'); + $userProfile->country = $data->filter('geo')->get('country'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Discord.php b/www/application/third_party/hybridauth/Provider/Discord.php new file mode 100644 index 00000000..d7ca2245 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Discord.php @@ -0,0 +1,96 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('users/@me'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + // Makes display name more unique. + $displayName = $data->get('username') ?: $data->get('login'); + if ($discriminator = $data->get('discriminator')) { + $displayName .= "#{$discriminator}"; + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $displayName; + $userProfile->email = $data->get('email'); + + if ($data->get('verified')) { + $userProfile->emailVerified = $data->get('email'); + } + + if ($data->get('avatar')) { + $userProfile->photoURL = 'https://cdn.discordapp.com/avatars/'; + $userProfile->photoURL .= $data->get('id') . '/' . $data->get('avatar') . '.png'; + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Disqus.php b/www/application/third_party/hybridauth/Provider/Disqus.php new file mode 100644 index 00000000..5fa7ac03 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Disqus.php @@ -0,0 +1,88 @@ +apiRequestParameters = [ + 'api_key' => $this->clientId, 'api_secret' => $this->clientSecret + ]; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('users/details'); + + $data = new Data\Collection($response); + + if (!$data->filter('response')->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $data = $data->filter('response'); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('name'); + $userProfile->description = $data->get('bio'); + $userProfile->profileURL = $data->get('profileUrl'); + $userProfile->email = $data->get('email'); + $userProfile->region = $data->get('location'); + $userProfile->description = $data->get('about'); + + $userProfile->photoURL = $data->filter('avatar')->get('permalink'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('username'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Dribbble.php b/www/application/third_party/hybridauth/Provider/Dribbble.php new file mode 100644 index 00000000..4b525184 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Dribbble.php @@ -0,0 +1,68 @@ +apiRequest('user'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->profileURL = $data->get('html_url'); + $userProfile->photoURL = $data->get('avatar_url'); + $userProfile->description = $data->get('bio'); + $userProfile->region = $data->get('location'); + $userProfile->displayName = $data->get('name'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('username'); + + $userProfile->webSiteURL = $data->filter('links')->get('web'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Dropbox.php b/www/application/third_party/hybridauth/Provider/Dropbox.php new file mode 100644 index 00000000..a3833670 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Dropbox.php @@ -0,0 +1,74 @@ +apiRequest('users/get_current_account', 'POST', [], [], true); + + $data = new Data\Collection($response); + + if (!$data->exists('account_id') || !$data->get('account_id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('account_id'); + $userProfile->displayName = $data->filter('name')->get('display_name'); + $userProfile->firstName = $data->filter('name')->get('given_name'); + $userProfile->lastName = $data->filter('name')->get('surname'); + $userProfile->email = $data->get('email'); + $userProfile->photoURL = $data->get('profile_photo_url'); + $userProfile->language = $data->get('locale'); + $userProfile->country = $data->get('country'); + if ($data->get('email_verified')) { + $userProfile->emailVerified = $data->get('email'); + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Facebook.php b/www/application/third_party/hybridauth/Provider/Facebook.php new file mode 100644 index 00000000..584e5ad7 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Facebook.php @@ -0,0 +1,451 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['id' => '', 'secret' => ''], + * 'scope' => 'email, user_status, user_posts', + * 'exchange_by_expiry_days' => 45, // null for no token exchange + * ]; + * + * $adapter = new Hybridauth\Provider\Facebook($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * $tokens = $adapter->getAccessToken(); + * $response = $adapter->setUserStatus("Hybridauth test message.."); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + */ +class Facebook extends OAuth2 +{ + /** + * {@inheritdoc} + */ + protected $scope = 'email, public_profile'; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://graph.facebook.com/v8.0/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://www.facebook.com/dialog/oauth'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://graph.facebook.com/oauth/access_token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://developers.facebook.com/docs/facebook-login/overview'; + + /** + * @var string Profile URL template as the fallback when no `link` returned from the API. + */ + protected $profileUrlTemplate = 'https://www.facebook.com/%s'; + + /** + * {@inheritdoc} + */ + protected function initialize() + { + parent::initialize(); + + // Require proof on all Facebook api calls + // https://developers.facebook.com/docs/graph-api/securing-requests#appsecret_proof + if ($accessToken = $this->getStoredData('access_token')) { + $this->apiRequestParameters['appsecret_proof'] = hash_hmac('sha256', $accessToken, $this->clientSecret); + } + } + + /** + * {@inheritdoc} + */ + public function maintainToken() + { + if (!$this->isConnected()) { + return; + } + + // Handle token exchange prior to the standard handler for an API request + $exchange_by_expiry_days = $this->config->get('exchange_by_expiry_days') ?: 45; + if ($exchange_by_expiry_days !== null) { + $projected_timestamp = time() + 60 * 60 * 24 * $exchange_by_expiry_days; + if (!$this->hasAccessTokenExpired() && $this->hasAccessTokenExpired($projected_timestamp)) { + $this->exchangeAccessToken(); + } + } + } + + /** + * Exchange the Access Token with one that expires further in the future. + * + * @return string Raw Provider API response + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws \Hybridauth\Exception\InvalidAccessTokenException + */ + public function exchangeAccessToken() + { + $exchangeTokenParameters = [ + 'grant_type' => 'fb_exchange_token', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'fb_exchange_token' => $this->getStoredData('access_token'), + ]; + + $response = $this->httpClient->request( + $this->accessTokenUrl, + 'GET', + $exchangeTokenParameters + ); + + $this->validateApiResponse('Unable to exchange the access token'); + + $this->validateAccessTokenExchange($response); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $fields = [ + 'id', + 'name', + 'first_name', + 'last_name', + 'website', + 'locale', + 'about', + 'email', + 'hometown', + 'birthday', + ]; + + if (strpos($this->scope, 'user_link') !== false) { + $fields[] = 'link'; + } + + if (strpos($this->scope, 'user_gender') !== false) { + $fields[] = 'gender'; + } + + // Note that en_US is needed for gender fields to match convention. + $locale = $this->config->get('locale') ?: 'en_US'; + $response = $this->apiRequest('me', 'GET', [ + 'fields' => implode(',', $fields), + 'locale' => $locale, + ]); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('name'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->profileURL = $data->get('link'); + $userProfile->webSiteURL = $data->get('website'); + $userProfile->gender = $data->get('gender'); + $userProfile->language = $data->get('locale'); + $userProfile->description = $data->get('about'); + $userProfile->email = $data->get('email'); + + // Fallback for profile URL in case Facebook does not provide "pretty" link with username (if user set it). + if (empty($userProfile->profileURL)) { + $userProfile->profileURL = $this->getProfileUrl($userProfile->identifier); + } + + $userProfile->region = $data->filter('hometown')->get('name'); + + $photoSize = $this->config->get('photo_size') ?: '150'; + + $userProfile->photoURL = $this->apiBaseUrl . $userProfile->identifier; + $userProfile->photoURL .= '/picture?width=' . $photoSize . '&height=' . $photoSize; + + $userProfile->emailVerified = $userProfile->email; + + $userProfile = $this->fetchUserRegion($userProfile); + + $userProfile = $this->fetchBirthday($userProfile, $data->get('birthday')); + + return $userProfile; + } + + /** + * Retrieve the user region. + * + * @param User\Profile $userProfile + * + * @return \Hybridauth\User\Profile + */ + protected function fetchUserRegion(User\Profile $userProfile) + { + if (!empty($userProfile->region)) { + $regionArr = explode(',', $userProfile->region); + + if (count($regionArr) > 1) { + $userProfile->city = trim($regionArr[0]); + $userProfile->country = trim($regionArr[1]); + } + } + + return $userProfile; + } + + /** + * Retrieve the user birthday. + * + * @param User\Profile $userProfile + * @param string $birthday + * + * @return \Hybridauth\User\Profile + */ + protected function fetchBirthday(User\Profile $userProfile, $birthday) + { + $result = (new Data\Parser())->parseBirthday($birthday, '/'); + + $userProfile->birthYear = (int)$result[0]; + $userProfile->birthMonth = (int)$result[1]; + $userProfile->birthDay = (int)$result[2]; + + return $userProfile; + } + + /** + * /v2.0/me/friends only returns the user's friends who also use the app. + * In the cases where you want to let people tag their friends in stories published by your app, + * you can use the Taggable Friends API. + * + * https://developers.facebook.com/docs/apps/faq#unable_full_friend_list + */ + public function getUserContacts() + { + $contacts = []; + + $apiUrl = 'me/friends?fields=link,name'; + + do { + $response = $this->apiRequest($apiUrl); + + $data = new Data\Collection($response); + + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + if (!$data->filter('data')->isEmpty()) { + foreach ($data->filter('data')->toArray() as $item) { + $contacts[] = $this->fetchUserContact($item); + } + } + + if ($data->filter('paging')->exists('next')) { + $apiUrl = $data->filter('paging')->get('next'); + + $pagedList = true; + } else { + $pagedList = false; + } + } while ($pagedList); + + return $contacts; + } + + /** + * Parse the user contact. + * + * @param array $item + * + * @return \Hybridauth\User\Contact + */ + protected function fetchUserContact($item) + { + $userContact = new User\Contact(); + + $item = new Data\Collection($item); + + $userContact->identifier = $item->get('id'); + $userContact->displayName = $item->get('name'); + + $userContact->profileURL = $item->exists('link') + ?: $this->getProfileUrl($userContact->identifier); + + $userContact->photoURL = $this->apiBaseUrl . $userContact->identifier . '/picture?width=150&height=150'; + + return $userContact; + } + + /** + * {@inheritdoc} + */ + public function setPageStatus($status, $pageId) + { + $status = is_string($status) ? ['message' => $status] : $status; + + // Post on user wall. + if ($pageId === 'me') { + return $this->setUserStatus($status); + } + + // Retrieve writable user pages and filter by given one. + $pages = $this->getUserPages(true); + $pages = array_filter($pages, function ($page) use ($pageId) { + return $page->id == $pageId; + }); + + if (!$pages) { + throw new InvalidArgumentException('Could not find a page with given id.'); + } + + $page = reset($pages); + + // Use page access token instead of user access token. + $headers = [ + 'Authorization' => 'Bearer ' . $page->access_token, + ]; + + // Refresh proof for API call. + $parameters = $status + [ + 'appsecret_proof' => hash_hmac('sha256', $page->access_token, $this->clientSecret), + ]; + + $response = $this->apiRequest("{$pageId}/feed", 'POST', $parameters, $headers); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function getUserPages($writable = false) + { + $pages = $this->apiRequest('me/accounts'); + + if (!$writable) { + return $pages->data; + } + + // Filter user pages by CREATE_CONTENT permission. + return array_filter($pages->data, function ($page) { + return in_array('CREATE_CONTENT', $page->tasks); + }); + } + + /** + * {@inheritdoc} + */ + public function getUserActivity($stream = 'me') + { + $apiUrl = $stream == 'me' ? 'me/feed' : 'me/home'; + + $response = $this->apiRequest($apiUrl); + + $data = new Data\Collection($response); + + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $activities = []; + + foreach ($data->filter('data')->toArray() as $item) { + $activities[] = $this->fetchUserActivity($item); + } + + return $activities; + } + + /** + * @param $item + * + * @return User\Activity + */ + protected function fetchUserActivity($item) + { + $userActivity = new User\Activity(); + + $item = new Data\Collection($item); + + $userActivity->id = $item->get('id'); + $userActivity->date = $item->get('created_time'); + + if ('video' == $item->get('type') || 'link' == $item->get('type')) { + $userActivity->text = $item->get('link'); + } + + if (empty($userActivity->text) && $item->exists('story')) { + $userActivity->text = $item->get('link'); + } + + if (empty($userActivity->text) && $item->exists('message')) { + $userActivity->text = $item->get('message'); + } + + if (!empty($userActivity->text) && $item->exists('from')) { + $userActivity->user->identifier = $item->filter('from')->get('id'); + $userActivity->user->displayName = $item->filter('from')->get('name'); + + $userActivity->user->profileURL = $this->getProfileUrl($userActivity->user->identifier); + + $userActivity->user->photoURL = $this->apiBaseUrl . $userActivity->user->identifier; + $userActivity->user->photoURL .= '/picture?width=150&height=150'; + } + + return $userActivity; + } + + /** + * Get profile URL. + * + * @param int $identity User ID. + * @return string|null NULL when identity is not provided. + */ + protected function getProfileUrl($identity) + { + if (!is_numeric($identity)) { + return null; + } + + return sprintf($this->profileUrlTemplate, $identity); + } +} diff --git a/www/application/third_party/hybridauth/Provider/Foursquare.php b/www/application/third_party/hybridauth/Provider/Foursquare.php new file mode 100644 index 00000000..67556fef --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Foursquare.php @@ -0,0 +1,140 @@ +config->get('api_version') ?: '20140201'; + + $this->apiRequestParameters = [ + 'oauth_token' => $this->getStoredData('access_token'), + 'v' => $apiVersion, + ]; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('users/self'); + + $data = new Data\Collection($response); + + if (!$data->exists('response')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $data = $data->filter('response')->filter('user'); + + $userProfile->identifier = $data->get('id'); + $userProfile->firstName = $data->get('firstName'); + $userProfile->lastName = $data->get('lastName'); + $userProfile->gender = $data->get('gender'); + $userProfile->city = $data->get('homeCity'); + $userProfile->email = $data->filter('contact')->get('email'); + $userProfile->emailVerified = $userProfile->email; + $userProfile->profileURL = 'https://www.foursquare.com/user/' . $userProfile->identifier; + $userProfile->displayName = trim($userProfile->firstName . ' ' . $userProfile->lastName); + + if ($data->exists('photo')) { + $photoSize = $this->config->get('photo_size') ?: '150x150'; + + $userProfile->photoURL = $data->filter('photo')->get('prefix'); + $userProfile->photoURL .= $photoSize . $data->filter('photo')->get('suffix'); + } + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function getUserContacts() + { + $response = $this->apiRequest('users/self/friends'); + + $data = new Data\Collection($response); + + if (!$data->exists('response')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $contacts = []; + + foreach ($data->filter('response')->filter('friends')->filter('items')->toArray() as $item) { + $contacts[] = $this->fetchUserContact($item); + } + + return $contacts; + } + + /** + * @param $item + * + * @return User\Contact + */ + protected function fetchUserContact($item) + { + $photoSize = $this->config->get('photo_size') ?: '150x150'; + + $item = new Data\Collection($item); + + $userContact = new User\Contact(); + + $userContact->identifier = $item->get('id'); + $userContact->photoURL = $item->filter('photo')->get('prefix'); + $userContact->photoURL .= $photoSize . $item->filter('photo')->get('suffix'); + $userContact->displayName = trim($item->get('firstName') . ' ' . $item->get('lastName')); + $userContact->email = $item->filter('contact')->get('email'); + + return $userContact; + } +} diff --git a/www/application/third_party/hybridauth/Provider/GitHub.php b/www/application/third_party/hybridauth/Provider/GitHub.php new file mode 100644 index 00000000..15b29f70 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/GitHub.php @@ -0,0 +1,110 @@ +apiRequest('user'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('name'); + $userProfile->description = $data->get('bio'); + $userProfile->photoURL = $data->get('avatar_url'); + $userProfile->profileURL = $data->get('html_url'); + $userProfile->email = $data->get('email'); + $userProfile->webSiteURL = $data->get('blog'); + $userProfile->region = $data->get('location'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('login'); + + if (empty($userProfile->email) && strpos($this->scope, 'user:email') !== false) { + try { + // user email is not mandatory so keep it quite. + $userProfile = $this->requestUserEmail($userProfile); + } catch (\Exception $e) { + } + } + + return $userProfile; + } + + /** + * Request connected user email + * + * https://developer.github.com/v3/users/emails/ + * @param User\Profile $userProfile + * + * @return User\Profile + * + * @throws \Exception + */ + protected function requestUserEmail(User\Profile $userProfile) + { + $response = $this->apiRequest('user/emails'); + + foreach ($response as $idx => $item) { + if (!empty($item->primary) && $item->primary == 1) { + $userProfile->email = $item->email; + + if (!empty($item->verified) && $item->verified == 1) { + $userProfile->emailVerified = $userProfile->email; + } + + break; + } + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/GitLab.php b/www/application/third_party/hybridauth/Provider/GitLab.php new file mode 100644 index 00000000..bf98f50f --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/GitLab.php @@ -0,0 +1,72 @@ +apiRequest('user'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('name'); + $userProfile->description = $data->get('bio'); + $userProfile->photoURL = $data->get('avatar_url'); + $userProfile->profileURL = $data->get('web_url'); + $userProfile->email = $data->get('email'); + $userProfile->webSiteURL = $data->get('website_url'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('username'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Google.php b/www/application/third_party/hybridauth/Provider/Google.php new file mode 100644 index 00000000..0defe99c --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Google.php @@ -0,0 +1,199 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['id' => '', 'secret' => ''], + * 'scope' => 'https://www.googleapis.com/auth/userinfo.profile', + * + * // google's custom auth url params + * 'authorize_url_parameters' => [ + * 'approval_prompt' => 'force', // to pass only when you need to acquire a new refresh token. + * 'access_type' => .., // is set to 'offline' by default + * 'hd' => .., + * 'state' => .., + * // etc. + * ] + * ]; + * + * $adapter = new Hybridauth\Provider\Google($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * $tokens = $adapter->getAccessToken(); + * $contacts = $adapter->getUserContacts(['max-results' => 75]); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + */ +class Google extends OAuth2 +{ + /** + * {@inheritdoc} + */ + // phpcs:ignore + protected $scope = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://www.googleapis.com/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://accounts.google.com/o/oauth2/v2/auth'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://oauth2.googleapis.com/token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://developers.google.com/identity/protocols/OAuth2'; + + /** + * {@inheritdoc} + */ + protected function initialize() + { + parent::initialize(); + + $this->AuthorizeUrlParameters += [ + 'access_type' => 'offline' + ]; + + if ($this->isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret + ]; + } + } + + /** + * {@inheritdoc} + * + * See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo + */ + public function getUserProfile() + { + $response = $this->apiRequest('oauth2/v3/userinfo'); + + $data = new Data\Collection($response); + + if (!$data->exists('sub')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('sub'); + $userProfile->firstName = $data->get('given_name'); + $userProfile->lastName = $data->get('family_name'); + $userProfile->displayName = $data->get('name'); + $userProfile->photoURL = $data->get('picture'); + $userProfile->profileURL = $data->get('profile'); + $userProfile->gender = $data->get('gender'); + $userProfile->language = $data->get('locale'); + $userProfile->email = $data->get('email'); + + $userProfile->emailVerified = $data->get('email_verified') ? $userProfile->email : ''; + + if ($this->config->get('photo_size')) { + $userProfile->photoURL .= '?sz=' . $this->config->get('photo_size'); + } + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function getUserContacts($parameters = []) + { + $parameters = ['max-results' => 500] + $parameters; + + // Google Gmail and Android contacts + if (false !== strpos($this->scope, '/m8/feeds/') || false !== strpos($this->scope, '/auth/contacts.readonly')) { + return $this->getGmailContacts($parameters); + } + + return []; + } + + /** + * Retrieve Gmail contacts + * + * @param array $parameters + * + * @return array + * + * @throws \Exception + */ + protected function getGmailContacts($parameters = []) + { + $url = 'https://www.google.com/m8/feeds/contacts/default/full?' + . http_build_query(array_replace(['alt' => 'json', 'v' => '3.0'], (array)$parameters)); + + $response = $this->apiRequest($url); + + if (!$response) { + return []; + } + + $contacts = []; + + if (isset($response->feed->entry)) { + foreach ($response->feed->entry as $idx => $entry) { + $uc = new User\Contact(); + + $uc->email = isset($entry->{'gd$email'}[0]->address) + ? (string)$entry->{'gd$email'}[0]->address + : ''; + + $uc->displayName = isset($entry->title->{'$t'}) ? (string)$entry->title->{'$t'} : ''; + $uc->identifier = ($uc->email != '') ? $uc->email : ''; + $uc->description = ''; + + if (property_exists($response, 'website')) { + if (is_array($response->website)) { + foreach ($response->website as $w) { + if ($w->primary == true) { + $uc->webSiteURL = $w->value; + } + } + } else { + $uc->webSiteURL = $response->website->value; + } + } else { + $uc->webSiteURL = ''; + } + + $contacts[] = $uc; + } + } + + return $contacts; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Instagram.php b/www/application/third_party/hybridauth/Provider/Instagram.php new file mode 100644 index 00000000..1f32e7bc --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Instagram.php @@ -0,0 +1,256 @@ +getStoredData($this->accessTokenName); + $this->apiRequestParameters[$this->accessTokenName] = $accessToken; + } + + /** + * {@inheritdoc} + */ + protected function validateAccessTokenExchange($response) + { + $collection = parent::validateAccessTokenExchange($response); + + if (!$collection->exists('expires_in')) { + // Instagram tokens always expire in an hour, but this is implicit not explicit + + $expires_in = 60 * 60; + + $expires_at = time() + $expires_in; + + $this->storeData('expires_in', $expires_in); + $this->storeData('expires_at', $expires_at); + } + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function maintainToken() + { + if (!$this->isConnected()) { + return; + } + + // Handle token exchange prior to the standard handler for an API request + $exchange_by_expiry_days = $this->config->get('exchange_by_expiry_days') ?: 45; + if ($exchange_by_expiry_days !== null) { + $projected_timestamp = time() + 60 * 60 * 24 * $exchange_by_expiry_days; + if (!$this->hasAccessTokenExpired() && $this->hasAccessTokenExpired($projected_timestamp)) { + $this->exchangeAccessToken(); + } + } + } + + /** + * Exchange the Access Token with one that expires further in the future. + * + * @return string Raw Provider API response + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws InvalidAccessTokenException + */ + public function exchangeAccessToken() + { + if ($this->getStoredData('expires_in') >= 5000000) { + /* + Refresh a long-lived token (needed on Instagram, but not Facebook). + It's not an oAuth style refresh using a refresh token. + Actually it's really just another exchange, and invalidates the old token. + Facebook/Instagram documentation is not very helpful at explaining that! + */ + $exchangeTokenParameters = [ + 'grant_type' => 'ig_refresh_token', + 'client_secret' => $this->clientSecret, + 'access_token' => $this->getStoredData('access_token'), + ]; + $url = 'https://graph.instagram.com/refresh_access_token'; + } else { + // Exchange short-lived to long-lived + $exchangeTokenParameters = [ + 'grant_type' => 'ig_exchange_token', + 'client_secret' => $this->clientSecret, + 'access_token' => $this->getStoredData('access_token'), + ]; + $url = 'https://graph.instagram.com/access_token'; + } + + $response = $this->httpClient->request( + $url, + 'GET', + $exchangeTokenParameters + ); + + $this->validateApiResponse('Unable to exchange the access token'); + + $this->validateAccessTokenExchange($response); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('me', 'GET', [ + 'fields' => 'id,username,account_type,media_count', + ]); + + $data = new Collection($response); + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('username'); + $userProfile->profileURL = "https://instagram.com/{$userProfile->displayName}"; + $userProfile->data = [ + 'account_type' => $data->get('account_type'), + 'media_count' => $data->get('media_count'), + ]; + + return $userProfile; + } + + /** + * Fetch user medias. + * + * @param int $limit Number of elements per page. + * @param string $pageId Current pager ID. + * @param array|null $fields Fields to fetch per media. + * + * @return \Hybridauth\Data\Collection + * + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws \Hybridauth\Exception\InvalidAccessTokenException + * @throws \Hybridauth\Exception\UnexpectedApiResponseException + */ + public function getUserMedia($limit = 12, $pageId = null, array $fields = null) + { + if (empty($fields)) { + $fields = [ + 'id', + 'caption', + 'media_type', + 'media_url', + 'thumbnail_url', + 'permalink', + 'timestamp', + 'username', + ]; + } + + $params = [ + 'fields' => implode(',', $fields), + 'limit' => $limit, + ]; + if ($pageId !== null) { + $params['after'] = $pageId; + } + + $response = $this->apiRequest('me/media', 'GET', $params); + + $data = new Collection($response); + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + return $data; + } + + /** + * Fetches a single user's media. + * + * @param string $mediaId Media ID. + * @param array|null $fields Fields to fetch per media. + * + * @return \Hybridauth\Data\Collection + * + * @throws \Hybridauth\Exception\HttpClientFailureException + * @throws \Hybridauth\Exception\HttpRequestFailedException + * @throws \Hybridauth\Exception\InvalidAccessTokenException + * @throws \Hybridauth\Exception\UnexpectedApiResponseException + */ + public function getMedia($mediaId, array $fields = null) + { + if (empty($fields)) { + $fields = [ + 'id', + 'caption', + 'media_type', + 'media_url', + 'thumbnail_url', + 'permalink', + 'timestamp', + 'username', + ]; + } + + $response = $this->apiRequest($mediaId, 'GET', [ + 'fields' => implode(',', $fields), + ]); + + $data = new Collection($response); + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + return $data; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Keycloak.php b/www/application/third_party/hybridauth/Provider/Keycloak.php new file mode 100644 index 00000000..25aa9d2b --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Keycloak.php @@ -0,0 +1,96 @@ + [ + * 'enabled' => true, + * 'url' => 'https://your-keycloak', // depending on your setup you might need to add '/auth' + * 'realm' => 'your-realm', + * 'keys' => [ + * 'id' => 'client-id', + * 'secret' => 'client-secret' + * ] + * ] + * + */ +class Keycloak extends OAuth2 +{ + + /** + * {@inheritdoc} + */ + public $scope = 'openid profile email'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://www.keycloak.org/docs/latest/securing_apps/#_oidc'; + + /** + * {@inheritdoc} + */ + protected function configure() + { + parent::configure(); + + if (!$this->config->exists('url')) { + throw new InvalidApplicationCredentialsException( + 'You must define a provider url' + ); + } + $url = $this->config->get('url'); + + if (!$this->config->exists('realm')) { + throw new InvalidApplicationCredentialsException( + 'You must define a realm' + ); + } + $realm = $this->config->get('realm'); + + $this->apiBaseUrl = $url . '/realms/' . $realm . '/protocol/openid-connect/'; + + $this->authorizeUrl = $this->apiBaseUrl . 'auth'; + $this->accessTokenUrl = $this->apiBaseUrl . 'token'; + + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('userinfo'); + + $data = new Data\Collection($response); + + if (!$data->exists('sub')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('sub'); + $userProfile->displayName = $data->get('preferred_username'); + $userProfile->email = $data->get('email'); + $userProfile->firstName = $data->get('given_name'); + $userProfile->lastName = $data->get('family_name'); + $userProfile->emailVerified = $data->get('email_verified'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/LinkedIn.php b/www/application/third_party/hybridauth/Provider/LinkedIn.php new file mode 100644 index 00000000..b7968553 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/LinkedIn.php @@ -0,0 +1,205 @@ +apiRequest('me', 'GET', ['projection' => '(' . implode(',', $fields) . ')']); + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + // Handle localized names. + $userProfile->firstName = $data + ->filter('firstName') + ->filter('localized') + ->get($this->getPreferredLocale($data, 'firstName')); + + $userProfile->lastName = $data + ->filter('lastName') + ->filter('localized') + ->get($this->getPreferredLocale($data, 'lastName')); + + $userProfile->identifier = $data->get('id'); + $userProfile->email = $this->getUserEmail(); + $userProfile->emailVerified = $userProfile->email; + $userProfile->displayName = trim($userProfile->firstName . ' ' . $userProfile->lastName); + + $photo_elements = $data + ->filter('profilePicture') + ->filter('displayImage~') + ->get('elements'); + $userProfile->photoURL = $this->getUserPhotoUrl($photo_elements); + + return $userProfile; + } + + /** + * Returns a user photo. + * + * @param array $elements + * List of file identifiers related to this artifact. + * + * @return string + * The user photo URL. + * + * @see https://docs.microsoft.com/en-us/linkedin/shared/references/v2/profile/profile-picture + */ + public function getUserPhotoUrl($elements) + { + if (is_array($elements)) { + // Get the largest picture from the list which is the last one. + $element = end($elements); + if (!empty($element->identifiers)) { + return reset($element->identifiers)->identifier; + } + } + + return null; + } + + /** + * Returns an email address of user. + * + * @return string + * The user email address. + * + * @throws \Exception + */ + public function getUserEmail() + { + $response = $this->apiRequest('emailAddress', 'GET', [ + 'q' => 'members', + 'projection' => '(elements*(handle~))', + ]); + $data = new Data\Collection($response); + + foreach ($data->filter('elements')->toArray() as $element) { + $item = new Data\Collection($element); + + if ($email = $item->filter('handle~')->get('emailAddress')) { + return $email; + } + } + + return null; + } + + /** + * {@inheritdoc} + * + * @see https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin + * @throws \Exception + */ + public function setUserStatus($status, $userID = null) + { + if (strpos($this->scope, 'w_member_social') === false) { + throw new \Exception('Set user status requires w_member_social permission!'); + } + + if (is_string($status)) { + $status = [ + 'author' => 'urn:li:person:' . $userID, + 'lifecycleState' => 'PUBLISHED', + 'specificContent' => [ + 'com.linkedin.ugc.ShareContent' => [ + 'shareCommentary' => [ + 'text' => $status, + ], + 'shareMediaCategory' => 'NONE', + ], + ], + 'visibility' => [ + 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC', + ], + ]; + } + + + $headers = [ + 'Content-Type' => 'application/json', + 'x-li-format' => 'json', + 'X-Restli-Protocol-Version' => '2.0.0', + ]; + + $response = $this->apiRequest("ugcPosts", 'POST', $status, $headers); + + return $response; + } + + /** + * Returns a preferred locale for given field. + * + * @param \Hybridauth\Data\Collection $data + * A data to check. + * @param string $field_name + * A field name to perform. + * + * @return string + * A field locale. + */ + protected function getPreferredLocale($data, $field_name) + { + $locale = $data->filter($field_name)->filter('preferredLocale'); + if ($locale) { + return $locale->get('language') . '_' . $locale->get('country'); + } + + return 'en_US'; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Mailru.php b/www/application/third_party/hybridauth/Provider/Mailru.php new file mode 100644 index 00000000..f2e4e53b --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Mailru.php @@ -0,0 +1,83 @@ + $this->clientId, + 'method' => 'users.getInfo', + 'secure' => 1, + 'session_key' => $this->getStoredData('access_token'), + ]; + $sign = md5(http_build_query($params, null, '') . $this->clientSecret); + + $param = [ + 'app_id' => $this->clientId, + 'method' => 'users.getInfo', + 'secure' => 1, + 'session_key' => $this->getStoredData('access_token'), + 'sig' => $sign, + ]; + + $response = $this->apiRequest('', 'GET', $param); + + $data = new Collection($response[0]); + + if (!$data->exists('uid')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new Profile(); + + $userProfile->identifier = $data->get('uid'); + $userProfile->email = $data->get('email'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->displayName = $data->get('nick'); + $userProfile->photoURL = $data->get('pic'); + $userProfile->profileURL = $data->get('link'); + $userProfile->gender = $data->get('sex'); + $userProfile->age = $data->get('age'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Medium.php b/www/application/third_party/hybridauth/Provider/Medium.php new file mode 100644 index 00000000..464709f7 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Medium.php @@ -0,0 +1,88 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + } + + /** + * {@inheritdoc} + * + * See: https://github.com/Medium/medium-api-docs#getting-the-authenticated-users-details + */ + public function getUserProfile() + { + $response = $this->apiRequest('me'); + + $data = new Data\Collection($response); + + $userProfile = new User\Profile(); + $data = $data->filter('data'); + + $full_name = explode(' ', $data->get('name')); + if (count($full_name) < 2) { + $full_name[1] = ''; + } + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('username'); + $userProfile->profileURL = $data->get('imageUrl'); + $userProfile->firstName = $full_name[0]; + $userProfile->lastName = $full_name[1]; + $userProfile->profileURL = $data->get('url'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/MicrosoftGraph.php b/www/application/third_party/hybridauth/Provider/MicrosoftGraph.php new file mode 100644 index 00000000..074fff0a --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/MicrosoftGraph.php @@ -0,0 +1,169 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['id' => '', 'secret' => ''], + * 'tenant' => 'user', + * // ^ May be 'common', 'organizations' or 'consumers' or a specific tenant ID or a domain + * ]; + * + * $adapter = new Hybridauth\Provider\MicrosoftGraph($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * $tokens = $adapter->getAccessToken(); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + */ +class MicrosoftGraph extends OAuth2 +{ + /** + * {@inheritdoc} + */ + protected $scope = 'openid user.read contacts.read'; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://graph.microsoft.com/v1.0/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://developer.microsoft.com/en-us/graph/docs/concepts/php'; + + /** + * {@inheritdoc} + */ + protected function initialize() + { + parent::initialize(); + + $tenant = $this->config->get('tenant'); + if (!empty($tenant)) { + $adjustedEndpoints = [ + 'authorize_url' => str_replace('/common/', '/' . $tenant . '/', $this->authorizeUrl), + 'access_token_url' => str_replace('/common/', '/' . $tenant . '/', $this->accessTokenUrl), + ]; + + $this->setApiEndpoints($adjustedEndpoints); + } + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('me'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('displayName'); + $userProfile->firstName = $data->get('givenName'); + $userProfile->lastName = $data->get('surname'); + $userProfile->language = $data->get('preferredLanguage'); + + $userProfile->phone = $data->get('mobilePhone'); + if (empty($userProfile->phone)) { + $businessPhones = $data->get('businessPhones'); + if (isset($businessPhones[0])) { + $userProfile->phone = $businessPhones[0]; + } + } + + $userProfile->email = $data->get('mail'); + if (empty($userProfile->email)) { + $email = $data->get('userPrincipalName'); + if (strpos($email, '@') !== false) { + $userProfile->email = $email; + } + } + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function getUserContacts() + { + $apiUrl = 'me/contacts?$top=50'; + $contacts = []; + + do { + $response = $this->apiRequest($apiUrl); + $data = new Data\Collection($response); + if (!$data->exists('value')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + foreach ($data->filter('value')->toArray() as $entry) { + $entry = new Data\Collection($entry); + $userContact = new User\Contact(); + $userContact->identifier = $entry->get('id'); + $userContact->displayName = $entry->get('displayName'); + $emailAddresses = $entry->get('emailAddresses'); + if (!empty($emailAddresses)) { + $userContact->email = $emailAddresses[0]->address; + } + // only add to collection if we have usefull data + if (!empty($userContact->displayName) || !empty($userContact->email)) { + $contacts[] = $userContact; + } + } + + if ($data->exists('@odata.nextLink')) { + $apiUrl = $data->get('@odata.nextLink'); + + $pagedList = true; + } else { + $pagedList = false; + } + } while ($pagedList); + + return $contacts; + } +} diff --git a/www/application/third_party/hybridauth/Provider/ORCID.php b/www/application/third_party/hybridauth/Provider/ORCID.php new file mode 100644 index 00000000..78878e68 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/ORCID.php @@ -0,0 +1,242 @@ +storeData('orcid', $data->get('orcid')); + return $data; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest($this->getStoredData('orcid') . '/record'); + $data = new Data\Collection($response['record']); + + if (!$data->exists('orcid-identifier')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $profile = new User\Profile(); + + $profile = $this->getDetails($profile, $data); + $profile = $this->getBiography($profile, $data); + $profile = $this->getWebsite($profile, $data); + $profile = $this->getName($profile, $data); + $profile = $this->getEmail($profile, $data); + $profile = $this->getLanguage($profile, $data); + $profile = $this->getAddress($profile, $data); + + return $profile; + } + + /** + * Get profile details. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getDetails(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('orcid-identifier')); + + $profile->identifier = $data->get('path'); + $profile->profileURL = $data->get('uri'); + + return $profile; + } + + /** + * Get profile biography. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getBiography(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('person')); + $data = new Data\Collection($data->get('biography')); + + $profile->description = $data->get('content'); + + return $profile; + } + + /** + * Get profile website. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getWebsite(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('person')); + $data = new Data\Collection($data->get('researcher-urls')); + $data = new Data\Collection($data->get('researcher-url')); + + if ($data->exists(0)) { + $data = new Data\Collection($data->get(0)); + } + + $profile->webSiteURL = $data->get('url'); + + return $profile; + } + + /** + * Get profile name. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getName(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('person')); + $data = new Data\Collection($data->get('name')); + + if ($data->exists('credit-name')) { + $profile->displayName = $data->get('credit-name'); + } else { + $profile->displayName = $data->get('given-names') . ' ' . $data->get('family-name'); + } + + $profile->firstName = $data->get('given-names'); + $profile->lastName = $data->get('family-name'); + + return $profile; + } + + /** + * Get profile email. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getEmail(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('person')); + $data = new Data\Collection($data->get('emails')); + $data = new Data\Collection($data->get('email')); + + if (!$data->exists(0)) { + $email = $data; + } else { + $email = new Data\Collection($data->get(0)); + + $i = 1; + while ($email->get('@attributes')['primary'] == 'false') { + $email = new Data\Collection($data->get($i)); + $i++; + } + } + + if ($email->get('@attributes')['primary'] == 'false') { + return $profile; + } + + $profile->email = $email->get('email'); + + if ($email->get('@attributes')['verified'] == 'true') { + $profile->emailVerified = $email->get('email'); + } + + return $profile; + } + + /** + * Get profile language. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getLanguage(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('preferences')); + + $profile->language = $data->get('locale'); + + return $profile; + } + + /** + * Get profile address. + * + * @param User\Profile $profile + * @param Data\Collection $data + * + * @return User\Profile + */ + protected function getAddress(User\Profile $profile, Data\Collection $data) + { + $data = new Data\Collection($data->get('person')); + $data = new Data\Collection($data->get('addresses')); + $data = new Data\Collection($data->get('address')); + + if ($data->exists(0)) { + $data = new Data\Collection($data->get(0)); + } + + $profile->country = $data->get('country'); + + return $profile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Odnoklassniki.php b/www/application/third_party/hybridauth/Provider/Odnoklassniki.php new file mode 100644 index 00000000..14df2983 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Odnoklassniki.php @@ -0,0 +1,110 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret + ]; + } + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $fields = array( + 'uid', 'locale', 'first_name', 'last_name', 'name', 'gender', 'age', 'birthday', + 'has_email', 'current_status', 'current_status_id', 'current_status_date', 'online', + 'photo_id', 'pic_1', 'pic_2', 'pic1024x768', 'location', 'email' + ); + + $sig = md5( + 'application_key=' . $this->config->get('keys')['key'] . + 'fields=' . implode(',', $fields) . + 'method=users.getCurrentUser' . + md5($this->getStoredData('access_token') . $this->config->get('keys')['secret']) + ); + + $parameters = [ + 'access_token' => $this->getStoredData('access_token'), + 'application_key' => $this->config->get('keys')['key'], + 'method' => 'users.getCurrentUser', + 'fields' => implode(',', $fields), + 'sig' => $sig, + ]; + + $response = $this->apiRequest('fb.do', 'GET', $parameters); + + $data = new Data\Collection($response); + + if (!$data->exists('uid')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + + $userProfile->identifier = $data->get('uid'); + $userProfile->email = $data->get('email'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->displayName = $data->get('name'); + $userProfile->photoURL = $data->get('pic1024x768'); + $userProfile->profileURL = 'http://ok.ru/profile/' . $data->get('uid'); + + // Handle birthday. + if ($data->get('birthday')) { + $bday = explode('-', $data->get('birthday')); + $userProfile->birthDay = (int)$bday[0]; + $userProfile->birthMonth = (int)$bday[1]; + $userProfile->birthYear = (int)$bday[2]; + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/OpenID.php b/www/application/third_party/hybridauth/Provider/OpenID.php new file mode 100644 index 00000000..d58a30d4 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/OpenID.php @@ -0,0 +1,44 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * + * // authenticate with Yahoo openid + * 'openid_identifier' => 'https://open.login.yahooapis.com/openid20/www.yahoo.com/xrds' + * + * // authenticate with stackexchange network openid + * // 'openid_identifier' => 'https://openid.stackexchange.com/', + * + * // authenticate with Steam openid + * // 'openid_identifier' => 'http://steamcommunity.com/openid', + * + * // etc. + * ]; + * + * $adapter = new Hybridauth\Provider\OpenID($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + */ +class OpenID extends Adapter\OpenID +{ +} diff --git a/www/application/third_party/hybridauth/Provider/Patreon.php b/www/application/third_party/hybridauth/Provider/Patreon.php new file mode 100644 index 00000000..83ff612f --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Patreon.php @@ -0,0 +1,194 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('oauth2/v2/identity', 'GET', [ + 'fields[user]' => 'created,first_name,last_name,email,full_name,is_email_verified,thumb_url,url', + ]); + + $collection = new Collection($response); + if (!$collection->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new Profile(); + + $data = $collection->filter('data'); + $attributes = $data->filter('attributes'); + + $userProfile->identifier = $data->get('id'); + $userProfile->email = $attributes->get('email'); + $userProfile->firstName = $attributes->get('first_name'); + $userProfile->lastName = $attributes->get('last_name'); + $userProfile->displayName = $attributes->get('full_name') ?: $data->get('id'); + $userProfile->photoURL = $attributes->get('thumb_url'); + $userProfile->profileURL = $attributes->get('url'); + + $userProfile->emailVerified = $attributes->get('is_email_verified') ? $userProfile->email : ''; + + return $userProfile; + } + + /** + * Contacts are defined as Patrons here + */ + public function getUserContacts() + { + $campaignId = $this->config->get('campaign_id') ?: null; + $tierFilter = $this->config->get('tier_filter') ?: null; + + $campaigns = []; + if ($campaignId === null) { + $campaignsUrl = 'oauth2/v2/campaigns'; + do { + $response = $this->apiRequest($campaignsUrl); + $data = new Collection($response); + + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + foreach ($data->filter('data')->toArray() as $item) { + $campaign = new Collection($item); + $campaigns[] = $campaign->get('id'); + } + + if ($data->filter('links')->exists('next')) { + $campaignsUrl = $data->filter('links')->get('next'); + + $pagedList = true; + } else { + $pagedList = false; + } + } while ($pagedList); + } else { + $campaigns[] = $campaignId; + } + + $contacts = []; + + foreach ($campaigns as $campaignId) { + $params = [ + 'include' => 'currently_entitled_tiers', + 'fields[member]' => 'full_name,patron_status,email', + 'fields[tier]' => 'title', + ]; + $membersUrl = 'oauth2/v2/campaigns/' . $campaignId . '/members?' . http_build_query($params); + + do { + $response = $this->apiRequest($membersUrl); + + $data = new Collection($response); + + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $tierTitles = []; + + foreach ($data->filter('included')->toArray() as $item) { + $includedItem = new Collection($item); + if ($includedItem->get('type') == 'tier') { + $tierTitles[$includedItem->get('id')] = $includedItem->filter('attributes')->get('title'); + } + } + + foreach ($data->filter('data')->toArray() as $item) { + $member = new Collection($item); + + if ($member->filter('attributes')->get('patron_status') == 'active_patron') { + $tiers = []; + $tierObs = $member->filter('relationships')->filter('currently_entitled_tiers')->get('data'); + foreach ($tierObs as $item) { + $tier = new Collection($item); + $tierId = $tier->get('id'); + $tiers[] = $tierTitles[$tierId]; + } + + if (($tierFilter === null) || (in_array($tierFilter, $tiers))) { + $userContact = new User\Contact(); + + $userContact->identifier = $member->get('id'); + $userContact->email = $member->filter('attributes')->get('email'); + $userContact->displayName = $member->filter('attributes')->get('full_name'); + $userContact->description = json_encode($tiers); + + $contacts[] = $userContact; + } + } + } + + if ($data->filter('links')->exists('next')) { + $membersUrl = $data->filter('links')->get('next'); + + $pagedList = true; + } else { + $pagedList = false; + } + } while ($pagedList); + } + + return $contacts; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Paypal.php b/www/application/third_party/hybridauth/Provider/Paypal.php new file mode 100644 index 00000000..be6ff423 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Paypal.php @@ -0,0 +1,113 @@ +AuthorizeUrlParameters += [ + 'flowEntry' => 'static' + ]; + + $this->tokenExchangeHeaders = [ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret) + ]; + + $this->tokenRefreshHeaders = [ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret) + ]; + } + + /** + * {@inheritdoc} + * + * See: https://developer.paypal.com/docs/api/identity/v1/ + * See: https://developer.paypal.com/docs/connect-with-paypal/integrate/ + */ + public function getUserProfile() + { + $headers = [ + 'Content-Type' => 'application/json', + ]; + + $parameters = [ + 'schema' => 'paypalv1.1' + ]; + + $response = $this->apiRequest('v1/identity/oauth2/userinfo', 'GET', $parameters, $headers); + $data = new Data\Collection($response); + + if (!$data->exists('user_id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + $userProfile->identifier = $data->get('user_id'); + $userProfile->firstName = $data->get('given_name'); + $userProfile->lastName = $data->get('family_name'); + $userProfile->displayName = $data->get('name'); + $userProfile->address = $data->filter('address')->get('street_address'); + $userProfile->city = $data->filter('address')->get('locality'); + $userProfile->country = $data->filter('address')->get('country'); + $userProfile->region = $data->filter('address')->get('region'); + $userProfile->zip = $data->filter('address')->get('postal_code'); + + $emails = $data->filter('emails')->toArray(); + foreach ($emails as $email) { + $email = new Data\Collection($email); + if ($email->get('confirmed')) { + $userProfile->emailVerified = $email->get('value'); + } + + if ($email->get('primary')) { + $userProfile->email = $email->get('value'); + } + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/PaypalOpenID.php b/www/application/third_party/hybridauth/Provider/PaypalOpenID.php new file mode 100644 index 00000000..9c455f82 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/PaypalOpenID.php @@ -0,0 +1,71 @@ +openIdClient->identity = $this->openidIdentifier; + $this->openIdClient->returnUrl = $this->callback; + $this->openIdClient->required = [ + 'namePerson/prefix', + 'namePerson/first', + 'namePerson/last', + 'namePerson/middle', + 'namePerson/suffix', + 'namePerson/friendly', + 'person/guid', + 'birthDate/birthYear', + 'birthDate/birthMonth', + 'birthDate/birthday', + 'gender', + 'language/pref', + 'contact/phone/default', + 'contact/phone/home', + 'contact/phone/business', + 'contact/phone/cell', + 'contact/phone/fax', + 'contact/postaladdress/home', + 'contact/postaladdressadditional/home', + 'contact/city/home', + 'contact/state/home', + 'contact/country/home', + 'contact/postalcode/home', + 'contact/postaladdress/business', + 'contact/postaladdressadditional/business', + 'contact/city/business', + 'contact/state/business', + 'contact/country/business', + 'contact/postalcode/business', + 'company/name', + 'company/title', + ]; + + HttpClient\Util::redirect($this->openIdClient->authUrl()); + } +} diff --git a/www/application/third_party/hybridauth/Provider/Pinterest.php b/www/application/third_party/hybridauth/Provider/Pinterest.php new file mode 100644 index 00000000..e6e7ee60 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Pinterest.php @@ -0,0 +1,74 @@ +apiRequest('me'); + + $data = new Data\Collection($response); + + $data = $data->filter('data'); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->description = $data->get('bio'); + $userProfile->photoURL = $data->get('image'); + $userProfile->displayName = $data->get('username'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->profileURL = "https://pinterest.com/{$data->get('username')}"; + + $userProfile->data = (array)$data->get('counts'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/QQ.php b/www/application/third_party/hybridauth/Provider/QQ.php new file mode 100644 index 00000000..88e1bbb6 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/QQ.php @@ -0,0 +1,138 @@ +isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]; + } + + $this->apiRequestParameters = [ + 'access_token' => $this->getStoredData('access_token') + ]; + + $this->apiRequestHeaders = []; + } + + /** + * {@inheritdoc} + */ + protected function validateAccessTokenExchange($response) + { + $collection = parent::validateAccessTokenExchange($response); + + $resp = $this->apiRequest($this->accessTokenInfoUrl); + $resp = key($resp); + + $len = strlen($resp); + $res = substr($resp, 10, $len - 14); + + $response = (new Data\Parser())->parse($res); + + if (!isset($response->openid)) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $this->storeData('openid', $response->openid); + + return $collection; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $openid = $this->getStoredData('openid'); + + $userRequestParameters = [ + 'oauth_consumer_key' => $this->clientId, + 'openid' => $openid, + 'format' => 'json' + ]; + + $response = $this->apiRequest($this->accessUserInfo, 'GET', $userRequestParameters); + + $data = new Data\Collection($response); + + if ($data->get('ret') < 0) { + throw new UnexpectedApiResponseException('Provider API returned an error: ' . $data->get('msg')); + } + + $userProfile = new Profile(); + + $userProfile->identifier = $openid; + $userProfile->displayName = $data->get('nickname'); + $userProfile->photoURL = $data->get('figureurl_2'); + $userProfile->gender = $data->get('gender'); + $userProfile->region = $data->get('province'); + $userProfile->city = $data->get('city'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Reddit.php b/www/application/third_party/hybridauth/Provider/Reddit.php new file mode 100644 index 00000000..fa48b0f4 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Reddit.php @@ -0,0 +1,91 @@ +AuthorizeUrlParameters += [ + 'duration' => 'permanent' + ]; + + $this->tokenExchangeParameters = [ + 'client_id' => $this->clientId, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->callback + ]; + + $this->tokenExchangeHeaders = [ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret) + ]; + + $this->tokenRefreshHeaders = $this->tokenExchangeHeaders; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('me.json'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('name'); + $userProfile->profileURL = 'https://www.reddit.com/user/' . $data->get('name') . '/'; + $userProfile->photoURL = $data->get('icon_img'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Slack.php b/www/application/third_party/hybridauth/Provider/Slack.php new file mode 100644 index 00000000..d424b8d7 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Slack.php @@ -0,0 +1,100 @@ +apiRequest('api/users.identity'); + + $data = new Data\Collection($response); + + if (!$data->exists('ok') || !$data->get('ok')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->filter('user')->get('id'); + $userProfile->displayName = $data->filter('user')->get('name'); + $userProfile->email = $data->filter('user')->get('email'); + $userProfile->photoURL = $this->findLargestImage($data); + + return $userProfile; + } + + /** + * Returns the url of the image with the highest resolution in the user + * object. + * + * Slack sends multiple image urls with different resolutions. As they make + * no guarantees which resolutions will be included we have to search all + * image_* properties for the one with the highest resolution. + * The resolution is attached to the property name such as + * image_32 or image_192. + * + * @param Data\Collection $data response object as returned by + * api/users.identity + * + * @return string|null the value of the image_* property with + * the highest resolution. + */ + private function findLargestImage(Data\Collection $data) + { + $maxSize = 0; + foreach ($data->filter('user')->properties() as $property) { + if (preg_match('/^image_(\d+)$/', $property, $matches) === 1) { + $availableSize = (int)$matches[1]; + if ($maxSize < $availableSize) { + $maxSize = $availableSize; + } + } + } + if ($maxSize > 0) { + return $data->filter('user')->get('image_' . $maxSize); + } + return null; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Spotify.php b/www/application/third_party/hybridauth/Provider/Spotify.php new file mode 100644 index 00000000..c995efe2 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Spotify.php @@ -0,0 +1,93 @@ +apiRequest('me'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('display_name'); + $userProfile->email = $data->get('email'); + $userProfile->emailVerified = $data->get('email'); + $userProfile->profileURL = $data->filter('external_urls')->get('spotify'); + $userProfile->photoURL = $data->filter('images')->get('url'); + $userProfile->country = $data->get('country'); + + if ($data->exists('birthdate')) { + $this->fetchBirthday($userProfile, $data->get('birthdate')); + } + + return $userProfile; + } + + /** + * Fetch use birthday + * + * @param User\Profile $userProfile + * @param $birthday + * + * @return User\Profile + */ + protected function fetchBirthday(User\Profile $userProfile, $birthday) + { + $result = (new Data\Parser())->parseBirthday($birthday, '-'); + + $userProfile->birthDay = (int)$result[0]; + $userProfile->birthMonth = (int)$result[1]; + $userProfile->birthYear = (int)$result[2]; + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/StackExchange.php b/www/application/third_party/hybridauth/Provider/StackExchange.php new file mode 100644 index 00000000..240b9f6d --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/StackExchange.php @@ -0,0 +1,106 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['id' => '', 'secret' => ''], + * 'site' => 'stackoverflow' // required parameter to call getUserProfile() + * 'api_key' => '...' // that thing to receive a higher request quota. + * ]; + * + * $adapter = new Hybridauth\Provider\StackExchange($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * $tokens = $adapter->getAccessToken(); + * } catch (\Exception $e ){ + * echo $e->getMessage() ; + * } + */ +class StackExchange extends OAuth2 +{ + /** + * {@inheritdoc} + */ + protected $scope = null; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://api.stackexchange.com/2.2/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://stackexchange.com/oauth'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://stackexchange.com/oauth/access_token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://api.stackexchange.com/docs/authentication'; + + /** + * {@inheritdoc} + */ + protected function initialize() + { + parent::initialize(); + + $apiKey = $this->config->get('api_key'); + + $this->apiRequestParameters = ['key' => $apiKey]; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $site = $this->config->get('site'); + + $response = $this->apiRequest('me', 'GET', [ + 'site' => $site, + 'access_token' => $this->getStoredData('access_token'), + ]); + + if (!$response || !isset($response->items) || !isset($response->items[0])) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $data = new Data\Collection($response->items[0]); + + $userProfile = new User\Profile(); + + $userProfile->identifier = strval($data->get('user_id')); + $userProfile->displayName = $data->get('display_name'); + $userProfile->photoURL = $data->get('profile_image'); + $userProfile->profileURL = $data->get('link'); + $userProfile->region = $data->get('location'); + $userProfile->age = $data->get('age'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/StackExchangeOpenID.php b/www/application/third_party/hybridauth/Provider/StackExchangeOpenID.php new file mode 100644 index 00000000..4e8d5763 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/StackExchangeOpenID.php @@ -0,0 +1,42 @@ +storage->get($this->providerId . '.user'); + + $userProfile->identifier = !empty($userProfile->identifier) ? $userProfile->identifier : $userProfile->email; + $userProfile->emailVerified = $userProfile->email; + + // re store the user profile + $this->storage->set($this->providerId . '.user', $userProfile); + } +} diff --git a/www/application/third_party/hybridauth/Provider/Steam.php b/www/application/third_party/hybridauth/Provider/Steam.php new file mode 100644 index 00000000..1288f0a7 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Steam.php @@ -0,0 +1,149 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['secret' => 'steam-api-key'] + * ]; + * + * $adapter = new Hybridauth\Provider\Steam($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + */ +class Steam extends OpenID +{ + /** + * {@inheritdoc} + */ + protected $openidIdentifier = 'http://steamcommunity.com/openid'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://steamcommunity.com/dev'; + + /** + * {@inheritdoc} + */ + public function authenticateFinish() + { + parent::authenticateFinish(); + + $userProfile = $this->storage->get($this->providerId . '.user'); + + $userProfile->identifier = str_ireplace([ + 'http://steamcommunity.com/openid/id/', + 'https://steamcommunity.com/openid/id/', + ], '', $userProfile->identifier); + + if (!$userProfile->identifier) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + try { + $apiKey = $this->config->filter('keys')->get('secret'); + + // if api key is provided, we attempt to use steam web api + if ($apiKey) { + $result = $this->getUserProfileWebAPI($apiKey, $userProfile->identifier); + } else { + // otherwise we fallback to community data + $result = $this->getUserProfileLegacyAPI($userProfile->identifier); + } + + // fetch user profile + foreach ($result as $k => $v) { + $userProfile->$k = $v ?: $userProfile->$k; + } + } catch (\Exception $e) { + } + + // store user profile + $this->storage->set($this->providerId . '.user', $userProfile); + } + + /** + * Fetch user profile on Steam web API + * + * @param $apiKey + * @param $steam64 + * + * @return array + */ + public function getUserProfileWebAPI($apiKey, $steam64) + { + $q = http_build_query(['key' => $apiKey, 'steamids' => $steam64]); + $apiUrl = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?' . $q; + + $response = $this->httpClient->request($apiUrl); + + $data = json_decode($response); + + $data = isset($data->response->players[0]) ? $data->response->players[0] : null; + + $data = new Data\Collection($data); + + $userProfile = []; + + $userProfile['displayName'] = (string)$data->get('personaname'); + $userProfile['firstName'] = (string)$data->get('realname'); + $userProfile['photoURL'] = (string)$data->get('avatarfull'); + $userProfile['profileURL'] = (string)$data->get('profileurl'); + $userProfile['country'] = (string)$data->get('loccountrycode'); + + return $userProfile; + } + + /** + * Fetch user profile on community API + * @param $steam64 + * @return array + */ + public function getUserProfileLegacyAPI($steam64) + { + libxml_use_internal_errors(false); + + $apiUrl = 'http://steamcommunity.com/profiles/' . $steam64 . '/?xml=1'; + + $response = $this->httpClient->request($apiUrl); + + $data = new \SimpleXMLElement($response); + + $data = new Data\Collection($data); + + $userProfile = []; + + $userProfile['displayName'] = (string)$data->get('steamID'); + $userProfile['firstName'] = (string)$data->get('realname'); + $userProfile['photoURL'] = (string)$data->get('avatarFull'); + $userProfile['description'] = (string)$data->get('summary'); + $userProfile['region'] = (string)$data->get('location'); + $userProfile['profileURL'] = (string)$data->get('customURL') + ? 'http://steamcommunity.com/id/' . (string)$data->get('customURL') + : 'http://steamcommunity.com/profiles/' . $steam64; + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/SteemConnect.php b/www/application/third_party/hybridauth/Provider/SteemConnect.php new file mode 100644 index 00000000..2bc0e5d1 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/SteemConnect.php @@ -0,0 +1,70 @@ +apiRequest('api/me'); + + $data = new Data\Collection($response); + + if (!$data->exists('result')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $data = $data->filter('result'); + + $userProfile->identifier = $data->get('id'); + $userProfile->description = $data->get('about'); + $userProfile->photoURL = $data->get('profile_image'); + $userProfile->webSiteURL = $data->get('website'); + $userProfile->displayName = $data->get('name'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Strava.php b/www/application/third_party/hybridauth/Provider/Strava.php new file mode 100644 index 00000000..f98cc3ab --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Strava.php @@ -0,0 +1,72 @@ +apiRequest('athlete'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->firstName = $data->get('firstname'); + $userProfile->lastName = $data->get('lastname'); + $userProfile->gender = $data->get('sex'); + $userProfile->country = $data->get('country'); + $userProfile->city = $data->get('city'); + $userProfile->email = $data->get('email'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('username'); + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Telegram.php b/www/application/third_party/hybridauth/Provider/Telegram.php new file mode 100644 index 00000000..059f34db --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Telegram.php @@ -0,0 +1,221 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['id' => 'your_bot_name', 'secret' => 'your_bot_token'], + * ]; + * + * $adapter = new Hybridauth\Provider\Telegram($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * } catch (\Exception $e) { + * print $e->getMessage(); + * } + */ +class Telegram extends AbstractAdapter implements AdapterInterface +{ + protected $botId = ''; + + protected $botSecret = ''; + + protected $callbackUrl = ''; + + /** + * IPD API Documentation + * + * OPTIONAL. + * + * @var string + */ + protected $apiDocumentation = 'https://core.telegram.org/bots'; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->botId = $this->config->filter('keys')->get('id'); + $this->botSecret = $this->config->filter('keys')->get('secret'); + $this->callbackUrl = $this->config->get('callback'); + + if (!$this->botId || !$this->botSecret) { + throw new InvalidApplicationCredentialsException( + 'Your application id is required in order to connect to ' . $this->providerId + ); + } + } + + /** + * {@inheritdoc} + */ + protected function initialize() + { + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $this->logger->info(sprintf('%s::authenticate()', get_class($this))); + if (!filter_input(INPUT_GET, 'hash')) { + $this->authenticateBegin(); + } else { + $this->authenticateCheckError(); + $this->authenticateFinish(); + } + return null; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + $authData = $this->getStoredData('auth_data'); + return !empty($authData); + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $data = new Collection($this->getStoredData('auth_data')); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->displayName = $data->get('username'); + $userProfile->photoURL = $data->get('photo_url'); + $username = $data->get('username'); + if (!empty($username)) { + // Only some accounts have usernames. + $userProfile->profileURL = "https://t.me/{$username}"; + } + + return $userProfile; + } + + /** + * See: https://telegram.im/widget-login.php + * See: https://gist.github.com/anonymous/6516521b1fb3b464534fbc30ea3573c2 + */ + protected function authenticateCheckError() + { + $auth_data = $this->parseAuthData(); + + $check_hash = $auth_data['hash']; + unset($auth_data['hash']); + $data_check_arr = []; + + foreach ($auth_data as $key => $value) { + if (!empty($value)) { + $data_check_arr[] = $key . '=' . $value; + } + } + sort($data_check_arr); + + $data_check_string = implode("\n", $data_check_arr); + $secret_key = hash('sha256', $this->botSecret, true); + $hash = hash_hmac('sha256', $data_check_string, $secret_key); + + if (strcmp($hash, $check_hash) !== 0) { + throw new InvalidAuthorizationCodeException( + sprintf('Provider returned an error: %s', 'Data is NOT from Telegram') + ); + } + + if ((time() - $auth_data['auth_date']) > 86400) { + throw new InvalidAuthorizationCodeException( + sprintf('Provider returned an error: %s', 'Data is outdated') + ); + } + } + + /** + * See: https://telegram.im/widget-login.php + */ + protected function authenticateBegin() + { + $this->logger->debug(sprintf('%s::authenticateBegin(), redirecting user to:', get_class($this))); + + $nonce = $this->config->get('nonce'); + $nonce_code = empty($nonce) ? '' : "nonce=\"{$nonce}\""; + + exit( + << + + +HTML + ); + } + + protected function authenticateFinish() + { + $this->logger->debug( + sprintf('%s::authenticateFinish(), callback url:', get_class($this)), + [Util::getCurrentUrl(true)] + ); + + $this->storeData('auth_data', $this->parseAuthData()); + + $this->initialize(); + } + + protected function parseAuthData() + { + return [ + 'id' => filter_input(INPUT_GET, 'id'), + 'first_name' => filter_input(INPUT_GET, 'first_name'), + 'last_name' => filter_input(INPUT_GET, 'last_name'), + 'username' => filter_input(INPUT_GET, 'username'), + 'photo_url' => filter_input(INPUT_GET, 'photo_url'), + 'auth_date' => filter_input(INPUT_GET, 'auth_date'), + 'hash' => filter_input(INPUT_GET, 'hash'), + ]; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Tumblr.php b/www/application/third_party/hybridauth/Provider/Tumblr.php new file mode 100644 index 00000000..9f899f15 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Tumblr.php @@ -0,0 +1,97 @@ +apiRequest('user/info'); + + $data = new Data\Collection($response); + + if (!$data->exists('response')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->displayName = $data->filter('response')->filter('user')->get('name'); + + foreach ($data->filter('response')->filter('user')->filter('blogs')->toArray() as $blog) { + $blog = new Data\Collection($blog); + + if ($blog->get('primary') && $blog->exists('url')) { + $userProfile->identifier = $blog->get('url'); + $userProfile->profileURL = $blog->get('url'); + $userProfile->webSiteURL = $blog->get('url'); + $userProfile->description = strip_tags($blog->get('description')); + + $bloghostname = explode('://', $blog->get('url')); + $bloghostname = substr($bloghostname[1], 0, -1); + + // store user's primary blog which will be used as target by setUserStatus + $this->storeData('primary_blog', $bloghostname); + + break; + } + } + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function setUserStatus($status) + { + $status = is_string($status) + ? ['type' => 'text', 'body' => $status] + : $status; + + $response = $this->apiRequest('blog/' . $this->getStoredData('primary_blog') . '/post', 'POST', $status); + + return $response; + } +} diff --git a/www/application/third_party/hybridauth/Provider/TwitchTV.php b/www/application/third_party/hybridauth/Provider/TwitchTV.php new file mode 100644 index 00000000..ae54cbf5 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/TwitchTV.php @@ -0,0 +1,82 @@ +apiRequestHeaders['Client-ID'] = $this->clientId; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('users'); + + $data = new Data\Collection($response); + + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $users = $data->filter('data')->values(); + $user = new Data\Collection($users[0]); + + $userProfile = new User\Profile(); + + $userProfile->identifier = $user->get('id'); + $userProfile->displayName = $user->get('display_name'); + $userProfile->photoURL = $user->get('profile_image_url'); + $userProfile->email = $user->get('email'); + $userProfile->description = strip_tags($user->get('description')); + $userProfile->profileURL = "https://www.twitch.tv/{$userProfile->displayName}"; + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Twitter.php b/www/application/third_party/hybridauth/Provider/Twitter.php new file mode 100644 index 00000000..be687683 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Twitter.php @@ -0,0 +1,264 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => ['key' => '', 'secret' => ''], // OAuth1 uses 'key' not 'id' + * 'authorize' => true // Needed to perform actions on behalf of users (see below link) + * // https://developer.twitter.com/en/docs/authentication/oauth-1-0a/obtaining-user-access-tokens + * ]; + * + * $adapter = new Hybridauth\Provider\Twitter($config); + * + * try { + * $adapter->authenticate(); + * + * $userProfile = $adapter->getUserProfile(); + * $tokens = $adapter->getAccessToken(); + * $contacts = $adapter->getUserContacts(['screen_name' =>'andypiper']); // get those of @andypiper + * $activity = $adapter->getUserActivity('me'); + * } catch (\Exception $e) { + * echo $e->getMessage() ; + * } + */ +class Twitter extends OAuth1 +{ + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://api.twitter.com/1.1/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://api.twitter.com/oauth/authenticate'; + + /** + * {@inheritdoc} + */ + protected $requestTokenUrl = 'https://api.twitter.com/oauth/request_token'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://api.twitter.com/oauth/access_token'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = 'https://dev.twitter.com/web/sign-in/implementing'; + + /** + * {@inheritdoc} + */ + protected function getAuthorizeUrl($parameters = []) + { + if ($this->config->get('authorize') === true) { + $this->authorizeUrl = 'https://api.twitter.com/oauth/authorize'; + } + + return parent::getAuthorizeUrl($parameters); + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('account/verify_credentials.json', 'GET', [ + 'include_email' => $this->config->get('include_email') === false ? 'false' : 'true', + ]); + + $data = new Data\Collection($response); + + if (!$data->exists('id_str')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id_str'); + $userProfile->displayName = $data->get('screen_name'); + $userProfile->description = $data->get('description'); + $userProfile->firstName = $data->get('name'); + $userProfile->email = $data->get('email'); + $userProfile->emailVerified = $data->get('email'); + $userProfile->webSiteURL = $data->get('url'); + $userProfile->region = $data->get('location'); + + $userProfile->profileURL = $data->exists('screen_name') + ? ('https://twitter.com/' . $data->get('screen_name')) + : ''; + + $photoSize = $this->config->get('photo_size') ?: 'original'; + $photoSize = $photoSize === 'original' ? '' : "_{$photoSize}"; + $userProfile->photoURL = $data->exists('profile_image_url_https') + ? str_replace('_normal', $photoSize, $data->get('profile_image_url_https')) + : ''; + + $userProfile->data = [ + 'followed_by' => $data->get('followers_count'), + 'follows' => $data->get('friends_count'), + ]; + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function getUserContacts($parameters = []) + { + $parameters = ['cursor' => '-1'] + $parameters; + + $response = $this->apiRequest('friends/ids.json', 'GET', $parameters); + + $data = new Data\Collection($response); + + if (!$data->exists('ids')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + if ($data->filter('ids')->isEmpty()) { + return []; + } + + $contacts = []; + + // 75 id per time should be okey + $contactsIds = array_chunk((array)$data->get('ids'), 75); + + foreach ($contactsIds as $chunk) { + $parameters = ['user_id' => implode(',', $chunk)]; + + try { + $response = $this->apiRequest('users/lookup.json', 'GET', $parameters); + + if ($response && count($response)) { + foreach ($response as $item) { + $contacts[] = $this->fetchUserContact($item); + } + } + } catch (\Exception $e) { + continue; + } + } + + return $contacts; + } + + /** + * @param $item + * + * @return User\Contact + */ + protected function fetchUserContact($item) + { + $item = new Data\Collection($item); + + $userContact = new User\Contact(); + + $userContact->identifier = $item->get('id_str'); + $userContact->displayName = $item->get('name'); + $userContact->photoURL = $item->get('profile_image_url'); + $userContact->description = $item->get('description'); + + $userContact->profileURL = $item->exists('screen_name') + ? ('https://twitter.com/' . $item->get('screen_name')) + : ''; + + return $userContact; + } + + /** + * {@inheritdoc} + */ + public function setUserStatus($status) + { + if (is_string($status)) { + $status = ['status' => $status]; + } + + // Prepare request parameters. + $params = []; + if (isset($status['status'])) { + $params['status'] = $status['status']; + } + if (isset($status['picture'])) { + $media = $this->apiRequest('https://upload.twitter.com/1.1/media/upload.json', 'POST', [ + 'media' => base64_encode(file_get_contents($status['picture'])), + ]); + $params['media_ids'] = $media->media_id; + } + + $response = $this->apiRequest('statuses/update.json', 'POST', $params); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function getUserActivity($stream = 'me') + { + $apiUrl = ($stream == 'me') + ? 'statuses/user_timeline.json' + : 'statuses/home_timeline.json'; + + $response = $this->apiRequest($apiUrl); + + if (!$response) { + return []; + } + + $activities = []; + + foreach ($response as $item) { + $activities[] = $this->fetchUserActivity($item); + } + + return $activities; + } + + /** + * @param $item + * @return User\Activity + */ + protected function fetchUserActivity($item) + { + $item = new Data\Collection($item); + + $userActivity = new User\Activity(); + + $userActivity->id = $item->get('id_str'); + $userActivity->date = $item->get('created_at'); + $userActivity->text = $item->get('text'); + + $userActivity->user->identifier = $item->filter('user')->get('id_str'); + $userActivity->user->displayName = $item->filter('user')->get('name'); + $userActivity->user->photoURL = $item->filter('user')->get('profile_image_url'); + + $userActivity->user->profileURL = $item->filter('user')->get('screen_name') + ? ('https://twitter.com/' . $item->filter('user')->get('screen_name')) + : ''; + + return $userActivity; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Vkontakte.php b/www/application/third_party/hybridauth/Provider/Vkontakte.php new file mode 100644 index 00000000..1cd1df39 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Vkontakte.php @@ -0,0 +1,216 @@ + Hybridauth\HttpClient\Util::getCurrentUrl(), + * 'keys' => [ + * 'id' => '', // App ID + * 'secret' => '' // Secure key + * ], + * ]; + * + * $adapter = new Hybridauth\Provider\Vkontakte($config); + * + * try { + * if (!$adapter->isConnected()) { + * $adapter->authenticate(); + * } + * + * $userProfile = $adapter->getUserProfile(); + * } catch (\Exception $e) { + * print $e->getMessage() ; + * } + */ +class Vkontakte extends OAuth2 +{ + const API_VERSION = '5.95'; + + const URL = 'https://vk.com/'; + + /** + * {@inheritdoc} + */ + protected $apiBaseUrl = 'https://api.vk.com/method/'; + + /** + * {@inheritdoc} + */ + protected $authorizeUrl = 'https://api.vk.com/oauth/authorize'; + + /** + * {@inheritdoc} + */ + protected $accessTokenUrl = 'https://api.vk.com/oauth/token'; + + /** + * {@inheritdoc} + */ + protected $scope = 'email,offline'; + + /** + * {@inheritdoc} + */ + protected $apiDocumentation = ''; // Not available + + /** + * {@inheritdoc} + */ + protected function initialize() + { + parent::initialize(); + + // The VK API requires version and access_token from authenticated users + // for each endpoint. + $accessToken = $this->getStoredData($this->accessTokenName); + $this->apiRequestParameters[$this->accessTokenName] = $accessToken; + $this->apiRequestParameters['v'] = static::API_VERSION; + } + + /** + * {@inheritdoc} + */ + protected function validateAccessTokenExchange($response) + { + $data = parent::validateAccessTokenExchange($response); + + // Need to store email for later use. + $this->storeData('email', $data->get('email')); + } + + /** + * {@inheritdoc} + */ + public function hasAccessTokenExpired($time = null) + { + if ($time === null) { + $time = time(); + } + + // If we are using offline scope, $expired will be false. + $expired = $this->getStoredData('expires_in') + ? $this->getStoredData('expires_at') <= $time + : false; + + return $expired; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $photoField = 'photo_' . ($this->config->get('photo_size') ?: 'max_orig'); + + $response = $this->apiRequest('users.get', 'GET', [ + 'fields' => 'screen_name,sex,education,bdate,has_photo,' . $photoField, + ]); + + if (property_exists($response, 'error')) { + throw new UnexpectedApiResponseException($response->error->error_msg); + } + + $data = new Collection($response->response[0]); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->email = $this->getStoredData('email'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->displayName = $data->get('screen_name'); + $userProfile->photoURL = $data->get('has_photo') === 1 ? $data->get($photoField) : ''; + + // Handle b-date. + if ($data->get('bdate')) { + $bday = explode('.', $data->get('bdate')); + $userProfile->birthDay = (int)$bday[0]; + $userProfile->birthMonth = (int)$bday[1]; + $userProfile->birthYear = (int)$bday[2]; + } + + $userProfile->data = [ + 'education' => $data->get('education'), + ]; + + $screen_name = static::URL . ($data->get('screen_name') ?: 'id' . $data->get('id')); + $userProfile->profileURL = $screen_name; + + switch ($data->get('sex')) { + case 1: + $userProfile->gender = 'female'; + break; + + case 2: + $userProfile->gender = 'male'; + break; + } + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function getUserContacts() + { + $response = $this->apiRequest('friends.get', 'GET', [ + 'fields' => 'uid,name,photo_200_orig', + ]); + + $data = new Data\Collection($response); + if (!$data->exists('response')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $contacts = []; + if (!$data->filter('response')->filter('items')->isEmpty()) { + foreach ($data->filter('response')->filter('items')->toArray() as $item) { + $contacts[] = $this->fetchUserContact($item); + } + } + + return $contacts; + } + + /** + * Parse the user contact. + * + * @param array $item + * + * @return \Hybridauth\User\Contact + */ + protected function fetchUserContact($item) + { + $userContact = new User\Contact(); + $data = new Data\Collection($item); + + $userContact->identifier = $data->get('id'); + $userContact->displayName = sprintf('%s %s', $data->get('first_name'), $data->get('last_name')); + $userContact->profileURL = static::URL . ($data->get('screen_name') ?: 'id' . $data->get('id')); + $userContact->photoURL = $data->get('photo_200_orig'); + + return $userContact; + } +} diff --git a/www/application/third_party/hybridauth/Provider/WeChat.php b/www/application/third_party/hybridauth/Provider/WeChat.php new file mode 100644 index 00000000..392c0e1f --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/WeChat.php @@ -0,0 +1,137 @@ +AuthorizeUrlParameters += [ + 'appid' => $this->clientId + ]; + unset($this->AuthorizeUrlParameters['client_id']); + + $this->tokenExchangeParameters += [ + 'appid' => $this->clientId, + 'secret' => $this->clientSecret + ]; + unset($this->tokenExchangeParameters['client_id']); + unset($this->tokenExchangeParameters['client_secret']); + + if ($this->isRefreshTokenAvailable()) { + $this->tokenRefreshParameters += [ + 'appid' => $this->clientId, + ]; + } + + $this->apiRequestParameters = [ + 'appid' => $this->clientId, + 'secret' => $this->clientSecret + ]; + } + + /** + * {@inheritdoc} + */ + protected function validateAccessTokenExchange($response) + { + $collection = parent::validateAccessTokenExchange($response); + + $this->storeData('openid', $collection->get('openid')); + $this->storeData('access_token', $collection->get('access_token')); + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $openid = $this->getStoredData('openid'); + $access_token = $this->getStoredData('access_token'); + + $response = $this->apiRequest('userinfo', 'GET', ['openid' => $openid, 'access_token' => $access_token]); + + $data = new Data\Collection($response); + + if (!$data->exists('openid')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('openid'); + $userProfile->displayName = $data->get('nickname'); + $userProfile->photoURL = $data->get('headimgurl'); + $userProfile->city = $data->get('city'); + $userProfile->region = $data->get('province'); + $userProfile->country = $data->get('country'); + $genders = ['', 'male', 'female']; + $userProfile->gender = $genders[(int)$data->get('sex')]; + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/WeChatChina.php b/www/application/third_party/hybridauth/Provider/WeChatChina.php new file mode 100644 index 00000000..d1539464 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/WeChatChina.php @@ -0,0 +1,34 @@ +apiRequest('me'); + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('id'); + $userProfile->displayName = $data->get('name'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->gender = $data->get('gender'); + $userProfile->profileURL = $data->get('link'); + $userProfile->email = $data->filter('emails')->get('preferred'); + $userProfile->emailVerified = $data->filter('emails')->get('account'); + $userProfile->birthDay = $data->get('birth_day'); + $userProfile->birthMonth = $data->get('birth_month'); + $userProfile->birthYear = $data->get('birth_year'); + $userProfile->language = $data->get('locale'); + + return $userProfile; + } + + /** + * {@inheritdoc} + */ + public function getUserContacts() + { + $response = $this->apiRequest('me/contacts'); + + $data = new Data\Collection($response); + + if (!$data->exists('data')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $contacts = []; + + foreach ($data->filter('data')->toArray() as $idx => $entry) { + $userContact = new User\Contact(); + + $userContact->identifier = $entry->get('id'); + $userContact->displayName = $entry->get('name'); + $userContact->email = $entry->filter('emails')->get('preferred'); + + $contacts[] = $userContact; + } + + return $contacts; + } +} diff --git a/www/application/third_party/hybridauth/Provider/WordPress.php b/www/application/third_party/hybridauth/Provider/WordPress.php new file mode 100644 index 00000000..a76e38c8 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/WordPress.php @@ -0,0 +1,68 @@ +apiRequest('me/'); + + $data = new Data\Collection($response); + + if (!$data->exists('ID')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('ID'); + $userProfile->displayName = $data->get('display_name'); + $userProfile->photoURL = $data->get('avatar_URL'); + $userProfile->profileURL = $data->get('profile_URL'); + $userProfile->email = $data->get('email'); + $userProfile->language = $data->get('language'); + + $userProfile->displayName = $userProfile->displayName ?: $data->get('username'); + + $userProfile->emailVerified = $data->get('email_verified') ? $data->get('email') : ''; + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Yahoo.php b/www/application/third_party/hybridauth/Provider/Yahoo.php new file mode 100644 index 00000000..c7774c54 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Yahoo.php @@ -0,0 +1,104 @@ +tokenExchangeHeaders = [ + 'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret) + ]; + + $this->tokenRefreshHeaders = $this->tokenExchangeHeaders; + } + + /** + * {@inheritdoc} + */ + public function getUserProfile() + { + $response = $this->apiRequest('userinfo'); + + $data = new Data\Collection($response); + + if (!$data->exists('sub')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + + $userProfile->identifier = $data->get('sub'); + $userProfile->firstName = $data->get('given_name'); + $userProfile->lastName = $data->get('family_name'); + $userProfile->displayName = $data->get('name'); + $userProfile->gender = $data->get('gender'); + $userProfile->language = $data->get('locale'); + $userProfile->email = $data->get('email'); + + $userProfile->emailVerified = $data->get('email_verified') ? $userProfile->email : ''; + + $profileImages = $data->get('profile_images'); + if ($this->config->get('photo_size')) { + $prop = 'image' . $this->config->get('photo_size'); + } else { + $prop = 'image192'; + } + $userProfile->photoURL = $profileImages->$prop; + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Provider/Yandex.php b/www/application/third_party/hybridauth/Provider/Yandex.php new file mode 100644 index 00000000..ef363254 --- /dev/null +++ b/www/application/third_party/hybridauth/Provider/Yandex.php @@ -0,0 +1,85 @@ +scope = implode(',', []); + + $response = $this->apiRequest($this->apiBaseUrl, 'GET', ['format' => 'json']); + + if (!isset($response->id)) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $data = new Data\Collection($response); + + if (!$data->exists('id')) { + throw new UnexpectedApiResponseException('Provider API returned an unexpected response.'); + } + + $userProfile = new User\Profile(); + $userProfile->identifier = $data->get('id'); + $userProfile->firstName = $data->get('first_name'); + $userProfile->lastName = $data->get('last_name'); + $userProfile->displayName = $data->get('display_name'); + $userProfile->photoURL + = 'https://avatars.yandex.net/get-yapic/' . + $data->get('default_avatar_id') . '/islands-200'; + $userProfile->gender = $data->get('sex'); + $userProfile->email = $data->get('default_email'); + $userProfile->emailVerified = $data->get('default_email'); + + if ($data->get('birthday')) { + list($birthday_year, $birthday_month, $birthday_day) + = explode('-', $response->birthday); + $userProfile->birthDay = (int)$birthday_day; + $userProfile->birthMonth = (int)$birthday_month; + $userProfile->birthYear = (int)$birthday_year; + } + + return $userProfile; + } +} diff --git a/www/application/third_party/hybridauth/Storage/Session.php b/www/application/third_party/hybridauth/Storage/Session.php new file mode 100644 index 00000000..553d356b --- /dev/null +++ b/www/application/third_party/hybridauth/Storage/Session.php @@ -0,0 +1,130 @@ +keyPrefix . strtolower($key); + + if (isset($_SESSION[$this->storeNamespace], $_SESSION[$this->storeNamespace][$key])) { + $value = $_SESSION[$this->storeNamespace][$key]; + + if (is_array($value) && array_key_exists('lateObject', $value)) { + $value = unserialize($value['lateObject']); + } + + return $value; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value) + { + $key = $this->keyPrefix . strtolower($key); + + if (is_object($value)) { + // We encapsulate as our classes may be defined after session is initialized. + $value = ['lateObject' => serialize($value)]; + } + + $_SESSION[$this->storeNamespace][$key] = $value; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $_SESSION[$this->storeNamespace] = []; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $key = $this->keyPrefix . strtolower($key); + + if (isset($_SESSION[$this->storeNamespace], $_SESSION[$this->storeNamespace][$key])) { + $tmp = $_SESSION[$this->storeNamespace]; + + unset($tmp[$key]); + + $_SESSION[$this->storeNamespace] = $tmp; + } + } + + /** + * {@inheritdoc} + */ + public function deleteMatch($key) + { + $key = $this->keyPrefix . strtolower($key); + + if (isset($_SESSION[$this->storeNamespace]) && count($_SESSION[$this->storeNamespace])) { + $tmp = $_SESSION[$this->storeNamespace]; + + foreach ($tmp as $k => $v) { + if (strstr($k, $key)) { + unset($tmp[$k]); + } + } + + $_SESSION[$this->storeNamespace] = $tmp; + } + } +} diff --git a/www/application/third_party/hybridauth/Storage/StorageInterface.php b/www/application/third_party/hybridauth/Storage/StorageInterface.php new file mode 100644 index 00000000..5438746e --- /dev/null +++ b/www/application/third_party/hybridauth/Storage/StorageInterface.php @@ -0,0 +1,50 @@ +key = $key; + $this->secret = $secret; + $this->callback_url = $callback_url; + } + + /** + * @return string + */ + public function __toString() + { + return "OAuthConsumer[key=$this->key,secret=$this->secret]"; + } +} diff --git a/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthRequest.php b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthRequest.php new file mode 100644 index 00000000..b13541d0 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthRequest.php @@ -0,0 +1,338 @@ +parameters = $parameters; + $this->http_method = $http_method; + $this->http_url = $http_url; + } + + /** + * attempt to build up a request from what was passed to the server + * + * @param null $http_method + * @param null $http_url + * @param null $parameters + * + * @return OAuthRequest + */ + public static function from_request($http_method = null, $http_url = null, $parameters = null) + { + $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https'; + $http_url = ($http_url) ? $http_url : $scheme . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT'] . $_SERVER['REQUEST_URI']; + $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD']; + + // We weren't handed any parameters, so let's find the ones relevant to + // this request. + // If you run XML-RPC or similar you should use this to provide your own + // parsed parameter-list + if (!$parameters) { + // Find request headers + $request_headers = OAuthUtil::get_headers(); + + // Parse the query-string to find GET parameters + $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); + + // It's a POST request of the proper content-type, so parse POST + // parameters and add those overriding any duplicates from GET + if ($http_method == "POST" && isset($request_headers['Content-Type']) && strstr($request_headers['Content-Type'], 'application/x-www-form-urlencoded')) { + $post_data = OAuthUtil::parse_parameters(file_get_contents(self::$POST_INPUT)); + $parameters = array_merge($parameters, $post_data); + } + + // We have a Authorization-header with OAuth data. Parse the header + // and add those overriding any duplicates from GET or POST + if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') { + $header_parameters = OAuthUtil::split_header($request_headers['Authorization']); + $parameters = array_merge($parameters, $header_parameters); + } + } + + return new OAuthRequest($http_method, $http_url, $parameters); + } + + /** + * pretty much a helper function to set up the request + * @param $consumer + * @param $token + * @param $http_method + * @param $http_url + * @param null $parameters + * @return OAuthRequest +*/ + public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters = null) + { + $parameters = ($parameters) ? $parameters : array(); + $defaults = array( + "oauth_version" => OAuthRequest::$version, + "oauth_nonce" => OAuthRequest::generate_nonce(), + "oauth_timestamp" => OAuthRequest::generate_timestamp(), + "oauth_consumer_key" => $consumer->key + ); + if ($token) { + $defaults['oauth_token'] = $token->key; + } + + $parameters = array_merge($defaults, $parameters); + + return new OAuthRequest($http_method, $http_url, $parameters); + } + + /** + * @param $name + * @param $value + * @param bool $allow_duplicates + */ + public function set_parameter($name, $value, $allow_duplicates = true) + { + if ($allow_duplicates && isset($this->parameters[$name])) { + // We have already added parameter(s) with this name, so add to the list + if (is_scalar($this->parameters[$name])) { + // This is the first duplicate, so transform scalar (string) + // into an array so we can add the duplicates + $this->parameters[$name] = array( + $this->parameters[$name] + ); + } + + $this->parameters[$name][] = $value; + } else { + $this->parameters[$name] = $value; + } + } + + /** + * @param $name + * + * @return |null + */ + public function get_parameter($name) + { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + /** + * @return array + */ + public function get_parameters() + { + return $this->parameters; + } + + /** + * @param $name + */ + public function unset_parameter($name) + { + unset($this->parameters[$name]); + } + + /** + * The request parameters, sorted and concatenated into a normalized string. + * + * @return string + */ + public function get_signable_parameters() + { + $params = []; + + // Grab all parameters. + foreach ($this->parameters as $key_param => $value_param) { + // Process only scalar values. + if (is_scalar($value_param)) { + $params[$key_param] = $value_param; + } + } + + // Remove oauth_signature if present + // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") + if (isset($params['oauth_signature'])) { + unset($params['oauth_signature']); + } + + return OAuthUtil::build_http_query($params); + } + + /** + * Returns the base string of this request + * + * The base string defined as the method, the url + * and the parameters (normalized), each urlencoded + * and the concated with &. + */ + public function get_signature_base_string() + { + $parts = array( + $this->get_normalized_http_method(), + $this->get_normalized_http_url(), + $this->get_signable_parameters() + ); + + $parts = OAuthUtil::urlencode_rfc3986($parts); + + return implode('&', $parts); + } + + /** + * just uppercases the http method + */ + public function get_normalized_http_method() + { + return strtoupper($this->http_method); + } + + /** + * parses the url and rebuilds it to be + * scheme://host/path + */ + public function get_normalized_http_url() + { + $parts = parse_url($this->http_url); + + $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http'; + $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80'); + $host = (isset($parts['host'])) ? strtolower($parts['host']) : ''; + $path = (isset($parts['path'])) ? $parts['path'] : ''; + + if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { + $host = "$host:$port"; + } + return "$scheme://$host$path"; + } + + /** + * builds a url usable for a GET request + */ + public function to_url() + { + $post_data = $this->to_postdata(); + $out = $this->get_normalized_http_url(); + if ($post_data) { + $out .= '?' . $post_data; + } + return $out; + } + + /** + * builds the data one would send in a POST request + */ + public function to_postdata() + { + return OAuthUtil::build_http_query($this->parameters); + } + + /** + * builds the Authorization: header + * @param null $realm + * @return array +*/ + public function to_header($realm = null) + { + $first = true; + if ($realm) { + $out = 'OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; + $first = false; + } else { + $out = 'OAuth'; + } + + foreach ($this->parameters as $k => $v) { + if (substr($k, 0, 5) != "oauth") { + continue; + } + if (is_array($v)) { + continue; + } + $out .= ($first) ? ' ' : ','; + $out .= OAuthUtil::urlencode_rfc3986($k) . '="' . OAuthUtil::urlencode_rfc3986($v) . '"'; + $first = false; + } + + return array( + 'Authorization' => $out + ); //- hacked into this to make it return an array. 15/11/2014. + } + + /** + * @return string + */ + public function __toString() + { + return $this->to_url(); + } + + /** + * @param $signature_method + * @param $consumer + * @param $token + */ + public function sign_request($signature_method, $consumer, $token) + { + $this->set_parameter("oauth_signature_method", $signature_method->get_name(), false); + $signature = $this->build_signature($signature_method, $consumer, $token); + $this->set_parameter("oauth_signature", $signature, false); + } + + /** + * @param $signature_method + * @param $consumer + * @param $token + * + * @return mixed + */ + public function build_signature($signature_method, $consumer, $token) + { + $signature = $signature_method->build_signature($this, $consumer, $token); + return $signature; + } + + /** + * util function: current timestamp + */ + private static function generate_timestamp() + { + return time(); + } + + /** + * util function: current nonce + */ + private static function generate_nonce() + { + $mt = microtime(); + $rand = mt_rand(); + + return md5($mt . $rand); // md5s look nicer than numbers + } +} diff --git a/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthSignatureMethod.php b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthSignatureMethod.php new file mode 100644 index 00000000..810be011 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthSignatureMethod.php @@ -0,0 +1,67 @@ +build_signature($request, $consumer, $token); + + // Check for zero length, although unlikely here + if (strlen($built) == 0 || strlen($signature) == 0) { + return false; + } + + if (strlen($built) != strlen($signature)) { + return false; + } + + // Avoid a timing leak with a (hopefully) time insensitive compare + $result = 0; + for ($i = 0; $i < strlen($signature); $i ++) { + $result |= ord($built[$i]) ^ ord($signature[$i]); + } + + return $result == 0; + } +} diff --git a/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthSignatureMethodHMACSHA1.php b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthSignatureMethodHMACSHA1.php new file mode 100644 index 00000000..43f6d81a --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthSignatureMethodHMACSHA1.php @@ -0,0 +1,44 @@ +get_signature_base_string(); + $request->base_string = $base_string; + + $key_parts = array( $consumer->secret, $token ? $token->secret : '' ); + + $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); + $key = implode('&', $key_parts); + + return base64_encode(hash_hmac('sha1', $base_string, $key, true)); + } +} diff --git a/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthUtil.php b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthUtil.php new file mode 100644 index 00000000..7c1b2233 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OAuth/OAuthUtil.php @@ -0,0 +1,199 @@ + $h) { + $params[$h] = OAuthUtil::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]); + } + if (isset($params['realm'])) { + unset($params['realm']); + } + } + return $params; + } + + // helper to try to sort out headers for people who aren't running apache + + /** + * @return array + */ + public static function get_headers() + { + if (function_exists('apache_request_headers')) { + // we need this to get the actual Authorization: header + // because apache tends to tell us it doesn't exist + $headers = apache_request_headers(); + + // sanitize the output of apache_request_headers because + // we always want the keys to be Cased-Like-This and arh() + // returns the headers in the same case as they are in the + // request + $out = array(); + foreach ($headers as $key => $value) { + $key = str_replace(" ", "-", ucwords(strtolower(str_replace("-", " ", $key)))); + $out[$key] = $value; + } + } else { + // otherwise we don't have apache and are just going to have to hope + // that $_SERVER actually contains what we need + $out = array(); + if (isset($_SERVER['CONTENT_TYPE'])) { + $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; + } + if (isset($_ENV['CONTENT_TYPE'])) { + $out['Content-Type'] = $_ENV['CONTENT_TYPE']; + } + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) == "HTTP_") { + // this is chaos, basically it is just there to capitalize the first + // letter of every word that is not an initial HTTP and strip HTTP + // code from przemek + $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5))))); + $out[$key] = $value; + } + } + } + return $out; + } + + // This function takes a input like a=b&a=c&d=e and returns the parsed + // parameters like this + // array('a' => array('b','c'), 'd' => 'e') + /** + * @param $input + * + * @return array + */ + public static function parse_parameters($input) + { + if (!isset($input) || !$input) { + return array(); + } + + $pairs = explode('&', $input); + + $parsed_parameters = array(); + foreach ($pairs as $pair) { + $split = explode('=', $pair, 2); + $parameter = OAuthUtil::urldecode_rfc3986($split[0]); + $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; + + if (isset($parsed_parameters[$parameter])) { + // We have already recieved parameter(s) with this name, so add to the list + // of parameters with this name + + if (is_scalar($parsed_parameters[$parameter])) { + // This is the first duplicate, so transform scalar (string) into an array + // so we can add the duplicates + $parsed_parameters[$parameter] = array( + $parsed_parameters[$parameter] + ); + } + + $parsed_parameters[$parameter][] = $value; + } else { + $parsed_parameters[$parameter] = $value; + } + } + return $parsed_parameters; + } + + /** + * @param $params + * + * @return string + */ + public static function build_http_query($params) + { + if (!$params) { + return ''; + } + + // Urlencode both keys and values + $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); + $values = OAuthUtil::urlencode_rfc3986(array_values($params)); + $params = array_combine($keys, $values); + + // Parameters are sorted by name, using lexicographical byte value ordering. + // Ref: Spec: 9.1.1 (1) + uksort($params, 'strcmp'); + + $pairs = array(); + foreach ($params as $parameter => $value) { + if (is_array($value)) { + // If two or more parameters share the same name, they are sorted by their value + // Ref: Spec: 9.1.1 (1) + // June 12th, 2010 - changed to sort because of issue 164 by hidetaka + sort($value, SORT_STRING); + foreach ($value as $duplicate_value) { + $pairs[] = $parameter . '=' . $duplicate_value; + } + } else { + $pairs[] = $parameter . '=' . $value; + } + } + // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) + // Each name-value pair is separated by an '&' character (ASCII code 38) + return implode('&', $pairs); + } +} diff --git a/www/application/third_party/hybridauth/Thirdparty/OAuth/README.md b/www/application/third_party/hybridauth/Thirdparty/OAuth/README.md new file mode 100644 index 00000000..5807ab13 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OAuth/README.md @@ -0,0 +1,7 @@ +This package contains OAuth PHP Library. + +OAuth PHP Library is an open source software available under the MIT License. + +https://code.google.com/p/oauth/ + +http://oauth.googlecode.com/svn/code/php/LICENSE.txt diff --git a/www/application/third_party/hybridauth/Thirdparty/OpenID/LightOpenID.php b/www/application/third_party/hybridauth/Thirdparty/OpenID/LightOpenID.php new file mode 100644 index 00000000..14deab36 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OpenID/LightOpenID.php @@ -0,0 +1,1256 @@ += 5.1.2 with cURL or HTTP/HTTPS stream wrappers enabled. + * + * @version v1.3.1 (2016-03-04) + * @link https://code.google.com/p/lightopenid/ Project URL + * @link https://github.com/iignatov/LightOpenID GitHub Repo + * @author Mewp + * @copyright Copyright (c) 2013 Mewp + * @license http://opensource.org/licenses/mit-license.php MIT License + */ +class LightOpenID +{ + public $returnUrl + ; + public $required = array() + ; + public $optional = array() + ; + public $verify_peer = null + ; + public $capath = null + ; + public $cainfo = null + ; + public $cnmatch = null + ; + public $data + ; + public $oauth = array() + ; + public $curl_time_out = 30 // in seconds + ; + public $curl_connect_time_out = 30; // in seconds + private $identity; + private $claimed_id; + protected $server; + protected $version; + protected $trustRoot; + protected $aliases; + protected $identifier_select = false + ; + protected $ax = false; + protected $sreg = false; + protected $setup_url = null; + protected $headers = array() + ; + protected $proxy = null; + protected $user_agent = 'LightOpenID' + ; + protected $xrds_override_pattern = null; + protected $xrds_override_replacement = null; + protected static $ax_to_sreg = array( + 'namePerson/friendly' => 'nickname', + 'contact/email' => 'email', + 'namePerson' => 'fullname', + 'birthDate' => 'dob', + 'person/gender' => 'gender', + 'contact/postalCode/home' => 'postcode', + 'contact/country/home' => 'country', + 'pref/language' => 'language', + 'pref/timezone' => 'timezone', + ); + + /** + * LightOpenID constructor. + * + * @param $host + * @param null $proxy + * + * @throws ErrorException + */ + public function __construct($host, $proxy = null) + { + $this->set_realm($host); + $this->set_proxy($proxy); + + $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); + $this->returnUrl = $this->trustRoot . $uri; + + $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; + + if (!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) { + throw new ErrorException('You must have either https wrappers or curl enabled.'); + } + } + + /** + * @param $name + * + * @return bool + */ + public function __isset($name) + { + return in_array($name, array('identity', 'trustRoot', 'realm', 'xrdsOverride', 'mode')); + } + + /** + * @param $name + * @param $value + */ + public function __set($name, $value) + { + switch ($name) { + case 'identity': + if (strlen($value = trim((String) $value))) { + if (preg_match('#^xri:/*#i', $value, $m)) { + $value = substr($value, strlen($m[0])); + } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { + $value = "http://$value"; + } + if (preg_match('#^https?://[^/]+$#i', $value, $m)) { + $value .= '/'; + } + } + $this->$name = $this->claimed_id = $value; + break; + case 'trustRoot': + case 'realm': + $this->trustRoot = trim($value); + break; + case 'xrdsOverride': + if (is_array($value)) { + list($pattern, $replacement) = $value; + $this->xrds_override_pattern = $pattern; + $this->xrds_override_replacement = $replacement; + } else { + trigger_error('Invalid value specified for "xrdsOverride".', E_USER_ERROR); + } + break; + } + } + + /** + * @param $name + * + * @return |null + */ + public function __get($name) + { + switch ($name) { + case 'identity': + # We return claimed_id instead of identity, + # because the developer should see the claimed identifier, + # i.e. what he set as identity, not the op-local identifier (which is what we verify) + return $this->claimed_id; + case 'trustRoot': + case 'realm': + return $this->trustRoot; + case 'mode': + return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; + } + } + + /** + * @param $proxy + * + * @throws ErrorException + */ + public function set_proxy($proxy) + { + if (!empty($proxy)) { + // When the proxy is a string - try to parse it. + if (!is_array($proxy)) { + $proxy = parse_url($proxy); + } + + // Check if $proxy is valid after the parsing. + if ($proxy && !empty($proxy['host'])) { + // Make sure that a valid port number is specified. + if (array_key_exists('port', $proxy)) { + if (!is_int($proxy['port'])) { + $proxy['port'] = is_numeric($proxy['port']) ? intval($proxy['port']) : 0; + } + + if ($proxy['port'] <= 0) { + throw new ErrorException('The specified proxy port number is invalid.'); + } + } + + $this->proxy = $proxy; + } + } + } + + /** + * Checks if the server specified in the url exists. + * + * @param $url string url to check + * @return true, if the server exists; false otherwise + */ + public function hostExists($url) + { + if (strpos($url, '/') === false) { + $server = $url; + } else { + $server = @parse_url($url, PHP_URL_HOST); + } + + if (!$server) { + return false; + } + + return !!gethostbynamel($server); + } + + /** + * @param $uri + */ + protected function set_realm($uri) + { + $realm = ''; + + # Set a protocol, if not specified. + $realm .= (($offset = strpos($uri, '://')) === false) ? $this->get_realm_protocol() : ''; + + # Set the offset properly. + $offset = (($offset !== false) ? $offset + 3 : 0); + + # Get only the root, without the path. + $realm .= (($end = strpos($uri, '/', $offset)) === false) ? $uri : substr($uri, 0, $end); + + $this->trustRoot = $realm; + } + + /** + * @return string + */ + protected function get_realm_protocol() + { + if (!empty($_SERVER['HTTPS'])) { + $use_secure_protocol = ($_SERVER['HTTPS'] !== 'off'); + } elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + $use_secure_protocol = ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); + } elseif (isset($_SERVER['HTTP__WSSC'])) { + $use_secure_protocol = ($_SERVER['HTTP__WSSC'] == 'https'); + } else { + $use_secure_protocol = false; + } + + return $use_secure_protocol ? 'https://' : 'http://'; + } + + /** + * @param $url + * @param string $method + * @param array $params + * @param $update_claimed_id + * + * @return array|bool|string + * @throws ErrorException + */ + protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id=false) + { + $params = http_build_query($params, '', '&'); + $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + if ($method == 'POST') { + curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); + } else { + curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); + } + + curl_setopt($curl, CURLOPT_TIMEOUT, $this->curl_time_out); // defaults to infinite + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->curl_connect_time_out); // defaults to 300s + + if (!empty($this->proxy)) { + curl_setopt($curl, CURLOPT_PROXY, $this->proxy['host']); + + if (!empty($this->proxy['port'])) { + curl_setopt($curl, CURLOPT_PROXYPORT, $this->proxy['port']); + } + + if (!empty($this->proxy['user'])) { + curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxy['user'] . ':' . $this->proxy['pass']); + } + } + + if ($this->verify_peer !== null) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); + if ($this->capath) { + curl_setopt($curl, CURLOPT_CAPATH, $this->capath); + } + + if ($this->cainfo) { + curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); + } + } + + if ($method == 'POST') { + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $params); + } elseif ($method == 'HEAD') { + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, true); + } else { + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_HTTPGET, true); + } + $response = curl_exec($curl); + + if ($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) { + curl_setopt($curl, CURLOPT_HTTPGET, true); + $response = curl_exec($curl); + $response = substr($response, 0, strpos($response, "\r\n\r\n")); + } + + if ($method == 'HEAD' || $method == 'GET') { + $header_response = $response; + + # If it's a GET request, we want to only parse the header part. + if ($method == 'GET') { + $header_response = substr($response, 0, strpos($response, "\r\n\r\n")); + } + + $headers = array(); + foreach (explode("\n", $header_response) as $header) { + $pos = strpos($header, ':'); + if ($pos !== false) { + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + } + } + + if ($update_claimed_id) { + # Update the claimed_id value in case of redirections. + $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + # Ignore the fragment (some cURL versions don't handle it well). + if (strtok($effective_url, '#') != strtok($url, '#')) { + $this->identity = $this->claimed_id = $effective_url; + } + } + + if ($method == 'HEAD') { + return $headers; + } else { + $this->headers = $headers; + } + } + + if (curl_errno($curl)) { + throw new ErrorException(curl_error($curl), curl_errno($curl)); + } + + return $response; + } + + /** + * @param $array + * @param $update_claimed_id + * + * @return array + */ + protected function parse_header_array($array, $update_claimed_id) + { + $headers = array(); + foreach ($array as $header) { + $pos = strpos($header, ':'); + if ($pos !== false) { + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + + # Following possible redirections. The point is just to have + # claimed_id change with them, because the redirections + # are followed automatically. + # We ignore redirections with relative paths. + # If any known provider uses them, file a bug report. + if ($name == 'location' && $update_claimed_id) { + if (strpos($headers[$name], 'http') === 0) { + $this->identity = $this->claimed_id = $headers[$name]; + } elseif ($headers[$name][0] == '/') { + $parsed_url = parse_url($this->claimed_id); + $this->identity = + $this->claimed_id = $parsed_url['scheme'] . '://' + . $parsed_url['host'] + . $headers[$name]; + } + } + } + } + return $headers; + } + + /** + * @param $url + * @param string $method + * @param array $params + * @param $update_claimed_id + * + * @return array|false|string + * @throws ErrorException + */ + protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id=false) + { + if (!$this->hostExists($url)) { + throw new ErrorException("Could not connect to $url.", 404); + } + + if (empty($this->cnmatch)) { + $this->cnmatch = parse_url($url, PHP_URL_HOST); + } + + $params = http_build_query($params, '', '&'); + switch ($method) { + case 'GET': + $opts = array( + 'http' => array( + 'method' => 'GET', + 'header' => 'Accept: application/xrds+xml, */*', + 'user_agent' => $this->user_agent, + 'ignore_errors' => true, + ), + 'ssl' => array( + 'CN_match' => $this->cnmatch + ) + ); + $url = $url . ($params ? '?' . $params : ''); + if (!empty($this->proxy)) { + $opts['http']['proxy'] = $this->proxy_url(); + } + break; + case 'POST': + $opts = array( + 'http' => array( + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'user_agent' => $this->user_agent, + 'content' => $params, + 'ignore_errors' => true, + ), + 'ssl' => array( + 'CN_match' => $this->cnmatch + ) + ); + if (!empty($this->proxy)) { + $opts['http']['proxy'] = $this->proxy_url(); + } + break; + case 'HEAD': + // We want to send a HEAD request, but since get_headers() doesn't + // accept $context parameter, we have to change the defaults. + $default = stream_context_get_options(stream_context_get_default()); + + // PHP does not reset all options. Instead, it just sets the options + // available in the passed array, therefore set the defaults manually. + $default += array( + 'http' => array(), + 'ssl' => array() + ); + $default['http'] += array( + 'method' => 'GET', + 'header' => '', + 'user_agent' => '', + 'ignore_errors' => false + ); + $default['ssl'] += array( + 'CN_match' => '' + ); + + $opts = array( + 'http' => array( + 'method' => 'HEAD', + 'header' => 'Accept: application/xrds+xml, */*', + 'user_agent' => $this->user_agent, + 'ignore_errors' => true, + ), + 'ssl' => array( + 'CN_match' => $this->cnmatch + ) + ); + + // Enable validation of the SSL certificates. + if ($this->verify_peer) { + $default['ssl'] += array( + 'verify_peer' => false, + 'capath' => '', + 'cafile' => '' + ); + $opts['ssl'] += array( + 'verify_peer' => true, + 'capath' => $this->capath, + 'cafile' => $this->cainfo + ); + } + + // Change the stream context options. + stream_context_get_default($opts); + + $headers = get_headers($url . ($params ? '?' . $params : '')); + + // Restore the stream context options. + stream_context_get_default($default); + + if (!empty($headers)) { + if (intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) { + // The server doesn't support HEAD - emulate it with a GET. + $args = func_get_args(); + $args[1] = 'GET'; + call_user_func_array(array($this, 'request_streams'), $args); + $headers = $this->headers; + } else { + $headers = $this->parse_header_array($headers, $update_claimed_id); + } + } else { + $headers = array(); + } + + return $headers; + } + + if ($this->verify_peer) { + $opts['ssl'] += array( + 'verify_peer' => true, + 'capath' => $this->capath, + 'cafile' => $this->cainfo + ); + } + + $context = stream_context_create($opts); + $data = file_get_contents($url, false, $context); + # This is a hack for providers who don't support HEAD requests. + # It just creates the headers array for the last request in $this->headers. + if (isset($http_response_header)) { + $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id); + } + + return $data; + } + + /** + * @param $url + * @param string $method + * @param array $params + * @param bool $update_claimed_id + * + * @return array|bool|false|string + * @throws ErrorException + */ + protected function request($url, $method='GET', $params=array(), $update_claimed_id=false) + { + $use_curl = false; + + if (function_exists('curl_init')) { + if (!$use_curl) { + # When allow_url_fopen is disabled, PHP streams will not work. + $use_curl = !ini_get('allow_url_fopen'); + } + + if (!$use_curl) { + # When there is no HTTPS wrapper, PHP streams cannott be used. + $use_curl = !in_array('https', stream_get_wrappers()); + } + + if (!$use_curl) { + # With open_basedir or safe_mode set, cURL can't follow redirects. + $use_curl = !(ini_get('safe_mode') || ini_get('open_basedir')); + } + } + + return + $use_curl + ? $this->request_curl($url, $method, $params, $update_claimed_id) + : $this->request_streams($url, $method, $params, $update_claimed_id); + } + + /** + * @return string + */ + protected function proxy_url() + { + $result = ''; + + if (!empty($this->proxy)) { + $result = $this->proxy['host']; + + if (!empty($this->proxy['port'])) { + $result = $result . ':' . $this->proxy['port']; + } + + if (!empty($this->proxy['user'])) { + $result = $this->proxy['user'] . ':' . $this->proxy['pass'] . '@' . $result; + } + + $result = 'http://' . $result; + } + + return $result; + } + + /** + * @param $url + * @param $parts + * + * @return string + */ + protected function build_url($url, $parts) + { + if (isset($url['query'], $parts['query'])) { + $parts['query'] = $url['query'] . '&' . $parts['query']; + } + + $url = $parts + $url; + $url = $url['scheme'] . '://' + . (empty($url['username'])?'' + :(empty($url['password'])? "{$url['username']}@" + :"{$url['username']}:{$url['password']}@")) + . $url['host'] + . (empty($url['port'])?'':":{$url['port']}") + . (empty($url['path'])?'':$url['path']) + . (empty($url['query'])?'':"?{$url['query']}") + . (empty($url['fragment'])?'':"#{$url['fragment']}"); + return $url; + } + + /** + * Helper function used to scan for / tags and extract information + * from them + * + * @param $content + * @param $tag + * @param $attrName + * @param $attrValue + * @param $valueName + * + * @return bool + */ + protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName) + { + preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); + preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); + + $result = array_merge($matches1[1], $matches2[1]); + return empty($result)?false:$result[0]; + } + + /** + * Performs Yadis and HTML discovery. Normally not used. + * @param $url Identity URL. + * @return String OP Endpoint (i.e. OpenID provider address). + * @throws ErrorException + */ + public function discover($url) + { + if (!$url) { + throw new ErrorException('No identity supplied.'); + } + # Use xri.net proxy to resolve i-name identities + if (!preg_match('#^https?:#', $url)) { + $url = "https://xri.net/$url"; + } + + # We save the original url in case of Yadis discovery failure. + # It can happen when we'll be lead to an XRDS document + # which does not have any OpenID2 services. + $originalUrl = $url; + + # A flag to disable yadis discovery in case of failure in headers. + $yadis = true; + + # Allows optional regex replacement of the URL, e.g. to use Google Apps + # as an OpenID provider without setting up XRDS on the domain hosting. + if (!is_null($this->xrds_override_pattern) && !is_null($this->xrds_override_replacement)) { + $url = preg_replace($this->xrds_override_pattern, $this->xrds_override_replacement, $url); + } + + # We'll jump a maximum of 5 times, to avoid endless redirections. + for ($i = 0; $i < 5; $i ++) { + if ($yadis) { + $headers = $this->request($url, 'HEAD', array(), true); + + $next = false; + if (isset($headers['x-xrds-location'])) { + $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location']))); + $next = true; + } + + if (isset($headers['content-type']) && $this->is_allowed_type($headers['content-type'])) { + # Found an XRDS document, now let's find the server, and optionally delegate. + $content = $this->request($url, 'GET'); + + preg_match_all('#(.*?)#s', $content, $m); + foreach ($m[1] as $content) { + $content = ' ' . $content; # The space is added, so that strpos doesn't return 0. + + # OpenID 2 + $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#'); + if (preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { + if ($type[1] == 'server') { + $this->identifier_select = true; + } + + preg_match('#(.*)#', $content, $server); + preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); + if (empty($server)) { + return false; + } + # Does the server advertise support for either AX or SREG? + $this->ax = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); + $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') + || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[2])) { + $this->identity = trim($delegate[2]); + } + $this->version = 2; + + $this->server = $server; + return $server; + } + + # OpenID 1.1 + $ns = preg_quote('http://openid.net/signon/1.1', '#'); + if (preg_match('#\s*'.$ns.'\s*#s', $content)) { + preg_match('#(.*)#', $content, $server); + preg_match('#<.*?Delegate>(.*)#', $content, $delegate); + if (empty($server)) { + return false; + } + # AX can be used only with OpenID 2.0, so checking only SREG + $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') + || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[1])) { + $this->identity = $delegate[1]; + } + $this->version = 1; + + $this->server = $server; + return $server; + } + } + + $next = true; + $yadis = false; + $url = $originalUrl; + $content = null; + break; + } + if ($next) { + continue; + } + + # There are no relevant information in headers, so we search the body. + $content = $this->request($url, 'GET', array(), true); + + if (isset($this->headers['x-xrds-location'])) { + $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location']))); + continue; + } + + $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); + if ($location) { + $url = $this->build_url(parse_url($url), parse_url($location)); + continue; + } + } + + if (!$content) { + $content = $this->request($url, 'GET'); + } + + # At this point, the YADIS Discovery has failed, so we'll switch + # to openid2 HTML discovery, then fallback to openid 1.1 discovery. + $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href'); + $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href'); + $this->version = 2; + + if (!$server) { + # The same with openid 1.1 + $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href'); + $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href'); + $this->version = 1; + } + + if ($server) { + # We found an OpenID2 OP Endpoint + if ($delegate) { + # We have also found an OP-Local ID. + $this->identity = $delegate; + } + $this->server = $server; + return $server; + } + + throw new ErrorException("No OpenID Server found at $url", 404); + } + throw new ErrorException('Endless redirection!', 500); + } + + /** + * @param $content_type + * + * @return bool + */ + protected function is_allowed_type($content_type) + { + # Apparently, some providers return XRDS documents as text/html. + # While it is against the spec, allowing this here shouldn't break + # compatibility with anything. + $allowed_types = array('application/xrds+xml', 'text/xml'); + + # Only allow text/html content type for the Yahoo logins, since + # it might cause an endless redirection for the other providers. + if ($this->get_provider_name($this->claimed_id) == 'yahoo') { + $allowed_types[] = 'text/html'; + } + + foreach ($allowed_types as $type) { + if (strpos($content_type, $type) !== false) { + return true; + } + } + + return false; + } + + /** + * @param $provider_url + * + * @return string + */ + protected function get_provider_name($provider_url) + { + $result = ''; + + if (!empty($provider_url)) { + $tokens = array_reverse( + explode('.', parse_url($provider_url, PHP_URL_HOST)) + ); + $result = strtolower( + (count($tokens) > 1 && strlen($tokens[1]) > 3) + ? $tokens[1] + : (count($tokens) > 2 ? $tokens[2] : '') + ); + } + + return $result; + } + + /** + * @return array + */ + protected function sregParams() + { + $params = array(); + # We always use SREG 1.1, even if the server is advertising only support for 1.0. + # That's because it's fully backwards compatible with 1.0, and some providers + # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com + $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; + if ($this->required) { + $params['openid.sreg.required'] = array(); + foreach ($this->required as $required) { + if (!isset(self::$ax_to_sreg[$required])) { + continue; + } + $params['openid.sreg.required'][] = self::$ax_to_sreg[$required]; + } + $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); + } + + if ($this->optional) { + $params['openid.sreg.optional'] = array(); + foreach ($this->optional as $optional) { + if (!isset(self::$ax_to_sreg[$optional])) { + continue; + } + $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional]; + } + $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); + } + return $params; + } + + /** + * @return array + */ + protected function axParams() + { + $params = array(); + if ($this->required || $this->optional) { + $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; + $params['openid.ax.mode'] = 'fetch_request'; + $this->aliases = array(); + $counts = array(); + $required = array(); + $optional = array(); + foreach (array('required','optional') as $type) { + foreach ($this->$type as $alias => $field) { + if (is_int($alias)) { + $alias = strtr($field, '/', '_'); + } + $this->aliases[$alias] = 'http://axschema.org/' . $field; + if (empty($counts[$alias])) { + $counts[$alias] = 0; + } + $counts[$alias] += 1; + ${$type}[] = $alias; + } + } + foreach ($this->aliases as $alias => $ns) { + $params['openid.ax.type.' . $alias] = $ns; + } + foreach ($counts as $alias => $count) { + if ($count == 1) { + continue; + } + $params['openid.ax.count.' . $alias] = $count; + } + + # Don't send empty ax.required and ax.if_available. + # Google and possibly other providers refuse to support ax when one of these is empty. + if ($required) { + $params['openid.ax.required'] = implode(',', $required); + } + if ($optional) { + $params['openid.ax.if_available'] = implode(',', $optional); + } + } + return $params; + } + + /** + * @param $immediate + * + * @return string + */ + protected function authUrl_v1($immediate) + { + $returnUrl = $this->returnUrl; + # If we have an openid.delegate that is different from our claimed id, + # we need to somehow preserve the claimed id between requests. + # The simplest way is to just send it along with the return_to url. + if ($this->identity != $this->claimed_id) { + $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id; + } + + $params = array( + 'openid.return_to' => $returnUrl, + 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', + 'openid.identity' => $this->identity, + 'openid.trust_root' => $this->trustRoot, + ) + $this->sregParams(); + + return $this->build_url(parse_url($this->server), array('query' => http_build_query($params, '', '&'))); + } + + /** + * @param $immediate + * + * @return string + */ + protected function authUrl_v2($immediate) + { + $params = array( + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', + 'openid.return_to' => $this->returnUrl, + 'openid.realm' => $this->trustRoot, + ); + + if ($this->ax) { + $params += $this->axParams(); + } + + if ($this->sreg) { + $params += $this->sregParams(); + } + + if (!$this->ax && !$this->sreg) { + # If OP doesn't advertise either SREG, nor AX, let's send them both + # in worst case we don't get anything in return. + $params += $this->axParams() + $this->sregParams(); + } + + if (!empty($this->oauth) && is_array($this->oauth)) { + $params['openid.ns.oauth'] = 'http://specs.openid.net/extensions/oauth/1.0'; + $params['openid.oauth.consumer'] = str_replace(array('http://', 'https://'), '', $this->trustRoot); + $params['openid.oauth.scope'] = implode(' ', $this->oauth); + } + + if ($this->identifier_select) { + $params['openid.identity'] = $params['openid.claimed_id'] + = 'http://specs.openid.net/auth/2.0/identifier_select'; + } else { + $params['openid.identity'] = $this->identity; + $params['openid.claimed_id'] = $this->claimed_id; + } + + return $this->build_url(parse_url($this->server), array('query' => http_build_query($params, '', '&'))); + } + + /** + * Returns authentication url. Usually, you want to redirect your user to it. + * @param bool $immediate + * @return String The authentication url. + * @throws ErrorException +*/ + public function authUrl($immediate = false) + { + if ($this->setup_url && !$immediate) { + return $this->setup_url; + } + if (!$this->server) { + $this->discover($this->identity); + } + + if ($this->version == 2) { + return $this->authUrl_v2($immediate); + } + return $this->authUrl_v1($immediate); + } + + /** + * Performs OpenID verification with the OP. + * @return Bool Whether the verification was successful. + * @throws ErrorException + */ + public function validate() + { + # If the request was using immediate mode, a failure may be reported + # by presenting user_setup_url (for 1.1) or reporting + # mode 'setup_needed' (for 2.0). Also catching all modes other than + # id_res, in order to avoid throwing errors. + if (isset($this->data['openid_user_setup_url'])) { + $this->setup_url = $this->data['openid_user_setup_url']; + return false; + } + if ($this->mode != 'id_res') { + return false; + } + + $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity']; + $params = array( + 'openid.assoc_handle' => $this->data['openid_assoc_handle'], + 'openid.signed' => $this->data['openid_signed'], + 'openid.sig' => $this->data['openid_sig'], + ); + + if (isset($this->data['openid_ns'])) { + # We're dealing with an OpenID 2.0 server, so let's set an ns + # Even though we should know location of the endpoint, + # we still need to verify it by discovery, so $server is not set here + $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; + } elseif (isset($this->data['openid_claimed_id']) + && $this->data['openid_claimed_id'] != $this->data['openid_identity'] + ) { + # If it's an OpenID 1 provider, and we've got claimed_id, + # we have to append it to the returnUrl, like authUrl_v1 does. + $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') + . 'openid.claimed_id=' . $this->claimed_id; + } + + if ($this->data['openid_return_to'] != $this->returnUrl) { + # The return_to url must match the url of current request. + # I'm assuming that no one will set the returnUrl to something that doesn't make sense. + return false; + } + + $server = $this->discover($this->claimed_id); + + foreach (explode(',', $this->data['openid_signed']) as $item) { + $value = $this->data['openid_' . str_replace('.', '_', $item)]; + $params['openid.' . $item] = $value; + } + + $params['openid.mode'] = 'check_authentication'; + + $response = $this->request($server, 'POST', $params); + + return preg_match('/is_valid\s*:\s*true/i', $response); + } + + /** + * @return array + */ + protected function getAxAttributes() + { + $result = array(); + + if ($alias = $this->getNamespaceAlias('http://openid.net/srv/ax/1.0', 'ax')) { + $prefix = 'openid_' . $alias; + $length = strlen('http://axschema.org/'); + + foreach (explode(',', $this->data['openid_signed']) as $key) { + $keyMatch = $alias . '.type.'; + + if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) { + continue; + } + + $key = substr($key, strlen($keyMatch)); + $idv = $prefix . '_value_' . $key; + $idc = $prefix . '_count_' . $key; + $key = substr($this->getItem($prefix . '_type_' . $key), $length); + + if (!empty($key)) { + if (($count = intval($this->getItem($idc))) > 0) { + $value = array(); + + for ($i = 1; $i <= $count; $i++) { + $value[] = $this->getItem($idv . '_' . $i); + } + + $value = ($count == 1) ? reset($value) : $value; + } else { + $value = $this->getItem($idv); + } + + if (!is_null($value)) { + $result[$key] = $value; + } + } + } + } else { + // No alias for the AX schema has been found, + // so there is no AX data in the OP's response. + } + + return $result; + } + + /** + * @return array + */ + protected function getSregAttributes() + { + $attributes = array(); + $sreg_to_ax = array_flip(self::$ax_to_sreg); + if ($alias = $this->getNamespaceAlias('http://openid.net/extensions/sreg/1.1', 'sreg')) { + foreach (explode(',', $this->data['openid_signed']) as $key) { + $keyMatch = $alias . '.'; + if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($sreg_to_ax[$key])) { + # The field name isn't part of the SREG spec, so we ignore it. + continue; + } + $attributes[$sreg_to_ax[$key]] = $this->data['openid_' . $alias . '_' . $key]; + } + } + return $attributes; + } + + /** + * Gets AX/SREG attributes provided by OP. should be used only after successful validation. + * Note that it does not guarantee that any of the required/optional parameters will be present, + * or that there will be no other attributes besides those specified. + * In other words. OP may provide whatever information it wants to. + * * SREG names will be mapped to AX names. + * * + * @return array Array of attributes with keys being the AX schema names, e.g. 'contact/email' @see http://www.axschema.org/types/ +*/ + public function getAttributes() + { + if (isset($this->data['openid_ns']) + && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0' + ) { # OpenID 2.0 + # We search for both AX and SREG attributes, with AX taking precedence. + return $this->getAxAttributes() + $this->getSregAttributes(); + } + return $this->getSregAttributes(); + } + + /** + * Gets an OAuth request token if the OpenID+OAuth hybrid protocol has been used. + * + * In order to use the OpenID+OAuth hybrid protocol, you need to add at least one + * scope to the $openid->oauth array before you get the call to getAuthUrl(), e.g.: + * $openid->oauth[] = 'https://www.googleapis.com/auth/plus.me'; + * + * Furthermore the registered consumer name must fit the OpenID realm. + * To register an OpenID consumer at Google use: https://www.google.com/accounts/ManageDomains + * + * @return string|bool OAuth request token on success, FALSE if no token was provided. + */ + public function getOAuthRequestToken() + { + $alias = $this->getNamespaceAlias('http://specs.openid.net/extensions/oauth/1.0'); + + return !empty($alias) ? $this->data['openid_' . $alias . '_request_token'] : false; + } + + /** + * Gets the alias for the specified namespace, if it's present. + * + * @param string $namespace The namespace for which an alias is needed. + * @param string $hint Common alias of this namespace, used for optimization. + * @return string|null The namespace alias if found, otherwise - NULL. + */ + private function getNamespaceAlias($namespace, $hint = null) + { + $result = null; + + if (empty($hint) || $this->getItem('openid_ns_' . $hint) != $namespace) { + // The common alias is either undefined or points to + // some other extension - search for another alias.. + $prefix = 'openid_ns_'; + $length = strlen($prefix); + + foreach ($this->data as $key => $val) { + if (strncmp($key, $prefix, $length) === 0 && $val === $namespace) { + $result = trim(substr($key, $length)); + break; + } + } + } else { + $result = $hint; + } + + return $result; + } + + /** + * Gets an item from the $data array by the specified id. + * + * @param string $id The id of the desired item. + * @return string|null The item if found, otherwise - NULL. + */ + private function getItem($id) + { + return isset($this->data[$id]) ? $this->data[$id] : null; + } +} diff --git a/www/application/third_party/hybridauth/Thirdparty/OpenID/README.md b/www/application/third_party/hybridauth/Thirdparty/OpenID/README.md new file mode 100644 index 00000000..2ff54da9 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/OpenID/README.md @@ -0,0 +1,7 @@ +This file is part of the LightOpenID PHP Library + +LightOpenID is an open source software available under the MIT License. + +https://github.com/iignatov/LightOpenID + +http://opensource.org/licenses/mit-license.php diff --git a/www/application/third_party/hybridauth/Thirdparty/readme.md b/www/application/third_party/hybridauth/Thirdparty/readme.md new file mode 100644 index 00000000..9036e0d3 --- /dev/null +++ b/www/application/third_party/hybridauth/Thirdparty/readme.md @@ -0,0 +1,14 @@ +##### Third party libraries + +Here we include a number of third party libraries. Those libraries are used by the various providers supported by Hybridauth. + +Library | Description +-------- | ------------- +[LightOpenID](https://gitorious.org/lightopenid) | Contain LightOpenID. Solid OpenID library licensed under the MIT License. +[OAuth Library](https://code.google.com/p/oauth/) | Contain OAuth Library licensed under the MIT License. + +Notes: + + We no longer use the old OAuth clients. Please don't add new libs to this folder, unless strictly necessary. + Both LightOpenID and OAuth are (to be) partially/indirectly tested within the Hybridauth library. + Both LightOpenID and OAuth libraries are excluded from Codeclimate.com Analysis/GPA. diff --git a/www/application/third_party/hybridauth/User/Activity.php b/www/application/third_party/hybridauth/User/Activity.php new file mode 100644 index 00000000..caf3c10e --- /dev/null +++ b/www/application/third_party/hybridauth/User/Activity.php @@ -0,0 +1,73 @@ +user = new \stdClass(); + + // typically, we should have a few information about the user who created the event from social apis + $this->user->identifier = null; + $this->user->displayName = null; + $this->user->profileURL = null; + $this->user->photoURL = null; + } + + /** + * Prevent the providers adapters from adding new fields. + * + * @throws UnexpectedValueException + * @var string $name + * + * @var mixed $value + * + */ + public function __set($name, $value) + { + // phpcs:ignore + throw new UnexpectedValueException(sprintf('Adding new property "%s\' to %s is not allowed.', $name, __CLASS__)); + } +} diff --git a/www/application/third_party/hybridauth/User/Contact.php b/www/application/third_party/hybridauth/User/Contact.php new file mode 100644 index 00000000..6118cae8 --- /dev/null +++ b/www/application/third_party/hybridauth/User/Contact.php @@ -0,0 +1,78 @@ +