| <?php |
| <?php |
| |
| |
| namespace OhMyBrew; |
| namespace OhMyBrew; |
| |
| |
| use Closure; |
| use Closure; |
| use Exception; |
| use Exception; |
| use GuzzleHttp\Client; |
| use GuzzleHttp\Client; |
| use GuzzleHttp\Exception\ClientException; |
| use GuzzleHttp\Exception\ClientException; |
| use GuzzleHttp\Exception\ServerException; |
| use GuzzleHttp\Exception\ServerException; |
| use GuzzleHttp\HandlerStack; |
| use GuzzleHttp\HandlerStack; |
| use GuzzleHttp\Middleware; |
| use GuzzleHttp\Middleware; |
| use GuzzleHttp\Psr7\Request; |
| use GuzzleHttp\Psr7\Request; |
| use GuzzleHttp\Psr7\Uri; |
| use GuzzleHttp\Psr7\Uri; |
| use Psr\Log\LoggerAwareInterface; |
| use Psr\Log\LoggerAwareInterface; |
| use Psr\Log\LoggerInterface; |
| use Psr\Log\LoggerInterface; |
| use Psr\Log\LogLevel; |
| use Psr\Log\LogLevel; |
| use stdClass; |
| use stdClass; |
. | |
| use Log; |
| |
| |
| /** |
| /** |
| * Basic Shopify API for REST & GraphQL. |
| * Basic Shopify API for REST & GraphQL. |
| */ |
| */ |
| class BasicShopifyAPI implements LoggerAwareInterface |
| class BasicShopifyAPI implements LoggerAwareInterface |
| { |
| { |
| /** |
| /** |
| * API version pattern. |
| * API version pattern. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| const VERSION_PATTERN = '/([0-9]{4}-[0-9]{2})|unstable/'; |
| const VERSION_PATTERN = '/([0-9]{4}-[0-9]{2})|unstable/'; |
| |
| |
| /** |
| /** |
| * The key to use for logging (prefix for filtering). |
| * The key to use for logging (prefix for filtering). |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| const LOG_KEY = '[BasicShopifyAPI]'; |
| const LOG_KEY = '[BasicShopifyAPI]'; |
| |
| |
| /** |
| /** |
| * The Guzzle client. |
| * The Guzzle client. |
| * |
| * |
| * @var \GuzzleHttp\Client |
| * @var \GuzzleHttp\Client |
| */ |
| */ |
| protected $client; |
| protected $client; |
| |
| |
| /** |
| /** |
| * The version of API. |
| * The version of API. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $version; |
| protected $version; |
| |
| |
| /** |
| /** |
| * The Shopify domain. |
| * The Shopify domain. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $shop; |
| protected $shop; |
| |
| |
| /** |
| /** |
| * The Shopify access token. |
| * The Shopify access token. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $accessToken; |
| protected $accessToken; |
| |
| |
| /** |
| /** |
| * The Shopify API key. |
| * The Shopify API key. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $apiKey; |
| protected $apiKey; |
| |
| |
| /** |
| /** |
| * The Shopify API password. |
| * The Shopify API password. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $apiPassword; |
| protected $apiPassword; |
| |
| |
| /** |
| /** |
| * The Shopify API secret. |
| * The Shopify API secret. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $apiSecret; |
| protected $apiSecret; |
| |
| |
| /** |
| /** |
| * If API calls are from a public or private app. |
| * If API calls are from a public or private app. |
| * |
| * |
| * @var string |
| * @var string |
| */ |
| */ |
| protected $private; |
| protected $private; |
| |
| |
| /** |
| /** |
| * If the API was called with per-user grant option, this will be filled. |
| * If the API was called with per-user grant option, this will be filled. |
| * |
| * |
| * @var stdClass |
| * @var stdClass |
| */ |
| */ |
| protected $user; |
| protected $user; |
| |
| |
| /** |
| /** |
. | |
| * Used for handling the API rate limit |
| |
| */ |
| |
| protected $retryCall; |
| |
| |
| |
| /** |
| * The current API call limits from last request. |
| * The current API call limits from last request. |
| * |
| * |
| * @var array |
| * @var array |
| */ |
| */ |
| protected $apiCallLimits = [ |
| protected $apiCallLimits = [ |
| 'rest' => [ |
| 'rest' => [ |
| 'left' => 0, |
| 'left' => 0, |
| 'made' => 0, |
| 'made' => 0, |
| 'limit' => 40, |
| 'limit' => 40, |
| ], |
| ], |
| 'graph' => [ |
| 'graph' => [ |
| 'left' => 0, |
| 'left' => 0, |
| 'made' => 0, |
| 'made' => 0, |
| 'limit' => 1000, |
| 'limit' => 1000, |
| 'restoreRate' => 50, |
| 'restoreRate' => 50, |
| 'requestedCost' => 0, |
| 'requestedCost' => 0, |
| 'actualCost' => 0, |
| 'actualCost' => 0, |
| ], |
| ], |
| ]; |
| ]; |
| |
| |
| /** |
| /** |
| * If rate limiting is enabled. |
| * If rate limiting is enabled. |
| * |
| * |
| * @var bool |
| * @var bool |
| */ |
| */ |
| protected $rateLimitingEnabled = false; |
| protected $rateLimitingEnabled = false; |
| |
| |
| /** |
| /** |
| * The rate limiting cycle (in ms). |
| * The rate limiting cycle (in ms). |
| * |
| * |
| * @var int |
| * @var int |
| */ |
| */ |
| protected $rateLimitCycle = 0.5 * 1000; |
| protected $rateLimitCycle = 0.5 * 1000; |
| |
| |
| /** |
| /** |
| * The rate limiting cycle buffer (in ms). |
| * The rate limiting cycle buffer (in ms). |
| * |
| * |
| * @var int |
| * @var int |
| */ |
| */ |
| protected $rateLimitCycleBuffer = 0.1 * 1000; |
| protected $rateLimitCycleBuffer = 0.1 * 1000; |
| |
| |
| /** |
| /** |
| * Request timestamp for every new call. |
| * Request timestamp for every new call. |
| * Used for rate limiting. |
| * Used for rate limiting. |
| * |
| * |
| * @var int |
| * @var int |
| */ |
| */ |
| protected $requestTimestamp; |
| protected $requestTimestamp; |
| |
| |
| /** |
| /** |
| * The logger. |
| * The logger. |
| * |
| * |
| * @var LoggerInterface |
| * @var LoggerInterface |
| */ |
| */ |
| protected $logger; |
| protected $logger; |
| |
| |
| /** |
| /** |
| * Constructor. |
| * Constructor. |
| * |
| * |
| * @param bool $private If this is a private or public app |
| * @param bool $private If this is a private or public app |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function __construct(bool $private = false) |
| public function __construct(bool $private = false) |
| { |
| { |
| // Set if app is private or public |
| // Set if app is private or public |
| $this->private = $private; |
| $this->private = $private; |
| |
| |
. | |
| // Set the retryCall as 1 for handling the API rate limit |
| |
| $this->retryCall = 1; |
| |
| |
| // Create the stack and assign the middleware which attempts to fix redirects |
| // Create the stack and assign the middleware which attempts to fix redirects |
| $stack = HandlerStack::create(); |
| $stack = HandlerStack::create(); |
| $stack->push(Middleware::mapRequest([$this, 'authRequest'])); |
| $stack->push(Middleware::mapRequest([$this, 'authRequest'])); |
| |
| |
| // Create a default Guzzle client with our stack |
| // Create a default Guzzle client with our stack |
| $this->client = new Client([ |
| $this->client = new Client([ |
| 'handler' => $stack, |
| 'handler' => $stack, |
| 'headers' => [ |
| 'headers' => [ |
| 'Accept' => 'application/json', |
| 'Accept' => 'application/json', |
| 'Content-Type' => 'application/json', |
| 'Content-Type' => 'application/json', |
| ], |
| ], |
| ]); |
| ]); |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Determines if the calls are private. |
| * Determines if the calls are private. |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| public function isPrivate() |
| public function isPrivate() |
| { |
| { |
| return $this->private === true; |
| return $this->private === true; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Determines if the calls are public. |
| * Determines if the calls are public. |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| public function isPublic() |
| public function isPublic() |
| { |
| { |
| return !$this->isPrivate(); |
| return !$this->isPrivate(); |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the Guzzle client for the API calls (allows for override with your own). |
| * Sets the Guzzle client for the API calls (allows for override with your own). |
| * |
| * |
| * @param \GuzzleHttp\Client $client The Guzzle client |
| * @param \GuzzleHttp\Client $client The Guzzle client |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setClient(Client $client) |
| public function setClient(Client $client) |
| { |
| { |
| $this->client = $client; |
| $this->client = $client; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the version of Shopify API to use. |
| * Sets the version of Shopify API to use. |
| * |
| * |
| * @param string $version |
| * @param string $version |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setVersion(string $version) |
| public function setVersion(string $version) |
| { |
| { |
| if (!preg_match(self::VERSION_PATTERN, $version)) { |
| if (!preg_match(self::VERSION_PATTERN, $version)) { |
| // Invalid version string |
| // Invalid version string |
| throw new Exception('Version string must be of YYYY-MM or unstable'); |
| throw new Exception('Version string must be of YYYY-MM or unstable'); |
| } |
| } |
| |
| |
| $this->version = $version; |
| $this->version = $version; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Returns the current in-use API version. |
| * Returns the current in-use API version. |
| * |
| * |
| * @return string |
| * @return string |
| */ |
| */ |
| public function getVersion() |
| public function getVersion() |
| { |
| { |
| return $this->version; |
| return $this->version; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the Shopify domain (*.myshopify.com) we're working with. |
| * Sets the Shopify domain (*.myshopify.com) we're working with. |
| * |
| * |
| * @param string $shop The myshopify domain |
| * @param string $shop The myshopify domain |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setShop(string $shop) |
| public function setShop(string $shop) |
| { |
| { |
| $this->shop = $shop; |
| $this->shop = $shop; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the Shopify domain (*.myshopify.com) we're working with. |
| * Gets the Shopify domain (*.myshopify.com) we're working with. |
| * |
| * |
| * @return string |
| * @return string |
| */ |
| */ |
| public function getShop() |
| public function getShop() |
| { |
| { |
| return $this->shop; |
| return $this->shop; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the access token for use with the Shopify API (public apps). |
| * Sets the access token for use with the Shopify API (public apps). |
| * |
| * |
| * @param string $accessToken The access token |
| * @param string $accessToken The access token |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setAccessToken(string $accessToken) |
| public function setAccessToken(string $accessToken) |
| { |
| { |
| $this->accessToken = $accessToken; |
| $this->accessToken = $accessToken; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the access token. |
| * Gets the access token. |
| * |
| * |
| * @return string |
| * @return string |
| */ |
| */ |
| public function getAccessToken() |
| public function getAccessToken() |
| { |
| { |
| return $this->accessToken; |
| return $this->accessToken; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the API key for use with the Shopify API (public or private apps). |
| * Sets the API key for use with the Shopify API (public or private apps). |
| * |
| * |
| * @param string $apiKey The API key |
| * @param string $apiKey The API key |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setApiKey(string $apiKey) |
| public function setApiKey(string $apiKey) |
| { |
| { |
| $this->apiKey = $apiKey; |
| $this->apiKey = $apiKey; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the API secret for use with the Shopify API (public apps). |
| * Sets the API secret for use with the Shopify API (public apps). |
| * |
| * |
| * @param string $apiSecret The API secret key |
| * @param string $apiSecret The API secret key |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setApiSecret(string $apiSecret) |
| public function setApiSecret(string $apiSecret) |
| { |
| { |
| $this->apiSecret = $apiSecret; |
| $this->apiSecret = $apiSecret; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the API password for use with the Shopify API (private apps). |
| * Sets the API password for use with the Shopify API (private apps). |
| * |
| * |
| * @param string $apiPassword The API password |
| * @param string $apiPassword The API password |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setApiPassword(string $apiPassword) |
| public function setApiPassword(string $apiPassword) |
| { |
| { |
| $this->apiPassword = $apiPassword; |
| $this->apiPassword = $apiPassword; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets the user (public apps). |
| * Sets the user (public apps). |
| * |
| * |
| * @param stdClass $user The user returned from the access request. |
| * @param stdClass $user The user returned from the access request. |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setUser(stdClass $user) |
| public function setUser(stdClass $user) |
| { |
| { |
| $this->user = $user; |
| $this->user = $user; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the user. |
| * Gets the user. |
| * |
| * |
| * @return stdClass |
| * @return stdClass |
| */ |
| */ |
| public function getUser() |
| public function getUser() |
| { |
| { |
| return $this->user; |
| return $this->user; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Checks if we have a user. |
| * Checks if we have a user. |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| public function hasUser() |
| public function hasUser() |
| { |
| { |
| return $this->user !== null; |
| return $this->user !== null; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Set the rate limiting state to enabled. |
| * Set the rate limiting state to enabled. |
| * |
| * |
| * @param int|null $cycle The rate limiting cycle (in ms, default 500ms). |
| * @param int|null $cycle The rate limiting cycle (in ms, default 500ms). |
| * @param int|null $buffer The rate limiting cycle buffer (in ms, default 100ms). |
| * @param int|null $buffer The rate limiting cycle buffer (in ms, default 100ms). |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function enableRateLimiting(int $cycle = null, int $buffer = null) |
| public function enableRateLimiting(int $cycle = null, int $buffer = null) |
| { |
| { |
| $this->rateLimitingEnabled = true; |
| $this->rateLimitingEnabled = true; |
| |
| |
| if (!is_null($cycle)) { |
| if (!is_null($cycle)) { |
| $this->rateLimitCycle = $cycle; |
| $this->rateLimitCycle = $cycle; |
| } |
| } |
| |
| |
| if (!is_null($cycle)) { |
| if (!is_null($cycle)) { |
| $this->rateLimitCycleBuffer = $buffer; |
| $this->rateLimitCycleBuffer = $buffer; |
| } |
| } |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Set the rate limiting state to disabled. |
| * Set the rate limiting state to disabled. |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function disableRateLimiting() |
| public function disableRateLimiting() |
| { |
| { |
| $this->rateLimitingEnabled = false; |
| $this->rateLimitingEnabled = false; |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Determines if rate limiting is enabled. |
| * Determines if rate limiting is enabled. |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| public function isRateLimitingEnabled() |
| public function isRateLimitingEnabled() |
| { |
| { |
| return $this->rateLimitingEnabled === true; |
| return $this->rateLimitingEnabled === true; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Simple quick method to set shop and access token in one shot. |
| * Simple quick method to set shop and access token in one shot. |
| * |
| * |
| * @param string $shop The shop's domain |
| * @param string $shop The shop's domain |
| * @param string $accessToken The access token for API requests |
| * @param string $accessToken The access token for API requests |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function setSession(string $shop, string $accessToken) |
| public function setSession(string $shop, string $accessToken) |
| { |
| { |
| $this->setShop($shop); |
| $this->setShop($shop); |
| $this->setAccessToken($accessToken); |
| $this->setAccessToken($accessToken); |
| |
| |
| return $this; |
| return $this; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Accepts a closure to do isolated API calls for a shop. |
| * Accepts a closure to do isolated API calls for a shop. |
| * |
| * |
| * @param string $shop The shop's domain |
| * @param string $shop The shop's domain |
| * @param string $accessToken The access token for API requests |
| * @param string $accessToken The access token for API requests |
| * @param Closure $closure The closure to run isolated |
| * @param Closure $closure The closure to run isolated |
| * |
| * |
| * @throws \Exception When closure is missing or not callable |
| * @throws \Exception When closure is missing or not callable |
| * |
| * |
| * @return self |
| * @return self |
| */ |
| */ |
| public function withSession(string $shop, string $accessToken, Closure $closure) |
| public function withSession(string $shop, string $accessToken, Closure $closure) |
| { |
| { |
| $this->log("WithSession started for {$shop}"); |
| $this->log("WithSession started for {$shop}"); |
| |
| |
| // Clone the API class and bind it to the closure |
| // Clone the API class and bind it to the closure |
| $clonedApi = clone $this; |
| $clonedApi = clone $this; |
| $clonedApi->setSession($shop, $accessToken); |
| $clonedApi->setSession($shop, $accessToken); |
| |
| |
| return $closure->call($clonedApi); |
| return $closure->call($clonedApi); |
| } |
| } |
| |
| |
| /** |
| /** |
| * Returns the base URI to use. |
| * Returns the base URI to use. |
| * |
| * |
| * @return \Guzzle\Psr7\Uri |
| * @return \Guzzle\Psr7\Uri |
| */ |
| */ |
| public function getBaseUri() |
| public function getBaseUri() |
| { |
| { |
| if ($this->shop === null) { |
| if ($this->shop === null) { |
| // Shop is required |
| // Shop is required |
| throw new Exception('Shopify domain missing for API calls'); |
| throw new Exception('Shopify domain missing for API calls'); |
| } |
| } |
| |
| |
| return new Uri("https://{$this->shop}"); |
| return new Uri("https://{$this->shop}"); |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the authentication URL for Shopify to allow the user to accept the app (for public apps). |
| * Gets the authentication URL for Shopify to allow the user to accept the app (for public apps). |
| * |
| * |
| * @param string|array $scopes The API scopes as a comma seperated string or array |
| * @param string|array $scopes The API scopes as a comma seperated string or array |
| * @param string $redirectUri The valid redirect URI for after acceptance of the permissions. |
| * @param string $redirectUri The valid redirect URI for after acceptance of the permissions. |
| * It must match the redirect_uri in your app settings. |
| * It must match the redirect_uri in your app settings. |
| * @param string|null $mode The API access mode, offline or per-user. |
| * @param string|null $mode The API access mode, offline or per-user. |
| * |
| * |
| * @return string Formatted URL |
| * @return string Formatted URL |
| */ |
| */ |
| public function getAuthUrl($scopes, string $redirectUri, string $mode = 'offline') |
| public function getAuthUrl($scopes, string $redirectUri, string $mode = 'offline') |
| { |
| { |
| if ($this->apiKey === null) { |
| if ($this->apiKey === null) { |
| throw new Exception('API key is missing'); |
| throw new Exception('API key is missing'); |
| } |
| } |
| |
| |
| if (is_array($scopes)) { |
| if (is_array($scopes)) { |
| $scopes = implode(',', $scopes); |
| $scopes = implode(',', $scopes); |
| } |
| } |
| |
| |
| $query = [ |
| $query = [ |
| 'client_id' => $this->apiKey, |
| 'client_id' => $this->apiKey, |
| 'scope' => $scopes, |
| 'scope' => $scopes, |
| 'redirect_uri' => $redirectUri, |
| 'redirect_uri' => $redirectUri, |
| ]; |
| ]; |
| |
| |
| if ($mode !== null && $mode !== 'offline') { |
| if ($mode !== null && $mode !== 'offline') { |
| $query['grant_options'] = [$mode]; |
| $query['grant_options'] = [$mode]; |
| } |
| } |
| |
| |
| return (string) $this->getBaseUri() |
| return (string) $this->getBaseUri() |
| ->withPath('/admin/oauth/authorize') |
| ->withPath('/admin/oauth/authorize') |
| ->withQuery( |
| ->withQuery( |
| preg_replace('/\%5B\d+\%5D/', '%5B%5D', http_build_query($query)) |
| preg_replace('/\%5B\d+\%5D/', '%5B%5D', http_build_query($query)) |
| ); |
| ); |
| } |
| } |
| |
| |
| /** |
| /** |
| * Verify the request is from Shopify using the HMAC signature (for public apps). |
| * Verify the request is from Shopify using the HMAC signature (for public apps). |
| * |
| * |
| * @param array $params The request parameters (ex. $_GET) |
| * @param array $params The request parameters (ex. $_GET) |
| * |
| * |
| * @return bool If the HMAC is validated |
| * @return bool If the HMAC is validated |
| */ |
| */ |
| public function verifyRequest(array $params) |
| public function verifyRequest(array $params) |
| { |
| { |
| if ($this->apiSecret === null) { |
| if ($this->apiSecret === null) { |
| // Secret is required |
| // Secret is required |
| throw new Exception('API secret is missing'); |
| throw new Exception('API secret is missing'); |
| } |
| } |
| |
| |
| // Ensure shop, timestamp, and HMAC are in the params |
| // Ensure shop, timestamp, and HMAC are in the params |
| if (array_key_exists('shop', $params) |
| if (array_key_exists('shop', $params) |
| && array_key_exists('timestamp', $params) |
| && array_key_exists('timestamp', $params) |
| && array_key_exists('hmac', $params) |
| && array_key_exists('hmac', $params) |
| ) { |
| ) { |
| // Grab the HMAC, remove it from the params, then sort the params for hashing |
| // Grab the HMAC, remove it from the params, then sort the params for hashing |
| $hmac = $params['hmac']; |
| $hmac = $params['hmac']; |
| unset($params['hmac']); |
| unset($params['hmac']); |
| ksort($params); |
| ksort($params); |
| |
| |
| // Encode and hash the params (without HMAC), add the API secret, and compare to the HMAC from params |
| // Encode and hash the params (without HMAC), add the API secret, and compare to the HMAC from params |
| return $hmac === hash_hmac('sha256', urldecode(http_build_query($params)), $this->apiSecret); |
| return $hmac === hash_hmac('sha256', urldecode(http_build_query($params)), $this->apiSecret); |
| } |
| } |
| |
| |
| // Not valid |
| // Not valid |
| return false; |
| return false; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the access object from a "code" supplied by Shopify request after successfull authentication (for public apps). |
| * Gets the access object from a "code" supplied by Shopify request after successfull authentication (for public apps). |
| * |
| * |
| * @param string $code The code from Shopify |
| * @param string $code The code from Shopify |
| * |
| * |
| * @throws \Exception When API secret is missing |
| * @throws \Exception When API secret is missing |
| * |
| * |
| * @return array The access object |
| * @return array The access object |
| */ |
| */ |
| public function requestAccess(string $code) |
| public function requestAccess(string $code) |
| { |
| { |
| if ($this->apiSecret === null || $this->apiKey === null) { |
| if ($this->apiSecret === null || $this->apiKey === null) { |
| // Key and secret required |
| // Key and secret required |
| throw new Exception('API key or secret is missing'); |
| throw new Exception('API key or secret is missing'); |
| } |
| } |
| |
| |
| // Do a JSON POST request to grab the access token |
| // Do a JSON POST request to grab the access token |
| $request = $this->client->request( |
| $request = $this->client->request( |
| 'POST', |
| 'POST', |
| $this->getBaseUri()->withPath('/admin/oauth/access_token'), |
| $this->getBaseUri()->withPath('/admin/oauth/access_token'), |
| [ |
| [ |
| 'json' => [ |
| 'json' => [ |
| 'client_id' => $this->apiKey, |
| 'client_id' => $this->apiKey, |
| 'client_secret' => $this->apiSecret, |
| 'client_secret' => $this->apiSecret, |
| 'code' => $code, |
| 'code' => $code, |
| ], |
| ], |
| ] |
| ] |
| ); |
| ); |
| |
| |
| // Decode the response body |
| // Decode the response body |
| $body = json_decode($request->getBody()); |
| $body = json_decode($request->getBody()); |
| |
| |
| $this->log('RequestAccess response: '.json_encode($body)); |
| $this->log('RequestAccess response: '.json_encode($body)); |
| |
| |
| return $body; |
| return $body; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the access token from a "code" supplied by Shopify request after successfull authentication (for public apps). |
| * Gets the access token from a "code" supplied by Shopify request after successfull authentication (for public apps). |
| * |
| * |
| * @param string $code The code from Shopify |
| * @param string $code The code from Shopify |
| * |
| * |
| * @return string The access token |
| * @return string The access token |
| */ |
| */ |
| public function requestAccessToken(string $code) |
| public function requestAccessToken(string $code) |
| { |
| { |
| return $this->requestAccess($code)->access_token; |
| return $this->requestAccess($code)->access_token; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Gets the access object from a "code" and sets it to the instance (for public apps). |
| * Gets the access object from a "code" and sets it to the instance (for public apps). |
| * |
| * |
| * @param string $code The code from Shopify |
| * @param string $code The code from Shopify |
| * |
| * |
| * @return void |
| * @return void |
| */ |
| */ |
| public function requestAndSetAccess(string $code) |
| public function requestAndSetAccess(string $code) |
| { |
| { |
| $access = $this->requestAccess($code); |
| $access = $this->requestAccess($code); |
| |
| |
| // Set the access token |
| // Set the access token |
| $this->setAccessToken($access->access_token); |
| $this->setAccessToken($access->access_token); |
| |
| |
| if (property_exists($access, 'associated_user')) { |
| if (property_exists($access, 'associated_user')) { |
| // Set the user if applicable |
| // Set the user if applicable |
| $this->setUser($access->associated_user); |
| $this->setUser($access->associated_user); |
| $this->log('User access: '.json_encode($access->associated_user)); |
| $this->log('User access: '.json_encode($access->associated_user)); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| /** |
| * Alias for REST method for backwards compatibility. |
| * Alias for REST method for backwards compatibility. |
| * |
| * |
| * @see rest |
| * @see rest |
| */ |
| */ |
| public function request() |
| public function request() |
| { |
| { |
| return call_user_func_array([$this, 'rest'], func_get_args()); |
| return call_user_func_array([$this, 'rest'], func_get_args()); |
| } |
| } |
| |
| |
| /** |
| /** |
| * Returns the current API call limits. |
| * Returns the current API call limits. |
| * |
| * |
| * @param string|null $key The key to grab (left, made, limit, etc) |
| * @param string|null $key The key to grab (left, made, limit, etc) |
| * |
| * |
| * @throws \Exception When attempting to grab a key that doesn't exist |
| * @throws \Exception When attempting to grab a key that doesn't exist |
| * |
| * |
| * @return array An array of the Guzzle response, and JSON-decoded body |
| * @return array An array of the Guzzle response, and JSON-decoded body |
| */ |
| */ |
| public function getApiCalls(string $type = 'rest', string $key = null) |
| public function getApiCalls(string $type = 'rest', string $key = null) |
| { |
| { |
| if ($key) { |
| if ($key) { |
| $keys = array_keys($this->apiCallLimits[$type]); |
| $keys = array_keys($this->apiCallLimits[$type]); |
| if (!in_array($key, $keys)) { |
| if (!in_array($key, $keys)) { |
| // No key like that in array |
| // No key like that in array |
| throw new Exception('Invalid API call limit key. Valid keys are: '.implode(', ', $keys)); |
| throw new Exception('Invalid API call limit key. Valid keys are: '.implode(', ', $keys)); |
| } |
| } |
| |
| |
| // Return the key value requested |
| // Return the key value requested |
| return $this->apiCallLimits[$type][$key]; |
| return $this->apiCallLimits[$type][$key]; |
| } |
| } |
| |
| |
| // Return all the values |
| // Return all the values |
| return $this->apiCallLimits[$type]; |
| return $this->apiCallLimits[$type]; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Runs a request to the Shopify API. |
| * Runs a request to the Shopify API. |
| * |
| * |
| * @param string $query The GraphQL query |
| * @param string $query The GraphQL query |
| * @param array $variables The optional variables for the query |
| * @param array $variables The optional variables for the query |
| * |
| * |
| * @throws \Exception When missing api password is missing for private apps |
| * @throws \Exception When missing api password is missing for private apps |
| * @throws \Exception When missing access key is missing for public apps |
| * @throws \Exception When missing access key is missing for public apps |
| * |
| * |
| * @return object An Object of the Guzzle response, and JSON-decoded body |
| * @return object An Object of the Guzzle response, and JSON-decoded body |
| */ |
| */ |
| public function graph(string $query, array $variables = []) |
| public function graph(string $query, array $variables = []) |
| { |
| { |
| // Build the request |
| // Build the request |
| $request = ['query' => $query]; |
| $request = ['query' => $query]; |
| if (count($variables) > 0) { |
| if (count($variables) > 0) { |
| $request['variables'] = $variables; |
| $request['variables'] = $variables; |
| } |
| } |
| |
| |
| // Update the timestamp of the request |
| // Update the timestamp of the request |
| $tmpTimestamp = $this->requestTimestamp; |
| $tmpTimestamp = $this->requestTimestamp; |
| $this->requestTimestamp = microtime(true); |
| $this->requestTimestamp = microtime(true); |
| |
| |
| // Create the request, pass the access token and optional parameters |
| // Create the request, pass the access token and optional parameters |
| $req = json_encode($request); |
| $req = json_encode($request); |
| $response = $this->client->request( |
| $response = $this->client->request( |
| 'POST', |
| 'POST', |
| $this->getBaseUri()->withPath( |
| $this->getBaseUri()->withPath( |
| $this->versionPath('/admin/api/graphql.json') |
| $this->versionPath('/admin/api/graphql.json') |
| ), |
| ), |
| ['body' => $req] |
| ['body' => $req] |
| ); |
| ); |
| $this->log("Graph request: {$req}"); |
| $this->log("Graph request: {$req}"); |
| |
| |
| // Grab the data result and extensions |
| // Grab the data result and extensions |
| $body = $this->jsonDecode($response->getBody()); |
| $body = $this->jsonDecode($response->getBody()); |
| if (property_exists($body, 'extensions') && property_exists($body->extensions, 'cost')) { |
| if (property_exists($body, 'extensions') && property_exists($body->extensions, 'cost')) { |
| // Update the API call information |
| // Update the API call information |
| $calls = $body->extensions->cost; |
| $calls = $body->extensions->cost; |
| $this->apiCallLimits['graph'] = [ |
| $this->apiCallLimits['graph'] = [ |
| 'left' => (int) $calls->throttleStatus->currentlyAvailable, |
| 'left' => (int) $calls->throttleStatus->currentlyAvailable, |
| 'made' => (int) ($calls->throttleStatus->maximumAvailable - $calls->throttleStatus->currentlyAvailable), |
| 'made' => (int) ($calls->throttleStatus->maximumAvailable - $calls->throttleStatus->currentlyAvailable), |
| 'limit' => (int) $calls->throttleStatus->maximumAvailable, |
| 'limit' => (int) $calls->throttleStatus->maximumAvailable, |
| 'restoreRate' => (int) $calls->throttleStatus->restoreRate, |
| 'restoreRate' => (int) $calls->throttleStatus->restoreRate, |
| 'requestedCost' => (int) $calls->requestedQueryCost, |
| 'requestedCost' => (int) $calls->requestedQueryCost, |
| 'actualCost' => (int) $calls->actualQueryCost, |
| 'actualCost' => (int) $calls->actualQueryCost, |
| ]; |
| ]; |
| } |
| } |
| |
| |
| $this->log('Graph response: '.json_encode(property_exists($body, 'errors') ? $body->errors : $body->data)); |
| $this->log('Graph response: '.json_encode(property_exists($body, 'errors') ? $body->errors : $body->data)); |
| |
| |
| // Return Guzzle response and JSON-decoded body |
| // Return Guzzle response and JSON-decoded body |
| return (object) [ |
| return (object) [ |
| 'response' => $response, |
| 'response' => $response, |
| 'body' => property_exists($body, 'errors') ? $body->errors : $body->data, |
| 'body' => property_exists($body, 'errors') ? $body->errors : $body->data, |
| 'errors' => property_exists($body, 'errors'), |
| 'errors' => property_exists($body, 'errors'), |
| 'timestamps' => [$tmpTimestamp, $this->requestTimestamp], |
| 'timestamps' => [$tmpTimestamp, $this->requestTimestamp], |
| ]; |
| ]; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Runs a request to the Shopify API. |
| * Runs a request to the Shopify API. |
| * |
| * |
| * @param string $type The type of request... GET, POST, PUT, DELETE |
| * @param string $type The type of request... GET, POST, PUT, DELETE |
| * @param string $path The Shopify API path... /admin/xxxx/xxxx.json |
| * @param string $path The Shopify API path... /admin/xxxx/xxxx.json |
| * @param array|null $params Optional parameters to send with the request |
| * @param array|null $params Optional parameters to send with the request |
| * |
| * |
| * @throws Exception |
| * @throws Exception |
| * |
| * |
| * @return object An Object of the Guzzle response, and JSON-decoded body |
| * @return object An Object of the Guzzle response, and JSON-decoded body |
| */ |
| */ |
| public function rest(string $type, string $path, array $params = null) |
| public function rest(string $type, string $path, array $params = null) |
| { |
| { |
| // Check the rate limit before firing the request |
| // Check the rate limit before firing the request |
| if ($this->isRateLimitingEnabled() && $this->requestTimestamp) { |
| if ($this->isRateLimitingEnabled() && $this->requestTimestamp) { |
| // Calculate in milliseconds the duration the API call took |
| // Calculate in milliseconds the duration the API call took |
| $duration = round(microtime(true) - $this->requestTimestamp, 3) * 1000; |
| $duration = round(microtime(true) - $this->requestTimestamp, 3) * 1000; |
| $waitTime = ($this->rateLimitCycle - $duration) + $this->rateLimitCycleBuffer; |
| $waitTime = ($this->rateLimitCycle - $duration) + $this->rateLimitCycleBuffer; |
| |
| |
| if ($waitTime > 0) { |
| if ($waitTime > 0) { |
| // Do the sleep for X mircoseconds (convert from milliseconds) |
| // Do the sleep for X mircoseconds (convert from milliseconds) |
| $this->log('Rest rate limit hit'); |
| $this->log('Rest rate limit hit'); |
| usleep($waitTime * 1000); |
| usleep($waitTime * 1000); |
| } |
| } |
| } |
| } |
| |
| |
| // Update the timestamp of the request |
| // Update the timestamp of the request |
| $tmpTimestamp = $this->requestTimestamp; |
| $tmpTimestamp = $this->requestTimestamp; |
| $this->requestTimestamp = microtime(true); |
| $this->requestTimestamp = microtime(true); |
| |
| |
| $errors = false; |
| $errors = false; |
| $response = null; |
| $response = null; |
| $body = null; |
| $body = null; |
| |
| |
| try { |
| try { |
| // Build URI and try the request |
| // Build URI and try the request |
| $uri = $this->getBaseUri()->withPath($this->versionPath($path)); |
| $uri = $this->getBaseUri()->withPath($this->versionPath($path)); |
| |
| |
| // Build the request parameters for Guzzle |
| // Build the request parameters for Guzzle |
| $guzzleParams = []; |
| $guzzleParams = []; |
| if ($params !== null) { |
| if ($params !== null) { |
| $guzzleParams[strtoupper($type) === 'GET' ? 'query' : 'json'] = $params; |
| $guzzleParams[strtoupper($type) === 'GET' ? 'query' : 'json'] = $params; |
| } |
| } |
| |
| |
| $this->log("[{$uri}:{$type}] Request Params: ".json_encode($params)); |
| $this->log("[{$uri}:{$type}] Request Params: ".json_encode($params)); |
| |
| |
| // Set the response |
| // Set the response |
| $response = $this->client->request($type, $uri, $guzzleParams); |
| $response = $this->client->request($type, $uri, $guzzleParams); |
| $body = $response->getBody(); |
| $body = $response->getBody(); |
| } catch (Exception $e) { |
| } catch (Exception $e) { |
| if ($e instanceof ClientException || $e instanceof ServerException) { |
| if ($e instanceof ClientException || $e instanceof ServerException) { |
| // 400 or 500 level error, set the response |
| // 400 or 500 level error, set the response |
| $response = $e->getResponse(); |
| $response = $e->getResponse(); |
| $body = $response->getBody(); |
| $body = $response->getBody(); |
| |
| |
| // Build the error object |
| // Build the error object |
| $errors = (object) [ |
| $errors = (object) [ |
| 'status' => $response->getStatusCode(), |
| 'status' => $response->getStatusCode(), |
| 'body' => $this->jsonDecode($body), |
| 'body' => $this->jsonDecode($body), |
| 'exception' => $e, |
| 'exception' => $e, |
| ]; |
| ]; |
| |
| |
| $this->log("[{$uri}:{$type}] {$response->getStatusCode()} Error: {$body}"); |
| $this->log("[{$uri}:{$type}] {$response->getStatusCode()} Error: {$body}"); |
| } else { |
| } else { |
| // Else, rethrow |
| // Else, rethrow |
| throw $e; |
| throw $e; |
| } |
| } |
| } |
| } |
| |
| |
| // Grab the API call limit header returned from Shopify |
| // Grab the API call limit header returned from Shopify |
| $callLimitHeader = $response->getHeader('http_x_shopify_shop_api_call_limit'); |
| $callLimitHeader = $response->getHeader('http_x_shopify_shop_api_call_limit'); |
| if ($callLimitHeader) { |
| if ($callLimitHeader) { |
| $calls = explode('/', $callLimitHeader[0]); |
| $calls = explode('/', $callLimitHeader[0]); |
| $this->apiCallLimits['rest'] = [ |
| $this->apiCallLimits['rest'] = [ |
| 'left' => (int) $calls[1] - $calls[0], |
| 'left' => (int) $calls[1] - $calls[0], |
| 'made' => (int) $calls[0], |
| 'made' => (int) $calls[0], |
| 'limit' => (int) $calls[1], |
| 'limit' => (int) $calls[1], |
| ]; |
| ]; |
| } |
| } |
| |
| |
| $this->log("[{$uri}:{$type}] {$response->getStatusCode()}: ".json_encode($errors ? $body->getContents() : $body)); |
| $this->log("[{$uri}:{$type}] {$response->getStatusCode()}: ".json_encode($errors ? $body->getContents() : $body)); |
. | |
| |
| |
| if(!empty($errors) && empty($body->getContents())){ |
| |
| |
| |
| if($errors->status == '429'){ |
| |
| Log::info("Api rate limit exceeds"); |
| |
| if ($this->retryCall <=3){ |
| |
| $this->retryCall++; |
| |
| Log::info("Retrying the API due to rate limit exceeds"); |
| |
| return $this->rest($type, $path, $params); |
| |
| } |
| |
| } |
| |
| } |
| // Return Guzzle response and JSON-decoded body |
| // Return Guzzle response and JSON-decoded body |
| return (object) [ |
| return (object) [ |
| 'response' => $response, |
| 'response' => $response, |
| 'errors' => $errors, |
| 'errors' => $errors, |
| 'body' => $errors ? $body->getContents() : $this->jsonDecode($body), |
| 'body' => $errors ? $body->getContents() : $this->jsonDecode($body), |
| 'timestamps' => [$tmpTimestamp, $this->requestTimestamp], |
| 'timestamps' => [$tmpTimestamp, $this->requestTimestamp], |
| ]; |
| ]; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Ensures we have the proper request for private and public calls. |
| * Ensures we have the proper request for private and public calls. |
| * Also modifies issues with redirects. |
| * Also modifies issues with redirects. |
| * |
| * |
| * @param Request $request |
| * @param Request $request |
| * |
| * |
| * @return void |
| * @return void |
| */ |
| */ |
| public function authRequest(Request $request) |
| public function authRequest(Request $request) |
| { |
| { |
| // Get the request URI |
| // Get the request URI |
| $uri = $request->getUri(); |
| $uri = $request->getUri(); |
| |
| |
| if ($this->isAuthableRequest((string) $uri)) { |
| if ($this->isAuthableRequest((string) $uri)) { |
| if ($this->isRestRequest((string) $uri)) { |
| if ($this->isRestRequest((string) $uri)) { |
| // Checks for REST |
| // Checks for REST |
| if ($this->private && ($this->apiKey === null || $this->apiPassword === null)) { |
| if ($this->private && ($this->apiKey === null || $this->apiPassword === null)) { |
| // Key and password are required for private API calls |
| // Key and password are required for private API calls |
| throw new Exception('API key and password required for private Shopify REST calls'); |
| throw new Exception('API key and password required for private Shopify REST calls'); |
| } |
| } |
| |
| |
| // Private: Add auth for REST calls |
| // Private: Add auth for REST calls |
| if ($this->private) { |
| if ($this->private) { |
| // Add the basic auth header |
| // Add the basic auth header |
| return $request->withHeader( |
| return $request->withHeader( |
| 'Authorization', |
| 'Authorization', |
| 'Basic '.base64_encode("{$this->apiKey}:{$this->apiPassword}") |
| 'Basic '.base64_encode("{$this->apiKey}:{$this->apiPassword}") |
| ); |
| ); |
| } |
| } |
| |
| |
| // Public: Add the token header |
| // Public: Add the token header |
| return $request->withHeader( |
| return $request->withHeader( |
| 'X-Shopify-Access-Token', |
| 'X-Shopify-Access-Token', |
| $this->accessToken |
| $this->accessToken |
| ); |
| ); |
| } else { |
| } else { |
| // Checks for Graph |
| // Checks for Graph |
| if ($this->private && ($this->apiPassword === null && $this->accessToken === null)) { |
| if ($this->private && ($this->apiPassword === null && $this->accessToken === null)) { |
| // Private apps need password for use as access token |
| // Private apps need password for use as access token |
| throw new Exception('API password/access token required for private Shopify GraphQL calls'); |
| throw new Exception('API password/access token required for private Shopify GraphQL calls'); |
| } elseif (!$this->private && $this->accessToken === null) { |
| } elseif (!$this->private && $this->accessToken === null) { |
| // Need access token for public calls |
| // Need access token for public calls |
| throw new Exception('Access token required for public Shopify GraphQL calls'); |
| throw new Exception('Access token required for public Shopify GraphQL calls'); |
| } |
| } |
| |
| |
| // Public/Private: Add the token header |
| // Public/Private: Add the token header |
| return $request->withHeader( |
| return $request->withHeader( |
| 'X-Shopify-Access-Token', |
| 'X-Shopify-Access-Token', |
| $this->apiPassword ?? $this->accessToken |
| $this->apiPassword ?? $this->accessToken |
| ); |
| ); |
| } |
| } |
| } |
| } |
| |
| |
| return $request; |
| return $request; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Sets a logger instance on the object. |
| * Sets a logger instance on the object. |
| * |
| * |
| * @param LoggerInterface $logger |
| * @param LoggerInterface $logger |
| * |
| * |
| * @return void |
| * @return void |
| */ |
| */ |
| public function setLogger(LoggerInterface $logger) |
| public function setLogger(LoggerInterface $logger) |
| { |
| { |
| $this->logger = $logger; |
| $this->logger = $logger; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Log a message to the logger. |
| * Log a message to the logger. |
| * |
| * |
| * @param string $msg The message to send. |
| * @param string $msg The message to send. |
| * @param int $level The level of message. |
| * @param int $level The level of message. |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| public function log(string $msg, string $level = LogLevel::DEBUG) |
| public function log(string $msg, string $level = LogLevel::DEBUG) |
| { |
| { |
| if ($this->logger === null) { |
| if ($this->logger === null) { |
| // No logger, do nothing |
| // No logger, do nothing |
| return false; |
| return false; |
| } |
| } |
| |
| |
| // Call the logger by level and pass the message |
| // Call the logger by level and pass the message |
| call_user_func([$this->logger, $level], self::LOG_KEY.' '.$msg); |
| call_user_func([$this->logger, $level], self::LOG_KEY.' '.$msg); |
| |
| |
| return true; |
| return true; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Decodes the JSON body. |
| * Decodes the JSON body. |
| * |
| * |
| * @param string $json The JSON body |
| * @param string $json The JSON body |
| * |
| * |
| * @return object The decoded JSON |
| * @return object The decoded JSON |
| */ |
| */ |
| protected function jsonDecode($json) |
| protected function jsonDecode($json) |
| { |
| { |
| // From firebase/php-jwt |
| // From firebase/php-jwt |
| if (!(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { |
| if (!(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { |
| /** |
| /** |
| * In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you |
| * In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you |
| * to specify that large ints (like Steam Transaction IDs) should be treated as |
| * to specify that large ints (like Steam Transaction IDs) should be treated as |
| * strings, rather than the PHP default behaviour of converting them to floats. |
| * strings, rather than the PHP default behaviour of converting them to floats. |
| */ |
| */ |
| $obj = json_decode($json, false, 512, JSON_BIGINT_AS_STRING); |
| $obj = json_decode($json, false, 512, JSON_BIGINT_AS_STRING); |
| } else { |
| } else { |
| // @codeCoverageIgnoreStart |
| // @codeCoverageIgnoreStart |
| /** |
| /** |
| * Not all servers will support that, however, so for older versions we must |
| * Not all servers will support that, however, so for older versions we must |
| * manually detect large ints in the JSON string and quote them (thus converting |
| * manually detect large ints in the JSON string and quote them (thus converting |
| * them to strings) before decoding, hence the preg_replace() call. |
| * them to strings) before decoding, hence the preg_replace() call. |
| * Currently not sure how to test this so I ignored it for now. |
| * Currently not sure how to test this so I ignored it for now. |
| */ |
| */ |
| $maxIntLength = strlen((string) PHP_INT_MAX) - 1; |
| $maxIntLength = strlen((string) PHP_INT_MAX) - 1; |
| $jsonWithoutBigints = preg_replace('/:\s*(-?\d{'.$maxIntLength.',})/', ': "$1"', $json); |
| $jsonWithoutBigints = preg_replace('/:\s*(-?\d{'.$maxIntLength.',})/', ': "$1"', $json); |
| $obj = json_decode($jsonWithoutBigints); |
| $obj = json_decode($jsonWithoutBigints); |
| // @codeCoverageIgnoreEnd |
| // @codeCoverageIgnoreEnd |
| } |
| } |
| |
| |
| return $obj; |
| return $obj; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Determines if the request is to Graph API. |
| * Determines if the request is to Graph API. |
| * |
| * |
| * @param string $uri |
| * @param string $uri |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| protected function isGraphRequest(string $uri) |
| protected function isGraphRequest(string $uri) |
| { |
| { |
| return strpos($uri, 'graphql.json') !== false; |
| return strpos($uri, 'graphql.json') !== false; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Determines if the request is to REST API. |
| * Determines if the request is to REST API. |
| * |
| * |
| * @param string $uri |
| * @param string $uri |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| protected function isRestRequest(string $uri) |
| protected function isRestRequest(string $uri) |
| { |
| { |
| return $this->isGraphRequest($uri) === false; |
| return $this->isGraphRequest($uri) === false; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Determines if the request requires auth headers. |
| * Determines if the request requires auth headers. |
| * |
| * |
| * @param string $uri |
| * @param string $uri |
| * |
| * |
| * @return bool |
| * @return bool |
| */ |
| */ |
| protected function isAuthableRequest(string $uri) |
| protected function isAuthableRequest(string $uri) |
| { |
| { |
| return preg_match('/\/admin\/oauth\/(authorize|access_token|access_scopes)/', $uri) === 0; |
| return preg_match('/\/admin\/oauth\/(authorize|access_token|access_scopes)/', $uri) === 0; |
| } |
| } |
| |
| |
| /** |
| /** |
| * Versions the API call with the set version. |
| * Versions the API call with the set version. |
| * |
| * |
| * @param string $uri |
| * @param string $uri |
| * |
| * |
| * @return string |
| * @return string |
| */ |
| */ |
| protected function versionPath(string $uri) |
| protected function versionPath(string $uri) |
| { |
| { |
| if ($this->version === null || preg_match(self::VERSION_PATTERN, $uri) || !$this->isAuthableRequest($uri)) { |
| if ($this->version === null || preg_match(self::VERSION_PATTERN, $uri) || !$this->isAuthableRequest($uri)) { |
| // No version set, or already versioned... nothing to do |
| // No version set, or already versioned... nothing to do |
| return $uri; |
| return $uri; |
| } |
| } |
| |
| |
| // Graph request |
| // Graph request |
| if ($this->isGraphRequest($uri)) { |
| if ($this->isGraphRequest($uri)) { |
| return str_replace('/admin/api', "/admin/api/{$this->version}", $uri); |
| return str_replace('/admin/api', "/admin/api/{$this->version}", $uri); |
| } |
| } |
| |
| |
| // REST request |
| // REST request |
| return preg_replace('/\/admin(\/api)?\//', "/admin/api/{$this->version}/", $uri); |
| return preg_replace('/\/admin(\/api)?\//', "/admin/api/{$this->version}/", $uri); |
| } |
| } |
| } |
| } |
| |
| |