diff --git a/application/Espo/Controllers/ExternalAccount.php b/application/Espo/Controllers/ExternalAccount.php index 731804c427..38427406d1 100644 --- a/application/Espo/Controllers/ExternalAccount.php +++ b/application/Espo/Controllers/ExternalAccount.php @@ -45,35 +45,33 @@ class ExternalAccount extends \Espo\Core\Controllers\Record ); } - public function actionGetOAuth2Credentials($params, $data, $request) + public function actionGetOAuth2Info($params, $data, $request) { $id = $request->get('id'); list($integration, $userId) = explode('__', $id); - if (!$this->getUser()->isAdmin()) { - if ($this->getUser()->id != $userId) { - throw new Forbidden(); - } - } + + if ($this->getUser()->id != $userId) { + throw new Forbidden(); + } $entity = $this->getEntityManager()->getEntity('Integration', $integration); if ($entity) { return array( 'clientId' => $entity->get('clientId'), - 'redirectUri' => $this->getConfig()->get('siteUrl') . '/oauthcallback' + 'redirectUri' => $this->getConfig()->get('siteUrl') . '/oauthcallback', + 'isConnected' => $this->getRecordService()->ping($integration, $userId) ); } } public function actionRead($params, $data, $request) { - list($integration, $userId) = explode('__', $params['id']); - - if (!$this->getUser()->isAdmin()) { - if ($this->getUser()->id != $userId) { - throw new Forbidden(); - } - } + list($integration, $userId) = explode('__', $params['id']); + + if ($this->getUser()->id != $userId) { + throw new Forbidden(); + } $entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']); return $entity->toArray(); @@ -88,12 +86,15 @@ class ExternalAccount extends \Espo\Core\Controllers\Record { list($integration, $userId) = explode('__', $params['id']); - if (!$this->getUser()->isAdmin()) { - if ($this->getUser()->id != $userId) { - throw new Forbidden(); - } + + if ($this->getUser()->id != $userId) { + throw new Forbidden(); } + if (isset($data['enabled']) && !$data['enabled']) { + $data['data'] = null; + } + $entity = $this->getEntityManager()->getEntity('ExternalAccount', $params['id']); $entity->set($data); $this->getEntityManager()->saveEntity($entity); @@ -104,18 +105,16 @@ class ExternalAccount extends \Espo\Core\Controllers\Record public function actionAuthorizationCode($params, $data, $request) { if (!$request->isPost()) { - throw Error('Bad HTTP method type.'); + throw new Error('Bad HTTP method type.'); } $id = $data['id']; $code = $data['code']; - list($integration, $userId) = explode('__', $id); - - if (!$this->getUser()->isAdmin()) { - if ($this->getUser()->id != $userId) { - throw new Forbidden(); - } + list($integration, $userId) = explode('__', $id); + + if ($this->getUser()->id != $userId) { + throw new Forbidden(); } $service = $this->getRecordService(); diff --git a/application/Espo/Core/ExternalAccount/ClientFactory.php b/application/Espo/Core/ExternalAccount/ClientManager.php similarity index 69% rename from application/Espo/Core/ExternalAccount/ClientFactory.php rename to application/Espo/Core/ExternalAccount/ClientManager.php index 3f597560e7..5a49b1e5b9 100644 --- a/application/Espo/Core/ExternalAccount/ClientFactory.php +++ b/application/Espo/Core/ExternalAccount/ClientManager.php @@ -6,12 +6,14 @@ use \Espo\Core\Exceptions\Error; use \Espo\Core\Exceptions\Forbidden; use \Espo\Core\Exceptions\NotFound; -class ClientFactory +class ClientManager { protected $entityManager; protected $metadata; + protected $clientMap = array(); + public function __construct($entityManager, $metadata, $config) { $this->entityManager = $entityManager; @@ -34,6 +36,16 @@ class ClientFactory return $this->config; } + public function storeAccessToken($hash, $data) + { + if (!empty($this->clientMap[$hash]) && !empty($this->clientMap[$hash]['externalAccountEntity'])) { + $externalAccountEntity = $this->clientMap[$hash]['externalAccountEntity']; + $externalAccountEntity->set('accessToken', $data['accessToken']); + $externalAccountEntity->set('tokenType', $data['tokenType']); + $this->getEntityManager()->saveEntity($externalAccountEntity); + } + } + public function create($integration, $userId) { $authMethod = $this->getMetadata()->get("integrations.{$integration}.authMethod"); @@ -61,10 +73,8 @@ class ClientFactory return null; } - $oauth2Client = new \Espo\Core\ExternalAccount\OAuth2\Client(); - - // TODO listen to client for token regresh - + $oauth2Client = new \Espo\Core\ExternalAccount\OAuth2\Client(); + $client = new $className($oauth2Client, array( 'endpoint' => $this->getMetadata()->get("integrations.{$integration}.params.endpoint"), 'tokenEndpoint' => $this->getMetadata()->get("integrations.{$integration}.params.tokenEndpoint"), @@ -74,10 +84,22 @@ class ClientFactory 'accessToken' => $externalAccountEntity->get('accessToken'), 'refreshToken' => $externalAccountEntity->get('refreshToken'), 'tokenType' => $externalAccountEntity->get('tokenType'), - )); + ), $this); - return $client; + $this->addToClientMap($client, $integrationEntity, $externalAccountEntity, $userId); + return $client; + } + + protected function addToClientMap($client, $integrationEntity, $externalAccountEntity, $userId) + { + $this->clientMap[spl_object_hash($client)] = array( + 'client' => $client, + 'userId' => $userId, + 'integration' => $integrationEntity->id, + 'integrationEntity' => $integrationEntity, + 'externalAccountEntity' => $externalAccountEntity, + ); } } diff --git a/application/Espo/Core/ExternalAccount/Clients/Google.php b/application/Espo/Core/ExternalAccount/Clients/Google.php index 7a9444a162..295bd3c5af 100644 --- a/application/Espo/Core/ExternalAccount/Clients/Google.php +++ b/application/Espo/Core/ExternalAccount/Clients/Google.php @@ -6,5 +6,9 @@ use \Espo\Core\Exceptions\Error; class Google extends OAuth2Abstract { - + protected function getPingUrl() + { + return 'https://www.googleapis.com/calendar/v3/users/me/calendarList'; + } } + diff --git a/application/Espo/Core/ExternalAccount/Clients/IClient.php b/application/Espo/Core/ExternalAccount/Clients/IClient.php new file mode 100644 index 0000000000..9ac47668ab --- /dev/null +++ b/application/Espo/Core/ExternalAccount/Clients/IClient.php @@ -0,0 +1,15 @@ +client = $client; $this->setParams($params); + + $this->manager = $manager; } public function getParam($name) @@ -55,7 +60,7 @@ abstract class OAuth2Abstract } } - public function setParams($params) + public function setParams(array $params) { foreach ($this->paramList as $name) { if (!empty($params[$name])) { @@ -64,13 +69,22 @@ abstract class OAuth2Abstract } } + protected function afterTokenRefreshed($data) + { + if ($this->manager) { + $this->manager->storeAccessToken(spl_object_hash($this), $data); + } + } + public function getAccessTokenFromAuthorizationCode($code) { $r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, array( 'code' => $code, 'redirect_uri' => $this->getParam('redirectUri') )); - if ($r['code'] == '200') { + + + if ($r['code'] == 200) { $data = array(); if (!empty($r['result'])) { $data['accessToken'] = $r['result']['access_token']; @@ -80,6 +94,91 @@ abstract class OAuth2Abstract return $data; } return null; - } + } + + abstract protected function getPingUrl(); + + public function ping() + { + if (empty($this->accessToken) || empty($this->clientId) || empty($this->clientSecret)) { + return false; + } + + $url = $this->getPingUrl(); + + try { + $this->request($url); + return true; + } catch (\Exception $e) { + return false; + } + } + + public function request($url, $params = array(), $httpMethod = Client::HTTP_METHOD_GET, $allowRenew = true) + { + $r = $this->client->request($url, $params, $httpMethod); + + if ($r['code'] == 200) { + return $r['result']; + } else { + $handledData = $this->handleErrorResponse($r); + + if ($allowRenew && is_array($handledData)) { + if ($handledData['action'] == 'refreshToken') { + if ($this->refreshToken()) { + return $this->request($url, $params, $httpMethod, false); + } + } else if ($handledData['action'] == 'renew') { + return $this->request($url, $params, $httpMethod, false); + } + } + } + + throw new Error("Error after requesting {$httpMethod} {$url}."); + } + + protected function refreshToken() + { + if (!empty($this->refreshToken)) { + $r = $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_REFRESH_TOKEN, array( + 'refresh_token' => $this->refreshToken, + )); + if ($r['code'] == 200) { + if (is_array($r['result'])) { + if (!empty($r['result']['access_token'])) { + $data = array(); + $data['accessToken'] = $r['result']['access_token']; + $data['tokenType'] = $r['result']['token_type']; + + $this->setParams($data); + $this->afterTokenRefreshed($data); + return true; + } + } + } + } + } + + protected function handleErrorResponse($r) + { + if ($r['code'] == 401 && !empty($r['result'])) { + $result = $r['result']; + if (strpos($r['header'], 'error=invalid_token') !== false) { + return array( + 'action' => 'refreshToken' + ); + } else { + return array( + 'action' => 'renew' + ); + } + } else if ($r['code'] == 400 && !empty($r['result'])) { + if ($r['result']['error'] == 'invalid_token') { + return array( + 'action' => 'refreshToken' + ); + } + } + } } diff --git a/application/Espo/Core/ExternalAccount/OAuth2/Client.php b/application/Espo/Core/ExternalAccount/OAuth2/Client.php index d71d6e4251..a54f4d9c93 100644 --- a/application/Espo/Core/ExternalAccount/OAuth2/Client.php +++ b/application/Espo/Core/ExternalAccount/OAuth2/Client.php @@ -98,7 +98,7 @@ class Client $this->accessTokenSecret = $accessTokenSecret; } - public function fetch($url, $params = array(), $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = array(), $contentType = self::CONTENT_TYPE_MULTIPART) + public function request($url, $params = array(), $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = array(), $contentType = self::CONTENT_TYPE_MULTIPART) { if ($this->accessToken) { switch ($this->tokenType) { @@ -116,6 +116,7 @@ class Client } } + return $this->execute($url, $params, $httpMethod, $httpHeaders, $contentType); } @@ -141,7 +142,10 @@ class Client $curlOptions[CURLOPT_NOBODY] = true; case self::HTTP_METHOD_DELETE: case self::HTTP_METHOD_GET: - $url .= '?' . http_build_query($parameters, null, '&'); + if (strpos($url, '?') === false) { + $url .= '?'; + } + $url .= http_build_query($params, null, '&'); break; default: break; @@ -151,42 +155,51 @@ class Client $curlOptHttpHeader = array(); foreach ($httpHeaders as $key => $value) { - $curlOptHttpHeader[] = "{$key}: {$parsed_urlvalue}"; + $curlOptHttpHeader[] = "{$key}: {$value}"; } $curlOptions[CURLOPT_HTTPHEADER] = $curlOptHttpHeader; - $curlResource = curl_init(); - curl_setopt_array($curlResource, $curlOptions); + $ch = curl_init(); + curl_setopt_array($ch, $curlOptions); + + curl_setopt($ch, CURLOPT_HEADER, 1); if (!empty($this->certificateFile)) { - curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($curlResource, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($curlResource, CURLOPT_CAINFO, $this->certificateFile); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_CAINFO, $this->certificateFile); } else { - curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($curlResource, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } if (!empty($this->curlOptions)) { - curl_setopt_array($curlResource, $this->curlOptions); + curl_setopt_array($ch, $this->curlOptions); } - - $result = curl_exec($curlResource); - $httpCode = curl_getinfo($curlResource, CURLINFO_HTTP_CODE); - $contentType = curl_getinfo($curlResource, CURLINFO_CONTENT_TYPE); + $response = curl_exec($ch); + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + + $responceHeader = substr($response, 0, $headerSize); + $responceBody = substr($response, $headerSize); + $resultArray = null; - if ($curlError = curl_error($curlResource)) { + + if ($curlError = curl_error($ch)) { throw new \Exception($curlError); } else { - $resultArray = json_decode($result, true); + $resultArray = json_decode($responceBody, true); } - curl_close($curlResource); + curl_close($ch); return array( - 'result' => (null !== $resultArray) ? $resultArray: $result, - 'code' => $httpCode, - 'contentType' => $contentType + 'result' => (null !== $resultArray) ? $resultArray: $responceBody, + 'code' => intval($httpCode), + 'contentType' => $contentType, + 'header' => $responceHeader, ); } diff --git a/application/Espo/Entities/Integration.php b/application/Espo/Entities/Integration.php index 36ad7ab68a..011b58cce1 100644 --- a/application/Espo/Entities/Integration.php +++ b/application/Espo/Entities/Integration.php @@ -80,6 +80,9 @@ class Integration extends \Espo\Core\ORM\Entity if ($this->hasField($name)) { $this->valuesContainer[$name] = $value; } else { + if (!$this->get('enabled')) { + return; + } $data = json_decode($this->get('data'), true); if (empty($data)) { $data = array(); diff --git a/application/Espo/Resources/metadata/integrations/Google.json b/application/Espo/Resources/metadata/integrations/Google.json index c23548a3fe..26b2502c36 100644 --- a/application/Espo/Resources/metadata/integrations/Google.json +++ b/application/Espo/Resources/metadata/integrations/Google.json @@ -14,7 +14,7 @@ "params": { "endpoint": "https://accounts.google.com/o/oauth2/auth", "tokenEndpoint": "https://accounts.google.com/o/oauth2/token", - "scope": "https://www.googleapis.com/auth/calendar" + "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/calendar" }, "allowUserAccounts": true, "authMethod": "OAuth2", diff --git a/application/Espo/Services/ExternalAccount.php b/application/Espo/Services/ExternalAccount.php index c8c2e670d9..ff01f6144b 100644 --- a/application/Espo/Services/ExternalAccount.php +++ b/application/Espo/Services/ExternalAccount.php @@ -43,13 +43,29 @@ class ExternalAccount extends Record throw new Error("{$integration} is disabled."); } - $factory = new \Espo\Core\ExternalAccount\ClientFactory($this->getEntityManager(), $this->getMetadata(), $this->getConfig()); + $factory = new \Espo\Core\ExternalAccount\ClientManager($this->getEntityManager(), $this->getMetadata(), $this->getConfig()); return $factory->create($integration, $id); } + public function getExternalAccountEntity($integration, $userId) + { + return $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId); + } + + public function ping($integration, $userId) + { + $entity = $this->getExternalAccountEntity($integration, $userId); + try { + $client = $this->getClient($integration, $userId); + if ($client) { + return $client->ping(); + } + } catch (\Exception $e) {} + } + public function authorizationCode($integration, $userId, $code) { - $entity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId); + $entity = $this->getExternalAccountEntity($integration, $userId); $client = $this->getClient($integration, $userId); if ($client instanceof \Espo\Core\ExternalAccount\Clients\OAuth2Abstract) { diff --git a/frontend/client/src/views/external-account/oauth2.js b/frontend/client/src/views/external-account/oauth2.js index bd66e8a090..09282d3487 100644 --- a/frontend/client/src/views/external-account/oauth2.js +++ b/frontend/client/src/views/external-account/oauth2.js @@ -84,7 +84,7 @@ Espo.define('Views.ExternalAccount.OAuth2', 'View', function (Dep) { this.createFieldView('bool', 'enabled'); $.ajax({ - url: 'ExternalAccount/action/getOAuth2Credentials?id=' + this.id, + url: 'ExternalAccount/action/getOAuth2Info?id=' + this.id, dataType: 'json' }).done(function (respose) { this.clientId = respose.clientId; @@ -213,7 +213,8 @@ Espo.define('Views.ExternalAccount.OAuth2', 'View', function (Dep) { window.clearInterval(interval); } else { var res = parseUrl(popup.location.href.toString()); - if (res) { + if (res) { + console.log('test'); callback.call(self, res); popup.close(); window.clearInterval(interval); @@ -244,7 +245,7 @@ Espo.define('Views.ExternalAccount.OAuth2', 'View', function (Dep) { }), dataType: 'json' }).done(function (response) { - if (response) { + if (response === true) { console.log('connected'); } // TODO show Connected and Disconnect button