1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LibLynx\Connect; |
4
|
|
|
|
5
|
|
|
use GuzzleHttp\Exception\RequestException; |
6
|
|
|
use GuzzleHttp\Psr7\Request; |
7
|
|
|
use kamermans\OAuth2\GrantType\ClientCredentials; |
8
|
|
|
use kamermans\OAuth2\OAuth2Middleware; |
9
|
|
|
use GuzzleHttp\HandlerStack; |
10
|
|
|
use GuzzleHttp\Client as GuzzleClient; |
11
|
|
|
use Kevinrob\GuzzleCache\CacheMiddleware; |
12
|
|
|
use Kevinrob\GuzzleCache\Storage\Psr16CacheStorage; |
13
|
|
|
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy; |
14
|
|
|
use LibLynx\Connect\Exception\APIException; |
15
|
|
|
use LibLynx\Connect\Exception\LogicException; |
16
|
|
|
use Psr\Log\LoggerAwareInterface; |
17
|
|
|
use Psr\Log\LoggerInterface; |
18
|
|
|
use Psr\Log\NullLogger; |
19
|
|
|
use Psr\SimpleCache\CacheInterface; |
20
|
|
|
use Psr\SimpleCache\InvalidArgumentException; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* LibLynx Connect API client |
24
|
|
|
* |
25
|
|
|
* $liblynx=new Liblynx\Connect\Client; |
26
|
|
|
* $liblynx->setCredentials('your client id', 'your client secret'); |
27
|
|
|
* |
28
|
|
|
* //must set a PSR-16 cache - this can be used for testing |
29
|
|
|
* $liblynx->setCache(new \Symfony\Component\Cache\Simple\ArrayCache); |
30
|
|
|
* |
31
|
|
|
* $identification=$liblynx->authorize(Identification::fromSuperglobals()); |
32
|
|
|
* if ($identification->isIdentified()) { |
33
|
|
|
* //good to go |
34
|
|
|
* } |
35
|
|
|
* |
36
|
|
|
* if ($identification->requiresWayf()) { |
37
|
|
|
* $identification->doWayfRedirect(); |
38
|
|
|
* } |
39
|
|
|
* |
40
|
|
|
* @package LibLynx\Connect |
41
|
|
|
*/ |
42
|
|
|
class Client implements LoggerAwareInterface |
43
|
|
|
{ |
44
|
|
|
private $apiroot = 'https://connect.liblynx.com'; |
45
|
|
|
|
46
|
|
|
/** @var string client ID obtain from LibLynx Connect admin portal */ |
47
|
|
|
private $clientId; |
48
|
|
|
|
49
|
|
|
/** @var string client secret obtain from LibLynx Connect admin portal */ |
50
|
|
|
private $clientSecret; |
51
|
|
|
|
52
|
|
|
/** @var GuzzleClient HTTP client for API requests */ |
53
|
|
|
private $guzzle; |
54
|
|
|
|
55
|
|
|
/** @var \stdClass entry point resource */ |
56
|
|
|
private $entrypoint; |
57
|
|
|
|
58
|
|
|
/** @var callable RequestStack handler for API requests */ |
59
|
|
|
private $apiHandler = null; |
60
|
|
|
|
61
|
|
|
/** @var callable RequestStack handler for OAuth2 requests */ |
62
|
|
|
private $oauth2Handler = null; |
63
|
|
|
|
64
|
|
|
/** @var CacheInterface */ |
65
|
|
|
protected $cache; |
66
|
|
|
|
67
|
|
|
/** @var LoggerInterface */ |
68
|
|
|
protected $log; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Create new LibLynx API client |
72
|
|
|
*/ |
73
|
16 |
|
public function __construct() |
74
|
|
|
{ |
75
|
16 |
|
if (isset($_SERVER['LIBLYNX_CLIENT_ID'])) { |
76
|
2 |
|
$this->clientId = $_SERVER['LIBLYNX_CLIENT_ID']; |
77
|
|
|
} |
78
|
16 |
|
if (isset($_SERVER['LIBLYNX_CLIENT_SECRET'])) { |
79
|
2 |
|
$this->clientSecret = $_SERVER['LIBLYNX_CLIENT_SECRET']; |
80
|
|
|
} |
81
|
|
|
|
82
|
16 |
|
$this->log = new NullLogger(); |
83
|
16 |
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @inheritdoc |
87
|
|
|
*/ |
88
|
2 |
|
public function setLogger(LoggerInterface $logger) |
89
|
|
|
{ |
90
|
2 |
|
$this->log = $logger; |
91
|
2 |
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Set API root |
95
|
|
|
* An alternative root may be provided to you by LibLynx Technical Support |
96
|
|
|
* @param string $url |
97
|
|
|
*/ |
98
|
10 |
|
public function setAPIRoot($url) |
99
|
|
|
{ |
100
|
10 |
|
$this->apiroot = $url; |
101
|
10 |
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Set OAuth2.0 client credentials |
105
|
|
|
* These will be provided to you by LibLynx Technical Support |
106
|
|
|
* |
107
|
|
|
* @param string $clientId |
108
|
|
|
* @param string $clientSecret |
109
|
|
|
*/ |
110
|
12 |
|
public function setCredentials($clientId, $clientSecret) |
111
|
|
|
{ |
112
|
12 |
|
$this->clientId = $clientId; |
113
|
12 |
|
$this->clientSecret = $clientSecret; |
114
|
12 |
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @return array containing client id and secret |
118
|
|
|
*/ |
119
|
2 |
|
public function getCredentials() |
120
|
|
|
{ |
121
|
2 |
|
return [$this->clientId, $this->clientSecret]; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Attempt an identification using the LibLynx API |
126
|
|
|
* |
127
|
|
|
* @param IdentificationRequest $request |
128
|
|
|
* @return Identification|null |
129
|
|
|
*/ |
130
|
8 |
|
public function authorize(IdentificationRequest $request) |
131
|
|
|
{ |
132
|
8 |
|
$payload = $request->getRequestJSON(); |
133
|
8 |
|
$response = $this->post('@new_identification', $payload); |
134
|
4 |
|
if (!isset($response->id)) { |
135
|
|
|
//failed |
136
|
2 |
|
$this->log->critical('Identification request failed {payload}', ['payload' => $payload]); |
137
|
2 |
|
return null; |
138
|
|
|
} |
139
|
|
|
|
140
|
2 |
|
$identification = new Identification($response); |
141
|
2 |
|
$this->log->info( |
142
|
2 |
|
'Identification request for ip {ip} on URL {url} succeeded status={status} id={id}', |
143
|
|
|
[ |
144
|
2 |
|
'status' => $identification->status, |
145
|
2 |
|
'id' => $identification->id, |
146
|
2 |
|
'ip' => $identification->ip, |
147
|
2 |
|
'url' => $identification->url |
148
|
|
|
] |
149
|
|
|
); |
150
|
|
|
|
151
|
2 |
|
return $identification; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* General purpose 'GET' request against API |
156
|
|
|
* @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource |
157
|
|
|
* @return mixed |
158
|
|
|
*/ |
159
|
|
|
public function get($entrypoint) |
160
|
|
|
{ |
161
|
|
|
return $this->makeAPIRequest('GET', $entrypoint); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* General purpose 'POST' request against API |
166
|
|
|
* @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource |
167
|
|
|
* @param $json string contains JSON formatted data to post |
168
|
|
|
* @return mixed |
169
|
|
|
*/ |
170
|
8 |
|
public function post($entrypoint, $json) |
171
|
|
|
{ |
172
|
8 |
|
return $this->makeAPIRequest('POST', $entrypoint, $json); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* General purpose 'PUT' request against API |
177
|
|
|
* @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource |
178
|
|
|
* @return mixed string contains JSON formatted data to put |
179
|
|
|
*/ |
180
|
|
|
public function put($entrypoint, $json) |
181
|
|
|
{ |
182
|
|
|
return $this->makeAPIRequest('PUT', $entrypoint, $json); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* This is primarily to facilitate testing - we can add a MockHandler to return |
188
|
|
|
* test responses |
189
|
|
|
* |
190
|
|
|
* @param callable $handler |
191
|
|
|
* @return self |
192
|
|
|
*/ |
193
|
10 |
|
public function setAPIHandler(callable $handler) |
194
|
|
|
{ |
195
|
10 |
|
$this->apiHandler = $handler; |
196
|
10 |
|
return $this; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* This is primarily to facilitate testing - we can add a MockHandler to return |
201
|
|
|
* test responses |
202
|
|
|
* |
203
|
|
|
* @param callable $handler |
204
|
|
|
* @return self |
205
|
|
|
*/ |
206
|
10 |
|
public function setOAuth2Handler(callable $handler) |
207
|
|
|
{ |
208
|
10 |
|
$this->oauth2Handler = $handler; |
209
|
10 |
|
return $this; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @param $method |
214
|
|
|
* @param $entrypoint |
215
|
|
|
* @param null $json |
216
|
|
|
* @return \stdClass object containing JSON decoded response |
217
|
|
|
*/ |
218
|
8 |
|
protected function makeAPIRequest($method, $entrypoint, $json = null) |
219
|
|
|
{ |
220
|
8 |
|
$this->log->debug('{method} {entrypoint} {json}', [ |
221
|
8 |
|
'method' => $method, |
222
|
8 |
|
'entrypoint' => $entrypoint, |
223
|
8 |
|
'json' => $json |
224
|
|
|
]); |
225
|
8 |
|
$url = $this->resolveEntryPoint($entrypoint); |
226
|
4 |
|
$client = $this->getClient(); |
227
|
|
|
|
228
|
4 |
|
$this->log->debug('{entrypoint} = {url}', ['entrypoint' => $entrypoint, 'url' => $url]); |
229
|
|
|
|
230
|
4 |
|
$headers = ['Accept' => 'application/json']; |
231
|
4 |
|
if (!empty($json)) { |
232
|
4 |
|
$headers['Content-Type'] = 'application/json'; |
233
|
|
|
} |
234
|
|
|
|
235
|
4 |
|
$request = new Request($method, $url, $headers, $json); |
236
|
|
|
|
237
|
|
|
try { |
238
|
4 |
|
$response = $client->send($request); |
239
|
|
|
|
240
|
2 |
|
$this->log->debug('{method} {entrypoint} succeeded {status}', [ |
241
|
2 |
|
'method' => $method, |
242
|
2 |
|
'entrypoint' => $entrypoint, |
243
|
2 |
|
'status' => $response->getStatusCode(), |
244
|
|
|
]); |
245
|
2 |
|
} catch (RequestException $e) { |
246
|
2 |
|
$response = $e->getResponse(); |
247
|
2 |
|
$this->log->error( |
248
|
2 |
|
'{method} {entrypoint} {json} failed ({status}): {body}', |
249
|
|
|
[ |
250
|
2 |
|
'method' => $method, |
251
|
2 |
|
'json' => $json, |
252
|
2 |
|
'entrypoint' => $entrypoint, |
253
|
2 |
|
'status' => $response->getStatusCode(), |
254
|
2 |
|
'body' => $response->getBody() |
255
|
|
|
] |
256
|
|
|
); |
257
|
|
|
} |
258
|
|
|
|
259
|
4 |
|
$payload = json_decode($response->getBody()); |
260
|
4 |
|
return $payload; |
261
|
|
|
} |
262
|
|
|
|
263
|
8 |
|
public function resolveEntryPoint($nameOrUrl) |
264
|
|
|
{ |
265
|
8 |
|
if ($nameOrUrl[0] === '@') { |
266
|
8 |
|
return $this->getEntryPoint($nameOrUrl); |
267
|
|
|
} |
268
|
|
|
//it's a URL |
269
|
|
|
return $nameOrUrl; |
270
|
|
|
} |
271
|
|
|
|
272
|
14 |
|
public function getEntryPoint($name) |
273
|
|
|
{ |
274
|
14 |
|
if (!is_array($this->entrypoint)) { |
275
|
14 |
|
$key = 'entrypoint' . $this->clientId; |
276
|
|
|
|
277
|
14 |
|
if ($this->cacheHas($key)) { |
278
|
2 |
|
$this->log->debug('loading entrypoint from persistent cache'); |
279
|
2 |
|
$this->entrypoint = $this->cacheGet($key); |
280
|
|
|
; |
281
|
|
|
} else { |
282
|
12 |
|
$this->log->debug('entrypoint not cached, requesting from API'); |
283
|
12 |
|
$client = $this->getClient(); |
284
|
|
|
|
285
|
10 |
|
$request = new Request('GET', 'api', [ |
286
|
10 |
|
'Content-Type' => 'application/json', |
287
|
|
|
'Accept' => 'application/json', |
288
|
|
|
]); |
289
|
|
|
|
290
|
|
|
try { |
291
|
10 |
|
$response = $client->send($request); |
292
|
|
|
|
293
|
8 |
|
$payload = json_decode($response->getBody()); |
294
|
8 |
|
if (is_object($payload) && isset($payload->_links)) { |
295
|
8 |
|
$this->log->info('entrypoint loaded from API and cached'); |
296
|
8 |
|
$this->entrypoint = $payload; |
297
|
|
|
|
298
|
8 |
|
$this->cacheSet($key, $payload, 86400); |
299
|
|
|
} |
300
|
2 |
|
} catch (RequestException $e) { |
301
|
10 |
|
throw new APIException("Unable to obtain LibLynx API entry point resource", 0, $e); |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
} else { |
305
|
|
|
$this->log->debug('using previously loaded entrypoint'); |
306
|
|
|
} |
307
|
|
|
|
308
|
8 |
|
if (!isset($this->entrypoint->_links->$name->href)) { |
309
|
2 |
|
throw new LogicException("Invalid LibLynx API entrypoint $name requested"); |
310
|
|
|
} |
311
|
|
|
|
312
|
6 |
|
return $this->entrypoint->_links->$name->href; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* @param $key |
317
|
|
|
* @return bool |
318
|
|
|
* @codeCoverageIgnore |
319
|
|
|
*/ |
320
|
|
View Code Duplication |
protected function cacheHas($key) |
|
|
|
|
321
|
|
|
{ |
322
|
|
|
$cache = $this->getCache(); |
323
|
|
|
try { |
324
|
|
|
return $cache->has($key); |
325
|
|
|
} catch (InvalidArgumentException $e) { |
326
|
|
|
throw new LogicException("Cache check rejected key $key", 0, $e); |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* @param $key |
332
|
|
|
* @return mixed |
333
|
|
|
* @codeCoverageIgnore |
334
|
|
|
*/ |
335
|
|
View Code Duplication |
protected function cacheGet($key) |
|
|
|
|
336
|
|
|
{ |
337
|
|
|
$cache = $this->getCache(); |
338
|
|
|
try { |
339
|
|
|
return $cache->get($key); |
340
|
|
|
} catch (InvalidArgumentException $e) { |
341
|
|
|
throw new LogicException("Cache retrieval rejected key $key", 0, $e); |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* @param $key |
347
|
|
|
* @param $value |
348
|
|
|
* @param null $ttl |
349
|
|
|
* @return mixed |
350
|
|
|
* @codeCoverageIgnore |
351
|
|
|
*/ |
352
|
|
View Code Duplication |
protected function cacheSet($key, $value, $ttl = null) |
|
|
|
|
353
|
|
|
{ |
354
|
|
|
$cache = $this->getCache(); |
355
|
|
|
try { |
356
|
|
|
return $cache->set($key, $value, $ttl); |
357
|
|
|
} catch (InvalidArgumentException $e) { |
358
|
|
|
throw new LogicException("Cache storage rejected key $key", 0, $e); |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
|
362
|
14 |
|
public function getCache() |
363
|
|
|
{ |
364
|
14 |
|
if (is_null($this->cache)) { |
365
|
2 |
|
throw new LogicException('LibLynx Connect Client requires a PSR-16 compatible cache'); |
366
|
|
|
} |
367
|
12 |
|
return $this->cache; |
368
|
|
|
} |
369
|
|
|
|
370
|
12 |
|
public function setCache(CacheInterface $cache) |
371
|
|
|
{ |
372
|
12 |
|
$this->cache = $cache; |
373
|
12 |
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Internal helper to provide an OAuth2 capable HTTP client |
377
|
|
|
*/ |
378
|
12 |
|
protected function getClient() |
379
|
|
|
{ |
380
|
12 |
|
if (empty($this->clientId)) { |
381
|
2 |
|
throw new LogicException('Cannot make API calls until setCredentials has been called'); |
382
|
|
|
} |
383
|
10 |
|
if (!is_object($this->guzzle)) { |
384
|
|
|
//create our handler stack (which may be mocked in tests) and add the oauth and cache middleware |
385
|
10 |
|
$handlerStack = HandlerStack::create($this->apiHandler); |
386
|
10 |
|
$handlerStack->push($this->createOAuth2Middleware()); |
387
|
10 |
|
$handlerStack->push($this->createCacheMiddleware(), 'cache'); |
388
|
|
|
|
389
|
|
|
//now we can make our client |
390
|
10 |
|
$this->guzzle = new GuzzleClient([ |
391
|
10 |
|
'handler' => $handlerStack, |
392
|
10 |
|
'auth' => 'oauth', |
393
|
10 |
|
'base_uri' => $this->apiroot |
394
|
|
|
]); |
395
|
|
|
} |
396
|
|
|
|
397
|
10 |
|
return $this->guzzle; |
398
|
|
|
} |
399
|
|
|
|
400
|
10 |
|
protected function createOAuth2Middleware(): OAuth2Middleware |
401
|
|
|
{ |
402
|
10 |
|
$handlerStack = HandlerStack::create($this->oauth2Handler); |
403
|
|
|
|
404
|
|
|
// Authorization client - this is used to request OAuth access tokens |
405
|
10 |
|
$reauth_client = new GuzzleClient([ |
406
|
10 |
|
'handler' => $handlerStack, |
407
|
|
|
// URL for access_token request |
408
|
10 |
|
'base_uri' => $this->apiroot . '/oauth/v2/token', |
409
|
|
|
]); |
410
|
|
|
$reauth_config = [ |
411
|
10 |
|
"client_id" => $this->clientId, |
412
|
10 |
|
"client_secret" => $this->clientSecret |
413
|
|
|
]; |
414
|
10 |
|
$grant_type = new ClientCredentials($reauth_client, $reauth_config); |
415
|
10 |
|
$oauth = new OAuth2Middleware($grant_type); |
416
|
|
|
|
417
|
|
|
//use our cache to store tokens |
418
|
10 |
|
$oauth->setTokenPersistence(new SimpleCacheTokenPersistence($this->getCache())); |
419
|
|
|
|
420
|
10 |
|
return $oauth; |
421
|
|
|
} |
422
|
|
|
|
423
|
10 |
|
protected function createCacheMiddleware(): CacheMiddleware |
424
|
|
|
{ |
425
|
10 |
|
return new CacheMiddleware( |
426
|
10 |
|
new PrivateCacheStrategy( |
427
|
10 |
|
new Psr16CacheStorage($this->cache) |
428
|
|
|
) |
429
|
|
|
); |
430
|
|
|
} |
431
|
|
|
} |
432
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.