-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from bookboon/feature/raw-client
feat: added raw client for making queries to the API without decoding the response
- Loading branch information
Showing
16 changed files
with
684 additions
and
178 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
<?php | ||
|
||
namespace Bookboon\ApiBundle\Client; | ||
|
||
use Bookboon\OauthClient\BookboonProvider; | ||
use Bookboon\OauthClient\OauthGrants; | ||
use Bookboon\ApiBundle\Exception\ApiAuthenticationException; | ||
use Bookboon\ApiBundle\Exception\ApiInvalidStateException; | ||
use Bookboon\ApiBundle\Exception\UsageException; | ||
use GuzzleHttp\TransferStats; | ||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException; | ||
use League\OAuth2\Client\Token\AccessTokenInterface; | ||
use Psr\Log\LoggerInterface; | ||
use Psr\SimpleCache\CacheInterface; | ||
|
||
class AccessTokenClient | ||
{ | ||
protected string $_apiUri; | ||
private ?AccessTokenInterface $accessToken; | ||
protected ?string $act; | ||
protected BookboonProvider $provider; | ||
protected array $requestOptions = []; | ||
protected string $apiId; | ||
protected Headers $headers; | ||
protected ?CacheInterface $cache; | ||
|
||
public function __construct( | ||
string $apiId, | ||
string $apiSecret, | ||
Headers $headers, | ||
array $scopes, | ||
CacheInterface $cache = null, | ||
?string $redirectUri = null, | ||
?string $appUserId = null, | ||
?string $authServiceUri = null, | ||
?string $apiUri = null, | ||
LoggerInterface $logger = null, | ||
array $clientOptions = [] | ||
) { | ||
if (empty($apiId)) { | ||
throw new UsageException("Client id is required"); | ||
} | ||
|
||
$clientOptions = array_merge( | ||
$clientOptions, | ||
[ | ||
'clientId' => $apiId, | ||
'clientSecret' => $apiSecret, | ||
'scope' => $scopes, | ||
'redirectUri' => $redirectUri, | ||
'baseUri' => $authServiceUri, | ||
] | ||
); | ||
|
||
if ($logger !== null) { | ||
$this->requestOptions = [ | ||
'on_stats' => function (TransferStats $stats) use ($logger) { | ||
if ($stats->hasResponse()) { | ||
$size = $stats->getHandlerStat('size_download') ?? 0; | ||
$statusCode = $stats->getResponse() ? $stats->getResponse()->getStatusCode() : 0; | ||
|
||
$logger->info( | ||
"Api request \"{$stats->getRequest()->getMethod()} {$stats->getRequest()->getRequestTarget()} HTTP/{$stats->getRequest()->getProtocolVersion()}\" {$statusCode} - {$size} - {$stats->getTransferTime()}" | ||
); | ||
} else { | ||
$logger->error( | ||
"Api request: No response received with error {$stats->getHandlerErrorData()}" | ||
); | ||
} | ||
} | ||
]; | ||
} | ||
|
||
$clientOptions['requestOptions'] = $this->requestOptions; | ||
$this->provider = new BookboonProvider($clientOptions); | ||
|
||
$this->apiId = $apiId; | ||
$this->cache = $cache; | ||
$this->headers = $headers; | ||
$this->act = $appUserId; | ||
|
||
$this->_apiUri = $this->parseUriOrDefault($apiUri); | ||
} | ||
|
||
/** | ||
* @param array $options | ||
* @param string $type | ||
* @return AccessTokenInterface | ||
* @throws ApiAuthenticationException | ||
* @throws UsageException | ||
*/ | ||
public function requestAccessToken( | ||
array $options = [], | ||
string $type = OauthGrants::AUTHORIZATION_CODE | ||
) : AccessTokenInterface { | ||
$provider = $this->provider; | ||
|
||
if ($type == OauthGrants::AUTHORIZATION_CODE && !isset($options["code"])) { | ||
throw new UsageException("This oauth flow requires a code"); | ||
} | ||
|
||
try { | ||
$this->accessToken = $provider->getAccessToken($type, $options); | ||
} | ||
|
||
catch (IdentityProviderException $e) { | ||
//TODO: Parse and send this with exception (string) $e->getResponseBody()->getBody() | ||
throw new ApiAuthenticationException("Authorization Failed"); | ||
} | ||
|
||
return $this->accessToken; | ||
} | ||
|
||
public function refreshAccessToken(AccessTokenInterface $accessToken) : AccessTokenInterface | ||
{ | ||
$this->accessToken = $this->provider->getAccessToken('refresh_token', [ | ||
'refresh_token' => $accessToken->getRefreshToken() | ||
]); | ||
|
||
return $accessToken; | ||
} | ||
|
||
public function generateState(): string | ||
{ | ||
return $this->provider->generateRandomState(); | ||
} | ||
|
||
public function isCorrectState(string $stateParameter, string $stateSession) : bool | ||
{ | ||
if (empty($stateParameter) || ($stateParameter !== $stateSession)) { | ||
throw new ApiInvalidStateException("State is invalid"); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public function getAuthorizationUrl(array $options = []): string | ||
{ | ||
$provider = $this->provider; | ||
|
||
if (null != $this->act && false === isset($options['act'])) { | ||
$options['act'] = $this->act; | ||
} | ||
|
||
return $provider->getAuthorizationUrl($options); | ||
} | ||
|
||
protected function parseUriOrDefault(?string $uri) : string | ||
{ | ||
$protocol = ClientConstants::API_PROTOCOL; | ||
$host = ClientConstants::API_HOST; | ||
$path = ClientConstants::API_PATH; | ||
|
||
if (!empty($uri)) { | ||
$parts = explode('://', $uri); | ||
$protocol = $parts[0]; | ||
$host = $parts[1]; | ||
if (strpos($host, '/') !== false) { | ||
throw new UsageException('URI must not contain forward slashes'); | ||
} | ||
} | ||
|
||
if ($protocol !== 'http' && $protocol !== 'https') { | ||
throw new UsageException('Invalid protocol specified in URI'); | ||
} | ||
|
||
return "${protocol}://${host}${path}"; | ||
} | ||
|
||
public function getAct(): ?string { | ||
return $this->act; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
namespace Bookboon\ApiBundle\Client; | ||
|
||
class ClientConstants | ||
{ | ||
const HTTP_HEAD = 'HEAD'; | ||
const HTTP_GET = 'GET'; | ||
const HTTP_POST = 'POST'; | ||
const HTTP_DELETE = 'DELETE'; | ||
const HTTP_PUT = 'PUT'; | ||
|
||
const CONTENT_TYPE_JSON = 'application/json'; | ||
const CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded'; | ||
|
||
const API_PROTOCOL = 'https'; | ||
const API_HOST = 'bookboon.com'; | ||
const API_PATH = '/api'; | ||
|
||
const VERSION = 'Bookboon-PHP/3.3'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
<?php | ||
|
||
namespace Bookboon\ApiBundle\Client; | ||
|
||
|
||
use ArrayAccess; | ||
|
||
class Headers implements ArrayAccess | ||
{ | ||
const HEADER_BRANDING = 'X-Bookboon-Branding'; | ||
const HEADER_ROTATION = 'X-Bookboon-Rotation'; | ||
const HEADER_PREMIUM = 'X-Bookboon-PremiumLevel'; | ||
const HEADER_CURRENCY = 'X-Bookboon-Currency'; | ||
const HEADER_LANGUAGE = 'Accept-Language'; | ||
const HEADER_XFF = 'X-Forwarded-For'; | ||
|
||
private array $headers = []; | ||
|
||
public function __construct(array $headers = []) | ||
{ | ||
foreach ($headers as $k => $v) { | ||
$this->offsetSet($k, $v); | ||
} | ||
|
||
$this->set(static::HEADER_XFF, $this->getRemoteAddress() ?? ''); | ||
} | ||
|
||
public function set(string $header, string $value) : void | ||
{ | ||
$this->headers[$header] = $value; | ||
} | ||
|
||
public function get(string $header) : ?string | ||
{ | ||
return $this->headers[$header] ?? null; | ||
} | ||
|
||
public function getAll() : array | ||
{ | ||
$headers = []; | ||
foreach ($this->headers as $h => $v) { | ||
$headers[] = $h.': '.$v; | ||
} | ||
|
||
return $headers; | ||
} | ||
|
||
public function getHeadersArray() : array | ||
{ | ||
return $this->headers; | ||
} | ||
|
||
/** | ||
* Returns the remote address either directly or if set XFF header value. | ||
* | ||
* @return string|null The ip address | ||
*/ | ||
private function getRemoteAddress() : ?string | ||
{ | ||
$hostname = null; | ||
|
||
if (isset($_SERVER['REMOTE_ADDR'])) { | ||
$hostname = filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP); | ||
|
||
if (false === $hostname) { | ||
$hostname = null; | ||
} | ||
} | ||
|
||
if (function_exists('apache_request_headers')) { | ||
$headers = apache_request_headers(); | ||
|
||
if ($headers === false) { | ||
return $hostname; | ||
} | ||
|
||
foreach ($headers as $k => $v) { | ||
if (strcasecmp($k, 'x-forwarded-for')) { | ||
continue; | ||
} | ||
|
||
$hostname = explode(',', $v); | ||
$hostname = trim($hostname[0]); | ||
break; | ||
} | ||
} | ||
|
||
return $hostname; | ||
} | ||
|
||
/** | ||
* Whether a offset exists | ||
* @link https://php.net/manual/en/arrayaccess.offsetexists.php | ||
* @param mixed $offset <p> | ||
* An offset to check for. | ||
* </p> | ||
* @return bool true on success or false on failure. | ||
* </p> | ||
* <p> | ||
* The return value will be casted to boolean if non-boolean was returned. | ||
* @since 5.0.0 | ||
*/ | ||
public function offsetExists($offset) | ||
{ | ||
return isset($this->headers[strtolower($offset)]); | ||
} | ||
|
||
/** | ||
* Offset to retrieve | ||
* @link https://php.net/manual/en/arrayaccess.offsetget.php | ||
* @param mixed $offset <p> | ||
* The offset to retrieve. | ||
* </p> | ||
* @return mixed Can return all value types. | ||
* @since 5.0.0 | ||
*/ | ||
public function offsetGet($offset) | ||
{ | ||
return $this->headers[strtolower($offset)] ?? null; | ||
} | ||
|
||
/** | ||
* Offset to set | ||
* @link https://php.net/manual/en/arrayaccess.offsetset.php | ||
* @param mixed $offset <p> | ||
* The offset to assign the value to. | ||
* </p> | ||
* @param mixed $value <p> | ||
* The value to set. | ||
* </p> | ||
* @return void | ||
* @since 5.0.0 | ||
*/ | ||
public function offsetSet($offset, $value) | ||
{ | ||
if (is_string($offset) && $offset !== '') { | ||
$this->headers[strtolower($offset)] = $value; | ||
} | ||
} | ||
|
||
/** | ||
* Offset to unset | ||
* @link https://php.net/manual/en/arrayaccess.offsetunset.php | ||
* @param mixed $offset <p> | ||
* The offset to unset. | ||
* </p> | ||
* @return void | ||
* @since 5.0.0 | ||
*/ | ||
public function offsetUnset($offset) | ||
{ | ||
unset($this->headers[strtolower($offset)]); | ||
} | ||
} |
Oops, something went wrong.