Skip to content

Commit

Permalink
Merge pull request #16 from bookboon/feature/raw-client
Browse files Browse the repository at this point in the history
feat: added raw client for making queries to the API without decoding the response
  • Loading branch information
lkm authored Jun 1, 2022
2 parents ecd3542 + 706e862 commit f06299f
Show file tree
Hide file tree
Showing 16 changed files with 684 additions and 178 deletions.
173 changes: 173 additions & 0 deletions Client/AccessTokenClient.php
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;
}
}
21 changes: 21 additions & 0 deletions Client/ClientConstants.php
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';
}
154 changes: 154 additions & 0 deletions Client/Headers.php
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)]);
}
}
Loading

0 comments on commit f06299f

Please sign in to comment.