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>(.*)\1ID>#', $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 @@
+