diff --git a/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php b/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php index 7af054179ce18fc07bc61e925ffa30b88c2b6ca0..931419cd4112d37df8ca12e0dddc6141a57d36ea 100644 --- a/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php +++ b/modules/salesforce_encrypt/src/Rest/EncryptedRestClientInterface.php @@ -2,16 +2,37 @@ namespace Drupal\salesforce_encrypt\Rest; -use Drupal\salesforce\Rest\RestClientInterface; +use Drupal\salesforce\Rest\RestClientBaseInterface; use Drupal\encrypt\Entity\EncryptionProfile; use Drupal\encrypt\EncryptServiceInterface; use Drupal\encrypt\EncryptionProfileManagerInterface; use Drupal\encrypt\EncryptionProfileInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\State\StateInterface; +use GuzzleHttp\ClientInterface; /** * Objects, properties, and methods to communicate with the Salesforce REST API. */ -interface EncryptedRestClientInterface extends RestClientInterface { +interface EncryptedRestClientInterface extends RestClientBaseInterface { + + /** + * Constructor which initializes the consumer. + * + * @param \GuzzleHttp\ClientInterface $http_client + * The GuzzleHttp Client. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + * @param \Drupal\Core\State\StateInterface $state + * The state service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache service. + * @param \Drupal\Component\Serialization\Json $json + * The JSON serializer service. + */ + public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, EncryptServiceInterface $encryption, EncryptionProfileManagerInterface $encryptionProfileManager, LockBackendInterface $lock); /** * Encrypts all sensitive salesforce config values. @@ -49,7 +70,7 @@ interface EncryptedRestClientInterface extends RestClientInterface { * it gets deleted. Check to see if the profile being deleted is the one * assigned for encryption; if so, decrypt our config and disable encryption. * - * @param EncryptionProfileInterface $profile + * @param EncryptionProfileInterface $profile */ public function hookEncryptionProfileDelete(EncryptionProfileInterface $profile); diff --git a/modules/salesforce_encrypt/src/Rest/RestClient.php b/modules/salesforce_encrypt/src/Rest/RestClient.php index 79fdfa6e583c226fdcf841c5c80ef1c12f0a017c..399136d98caca8687a6857187ee27713aa3c466d 100644 --- a/modules/salesforce_encrypt/src/Rest/RestClient.php +++ b/modules/salesforce_encrypt/src/Rest/RestClient.php @@ -13,13 +13,14 @@ use Drupal\encrypt\EncryptServiceInterface; use Drupal\encrypt\EncryptionProfileInterface; use Drupal\encrypt\EncryptionProfileManagerInterface; use Drupal\salesforce\EntityNotFoundException; -use Drupal\salesforce\Rest\RestClient as SalesforceRestClient; +use Drupal\salesforce\Rest\RestClientBase; use GuzzleHttp\ClientInterface; +use Drupal\salesforce_encrypt\Rest\EncryptedRestClientInterface; /** * Objects, properties, and methods to communicate with the Salesforce REST API. */ -class RestClient extends SalesforceRestClient implements EncryptedRestClientInterface { +class RestClient extends RestClientBase implements EncryptedRestClientInterface { use StringTranslationTrait; @@ -30,14 +31,18 @@ class RestClient extends SalesforceRestClient implements EncryptedRestClientInte /** * Constructor which initializes the consumer. * - * @param \Drupal\Core\Http\Client $http_client + * @param \GuzzleHttp\ClientInterface $http_client * The config factory. * @param \Guzzle\Http\ClientInterface $http_client * The config factory. */ public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, EncryptServiceInterface $encryption, EncryptionProfileManagerInterface $encryptionProfileManager, LockBackendInterface $lock) { - parent::__construct($http_client, $config_factory, $state, $cache); + $this->configFactory = $config_factory; + $this->httpClient = $http_client; + $this->config = $this->configFactory->get('salesforce.settings'); + $this->configEditable = $this->configFactory->getEditable('salesforce.settings'); $this->state = $state; + $this->cache = $cache; $this->encryption = $encryption; $this->encryptionProfileId = $state->get('salesforce_encrypt.profile'); $this->encryptionProfileManager = $encryptionProfileManager; @@ -95,7 +100,7 @@ class RestClient extends SalesforceRestClient implements EncryptedRestClientInte $this->setConsumerKey($consumerKey); $this->setConsumerSecret($consumerSecret); - $this->lock->release('salesforce_encrypt'); + $this->lock->release('salesforce_encrypt'); } public function getEncryptionProfile() { diff --git a/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php b/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php index 9ca901664b15fddc0fe06b6b4361a1c29b7a45b9..7919b8c78a426a2ca2ca1508c591e8fce64ce6c3 100644 --- a/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php +++ b/modules/salesforce_encrypt/tests/src/Unit/RestClientTest.php @@ -17,7 +17,6 @@ use Drupal\encrypt\EncryptionProfileInterface; * @coversDefaultClass \Drupal\salesforce_encrypt\Rest\RestClient * @group salesforce */ - class RestClientTest extends UnitTestCase { static $modules = ['key', 'encrypt', 'salesforce', 'salesforce_encrypt']; @@ -27,9 +26,9 @@ class RestClientTest extends UnitTestCase { $this->accessToken = 'foo'; $this->refreshToken = 'bar'; $this->identity = array('zee' => 'bang'); - + $this->httpClient = $this->getMock(Client::CLASS); - $this->configFactory = + $this->configFactory = $this->getMockBuilder(ConfigFactory::CLASS) ->disableOriginalConstructor() ->getMock(); diff --git a/modules/salesforce_pull/src/DeleteHandler.php b/modules/salesforce_pull/src/DeleteHandler.php index 593a119697efdb5ab8ecf45f5b023e2f5be0a963..4b3fd44ca456fdad681d69958409a8eb5a14de9a 100644 --- a/modules/salesforce_pull/src/DeleteHandler.php +++ b/modules/salesforce_pull/src/DeleteHandler.php @@ -5,7 +5,7 @@ namespace Drupal\salesforce_pull; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Utility\Error; -use Drupal\salesforce\Rest\RestClient; +use Drupal\salesforce\Rest\RestClientInterface; use Drupal\salesforce\SFID; use Drupal\salesforce_mapping\MappedObjectStorage; use Drupal\salesforce_mapping\MappingConstants; @@ -23,7 +23,7 @@ class DeleteHandler { /** * Rest client service. * - * @var \Drupal\salesforce\Rest\RestClient + * @var \Drupal\salesforce\Rest\RestClientInterface */ protected $sfapi; @@ -72,7 +72,7 @@ class DeleteHandler { /** * Constructor. * - * @param \Drupal\salesforce\Rest\RestClient $sfapi + * @param \Drupal\salesforce\Rest\RestClientInterface $sfapi * RestClient object. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity Manager service. @@ -81,7 +81,7 @@ class DeleteHandler { * @param Psr\Log\LoggerInterface $logger * Logging service. */ - private function __construct(RestClient $sfapi, EntityTypeManagerInterface $entity_type_manager, StateInterface $state, LoggerInterface $logger, Request $request) { + private function __construct(RestClientInterface $sfapi, EntityTypeManagerInterface $entity_type_manager, StateInterface $state, LoggerInterface $logger, Request $request) { $this->sfapi = $sfapi; $this->etm = $entity_type_manager; $this->mappingStorage = $this->etm->getStorage('salesforce_mapping'); @@ -94,7 +94,7 @@ class DeleteHandler { /** * Chainable instantiation method for class. * - * @param \Drupal\salesforce\Rest\RestClient $sfapi + * @param \Drupal\salesforce\Rest\RestClientInterface $sfapi * RestClient object. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity Manager service. diff --git a/modules/salesforce_pull/src/QueueHandler.php b/modules/salesforce_pull/src/QueueHandler.php index 7cb6fd64b98c8057b5c9cfecf46bcf48952e5605..a3b8141e6d3723aa4f60db5d31a38263cb2ddc2e 100644 --- a/modules/salesforce_pull/src/QueueHandler.php +++ b/modules/salesforce_pull/src/QueueHandler.php @@ -26,7 +26,7 @@ use Symfony\Component\HttpFoundation\Request; class QueueHandler { /** - * @var \Drupal\salesforce\Rest\RestClient + * @var \Drupal\salesforce\Rest\RestClientInterface */ protected $sfapi; @@ -61,7 +61,7 @@ class QueueHandler { protected $request; /** - * @param RestClient $sfapi + * @param RestClientInterface $sfapi * @param array $mappings * @param QueueInterface $queue * @param StateInterface $state @@ -69,7 +69,7 @@ class QueueHandler { * @param EventDispatcherInterface $event_dispatcher * @param Request $request */ - public function __construct(RestClient $sfapi, array $mappings, QueueInterface $queue, StateInterface $state, LoggerInterface $logger, EventDispatcherInterface $event_dispatcher, Request $request) { + public function __construct(RestClientInterface $sfapi, array $mappings, QueueInterface $queue, StateInterface $state, LoggerInterface $logger, EventDispatcherInterface $event_dispatcher, Request $request) { $this->sfapi = $sfapi; $this->queue = $queue; $this->state = $state; @@ -84,7 +84,7 @@ class QueueHandler { /** * Chainable instantiation method for class * - * @param RestClient $sfapi + * @param RestClientInterface $sfapi * @param array $mappings * @param QueueInterface $queue * @param StateInterface $state diff --git a/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php b/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php index 0070dde47cffca398c438c221300549645448c5c..96db0d632818eb84c8ee0380b8f1368cc0c0bdba 100644 --- a/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php +++ b/modules/salesforce_pull/tests/src/Unit/DeleteHandlerTest.php @@ -4,7 +4,6 @@ namespace Drupal\Tests\salesforce_pull\Unit; use Drupal\Core\Entity\Entity; use Drupal\Core\Entity\EntityStorageBase; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Queue\QueueInterface; use Drupal\Core\State\StateInterface; use Drupal\salesforce_mapping\Entity\MappedObjectInterface; use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface; @@ -13,8 +12,6 @@ use Drupal\salesforce_mapping\MappingConstants; use Drupal\salesforce_mapping\SalesforceMappingStorage; use Drupal\salesforce_pull\DeleteHandler; use Drupal\salesforce\Rest\RestClientInterface; -use Drupal\salesforce\SelectQueryResult; -use Drupal\salesforce\SObject; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; use Psr\Log\LoggerInterface; @@ -22,13 +19,12 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\ServerBag; /** - * Test Object instantitation + * Test Object instantitation. * * @group salesforce_pull */ - class DeleteHandlerTest extends UnitTestCase { - static $modules = ['salesforce_pull']; + protected static $modules = ['salesforce_pull']; /** * {@inheritdoc} @@ -37,7 +33,7 @@ class DeleteHandlerTest extends UnitTestCase { parent::setUp(); $result = [ 'totalSize' => 1, - 'done' => true, + 'done' => TRUE, 'deletedRecords' => [ [ 'id' => '1234567890abcde', @@ -48,19 +44,17 @@ class DeleteHandlerTest extends UnitTestCase { ]; $prophecy = $this->prophesize(RestClientInterface::CLASS); - $prophecy->getDeleted(Argument::any(),Argument::any(),Argument::any()) - ->willReturn($result); // revisit + $prophecy->getDeleted(Argument::any(), Argument::any(), Argument::any()) + ->willReturn($result); $this->sfapi = $prophecy->reveal(); - // mock Drupal entity + // Mock Drupal entity. $prophecy = $this->prophesize(Entity::CLASS); - $prophecy->delete()->willReturn(true); + $prophecy->delete()->willReturn(TRUE); $prophecy->id()->willReturn(1); $this->entity = $prophecy->reveal(); $this->mapping = $this->getMock(SalesforceMappingInterface::CLASS); - // ->setMethods(['__get', 'getSalesforceObjectType', 'getPullFieldsArray', 'checkTriggers']) - // ->getMock(); $this->mapping->expects($this->any()) ->method('__get') ->with($this->equalTo('id')) @@ -78,9 +72,9 @@ class DeleteHandlerTest extends UnitTestCase { $this->mapping->expects($this->any()) ->method('checkTriggers') ->with([MappingConstants::SALESFORCE_MAPPING_SYNC_SF_DELETE]) - ->willReturn(true); + ->willReturn(TRUE); - // mock mapped object + // Mock mapped object. $this->entityTypeId = new \stdClass(); $this->entityId = new \stdClass(); $this->entityRef = new \stdClass(); @@ -92,7 +86,7 @@ class DeleteHandlerTest extends UnitTestCase { $this->mappedObject ->expects($this->any()) ->method('delete') - ->willReturn(true); + ->willReturn(TRUE); $this->mappedObject ->expects($this->any()) ->method('getMapping') @@ -100,13 +94,13 @@ class DeleteHandlerTest extends UnitTestCase { $this->mappedObject ->expects($this->any()) ->method('getFieldDefinitions') - ->willReturn(['entity_type_id','entity_id','salesforce_mapping']); + ->willReturn(['entity_type_id', 'entity_id', 'salesforce_mapping']); $this->mappedObject ->expects($this->any()) ->method('getMappedEntity') ->willReturn($this->entity); - // mock mapping ConfigEntityStorage object + // Mock mapping ConfigEntityStorage object. $prophecy = $this->prophesize(SalesforceMappingStorage::CLASS); $prophecy->loadByProperties(Argument::any())->willReturn([$this->mapping]); $prophecy->load(Argument::any())->willReturn($this->mapping); @@ -115,7 +109,7 @@ class DeleteHandlerTest extends UnitTestCase { ]); $this->configStorage = $prophecy->reveal(); - // mock mapped object EntityStorage object + // Mock mapped object EntityStorage object. $this->entityStorage = $this->getMockBuilder(MappedObjectStorage::CLASS) ->disableOriginalConstructor() ->getMock(); @@ -123,35 +117,35 @@ class DeleteHandlerTest extends UnitTestCase { ->method('loadBySfid') ->willReturn([$this->mappedObject]); - // mock Drupal entity EntityStorage object + // Mock Drupal entity EntityStorage object. $prophecy = $this->prophesize(EntityStorageBase::CLASS); $prophecy->load(Argument::any())->willReturn($this->entity); $this->drupalEntityStorage = $prophecy->reveal(); - // mock EntityTypeManagerInterface + // Mock EntityTypeManagerInterface. $prophecy = $this->prophesize(EntityTypeManagerInterface::CLASS); $prophecy->getStorage('salesforce_mapping')->willReturn($this->configStorage); $prophecy->getStorage('salesforce_mapped_object')->willReturn($this->entityStorage); $prophecy->getStorage('test')->willReturn($this->drupalEntityStorage); $this->etm = $prophecy->reveal(); - // mock state + // Mock state. $prophecy = $this->prophesize(StateInterface::CLASS); $prophecy->get('salesforce_pull_last_delete_default', Argument::any())->willReturn('1485787434'); $prophecy->set('salesforce_pull_last_delete_default', Argument::any())->willReturn(null); $this->state = $prophecy->reveal(); - // mock logger + // Mock logger. $prophecy = $this->prophesize(LoggerInterface::CLASS); - $prophecy->log(Argument::any(), Argument::any(), Argument::any())->willReturn(null); + $prophecy->log(Argument::any(), Argument::any(), Argument::any())->willReturn(NULL); $this->logger = $prophecy->reveal(); - // mock server + // Mock server. $prophecy = $this->prophesize(ServerBag::CLASS); $prophecy->get(Argument::any())->willReturn('1485787434'); $this->server = $prophecy->reveal(); - // mock request + // Mock request. $prophecy = $this->prophesize(Request::CLASS); $prophecy->server = $this->server; $this->request = $prophecy->reveal(); @@ -166,17 +160,18 @@ class DeleteHandlerTest extends UnitTestCase { } /** - * Test object instantiation + * Test object creation. */ public function testObject() { $this->assertTrue($this->dh instanceof DeleteHandler); } /** - * Test handler operation, good data + * Test processDeletedRecords. */ public function testGetUpdatedRecords() { $result = $this->dh->processDeletedRecords(); $this->assertTrue($result); } + } diff --git a/src/Rest/RestClient.php b/src/Rest/RestClient.php index 595df49000403bd7bdbbb814e2700acb2726d71f..a26e5353cd78e946c701ac39c962e11243f5c01d 100644 --- a/src/Rest/RestClient.php +++ b/src/Rest/RestClient.php @@ -21,7 +21,7 @@ use GuzzleHttp\Psr7\Response; /** * Objects, properties, and methods to communicate with the Salesforce REST API. */ -class RestClient implements RestClientInterface { +class RestClient extends RestClientBase implements RestClientInterface { /** * Reponse object. @@ -59,7 +59,7 @@ class RestClient implements RestClientInterface { private $config; /** - * editable version of config entity. + * Editable version of config entity. * * @var \Drupal\Core\Config\Config */ @@ -75,7 +75,7 @@ class RestClient implements RestClientInterface { /** * The cache service. * - * @var Drupal\Core\Cache\CacheBackendInterface cache + * @var Drupal\Core\Cache\CacheBackendInterface Scache */ protected $cache; diff --git a/src/Rest/RestClientBase.php b/src/Rest/RestClientBase.php new file mode 100644 index 0000000000000000000000000000000000000000..d18f47b7b57d7f02c8446f916dfde49ebe535c0c --- /dev/null +++ b/src/Rest/RestClientBase.php @@ -0,0 +1,856 @@ +<?php + +namespace Drupal\salesforce\Rest; + +use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Unicode; +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\State\StateInterface; +use Drupal\Core\Url; +use Drupal\salesforce\Rest\RestException; +use Drupal\salesforce\SelectQuery; +use Drupal\salesforce\SelectQueryResult; +use Drupal\salesforce\SFID; +use Drupal\salesforce\SObject; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Response; + +/** + * Objects, properties, and methods to communicate with the Salesforce REST API. + */ +class RestClientBase implements RestClientBaseInterface { + + /** + * Reponse object. + * + * @var \GuzzleHttp\Psr7\Response + */ + public $response; + + /** + * GuzzleHttp client. + * + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * Config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Salesforce API URL. + * + * @var Drupal\Core\Url + */ + protected $url; + + /** + * Salesforce config entity. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + private $config; + + /** + * Editable version of config entity. + * + * @var \Drupal\Core\Config\Config + */ + private $configEditable; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface $state + */ + private $state; + + /** + * The cache service. + * + * @var Drupal\Core\Cache\CacheBackendInterface Scache + */ + protected $cache; + + /** + * The JSON serializer service. + * + * @var \Drupal\Component\Serialization\Json $json + */ + protected $json; + + const CACHE_LIFETIME = 300; + + /** + * Determine if this SF instance is fully configured. + * + * @TODO: Consider making a test API call. + */ + public function isAuthorized() { + return $this->getConsumerKey() && $this->getConsumerSecret() && $this->getRefreshToken(); + } + + /** + * {@inheritdoc} + */ + public function apiCall($path, array $params = [], $method = 'GET', $returnObject = FALSE) { + if (!$this->getAccessToken()) { + $this->refreshToken(); + } + + try { + $this->response = new RestResponse($this->apiHttpRequest($path, $params, $method)); + } + catch (RequestException $e) { + // RequestException gets thrown for any response status but 2XX. + $this->response = $e->getResponse(); + + // Any exceptions besides 401 get bubbled up. + if (!$this->response || $this->response->getStatusCode() != 401) { + throw new RestException($this->response, $e->getMessage()); + } + } + + if ($this->response->getStatusCode() == 401) { + // The session ID or OAuth token used has expired or is invalid: refresh + // token. If refreshToken() throws an exception, or if apiHttpRequest() + // throws anything but a RequestException, let it bubble up. + $this->refreshToken(); + try { + $this->response = new RestResponse($this->apiHttpRequest($path, $params, $method)); + } + catch (RequestException $e) { + $this->response = $e->getResponse(); + throw new RestException($this->response, $e->getMessage()); + } + } + + if (empty($this->response) + || ((int)floor($this->response->getStatusCode() / 100)) != 2) { + throw new RestException($this->response, 'Unknown error occurred during API call'); + } + + if ($returnObject) { + return $this->response; + } + else { + return $this->response->data; + } + } + + /** + * Private helper to issue an SF API request. + * + * @param string $path + * Path to resource. + * @param array $params + * Parameters to provide. + * @param string $method + * Method to initiate the call, such as GET or POST. Defaults to GET. + * + * @return GuzzleHttp\Psr7\Response + */ + protected function apiHttpRequest($path, array $params, $method) { + if (!$this->getAccessToken()) { + throw new \Exception('Missing OAuth Token'); + } + $url = $this->getApiEndPoint() . $path; + + $headers = [ + 'Authorization' => 'OAuth ' . $this->getAccessToken(), + 'Content-type' => 'application/json', + ]; + $data = NULL; + if (!empty($params)) { + $data = $this->json->encode($params); + } + return $this->httpRequest($url, $data, $headers, $method); + } + + /** + * Make the HTTP request. Wrapper around drupal_http_request(). + * + * @param string $url + * Path to make request from. + * @param array $data + * The request body. + * @param array $headers + * Request headers to send as name => value. + * @param string $method + * Method to initiate the call, such as GET or POST. Defaults to GET. + * + * @throws RequestException + * + * @return GuzzleHttp\Psr7\Response + */ + protected function httpRequest($url, $data = NULL, array $headers = [], $method = 'GET') { + // Build the request, including path and headers. Internal use. + return $this->httpClient->$method($url, ['headers' => $headers, 'body' => $data]); + } + + /** + * Extract normalized error information from a RequestException + * + * @param RequestException $e + * @return array + * Error array with keys: + * * message + * * errorCode + * * fields + */ + protected function getErrorData(RequestException $e) { + $response = $e->getResponse(); + $response_body = $response->getBody()->getContents(); + $data = $this->json->decode($response_body); + if (!empty($data[0])) { + $data = $data[0]; + } + return $data; + } + + + /** + * Get the API end point for a given type of the API. + * + * @param string $api_type + * E.g., rest, partner, enterprise. + * + * @return string + * Complete URL endpoint for API access. + */ + public function getApiEndPoint($api_type = 'rest') { + $url = &drupal_static(__FUNCTION__ . $api_type); + if (!isset($url)) { + $identity = $this->getIdentity(); + if (is_string($identity)) { + $url = $identity; + } + elseif (isset($identity['urls'][$api_type])) { + $url = $identity['urls'][$api_type]; + } + $url = str_replace('{version}', $this->config->get('rest_api_version.version'), $url); + } + return $url; + } + + /** + * + */ + public function getConsumerKey() { + return $this->state->get('salesforce.consumer_key'); + } + + /** + * + */ + public function setConsumerKey($value) { + return $this->state->set('salesforce.consumer_key', $value); + } + + /** + * + */ + public function getConsumerSecret() { + return $this->state->get('salesforce.consumer_secret'); + } + + /** + * + */ + public function setConsumerSecret($value) { + return $this->state->set('salesforce.consumer_secret', $value); + } + + /** + * + */ + public function getLoginUrl() { + $login_url = $this->state->get('salesforce.login_url'); + return empty($login_url) ? 'https://login.salesforce.com' : $login_url; + } + + /** + * + */ + public function setLoginUrl($value) { + return $this->state->set('salesforce.login_url', $value); + } + + /** + * Get the SF instance URL. Useful for linking to objects. + */ + public function getInstanceUrl() { + return $this->state->get('salesforce.instance_url'); + } + + /** + * Set the SF instance URL. + * + * @param string $url + * URL to set. + */ + protected function setInstanceUrl($url) { + $this->state->set('salesforce.instance_url', $url); + return $this; + } + + /** + * Get the access token. + */ + public function getAccessToken() { + $access_token = $this->state->get('salesforce.access_token'); + return isset($access_token) && Unicode::strlen($access_token) !== 0 ? $access_token : FALSE; + } + + /** + * Set the access token. + * + * @param string $token + * Access token from Salesforce. + */ + public function setAccessToken($token) { + $this->state->set('salesforce.access_token', $token); + return $this; + } + + /** + * Get refresh token. + */ + protected function getRefreshToken() { + return $this->state->get('salesforce.refresh_token'); + } + + /** + * Set refresh token. + * + * @param string $token + * Refresh token from Salesforce. + */ + protected function setRefreshToken($token) { + $this->state->set('salesforce.refresh_token', $token); + return $this; + } + + /** + * Refresh access token based on the refresh token. + * + * @throws Exception + */ + protected function refreshToken() { + $refresh_token = $this->getRefreshToken(); + if (empty($refresh_token)) { + throw new \Exception(t('There is no refresh token.')); + } + + $data = UrlHelper::buildQuery([ + 'grant_type' => 'refresh_token', + 'refresh_token' => urldecode($refresh_token), + 'client_id' => $this->getConsumerKey(), + 'client_secret' => $this->getConsumerSecret(), + ]); + + $url = $this->getAuthTokenUrl(); + $headers = [ + // This is an undocumented requirement on Salesforce's end. + 'Content-Type' => 'application/x-www-form-urlencoded', + ]; + $response = $this->httpRequest($url, $data, $headers, 'POST'); + + $this->handleAuthResponse($response); + return $this; + } + + /** + * Helper callback for OAuth handshake, and refreshToken() + * + * @param GuzzleHttp\Psr7\Response $response + * Response object from refreshToken or authToken endpoints. + * + * @see SalesforceController::oauthCallback() + * @see self::refreshToken() + */ + public function handleAuthResponse(Response $response) { + if ($response->getStatusCode() != 200) { + throw new \Exception($response->getReasonPhrase(), $response->getStatusCode()); + } + + $data = (new RestResponse($response))->data; + + $this + ->setAccessToken($data['access_token']) + ->initializeIdentity($data['id']) + ->setInstanceUrl($data['instance_url']); + + // Do not overwrite an existing refresh token with an empty value. + if (!empty($data['refresh_token'])) { + $this->setRefreshToken($data['refresh_token']); + } + return $this; + } + + /** + * Retrieve and store the Salesforce identity given an ID url. + * + * @param string $id + * Identity URL. + * + * @throws Exception + */ + public function initializeIdentity($id) { + $headers = [ + 'Authorization' => 'OAuth ' . $this->getAccessToken(), + 'Content-type' => 'application/json', + ]; + $response = $this->httpRequest($id, NULL, $headers); + + if ($response->getStatusCode() != 200) { + throw new \Exception(t('Unable to access identity service.'), $response->getStatusCode()); + } + $data = (new RestResponse($response))->data; + + $this->setIdentity($data); + return $this; + } + + /** + * + */ + protected function setIdentity($data) { + $this->state->set('salesforce.identity', $data); + return $this; + } + + /** + * Return the Salesforce identity, which is stored in a variable. + * + * @return array + * Returns FALSE is no identity has been stored. + */ + public function getIdentity() { + return $this->state->get('salesforce.identity'); + } + + /** + * Helper to build the redirect URL for OAUTH workflow. + * + * @return string + * Redirect URL. + * + * @see Drupal\salesforce\Controller\SalesforceController + */ + public function getAuthCallbackUrl() { + return Url::fromRoute('salesforce.oauth_callback', [], [ + 'absolute' => TRUE, + 'https' => TRUE, + ])->toString(); + } + + /** + * Get Salesforce oauth login endpoint. (OAuth step 1) + * + * @return string + * REST OAuth Login URL. + */ + public function getAuthEndpointUrl() { + return $this->getLoginUrl() . '/services/oauth2/authorize'; + } + + /** + * Get Salesforce oauth token endpoint. (OAuth step 2) + * + * @return string + * REST OAuth Token URL. + */ + public function getAuthTokenUrl() { + return $this->getLoginUrl() . '/services/oauth2/token'; + } + + /** + * @defgroup salesforce_apicalls Wrapper calls around core apiCall() + */ + + /** + * Available objects and their metadata for your organization's data. + * + * @param array $conditions + * Associative array of filters to apply to the returned objects. Filters + * are applied after the list is returned from Salesforce. + * @param bool $reset + * Whether to reset the cache and retrieve a fresh version from Salesforce. + * + * @return array + * Available objects and metadata. + * + * @addtogroup salesforce_apicalls + */ + public function objects(array $conditions = ['updateable' => TRUE], $reset = FALSE) { + $cache = $this->cache->get('salesforce:objects'); + + // Force the recreation of the cache when it is older than 5 minutes. + if ($cache && $this->getRequestTime() < ($cache->created + self::CACHE_LIFETIME) && !$reset) { + $result = $cache->data; + } + else { + $result = $this->apiCall('sobjects'); + $this->cache->set('salesforce:objects', $result, 0, ['salesforce']); + } + + if (!empty($conditions)) { + foreach ($result['sobjects'] as $key => $object) { + foreach ($conditions as $condition => $value) { + if (!$object[$condition] == $value) { + unset($result['sobjects'][$key]); + } + } + } + } + + return $result['sobjects']; + } + + /** + * Use SOQL to get objects based on query string. + * + * @param SelectQuery $query + * The constructed SOQL query. + * + * @return SelectQueryResult + * + * @addtogroup salesforce_apicalls + */ + public function query(SelectQuery $query) { + // $this->moduleHandler->alter('salesforce_query', $query); + // Casting $query as a string calls SelectQuery::__toString(). + return new SelectQueryResult($this->apiCall('query?q=' . (string) $query)); + } + + /** + * Retreieve all the metadata for an object. + * + * @param string $name + * Object type name, E.g., Contact, Account, etc. + * @param bool $reset + * Whether to reset the cache and retrieve a fresh version from Salesforce. + * + * @return RestResponse_Describe + * + * @addtogroup salesforce_apicalls + */ + public function objectDescribe($name, $reset = FALSE) { + if (empty($name)) { + throw new \Exception('No name provided to describe'); + } + + $cache = $this->cache->get('salesforce:object:' . $name); + // Force the recreation of the cache when it is older than 5 minutes. + if ($cache && $this->getRequestTime() < ($cache->created + self::CACHE_LIFETIME) && !$reset) { + return $cache->data; + } + else { + $response = new RestResponse_Describe($this->apiCall("sobjects/{$name}/describe", [], 'GET', TRUE)); + $this->cache->set('salesforce:object:' . $name, $response, 0, ['salesforce']); + return $response; + } + } + + /** + * Create a new object of the given type. + * + * @param string $name + * Object type name, E.g., Contact, Account, etc. + * @param array $params + * Values of the fields to set for the object. + * + * @return Drupal\salesforce\SFID + * + * @addtogroup salesforce_apicalls + */ + public function objectCreate($name, array $params) { + $response = $this->apiCall("sobjects/{$name}", $params, 'POST', TRUE); + $data = $response->data; + return new SFID($data['id']); + } + + /** + * Create new records or update existing records. + * + * The new records or updated records are based on the value of the specified + * field. If the value is not unique, REST API returns a 300 response with + * the list of matching records and throws an Exception. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $key + * The field to check if this record should be created or updated. + * @param string $value + * The value for this record of the field specified for $key. + * @param array $params + * Values of the fields to set for the object. + * + * @return Drupal\salesforce\SFID or NULL + * + * @addtogroup salesforce_apicalls + */ + public function objectUpsert($name, $key, $value, array $params) { + // If key is set, remove from $params to avoid UPSERT errors. + if (isset($params[$key])) { + unset($params[$key]); + } + + $response = $this->apiCall("sobjects/{$name}/{$key}/{$value}", $params, 'PATCH', TRUE); + + // On update, upsert method returns an empty body. Retreive object id, so that we can return a consistent response. + if ($response->getStatusCode() == 204) { + // We need a way to allow callers to distinguish updates and inserts. To + // that end, cache the original response and reset it after fetching the + // ID. + $this->original_response = $response; + $sf_object = $this->objectReadbyExternalId($name, $key, $value); + return $sf_object->id(); + } + $data = $response->data; + return new SFID($data['id']); + } + + /** + * Update an existing object. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $id + * Salesforce id of the object. + * @param array $params + * Values of the fields to set for the object. + * + * @return null + * Update() doesn't return any data. Examine HTTP response or Exception. + * + * @addtogroup salesforce_apicalls + */ + public function objectUpdate($name, $id, array $params) { + $this->apiCall("sobjects/{$name}/{$id}", $params, 'PATCH'); + } + + /** + * Return a full loaded Salesforce object. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $id + * Salesforce id of the object. + * + * @return SObject + * Object of the requested Salesforce object. + * + * @addtogroup salesforce_apicalls + */ + public function objectRead($name, $id) { + return new SObject($this->apiCall("sobjects/{$name}/{$id}")); + } + + /** + * Return a full loaded Salesforce object from External ID. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $field + * Salesforce external id field name. + * @param string $value + * Value of external id. + * + * @return SObject + * Object of the requested Salesforce object. + * + * @addtogroup salesforce_apicalls + */ + public function objectReadbyExternalId($name, $field, $value) { + return new SObject($this->apiCall("sobjects/{$name}/{$field}/{$value}")); + } + + /** + * Delete a Salesforce object. Note: if Object with given $id doesn't exist, + * objectDelete() will assume success unless $throw_exception is given. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $id + * Salesforce id of the object. + * @pararm bool $throw_exception + * (optional) If TRUE, 404 response code will cause RequestException to be + * thrown. Otherwise, hide those errors. Default is FALSE. + * + * @addtogroup salesforce_apicalls + * + * @return null + * Delete() doesn't return any data. Examine HTTP response or Exception. + */ + public function objectDelete($name, $id, $throw_exception = FALSE) { + try { + $this->apiCall("sobjects/{$name}/{$id}", [], 'DELETE'); + } + catch (RequestException $e) { + if ($throw_exception || $e->getResponse()->getStatusCode() != 404) { + throw $e; + } + } + } + + /** + * Retrieves the list of individual objects that have been deleted within the + * given timespan for a specified object type. + * + * @param string $type + * Object type name, E.g., Contact, Account. + * @param string $startDate + * Start date to check for deleted objects (in ISO 8601 format). + * @param string $endDate + * End date to check for deleted objects (in ISO 8601 format). + * @return GetDeletedResult + */ + public function getDeleted($type, $startDate, $endDate) { + return $this->apiCall("sobjects/{$type}/deleted/?start={$startDate}&end={$endDate}"); + } + + /** + * Return a list of available resources for the configured API version. + * + * @return Drupal\salesforce\Rest\RestResponse_Resources + * + * @addtogroup salesforce_apicalls + */ + public function listResources() { + return new RestResponse_Resources($this->apiCall('', [], 'GET', TRUE)); + } + + /** + * Return a list of SFIDs for the given object, which have been created or + * updated in the given timeframe. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * + * @param int $start + * unix timestamp for older timeframe for updates. + * Defaults to "-29 days" if empty. + * + * @param int $end + * unix timestamp for end of timeframe for updates. + * Defaults to now if empty + * + * @return array + * return array has 2 indexes: + * "ids": a list of SFIDs of those records which have been created or + * updated in the given timeframe. + * "latestDateCovered": ISO 8601 format timestamp (UTC) of the last date + * covered in the request. + * + * @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getupdated.htm + * + * @addtogroup salesforce_apicalls + */ + public function getUpdated($name, $start = null, $end = null) { + if (empty($start)) { + $start = strtotime('-29 days'); + } + $start = urlencode(gmdate(DATE_ATOM, $start)); + + if (empty($end)) { + $end = time(); + } + $end = urlencode(gmdate(DATE_ATOM, $end)); + + return $this->apiCall("sobjects/{$name}/updated/?start=$start&end=$end"); + } + + /** + * Retrieve all record types for this org. If $name is provided, retrieve + * record types for the given object type only. + * + * @param string $name + * Object type name, e.g. Contact, Account, etc. + * + * @return array + * If $name is given, an array of record types indexed by developer name. + * Otherwise, an array of record type arrays, indexed by object type name. + */ + public function getRecordTypes($name = NULL, $reset = FALSE) { + $cache = $this->cache->get('salesforce:record_types'); + + // Force the recreation of the cache when it is older than CACHE_LIFETIME + if ($cache && $this->getRequestTime() < ($cache->created + self::CACHE_LIFETIME) && !$reset) { + $record_types = $cache->data; + } + else { + $query = new SelectQuery('RecordType'); + $query->fields = array('Id', 'Name', 'DeveloperName', 'SobjectType'); + $result = $this->query($query); + $record_types = array(); + foreach ($result->records() as $rt) { + $record_types[$rt->field('SobjectType')][$rt->field('DeveloperName')] = $rt; + } + $this->cache->set('salesforce:record_types', $record_types, 0, ['salesforce']); + } + + if ($name != NULL) { + if (!isset($record_types[$name])) { + throw new \Exception("No record types for $name"); + } + return $record_types[$name]; + } + return $record_types; + } + + /** + * Given a DeveloperName and SObject Name, return the SFID of the + * corresponding RecordType. DeveloperName doesn't change between Salesforce + * environments, so it's safer to rely on compared to SFID. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * + * @param string $devname + * RecordType DeveloperName, e.g. Donation, Membership, etc. + * + * @return SFID + * The Salesforce ID of the given Record Type, or null. + * + * @throws Exception if record type not found + */ + public function getRecordTypeIdByDeveloperName($name, $devname, $reset = FALSE) { + $record_types = $this->getRecordTypes(); + if (empty($record_types[$name][$devname])) { + throw new \Exception("No record type $devname for $name"); + } + return $record_types[$name][$devname]->id(); + } + + /** + * Utility function to determine object type for given SFID + * + * @param SFID $id + * @return string + * @throws Exception if SFID doesn't match any object type + */ + public static function getObjectTypeName(SFID $id) { + $prefix = substr((string)$id, 0, 3); + $describe = $this->objects(); + foreach ($describe as $object) { + if ($prefix == $object['keyPrefix']) { + return $object['name']; + } + } + throw new \Exception('No matching object type'); + } + + protected function getRequestTime() { + return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME']; + } + +} diff --git a/src/Rest/RestClientBaseInterface.php b/src/Rest/RestClientBaseInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..d2bfbe5929fe37e0e268774196e7290157226431 --- /dev/null +++ b/src/Rest/RestClientBaseInterface.php @@ -0,0 +1,408 @@ +<?php + +namespace Drupal\salesforce\Rest; + +use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Unicode; +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\State\StateInterface; +use Drupal\Core\Url; +use Drupal\salesforce\SFID; +use Drupal\salesforce\SObject; +use Drupal\salesforce\SelectQuery; +use Drupal\salesforce\SelectQueryResult; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Response; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Objects, properties, and methods to communicate with the Salesforce REST API. + */ +interface RestClientBaseInterface { + + /** + * Determine if this SF instance is fully configured. + * + * @TODO: Consider making a test API call. + */ + public function isAuthorized(); + + /** + * Make a call to the Salesforce REST API. + * + * @param string $path + * Path to resource. + * @param array $params + * Parameters to provide. + * @param string $method + * Method to initiate the call, such as GET or POST. Defaults to GET. + * @param bool $returnObject + * If true, return a Drupal\salesforce\Rest\RestResponse; + * Otherwise, return json-decoded response body only. + * Defaults to FALSE for backwards compatibility. + * + * @return mixed + * + * @throws GuzzleHttp\Exception\RequestException + */ + public function apiCall($path, array $params = [], $method = 'GET', $returnObject = FALSE); + + /** + * Get the API end point for a given type of the API. + * + * @param string $api_type + * E.g., rest, partner, enterprise. + * + * @return string + * Complete URL endpoint for API access. + */ + public function getApiEndPoint($api_type = 'rest'); + + /** + * + */ + public function getConsumerKey(); + + /** + * + */ + public function setConsumerKey($value); + + /** + * + */ + public function getConsumerSecret(); + + /** + * + */ + public function setConsumerSecret($value); + + /** + * + */ + public function getLoginUrl(); + + /** + * + */ + public function setLoginUrl($value); + + /** + * Get the SF instance URL. Useful for linking to objects. + */ + public function getInstanceUrl(); + + /** + * Get the access token. + */ + public function getAccessToken(); + + /** + * Set the access token. + * + * @param string $token + * Access token from Salesforce. + */ + public function setAccessToken($token); + + /** + * Helper callback for OAuth handshake, and refreshToken() + * + * @param GuzzleHttp\Psr7\Response $response + * Response object from refreshToken or authToken endpoints. + * + * @see SalesforceController::oauthCallback() + * @see self::refreshToken() + */ + public function handleAuthResponse(Response $response); + + /** + * Retrieve and store the Salesforce identity given an ID url. + * + * @param string $id + * Identity URL. + * + * @throws Exception + */ + public function initializeIdentity($id); + + /** + * Return the Salesforce identity, which is stored in a variable. + * + * @return array + * Returns FALSE is no identity has been stored. + */ + public function getIdentity(); + + /** + * Helper to build the redirect URL for OAUTH workflow. + * + * @return string + * Redirect URL. + * + * @see Drupal\salesforce\Controller\SalesforceController + */ + public function getAuthCallbackUrl(); + + /** + * Get Salesforce oauth login endpoint. (OAuth step 1) + * + * @return string + * REST OAuth Login URL. + */ + public function getAuthEndpointUrl(); + + /** + * Get Salesforce oauth token endpoint. (OAuth step 2) + * + * @return string + * REST OAuth Token URL. + */ + public function getAuthTokenUrl(); + + /** + * @defgroup salesforce_apicalls Wrapper calls around core apiCall() + */ + + /** + * Available objects and their metadata for your organization's data. + * + * @param array $conditions + * Associative array of filters to apply to the returned objects. Filters + * are applied after the list is returned from Salesforce. + * @param bool $reset + * Whether to reset the cache and retrieve a fresh version from Salesforce. + * + * @return array + * Available objects and metadata. + * + * @addtogroup salesforce_apicalls + */ + public function objects(array $conditions = ['updateable' => TRUE], $reset = FALSE); + + /** + * Use SOQL to get objects based on query string. + * + * @param SelectQuery $query + * The constructed SOQL query. + * + * @return SelectQueryResult + * + * @addtogroup salesforce_apicalls + */ + public function query(SelectQuery $query); + + /** + * Retreieve all the metadata for an object. + * + * @param string $name + * Object type name, E.g., Contact, Account, etc. + * @param bool $reset + * Whether to reset the cache and retrieve a fresh version from Salesforce. + * + * @return RestResponse_Describe + * + * @addtogroup salesforce_apicalls + */ + public function objectDescribe($name, $reset = FALSE); + + /** + * Create a new object of the given type. + * + * @param string $name + * Object type name, E.g., Contact, Account, etc. + * @param array $params + * Values of the fields to set for the object. + * + * @return Drupal\salesforce\SFID + * + * @addtogroup salesforce_apicalls + */ + public function objectCreate($name, array $params); + + /** + * Create new records or update existing records. + * + * The new records or updated records are based on the value of the specified + * field. If the value is not unique, REST API returns a 300 response with + * the list of matching records and throws an Exception. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $key + * The field to check if this record should be created or updated. + * @param string $value + * The value for this record of the field specified for $key. + * @param array $params + * Values of the fields to set for the object. + * + * @return Drupal\salesforce\SFID or NULL + * + * @addtogroup salesforce_apicalls + */ + public function objectUpsert($name, $key, $value, array $params); + + /** + * Update an existing object. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $id + * Salesforce id of the object. + * @param array $params + * Values of the fields to set for the object. + * + * @return null + * Update() doesn't return any data. Examine HTTP response or Exception. + * + * @addtogroup salesforce_apicalls + */ + public function objectUpdate($name, $id, array $params); + + /** + * Return a full loaded Salesforce object. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $id + * Salesforce id of the object. + * + * @return SObject + * Object of the requested Salesforce object. + * + * @addtogroup salesforce_apicalls + */ + public function objectRead($name, $id); + + /** + * Return a full loaded Salesforce object from External ID. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $field + * Salesforce external id field name. + * @param string $value + * Value of external id. + * + * @return SObject + * Object of the requested Salesforce object. + * + * @addtogroup salesforce_apicalls + */ + public function objectReadbyExternalId($name, $field, $value); + + /** + * Delete a Salesforce object. Note: if Object with given $id doesn't exist, + * objectDelete() will assume success unless $throw_exception is given. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * @param string $id + * Salesforce id of the object. + * @pararm bool $throw_exception + * (optional) If TRUE, 404 response code will cause RequestException to be + * thrown. Otherwise, hide those errors. Default is FALSE. + * + * @addtogroup salesforce_apicalls + * + * @return null + * Delete() doesn't return any data. Examine HTTP response or Exception. + */ + public function objectDelete($name, $id, $throw_exception = FALSE); + + /** + * Retrieves the list of individual objects that have been deleted within the + * given timespan for a specified object type. + * + * @param string $type + * Object type name, E.g., Contact, Account. + * @param string $startDate + * Start date to check for deleted objects (in ISO 8601 format). + * @param string $endDate + * End date to check for deleted objects (in ISO 8601 format). + * @return GetDeletedResult + */ + public function getDeleted($type, $startDate, $endDate); + + /** + * Return a list of available resources for the configured API version. + * + * @return Drupal\salesforce\Rest\RestResponse_Resources + * + * @addtogroup salesforce_apicalls + */ + public function listResources(); + + /** + * Return a list of SFIDs for the given object, which have been created or + * updated in the given timeframe. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * + * @param int $start + * unix timestamp for older timeframe for updates. + * Defaults to "-29 days" if empty. + * + * @param int $end + * unix timestamp for end of timeframe for updates. + * Defaults to now if empty + * + * @return array + * return array has 2 indexes: + * "ids": a list of SFIDs of those records which have been created or + * updated in the given timeframe. + * "latestDateCovered": ISO 8601 format timestamp (UTC) of the last date + * covered in the request. + * + * @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getupdated.htm + * + * @addtogroup salesforce_apicalls + */ + public function getUpdated($name, $start = null, $end = null); + + /** + * Retrieve all record types for this org. If $name is provided, retrieve + * record types for the given object type only. + * + * @param string $name + * Object type name, e.g. Contact, Account, etc. + * + * @return array + * If $name is given, an array of record types indexed by developer name. + * Otherwise, an array of record type arrays, indexed by object type name. + */ + public function getRecordTypes($name = NULL); + + /** + * Given a DeveloperName and SObject Name, return the SFID of the + * corresponding RecordType. DeveloperName doesn't change between Salesforce + * environments, so it's safer to rely on compared to SFID. + * + * @param string $name + * Object type name, E.g., Contact, Account. + * + * @param string $devname + * RecordType DeveloperName, e.g. Donation, Membership, etc. + * + * @return SFID + * The Salesforce ID of the given Record Type, or null. + * + * @throws Exception if record type not found + */ + public function getRecordTypeIdByDeveloperName($name, $devname, $reset = FALSE); + + /** + * Utility function to determine object type for given SFID + * + * @param SFID $id + * @return string + * @throws Exception if SFID doesn't match any object type + */ + public static function getObjectTypeName(SFID $id); + +} diff --git a/src/Rest/RestClientInterface.php b/src/Rest/RestClientInterface.php index 09d818942c74e8e77b54ca993d584f32ea5882cc..8f9181a36edaa26c0abef36b13fbcd0ce4009593 100644 --- a/src/Rest/RestClientInterface.php +++ b/src/Rest/RestClientInterface.php @@ -17,408 +17,27 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Response; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\salesforce\Rest\RestClientBaseInterface; /** * Objects, properties, and methods to communicate with the Salesforce REST API. */ -interface RestClientInterface { +interface RestClientInterface extends RestClientBaseInterface { /** * Constructor which initializes the consumer. * - * @param \Drupal\Core\Http\Client $http_client - * The HTTP Client. - * @param \Guzzle\Http\ClientInterface $http_client - * The Guzzle Client. + * @param \GuzzleHttp\ClientInterface $http_client + * The GuzzleHttp Client. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. * @param \Drupal\Core\State\StateInterface $state * The state service. - * @param \Drupal\Core\Cache\CacheBackendInterface cache + * @param \Drupal\Core\Cache\CacheBackendInterface $cache * The cache service. * @param \Drupal\Component\Serialization\Json $json * The JSON serializer service. */ public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, CacheBackendInterface $cache, Json $json); - /** - * Determine if this SF instance is fully configured. - * - * @TODO: Consider making a test API call. - */ - public function isAuthorized(); - - /** - * Make a call to the Salesforce REST API. - * - * @param string $path - * Path to resource. - * @param array $params - * Parameters to provide. - * @param string $method - * Method to initiate the call, such as GET or POST. Defaults to GET. - * @param bool $returnObject - * If true, return a Drupal\salesforce\Rest\RestResponse; - * Otherwise, return json-decoded response body only. - * Defaults to FALSE for backwards compatibility. - * - * @return mixed - * - * @throws GuzzleHttp\Exception\RequestException - */ - public function apiCall($path, array $params = [], $method = 'GET', $returnObject = FALSE); - - /** - * Get the API end point for a given type of the API. - * - * @param string $api_type - * E.g., rest, partner, enterprise. - * - * @return string - * Complete URL endpoint for API access. - */ - public function getApiEndPoint($api_type = 'rest'); - - /** - * - */ - public function getConsumerKey(); - - /** - * - */ - public function setConsumerKey($value); - - /** - * - */ - public function getConsumerSecret(); - - /** - * - */ - public function setConsumerSecret($value); - - /** - * - */ - public function getLoginUrl(); - - /** - * - */ - public function setLoginUrl($value); - - /** - * Get the SF instance URL. Useful for linking to objects. - */ - public function getInstanceUrl(); - - /** - * Get the access token. - */ - public function getAccessToken(); - - /** - * Set the access token. - * - * @param string $token - * Access token from Salesforce. - */ - public function setAccessToken($token); - - /** - * Helper callback for OAuth handshake, and refreshToken() - * - * @param GuzzleHttp\Psr7\Response $response - * Response object from refreshToken or authToken endpoints. - * - * @see SalesforceController::oauthCallback() - * @see self::refreshToken() - */ - public function handleAuthResponse(Response $response); - - /** - * Retrieve and store the Salesforce identity given an ID url. - * - * @param string $id - * Identity URL. - * - * @throws Exception - */ - public function initializeIdentity($id); - - /** - * Return the Salesforce identity, which is stored in a variable. - * - * @return array - * Returns FALSE is no identity has been stored. - */ - public function getIdentity(); - - /** - * Helper to build the redirect URL for OAUTH workflow. - * - * @return string - * Redirect URL. - * - * @see Drupal\salesforce\Controller\SalesforceController - */ - public function getAuthCallbackUrl(); - - /** - * Get Salesforce oauth login endpoint. (OAuth step 1) - * - * @return string - * REST OAuth Login URL. - */ - public function getAuthEndpointUrl(); - - /** - * Get Salesforce oauth token endpoint. (OAuth step 2) - * - * @return string - * REST OAuth Token URL. - */ - public function getAuthTokenUrl(); - - /** - * @defgroup salesforce_apicalls Wrapper calls around core apiCall() - */ - - /** - * Available objects and their metadata for your organization's data. - * - * @param array $conditions - * Associative array of filters to apply to the returned objects. Filters - * are applied after the list is returned from Salesforce. - * @param bool $reset - * Whether to reset the cache and retrieve a fresh version from Salesforce. - * - * @return array - * Available objects and metadata. - * - * @addtogroup salesforce_apicalls - */ - public function objects(array $conditions = ['updateable' => TRUE], $reset = FALSE); - - /** - * Use SOQL to get objects based on query string. - * - * @param SelectQuery $query - * The constructed SOQL query. - * - * @return SelectQueryResult - * - * @addtogroup salesforce_apicalls - */ - public function query(SelectQuery $query); - - /** - * Retreieve all the metadata for an object. - * - * @param string $name - * Object type name, E.g., Contact, Account, etc. - * @param bool $reset - * Whether to reset the cache and retrieve a fresh version from Salesforce. - * - * @return RestResponse_Describe - * - * @addtogroup salesforce_apicalls - */ - public function objectDescribe($name, $reset = FALSE); - - /** - * Create a new object of the given type. - * - * @param string $name - * Object type name, E.g., Contact, Account, etc. - * @param array $params - * Values of the fields to set for the object. - * - * @return Drupal\salesforce\SFID - * - * @addtogroup salesforce_apicalls - */ - public function objectCreate($name, array $params); - - /** - * Create new records or update existing records. - * - * The new records or updated records are based on the value of the specified - * field. If the value is not unique, REST API returns a 300 response with - * the list of matching records and throws an Exception. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * @param string $key - * The field to check if this record should be created or updated. - * @param string $value - * The value for this record of the field specified for $key. - * @param array $params - * Values of the fields to set for the object. - * - * @return Drupal\salesforce\SFID or NULL - * - * @addtogroup salesforce_apicalls - */ - public function objectUpsert($name, $key, $value, array $params); - - /** - * Update an existing object. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * @param string $id - * Salesforce id of the object. - * @param array $params - * Values of the fields to set for the object. - * - * @return null - * Update() doesn't return any data. Examine HTTP response or Exception. - * - * @addtogroup salesforce_apicalls - */ - public function objectUpdate($name, $id, array $params); - - /** - * Return a full loaded Salesforce object. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * @param string $id - * Salesforce id of the object. - * - * @return SObject - * Object of the requested Salesforce object. - * - * @addtogroup salesforce_apicalls - */ - public function objectRead($name, $id); - - /** - * Return a full loaded Salesforce object from External ID. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * @param string $field - * Salesforce external id field name. - * @param string $value - * Value of external id. - * - * @return SObject - * Object of the requested Salesforce object. - * - * @addtogroup salesforce_apicalls - */ - public function objectReadbyExternalId($name, $field, $value); - - /** - * Delete a Salesforce object. Note: if Object with given $id doesn't exist, - * objectDelete() will assume success unless $throw_exception is given. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * @param string $id - * Salesforce id of the object. - * @pararm bool $throw_exception - * (optional) If TRUE, 404 response code will cause RequestException to be - * thrown. Otherwise, hide those errors. Default is FALSE. - * - * @addtogroup salesforce_apicalls - * - * @return null - * Delete() doesn't return any data. Examine HTTP response or Exception. - */ - public function objectDelete($name, $id, $throw_exception = FALSE); - - /** - * Retrieves the list of individual objects that have been deleted within the - * given timespan for a specified object type. - * - * @param string $type - * Object type name, E.g., Contact, Account. - * @param string $startDate - * Start date to check for deleted objects (in ISO 8601 format). - * @param string $endDate - * End date to check for deleted objects (in ISO 8601 format). - * @return GetDeletedResult - */ - public function getDeleted($type, $startDate, $endDate); - - /** - * Return a list of available resources for the configured API version. - * - * @return Drupal\salesforce\Rest\RestResponse_Resources - * - * @addtogroup salesforce_apicalls - */ - public function listResources(); - - /** - * Return a list of SFIDs for the given object, which have been created or - * updated in the given timeframe. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * - * @param int $start - * unix timestamp for older timeframe for updates. - * Defaults to "-29 days" if empty. - * - * @param int $end - * unix timestamp for end of timeframe for updates. - * Defaults to now if empty - * - * @return array - * return array has 2 indexes: - * "ids": a list of SFIDs of those records which have been created or - * updated in the given timeframe. - * "latestDateCovered": ISO 8601 format timestamp (UTC) of the last date - * covered in the request. - * - * @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getupdated.htm - * - * @addtogroup salesforce_apicalls - */ - public function getUpdated($name, $start = null, $end = null); - - /** - * Retrieve all record types for this org. If $name is provided, retrieve - * record types for the given object type only. - * - * @param string $name - * Object type name, e.g. Contact, Account, etc. - * - * @return array - * If $name is given, an array of record types indexed by developer name. - * Otherwise, an array of record type arrays, indexed by object type name. - */ - public function getRecordTypes($name = NULL); - - /** - * Given a DeveloperName and SObject Name, return the SFID of the - * corresponding RecordType. DeveloperName doesn't change between Salesforce - * environments, so it's safer to rely on compared to SFID. - * - * @param string $name - * Object type name, E.g., Contact, Account. - * - * @param string $devname - * RecordType DeveloperName, e.g. Donation, Membership, etc. - * - * @return SFID - * The Salesforce ID of the given Record Type, or null. - * - * @throws Exception if record type not found - */ - public function getRecordTypeIdByDeveloperName($name, $devname, $reset = FALSE); - - /** - * Utility function to determine object type for given SFID - * - * @param SFID $id - * @return string - * @throws Exception if SFID doesn't match any object type - */ - public static function getObjectTypeName(SFID $id); - } diff --git a/tests/src/Unit/RestClientTest.php b/tests/src/Unit/RestClientTest.php index 9500d15458f2386ad9a29b320dc3e3f63a7e92d1..d208ccab6b9f3545e3878cabd2a2f34a05bc0728 100644 --- a/tests/src/Unit/RestClientTest.php +++ b/tests/src/Unit/RestClientTest.php @@ -54,7 +54,7 @@ class RestClientTest extends UnitTestCase { } /** - * @covers ::__constrict + * @covers ::__construct */ private function initClient($methods = NULL) { if (empty($methods)) {