diff --git a/application/Espo/Controllers/ExternalAccount.php b/application/Espo/Controllers/ExternalAccount.php index 3c4b2c887b..3c9454f1f4 100644 --- a/application/Espo/Controllers/ExternalAccount.php +++ b/application/Espo/Controllers/ExternalAccount.php @@ -100,5 +100,26 @@ class ExternalAccount extends \Espo\Core\Controllers\Record return $entity->toArray(); } + + public function actionAuthorizationCode($params, $data, $request) + { + if (!$request->isPost()) { + throw 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(); + } + } + + $service = $this->getRecordService(); + return $service->authorizationCode($integration, $userId, $code); + } } diff --git a/application/Espo/Core/ExternalAccount/ClientFactory.php b/application/Espo/Core/ExternalAccount/ClientFactory.php new file mode 100644 index 0000000000..9769fecc1b --- /dev/null +++ b/application/Espo/Core/ExternalAccount/ClientFactory.php @@ -0,0 +1,80 @@ +entityManager = $entityManager; + $this->metadata = $metadata; + $this->config = $config; + } + + protected function getMetadata() + { + return $this->metadata; + } + + protected function getEntityManager() + { + return $this->entityManager; + } + + protected function getConfig() + { + return $this->config; + } + + public function create($integration, $userId) + { + $authMethod = $this->getMetadata()->get("integrations.{$integration}.authMethod"); + $methodName = 'create' . ucfirst($authMethod); + return $this->$methodName($integration, $userId); + } + + protected function createOAuth2($integration, $userId) + { + $integrationEntity = $this->getEntityManager()->getEntity('Integration', $integration); + $externalAccountEntity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId); + + $className = $this->getMetadata()->get("integrations.{$integration}.clientClassName"); + + $redirectUri = $this->getConfig()->get('siteUrl') . '/oauthcallback'; // TODO move to client class + + if (!$externalAccountEntity) { + throw new Error("External Account {$integration} not found for {$userId}"); + } + + if (!$integrationEntity->get('enabled')) { + return null; + } + if (!$externalAccountEntity->get('enabled')) { + return null; + } + + $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"), + 'clientId' => $integrationEntity->get('clientId'), + 'clientSecret' => $integrationEntity->get('clientSecret'), + 'redirectUri' => $redirectUri, + 'accessToken' => $externalAccountEntity->get('accessToken'), + 'refreshToken' => $externalAccountEntity->get('refreshToken'), + )); + + return $client; + + } +} + diff --git a/application/Espo/Core/ExternalAccount/Clients/Google.php b/application/Espo/Core/ExternalAccount/Clients/Google.php new file mode 100644 index 0000000000..7a9444a162 --- /dev/null +++ b/application/Espo/Core/ExternalAccount/Clients/Google.php @@ -0,0 +1,10 @@ +client = $client; + + $this->setParams($params); + } + + public function getParam($name) + { + if (in_array($name, $this->paramList)) { + return $this->$name; + } + } + + public function setParam($name, $value) + { + if (in_array($name, $this->paramList)) { + $methodName = 'set' . ucfirst($name); + if (method_exists($this->client, $methodName)) { + $this->client->$methodName($value); + } + $this->$name = $value; + } + } + + public function setParams($params) + { + foreach ($this->paramList as $name) { + if (!empty($params[$name])) { + $this->setParam($name, $params[$name]); + } + } + } + + public function getAccessTokenFromAuthorizationCode($code) + { + return $this->client->getAccessToken($this->getParam('tokenEndpoint'), Client::GRANT_TYPE_AUTHORIZATION_CODE, array( + 'code' => $code, + 'redirect_uri' => $this->getParam('redirectUri') + )); + } +} + diff --git a/application/Espo/Core/ExternalAccount/OAuth2/Client.php b/application/Espo/Core/ExternalAccount/OAuth2/Client.php new file mode 100644 index 0000000000..8676d76491 --- /dev/null +++ b/application/Espo/Core/ExternalAccount/OAuth2/Client.php @@ -0,0 +1,215 @@ +clientId = $clientId; + } + + public function setClientSecret($clientSecret) + { + $this->clientSecret = $clientSecret; + } + + public function setAccessToken($accessToken) + { + $this->accessToken = $accessToken; + } + + public function setAuthType($authType) + { + $this->authType = $authType; + } + + public function setCertificateFile($certificateFile) + { + $this->certificateFile = $certificateFile; + } + + public function setCurlOption($option, $value) + { + $this->curlOptions[$option] = $value; + } + + public function setCurlOptions($options) + { + $this->curlOptions = array_merge($this->curlOptions, $options); + } + + public function setAccessTokenType($accessTokenType) + { + $this->accessTokenType = $accessTokenType; + } + + public function setAccessTokenSecret($accessTokenSecret) + { + $this->accessTokenSecret = $accessTokenSecret; + } + + public function fetch($url, $params = array(), $httpMethod = self::HTTP_METHOD_GET, array $httpHeaders = array(), $contentType = self::CONTENT_TYPE_MULTIPART) + { + if ($this->accessToken) { + switch ($this->accessTokenType) { + case self::ACCESS_TOKEN_TYPE_URI: + $params[$this->accessTokenParamName] = $this->accessToken; + break; + case self::ACCESS_TOKEN_TYPE_BEARER: + $httpHeaders['Authorization'] = 'Bearer ' . $this->accessToken; + break; + case self::ACCESS_TOKEN_TYPE_OAUTH: + $httpHeaders['Authorization'] = 'OAuth ' . $this->accessToken; + break; + default: + throw new \Exception('Unknown access token type.'); + + } + } + return $this->execute($url, $params, $httpMethod, $httpHeaders, $contentType); + } + + private function execute($url, $params = array(), $httpMethod, array $httpHeaders = array(), $contentType = self::CONTENT_TYPE_MULTIPART) + { + $curlOptions = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_CUSTOMREQUEST => $httpMethod + ); + + switch ($httpMethod) { + case self::HTTP_METHOD_POST: + $curlOptions[CURLOPT_POST] = true; + case self::HTTP_METHOD_PUT: + case self::HTTP_METHOD_PATCH: + if (self::CONTENT_TYPE_APPLICATION === $contentType) { + $postFields = http_build_query($params, null, '&'); + } + $curlOptions[CURLOPT_POSTFIELDS] = $postFields; + break; + case self::HTTP_METHOD_HEAD: + $curlOptions[CURLOPT_NOBODY] = true; + case self::HTTP_METHOD_DELETE: + case self::HTTP_METHOD_GET: + $url .= '?' . http_build_query($parameters, null, '&'); + break; + default: + break; + } + + $curlOptions[CURLOPT_URL] = $url; + + $curlOptHttpHeader = array(); + foreach ($httpHeaders as $key => $value) { + $curlOptHttpHeader[] = "{$key}: {$parsed_urlvalue}"; + } + $curlOptions[CURLOPT_HTTPHEADER] = $curlOptHttpHeader; + + $curlResource = curl_init(); + curl_setopt_array($curlResource, $curlOptions); + + 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); + } else { + curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curlResource, CURLOPT_SSL_VERIFYHOST, 0); + } + + if (!empty($this->curlOptions)) { + curl_setopt_array($curlResource, $this->curlOptions); + } + + + $result = curl_exec($curlResource); + $httpCode = curl_getinfo($curlResource, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($curlResource, CURLINFO_CONTENT_TYPE); + $resultArray = null; + if ($curlError = curl_error($curlResource)) { + throw new \Exception($curlError); + } else { + $resultArray = json_decode($result, true); + } + curl_close($curlResource); + + return array( + 'result' => (null !== $resultArray) ? $resultArray: $result, + 'code' => $httpCode, + 'contentType' => $contentType + ); + } + + public function getAccessToken($url, $grantType, array $params) + { + $params['grant_type'] = $grantType; + + $httpHeaders = array(); + switch ($this->clientAuth) { + case self::AUTH_TYPE_URI: + case self::AUTH_TYPE_FORM: + $params['client_id'] = $this->clientId; + $params['client_secret'] = $this->clientSecret; + break; + case self::AUTH_TYPE_AUTHORIZATION_BASIC: + $params['client_id'] = $this->clientId; + $httpHeaders['Authorization'] = 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret); + break; + default: + throw new \Exception(); + } + + return $this->execute($url, $params, self::HTTP_METHOD_POST, $httpHeaders, self::CONTENT_TYPE_APPLICATION); + } +} + diff --git a/application/Espo/Resources/metadata/integrations/Google.json b/application/Espo/Resources/metadata/integrations/Google.json index c16428360d..c23548a3fe 100644 --- a/application/Espo/Resources/metadata/integrations/Google.json +++ b/application/Espo/Resources/metadata/integrations/Google.json @@ -12,9 +12,11 @@ } }, "params": { - "url": "https://accounts.google.com/o/oauth2/auth", + "endpoint": "https://accounts.google.com/o/oauth2/auth", + "tokenEndpoint": "https://accounts.google.com/o/oauth2/token", "scope": "https://www.googleapis.com/auth/calendar" }, "allowUserAccounts": true, - "authMethod": "OAuth2" + "authMethod": "OAuth2", + "clientClassName": "\\Espo\\Core\\ExternalAccount\\Clients\\Google" } diff --git a/application/Espo/Services/ExternalAccount.php b/application/Espo/Services/ExternalAccount.php new file mode 100644 index 0000000000..836db3655b --- /dev/null +++ b/application/Espo/Services/ExternalAccount.php @@ -0,0 +1,69 @@ +getEntityManager()->getEntity('Integration', $integration); + + if (!$integrationEntity) { + throw new NotFound(); + } + $d = $integrationEntity->toArray(); + + if (!$integrationEntity->get('enabled')) { + throw new Error("{$integration} is disabled."); + } + + $factory = new \Espo\Core\ExternalAccount\ClientFactory($this->getEntityManager(), $this->getMetadata(), $this->getConfig()); + return $factory->create($integration, $id); + } + + public function authorizationCode($integration, $userId, $code) + { + $entity = $this->getEntityManager()->getEntity('ExternalAccount', $integration . '__' . $userId); + + + + $client = $this->getClient($integration, $userId); + if ($client instanceof \Espo\Core\ExternalAccount\Clients\OAuth2Abstract) { + $result = $client->getAccessTokenFromAuthorizationCode($code); + + print_r($result); + die; + + return $result; + } else { + throw new Error("Could not load client for {$integration}."); + } + } +} + diff --git a/frontend/client/src/views/external-account/oauth2.js b/frontend/client/src/views/external-account/oauth2.js index 9dab37f020..bda2803ddd 100644 --- a/frontend/client/src/views/external-account/oauth2.js +++ b/frontend/client/src/views/external-account/oauth2.js @@ -188,26 +188,21 @@ Espo.define('Views.ExternalAccount.OAuth2', 'View', function (Dep) { path += '?' + arr.join('&'); var parseUrl = function (str) { - var accessToken = false; - var expires = false; + var code = null; - str = str.substr(str.indexOf('#') + 1, str.length); + str = str.substr(str.indexOf('?') + 1, str.length); str.split('&').forEach(function (part) { var arr = part.split('='); var name = decodeURI(arr[0]); var value = decodeURI(arr[1] || ''); - if (name == 'access_token') { - accessToken = value; - } - if (name == 'expires') { - expires = value; + if (name == 'code') { + code = value; } }, this); - if (accessToken) { + if (code) { return { - accessToken: accessToken, - expires: expires + code: code, } } } @@ -217,28 +212,41 @@ Espo.define('Views.ExternalAccount.OAuth2', 'View', function (Dep) { if (popup.closed) { window.clearInterval(interval); } else { - var res = parseUrl(popup.location.href.toString()); - callback.call(self, res.accessToken, res.expires); - popup.close(); + var res = parseUrl(popup.location.href.toString()); + if (res) { + callback.call(self, res); + popup.close(); + window.clearInterval(interval); + } } }, 500); }, connect: function () { this.popup({ - path: this.getMetadata().get('integrations.' + this.integration + '.params.url'), + path: this.getMetadata().get('integrations.' + this.integration + '.params.endpoint'), params: { client_id: this.clientId, redirect_uri: this.redirectUri, scope: this.getMetadata().get('integrations.' + this.integration + '.params.scope'), - response_type: 'token' + response_type: 'code', + access_type: 'offline' } - }, function (accessToken, expires) { - if (accessToken) { - this.model.set('accessToken', accessToken); - this.model.set('expires', expires); - this.model.save(); - // TODO show Connected and Disconnect button + }, function (res) { + if (res.code) { + $.ajax({ + url: 'ExternalAccount/action/authorizationCode', + type: 'POST', + data: JSON.stringify({ + 'id': this.id, + 'code': res.code + }) + }).done(function () { + + // TODO show Connected and Disconnect button + + }.bind(this)); + } else { this.notify('Error occured', 'error'); }