Skip to content

Commit f06299f

Browse files
authored
Merge pull request #16 from bookboon/feature/raw-client
feat: added raw client for making queries to the API without decoding the response
2 parents ecd3542 + 706e862 commit f06299f

16 files changed

Lines changed: 684 additions & 178 deletions

Client/AccessTokenClient.php

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
namespace Bookboon\ApiBundle\Client;
4+
5+
use Bookboon\OauthClient\BookboonProvider;
6+
use Bookboon\OauthClient\OauthGrants;
7+
use Bookboon\ApiBundle\Exception\ApiAuthenticationException;
8+
use Bookboon\ApiBundle\Exception\ApiInvalidStateException;
9+
use Bookboon\ApiBundle\Exception\UsageException;
10+
use GuzzleHttp\TransferStats;
11+
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
12+
use League\OAuth2\Client\Token\AccessTokenInterface;
13+
use Psr\Log\LoggerInterface;
14+
use Psr\SimpleCache\CacheInterface;
15+
16+
class AccessTokenClient
17+
{
18+
protected string $_apiUri;
19+
private ?AccessTokenInterface $accessToken;
20+
protected ?string $act;
21+
protected BookboonProvider $provider;
22+
protected array $requestOptions = [];
23+
protected string $apiId;
24+
protected Headers $headers;
25+
protected ?CacheInterface $cache;
26+
27+
public function __construct(
28+
string $apiId,
29+
string $apiSecret,
30+
Headers $headers,
31+
array $scopes,
32+
CacheInterface $cache = null,
33+
?string $redirectUri = null,
34+
?string $appUserId = null,
35+
?string $authServiceUri = null,
36+
?string $apiUri = null,
37+
LoggerInterface $logger = null,
38+
array $clientOptions = []
39+
) {
40+
if (empty($apiId)) {
41+
throw new UsageException("Client id is required");
42+
}
43+
44+
$clientOptions = array_merge(
45+
$clientOptions,
46+
[
47+
'clientId' => $apiId,
48+
'clientSecret' => $apiSecret,
49+
'scope' => $scopes,
50+
'redirectUri' => $redirectUri,
51+
'baseUri' => $authServiceUri,
52+
]
53+
);
54+
55+
if ($logger !== null) {
56+
$this->requestOptions = [
57+
'on_stats' => function (TransferStats $stats) use ($logger) {
58+
if ($stats->hasResponse()) {
59+
$size = $stats->getHandlerStat('size_download') ?? 0;
60+
$statusCode = $stats->getResponse() ? $stats->getResponse()->getStatusCode() : 0;
61+
62+
$logger->info(
63+
"Api request \"{$stats->getRequest()->getMethod()} {$stats->getRequest()->getRequestTarget()} HTTP/{$stats->getRequest()->getProtocolVersion()}\" {$statusCode} - {$size} - {$stats->getTransferTime()}"
64+
);
65+
} else {
66+
$logger->error(
67+
"Api request: No response received with error {$stats->getHandlerErrorData()}"
68+
);
69+
}
70+
}
71+
];
72+
}
73+
74+
$clientOptions['requestOptions'] = $this->requestOptions;
75+
$this->provider = new BookboonProvider($clientOptions);
76+
77+
$this->apiId = $apiId;
78+
$this->cache = $cache;
79+
$this->headers = $headers;
80+
$this->act = $appUserId;
81+
82+
$this->_apiUri = $this->parseUriOrDefault($apiUri);
83+
}
84+
85+
/**
86+
* @param array $options
87+
* @param string $type
88+
* @return AccessTokenInterface
89+
* @throws ApiAuthenticationException
90+
* @throws UsageException
91+
*/
92+
public function requestAccessToken(
93+
array $options = [],
94+
string $type = OauthGrants::AUTHORIZATION_CODE
95+
) : AccessTokenInterface {
96+
$provider = $this->provider;
97+
98+
if ($type == OauthGrants::AUTHORIZATION_CODE && !isset($options["code"])) {
99+
throw new UsageException("This oauth flow requires a code");
100+
}
101+
102+
try {
103+
$this->accessToken = $provider->getAccessToken($type, $options);
104+
}
105+
106+
catch (IdentityProviderException $e) {
107+
//TODO: Parse and send this with exception (string) $e->getResponseBody()->getBody()
108+
throw new ApiAuthenticationException("Authorization Failed");
109+
}
110+
111+
return $this->accessToken;
112+
}
113+
114+
public function refreshAccessToken(AccessTokenInterface $accessToken) : AccessTokenInterface
115+
{
116+
$this->accessToken = $this->provider->getAccessToken('refresh_token', [
117+
'refresh_token' => $accessToken->getRefreshToken()
118+
]);
119+
120+
return $accessToken;
121+
}
122+
123+
public function generateState(): string
124+
{
125+
return $this->provider->generateRandomState();
126+
}
127+
128+
public function isCorrectState(string $stateParameter, string $stateSession) : bool
129+
{
130+
if (empty($stateParameter) || ($stateParameter !== $stateSession)) {
131+
throw new ApiInvalidStateException("State is invalid");
132+
}
133+
134+
return true;
135+
}
136+
137+
public function getAuthorizationUrl(array $options = []): string
138+
{
139+
$provider = $this->provider;
140+
141+
if (null != $this->act && false === isset($options['act'])) {
142+
$options['act'] = $this->act;
143+
}
144+
145+
return $provider->getAuthorizationUrl($options);
146+
}
147+
148+
protected function parseUriOrDefault(?string $uri) : string
149+
{
150+
$protocol = ClientConstants::API_PROTOCOL;
151+
$host = ClientConstants::API_HOST;
152+
$path = ClientConstants::API_PATH;
153+
154+
if (!empty($uri)) {
155+
$parts = explode('://', $uri);
156+
$protocol = $parts[0];
157+
$host = $parts[1];
158+
if (strpos($host, '/') !== false) {
159+
throw new UsageException('URI must not contain forward slashes');
160+
}
161+
}
162+
163+
if ($protocol !== 'http' && $protocol !== 'https') {
164+
throw new UsageException('Invalid protocol specified in URI');
165+
}
166+
167+
return "${protocol}://${host}${path}";
168+
}
169+
170+
public function getAct(): ?string {
171+
return $this->act;
172+
}
173+
}

Client/ClientConstants.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Bookboon\ApiBundle\Client;
4+
5+
class ClientConstants
6+
{
7+
const HTTP_HEAD = 'HEAD';
8+
const HTTP_GET = 'GET';
9+
const HTTP_POST = 'POST';
10+
const HTTP_DELETE = 'DELETE';
11+
const HTTP_PUT = 'PUT';
12+
13+
const CONTENT_TYPE_JSON = 'application/json';
14+
const CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded';
15+
16+
const API_PROTOCOL = 'https';
17+
const API_HOST = 'bookboon.com';
18+
const API_PATH = '/api';
19+
20+
const VERSION = 'Bookboon-PHP/3.3';
21+
}

Client/Headers.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
namespace Bookboon\ApiBundle\Client;
4+
5+
6+
use ArrayAccess;
7+
8+
class Headers implements ArrayAccess
9+
{
10+
const HEADER_BRANDING = 'X-Bookboon-Branding';
11+
const HEADER_ROTATION = 'X-Bookboon-Rotation';
12+
const HEADER_PREMIUM = 'X-Bookboon-PremiumLevel';
13+
const HEADER_CURRENCY = 'X-Bookboon-Currency';
14+
const HEADER_LANGUAGE = 'Accept-Language';
15+
const HEADER_XFF = 'X-Forwarded-For';
16+
17+
private array $headers = [];
18+
19+
public function __construct(array $headers = [])
20+
{
21+
foreach ($headers as $k => $v) {
22+
$this->offsetSet($k, $v);
23+
}
24+
25+
$this->set(static::HEADER_XFF, $this->getRemoteAddress() ?? '');
26+
}
27+
28+
public function set(string $header, string $value) : void
29+
{
30+
$this->headers[$header] = $value;
31+
}
32+
33+
public function get(string $header) : ?string
34+
{
35+
return $this->headers[$header] ?? null;
36+
}
37+
38+
public function getAll() : array
39+
{
40+
$headers = [];
41+
foreach ($this->headers as $h => $v) {
42+
$headers[] = $h.': '.$v;
43+
}
44+
45+
return $headers;
46+
}
47+
48+
public function getHeadersArray() : array
49+
{
50+
return $this->headers;
51+
}
52+
53+
/**
54+
* Returns the remote address either directly or if set XFF header value.
55+
*
56+
* @return string|null The ip address
57+
*/
58+
private function getRemoteAddress() : ?string
59+
{
60+
$hostname = null;
61+
62+
if (isset($_SERVER['REMOTE_ADDR'])) {
63+
$hostname = filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP);
64+
65+
if (false === $hostname) {
66+
$hostname = null;
67+
}
68+
}
69+
70+
if (function_exists('apache_request_headers')) {
71+
$headers = apache_request_headers();
72+
73+
if ($headers === false) {
74+
return $hostname;
75+
}
76+
77+
foreach ($headers as $k => $v) {
78+
if (strcasecmp($k, 'x-forwarded-for')) {
79+
continue;
80+
}
81+
82+
$hostname = explode(',', $v);
83+
$hostname = trim($hostname[0]);
84+
break;
85+
}
86+
}
87+
88+
return $hostname;
89+
}
90+
91+
/**
92+
* Whether a offset exists
93+
* @link https://php.net/manual/en/arrayaccess.offsetexists.php
94+
* @param mixed $offset <p>
95+
* An offset to check for.
96+
* </p>
97+
* @return bool true on success or false on failure.
98+
* </p>
99+
* <p>
100+
* The return value will be casted to boolean if non-boolean was returned.
101+
* @since 5.0.0
102+
*/
103+
public function offsetExists($offset)
104+
{
105+
return isset($this->headers[strtolower($offset)]);
106+
}
107+
108+
/**
109+
* Offset to retrieve
110+
* @link https://php.net/manual/en/arrayaccess.offsetget.php
111+
* @param mixed $offset <p>
112+
* The offset to retrieve.
113+
* </p>
114+
* @return mixed Can return all value types.
115+
* @since 5.0.0
116+
*/
117+
public function offsetGet($offset)
118+
{
119+
return $this->headers[strtolower($offset)] ?? null;
120+
}
121+
122+
/**
123+
* Offset to set
124+
* @link https://php.net/manual/en/arrayaccess.offsetset.php
125+
* @param mixed $offset <p>
126+
* The offset to assign the value to.
127+
* </p>
128+
* @param mixed $value <p>
129+
* The value to set.
130+
* </p>
131+
* @return void
132+
* @since 5.0.0
133+
*/
134+
public function offsetSet($offset, $value)
135+
{
136+
if (is_string($offset) && $offset !== '') {
137+
$this->headers[strtolower($offset)] = $value;
138+
}
139+
}
140+
141+
/**
142+
* Offset to unset
143+
* @link https://php.net/manual/en/arrayaccess.offsetunset.php
144+
* @param mixed $offset <p>
145+
* The offset to unset.
146+
* </p>
147+
* @return void
148+
* @since 5.0.0
149+
*/
150+
public function offsetUnset($offset)
151+
{
152+
unset($this->headers[strtolower($offset)]);
153+
}
154+
}

0 commit comments

Comments
 (0)