Newer
Older
<?php
/**
* @file
* Contains \Drupal\salesforce\RestClient.
*/
namespace Drupal\salesforce;

Aaron Bauman
committed
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\CacheBackendInterface;

Aaron Bauman
committed
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;

Aaron Bauman
committed
use Drupal\Core\State\StateInterface;

Aaron Bauman
committed
use Drupal\salesforce\SelectQuery;
use GuzzleHttp\Exception\RequestException;

Aaron Bauman
committed
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Serialization\Json;
/**
* Objects, properties, and methods to communicate with the Salesforce REST API.
*/
class RestClient {
public $response;
protected $httpClient;
protected $configFactory;
protected $urlGenerator;
private $config;
private $configEditable;

Aaron Bauman
committed
private $state;
/**
* Constructor which initializes the consumer.
* @param \Drupal\Core\Http\Client $http_client
* The config factory
* @param \Guzzle\Http\ClientInterface $http_client
* The config factory
*/

Aaron Bauman
committed
public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, UrlGeneratorInterface $url_generator, StateInterface $state) {
$this->configFactory = $config_factory;
$this->httpClient = $http_client;
$this->urlGenerator = $url_generator;
$this->config = $this->configFactory->get('salesforce.settings');
$this->configEditable = $this->configFactory->getEditable('salesforce.settings');

Aaron Bauman
committed
$this->state = $state;
}
/**
* 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();
}
/**
* 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.
*
* @return mixed
* The requested response.
*
* @throws Exception

Aaron Bauman
committed
public function apiCall($path, array $params = [], $method = 'GET') {
if (!$this->getAccessToken()) {
$this->refreshToken();
}
try {
$this->response = $this->apiHttpRequest($path, $params, $method);
}
catch (RequestException $e) {
// A RequestException gets thrown if the response has any error status.
$this->response = $e->getResponse();
}
if (!is_object($this->response)) {
throw new Exception('Unknown error occurred during API call');
}
switch ($this->response->getStatusCode()) {
case 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 = $this->apiHttpRequest($path, $params, $method);
}
catch (RequestException $e) {
$this->response = $e->getResponse();
throw new Exception($this->response->getReasonPhrase(), $this->response->getStatusCode());
}
break;
case 200:
case 201:
case 204:
// All clear.
break;
default:
// We have problem and no specific Salesforce error provided.
if (empty($this->response)) {
throw new Exception('Unknown error occurred during API call');

Aaron Bauman
committed
// Parse a json response, if body is not empty. Sometimes an empty body is valid, e.g. for upsert.
$data = '';
$response_body = $this->response->getBody()->getContents();
if (!empty($response_body)) {
$this->response->getBody()->rewind();
$data = $this->handleJsonResponse($this->response);
}
return $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 object
* The requested data.
*/

Aaron Bauman
committed
protected function apiHttpRequest($path, array $params, $method) {
if (!$this->getAccessToken()) {
throw new Exception('Missing OAuth Token');
}
$url = $this->getApiEndPoint() . $path;

Aaron Bauman
committed
$headers = [
'Authorization' => 'OAuth ' . $this->getAccessToken(),
'Content-type' => 'application/json',

Aaron Bauman
committed
];
$data = NULL;
if (!empty($params)) {
// @TODO: convert this into Dependency Injection
$data = 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 object
* Salesforce response object.
*/

Aaron Bauman
committed
protected function httpRequest($url, $data = NULL, array $headers = [], $method = 'GET') {
// Build the request, including path and headers. Internal use.

Aaron Bauman
committed
$request = $this->httpClient->$method($url, ['headers' => $headers, 'body' => $data]);
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
return $request;
}
/**
* 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() {

Aaron Bauman
committed
return $this->state->get('salesforce.consumer_key');
}
public function setConsumerKey($value) {

Aaron Bauman
committed
return $this->state->set('salesforce.consumer_key', $value);
}
public function getConsumerSecret() {

Aaron Bauman
committed
return $this->state->get('salesforce.consumer_secret');
}
public function setConsumerSecret($value) {

Aaron Bauman
committed
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;

Aaron Bauman
committed
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() {

Aaron Bauman
committed
return $this->state->get('salesforce.instance_url');
}
/**
* Set the SF instanc URL.
*
* @param string $url
* URL to set.
*/
protected function setInstanceUrl($url) {

Aaron Bauman
committed
$this->state->set('salesforce.instance_url', $url);
}
/**
* Get the access token.
*/
public function getAccessToken() {

Aaron Bauman
committed
$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.
*/

Aaron Bauman
committed
public function setAccessToken($token) {
$this->state->set('salesforce.access_token', $token);
}
/**
* Get refresh token.
*/
protected function getRefreshToken() {

Aaron Bauman
committed
return $this->state->get('salesforce.refresh_token');
}
/**
* Set refresh token.
*
* @param string $token
* Refresh token from Salesforce.
*/
protected function setRefreshToken($token) {

Aaron Bauman
committed
$this->state->set('salesforce.refresh_token', $token);

Aaron Bauman
committed
* 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.'));

Aaron Bauman
committed
$data = UrlHelper::buildQuery([
'grant_type' => 'refresh_token',
'refresh_token' => urldecode($refresh_token),
'client_id' => $this->getConsumerKey(),
'client_secret' => $this->getConsumerSecret(),

Aaron Bauman
committed
]);

Aaron Bauman
committed
$url = $this->getAuthTokenUrl();
$headers = [
// This is an undocumented requirement on Salesforce's end.
'Content-Type' => 'application/x-www-form-urlencoded',

Aaron Bauman
committed
];
$response = $this->httpRequest($url, $data, $headers, 'POST');

Aaron Bauman
committed
$this->handleAuthResponse($response);
}
/**
* 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());

Aaron Bauman
committed
$data = $this->handleJsonResponse($response);

Aaron Bauman
committed
$this->setAccessToken($data['access_token']);
$this->setRefreshToken($data['refresh_token']);
$this->initializeIdentity($data['id']);
$this->setInstanceUrl($data['instance_url']);
}
/**
* Helper function to eliminate repetitive json parsing.
*
* @param Response $response
* @return array
* @throws Drupal\salesforce\Exception
*/
private function handleJsonResponse($response) {
// Allow any exceptions here to bubble up:
$data = Json::decode($response->getBody()->getContents());
if (empty($data)) {
throw new Exception('Invalid response ' . print_r($response->getBody()->getContents(), 1));
}
if (isset($data['error'])) {
throw new Exception($data['error_description'], $data['error']);

Aaron Bauman
committed
if (!empty($data[0]) && count($data) == 1) {
$data = $data[0];
}
if (isset($data['error'])) {
throw new Exception($data['error_description'], $data['error']);

Aaron Bauman
committed
if (!empty($data['errorCode'])) {
throw new Exception($data['message'], $this->response->getStatusCode());
}
return $data;
}
/**
* Retrieve and store the Salesforce identity given an ID url.
*
* @param string $id
* Identity URL.
*
* @throws Exception

Aaron Bauman
committed
public function initializeIdentity($id) {
$headers = [
'Authorization' => 'OAuth ' . $this->getAccessToken(),
'Content-type' => 'application/json',

Aaron Bauman
committed
];
$response = $this->httpRequest($id, NULL, $headers);

Aaron Bauman
committed
if ($response->getStatusCode() != 200) {
throw new Exception(t('Unable to access identity service.'), $response->getStatusCode());

Aaron Bauman
committed
$data = $this->handleJsonResponse($response);
$this->setIdentity($data);
}

Aaron Bauman
committed
protected function setIdentity(array $data) {
$this->state->set('salesforce.identity', $data);
}
/**
* Return the Salesforce identity, which is stored in a variable.
*
* @return array
* Returns FALSE is no identity has been stored.
*/
public function getIdentity() {

Aaron Bauman
committed
return $this->state->get('salesforce.identity');

Aaron Bauman
committed
* Helper to build the redirect URL for OAUTH workflow.
*
* @return string
* Redirect URL.
*
* @see Drupal\salesforce\Controller\SalesforceController

Aaron Bauman
committed
public function getAuthCallbackUrl() {
return \Drupal::url('salesforce.oauth_callback', [], [
'absolute' => TRUE,
'https' => TRUE,
]);

Aaron Bauman
committed
* Get Salesforce oauth login endpoint. (OAuth step 1)

Aaron Bauman
committed
* @return string
* REST OAuth Login URL.

Aaron Bauman
committed
public function getAuthEndpointUrl() {
return $this->getLoginUrl() . '/services/oauth2/authorize';

Aaron Bauman
committed
* Get Salesforce oauth token endpoint. (OAuth step 2)
*
* @return string

Aaron Bauman
committed
* REST OAuth Token URL.

Aaron Bauman
committed
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
*/

Aaron Bauman
committed
public function objects(array $conditions = ['updateable' => TRUE], $reset = FALSE) {
$cache = \Drupal::cache()->get('salesforce:objects');
// Force the recreation of the cache when it is older than 5 minutes.
if ($cache && REQUEST_TIME < ($cache->created + 300) && !$reset) {
$result = $cache->data;
}
else {
$result = $this->apiCall('sobjects');

Aaron Bauman
committed
\Drupal::cache()->set('salesforce:objects', $result, 0, ['salesforce']);
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
}
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 SalesforceSelectQuery $query
* The constructed SOQL query.
*
* @return array
* Array of Salesforce objects that match the query.
*
* @addtogroup salesforce_apicalls
*/

Aaron Bauman
committed
public function query(SelectQuery $query) {
//$this->moduleHander->alter('salesforce_query', $query);
// Casting $query as a string calls SalesforceSelectQuery::__toString().
$result = $this->apiCall('query?q=' . (string) $query);
return $result;
}
/**
* 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 array
* All the metadata for an object, including information about each field,
* URLs, and child relationships.
*
* @addtogroup salesforce_apicalls
*/
public function objectDescribe($name, $reset = FALSE) {
if (empty($name)) {

Aaron Bauman
committed
return [];

Aaron Bauman
committed
$cache = \Drupal::cache()->get('salesforce:object:' . $name);
// Force the recreation of the cache when it is older than 5 minutes.
if ($cache && REQUEST_TIME < ($cache->created + 300) && !$reset) {
return $cache->data;
}
else {
$object = $this->apiCall("sobjects/{$name}/describe");

Aaron Bauman
committed
// Index fields by machine name, so we don't have to search every time.
$new_fields = [];
foreach ($object['fields'] as $field) {
$new_fields[$field['name']] = $field;
}
$object['fields'] = $new_fields;
\Drupal::cache()->set('salesforce:object:' . $name, $object, 0, ['salesforce']);
return $object;
}
}
/**
* 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 array
* "id" : "001D000000IqhSLIAZ",
* "errors" : [ ],
* "success" : true
*
* @addtogroup salesforce_apicalls
*/

Aaron Bauman
committed
public function objectCreate($name, array $params) {
return $this->apiCall("sobjects/{$name}", $params, 'POST');
}
/**
* 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

Aaron Bauman
committed
* 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 array

Aaron Bauman
committed
* 1) successful create:
* "id" : "00190000001pPvHAAU",
* "errors" : [ ],
* "success" : true

Aaron Bauman
committed
*
* 2) unsuccessful upsert:
* "message" : "The requested resource does not exist"
* "errorCode" : "NOT_FOUND"
*
* @addtogroup salesforce_apicalls
*/

Aaron Bauman
committed
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]);
}

Aaron Bauman
committed
$data = $this->apiCall("sobjects/{$name}/{$key}/{$value}", $params, 'PATCH');

Aaron Bauman
committed
// On update, upsert method returns an empty body. Retreive object id, so that we can return a consistent response.
if ($this->response->getStatusCode() == 204) {
$sf_object = $this->objectReadbyExternalId($name, $key, $value);
$data['id'] = $sf_object['Id'];
$data['success'] = TRUE;
$data['errors'] = [];
}
return $data;
}
/**
* 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
*
* @addtogroup salesforce_apicalls
*/

Aaron Bauman
committed
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 array
* Object of the requested Salesforce object.
*
* @addtogroup salesforce_apicalls
*/
public function objectRead($name, $id) {

Aaron Bauman
committed
return $this->apiCall("sobjects/{$name}/{$id}", [], 'GET');

Aaron Bauman
committed
* Return a full loaded Salesforce object from External ID.
*
* @param string $name

Aaron Bauman
committed
* Object type name, E.g., Contact, Account.
* @param string $field
* Salesforce external id field name.
* @param string $value
* Value of external id.

Aaron Bauman
committed
* @return object
* Object of the requested Salesforce object.
*
* @addtogroup salesforce_apicalls
*/

Aaron Bauman
committed
public function objectReadbyExternalId($name, $field, $value) {
return $this->apiCall("sobjects/{$name}/{$field}/{$value}");
}
/**
* Delete a Salesforce object.
*
* @param string $name
* Object type name, E.g., Contact, Account.
* @param string $id
* Salesforce id of the object.
*
* @addtogroup salesforce_apicalls
*/
public function objectDelete($name, $id) {

Aaron Bauman
committed
$this->apiCall("sobjects/{$name}/{$id}", [], 'DELETE');
}
/**
* Return a list of available resources for the configured API version.
*
* @return array
* Associative array keyed by name with a URI value.
*
* @addtogroup salesforce_apicalls
*/
public function listResources() {
$resources = $this->apiCall('');
foreach ($resources as $key => $path) {
$items[$key] = $path;
}
return $items;
}
}