1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LibLynx\Connect; |
4
|
|
|
|
5
|
|
|
use GuzzleHttp\ClientInterface; |
6
|
|
|
use GuzzleHttp\Exception\RequestException; |
7
|
|
|
use GuzzleHttp\Exception\GuzzleException; |
8
|
|
|
use GuzzleHttp\Psr7\Request; |
9
|
|
|
use LibLynx\Connect\Exception\APIException; |
10
|
|
|
use LibLynx\Connect\Exception\LogicException; |
11
|
|
|
use LibLynx\Connect\HTTPClient\HTTPClientFactory; |
12
|
|
|
use LibLynx\Connect\Resource\Identification; |
13
|
|
|
use Psr\Log\LoggerAwareInterface; |
14
|
|
|
use Psr\Log\LoggerInterface; |
15
|
|
|
use Psr\Log\NullLogger; |
16
|
|
|
use Psr\SimpleCache\CacheInterface; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* LibLynx Connect API client |
20
|
|
|
* |
21
|
|
|
* $liblynx=new Liblynx\Connect\Client; |
22
|
|
|
* $liblynx->setCredentials('your client id', 'your client secret'); |
23
|
|
|
* |
24
|
|
|
* //must set a PSR-16 cache - this can be used for testing |
25
|
|
|
* $liblynx->setCache(new \Symfony\Component\Cache\Simple\ArrayCache); |
26
|
|
|
* |
27
|
|
|
* $identification=$liblynx->authorize(Identification::fromSuperglobals()); |
28
|
|
|
* if ($identification->isIdentified()) { |
29
|
|
|
* //good to go |
30
|
|
|
* } |
31
|
|
|
* |
32
|
|
|
* if ($identification->requiresWayf()) { |
33
|
|
|
* $identification->doWayfRedirect(); |
34
|
|
|
* } |
35
|
|
|
* |
36
|
|
|
* @package LibLynx\Connect |
37
|
|
|
*/ |
38
|
|
|
class Client implements LoggerAwareInterface |
39
|
|
|
{ |
40
|
|
|
private $apiroot = 'https://connect.liblynx.com'; |
41
|
|
|
|
42
|
|
|
/** @var string client ID obtain from LibLynx Connect admin portal */ |
43
|
|
|
private $clientId; |
44
|
|
|
|
45
|
|
|
/** @var string client secret obtain from LibLynx Connect admin portal */ |
46
|
|
|
private $clientSecret; |
47
|
|
|
|
48
|
|
|
/** @var ClientInterface HTTP client for API requests */ |
49
|
|
|
private $guzzle; |
50
|
|
|
|
51
|
|
|
/** @var \stdClass entry point resource */ |
52
|
|
|
private $entrypoint; |
53
|
|
|
|
54
|
|
|
/** @var CacheInterface */ |
55
|
|
|
protected $cache; |
56
|
|
|
|
57
|
|
|
/** @var LoggerInterface */ |
58
|
|
|
protected $log; |
59
|
|
|
|
60
|
|
|
/** @var HTTPClientFactory */ |
61
|
|
|
protected $httpClientFactory; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Create new LibLynx API client |
65
|
|
|
*/ |
66
|
16 |
|
public function __construct(HTTPClientFactory $clientFactory = null) |
67
|
|
|
{ |
68
|
16 |
|
if (isset($_SERVER['LIBLYNX_CLIENT_ID'])) { |
69
|
2 |
|
$this->clientId = $_SERVER['LIBLYNX_CLIENT_ID']; |
70
|
|
|
} |
71
|
16 |
|
if (isset($_SERVER['LIBLYNX_CLIENT_SECRET'])) { |
72
|
2 |
|
$this->clientSecret = $_SERVER['LIBLYNX_CLIENT_SECRET']; |
73
|
|
|
} |
74
|
|
|
|
75
|
16 |
|
$this->log = new NullLogger(); |
76
|
16 |
|
$this->httpClientFactory = $clientFactory ?? new HTTPClientFactory; |
77
|
16 |
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @inheritdoc |
81
|
|
|
*/ |
82
|
2 |
|
public function setLogger(LoggerInterface $logger) |
83
|
|
|
{ |
84
|
2 |
|
$this->log = $logger; |
85
|
2 |
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Set API root |
89
|
|
|
* An alternative root may be provided to you by LibLynx Technical Support |
90
|
|
|
* @param string $url |
91
|
|
|
*/ |
92
|
10 |
|
public function setAPIRoot($url) |
93
|
|
|
{ |
94
|
10 |
|
$this->apiroot = $url; |
95
|
10 |
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Set OAuth2.0 client credentials |
99
|
|
|
* These will be provided to you by LibLynx Technical Support |
100
|
|
|
* |
101
|
|
|
* @param string $clientId |
102
|
|
|
* @param string $clientSecret |
103
|
|
|
*/ |
104
|
12 |
|
public function setCredentials($clientId, $clientSecret) |
105
|
|
|
{ |
106
|
12 |
|
$this->clientId = $clientId; |
107
|
12 |
|
$this->clientSecret = $clientSecret; |
108
|
12 |
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* @return array containing client id and secret |
112
|
|
|
*/ |
113
|
2 |
|
public function getCredentials() |
114
|
|
|
{ |
115
|
2 |
|
return [$this->clientId, $this->clientSecret]; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Attempt an identification using the LibLynx API |
120
|
|
|
* |
121
|
|
|
* @param IdentificationRequest $request |
122
|
|
|
* @return Identification|null |
123
|
|
|
*/ |
124
|
8 |
|
public function authorize(IdentificationRequest $request) |
125
|
|
|
{ |
126
|
8 |
|
$payload = $request->getRequestJSON(); |
127
|
8 |
|
$response = $this->post('@new_identification', $payload); |
128
|
4 |
|
if (!isset($response->id)) { |
129
|
|
|
//failed |
130
|
2 |
|
$this->log->critical('Identification request failed {payload}', ['payload' => $payload]); |
131
|
2 |
|
return null; |
132
|
|
|
} |
133
|
|
|
|
134
|
2 |
|
$identification = new Identification($response); |
135
|
2 |
|
$this->log->info( |
136
|
2 |
|
'Identification request for ip {ip} on URL {url} succeeded status={status} id={id}', |
137
|
|
|
[ |
138
|
2 |
|
'status' => $identification->status, |
139
|
2 |
|
'id' => $identification->id, |
140
|
2 |
|
'ip' => $identification->ip, |
141
|
2 |
|
'url' => $identification->url |
142
|
|
|
] |
143
|
|
|
); |
144
|
|
|
|
145
|
2 |
|
return $identification; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* General purpose 'GET' request against API |
150
|
|
|
* @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource |
151
|
|
|
* @return mixed |
152
|
|
|
*/ |
153
|
|
|
public function get($entrypoint) |
154
|
|
|
{ |
155
|
|
|
return $this->makeAPIRequest('GET', $entrypoint); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* General purpose 'POST' request against API |
160
|
|
|
* @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource |
161
|
|
|
* @param $json string contains JSON formatted data to post |
162
|
|
|
* @return mixed |
163
|
|
|
*/ |
164
|
8 |
|
public function post($entrypoint, $json) |
165
|
|
|
{ |
166
|
8 |
|
return $this->makeAPIRequest('POST', $entrypoint, $json); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* General purpose 'PUT' request against API |
171
|
|
|
* @param $entrypoint string contains either an @entrypoint or full URL obtained from a resource |
172
|
|
|
* @return mixed string contains JSON formatted data to put |
173
|
|
|
*/ |
174
|
|
|
public function put($entrypoint, $json) |
175
|
|
|
{ |
176
|
|
|
return $this->makeAPIRequest('PUT', $entrypoint, $json); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @param $method |
181
|
|
|
* @param $entrypoint |
182
|
|
|
* @param null $json |
183
|
|
|
* @return \stdClass object containing JSON decoded response - note this can be an error response for normally |
184
|
|
|
* handled errors |
185
|
|
|
* @throws LogicException for integration errors, e.g. not setting a cache |
186
|
|
|
* @throws APIException for unexpected API failures |
187
|
|
|
*/ |
188
|
8 |
|
protected function makeAPIRequest($method, $entrypoint, $json = null) |
189
|
|
|
{ |
190
|
8 |
|
$this->log->debug('{method} {entrypoint} {json}', [ |
191
|
8 |
|
'method' => $method, |
192
|
8 |
|
'entrypoint' => $entrypoint, |
193
|
8 |
|
'json' => $json |
194
|
|
|
]); |
195
|
8 |
|
$url = $this->resolveEntryPoint($entrypoint); |
196
|
4 |
|
$client = $this->getClient(); |
197
|
|
|
|
198
|
4 |
|
$this->log->debug('{entrypoint} = {url}', ['entrypoint' => $entrypoint, 'url' => $url]); |
199
|
|
|
|
200
|
4 |
|
$headers = ['Accept' => 'application/json']; |
201
|
4 |
|
if (!empty($json)) { |
202
|
4 |
|
$headers['Content-Type'] = 'application/json'; |
203
|
|
|
} |
204
|
|
|
|
205
|
4 |
|
$request = new Request($method, $url, $headers, $json); |
206
|
|
|
|
207
|
|
|
try { |
208
|
4 |
|
$response = $client->send($request); |
209
|
|
|
|
210
|
2 |
|
$this->log->debug('{method} {entrypoint} succeeded {status}', [ |
211
|
2 |
|
'method' => $method, |
212
|
2 |
|
'entrypoint' => $entrypoint, |
213
|
2 |
|
'status' => $response->getStatusCode(), |
214
|
|
|
]); |
215
|
2 |
|
} catch (RequestException $e) { |
216
|
2 |
|
$response = $e->getResponse(); |
217
|
2 |
|
$this->log->error( |
218
|
2 |
|
'{method} {entrypoint} {json} failed ({status}): {body}', |
219
|
|
|
[ |
220
|
2 |
|
'method' => $method, |
221
|
2 |
|
'json' => $json, |
222
|
2 |
|
'entrypoint' => $entrypoint, |
223
|
2 |
|
'status' => $response->getStatusCode(), |
224
|
2 |
|
'body' => $response->getBody() |
225
|
|
|
] |
226
|
|
|
); |
227
|
|
|
} catch (GuzzleException $e) { |
228
|
|
|
$this->log->critical( |
229
|
|
|
'{method} {entrypoint} {json} failed', |
230
|
|
|
[ |
231
|
|
|
'method' => $method, |
232
|
|
|
'json' => $json, |
233
|
|
|
'entrypoint' => $entrypoint, |
234
|
|
|
], |
235
|
|
|
$e |
|
|
|
|
236
|
|
|
); |
237
|
|
|
|
238
|
|
|
throw new APIException(); |
239
|
|
|
} |
240
|
|
|
|
241
|
4 |
|
$payload = json_decode($response->getBody()); |
242
|
4 |
|
return $payload; |
243
|
|
|
} |
244
|
|
|
|
245
|
8 |
|
public function resolveEntryPoint($nameOrUrl) |
246
|
|
|
{ |
247
|
8 |
|
if ($nameOrUrl[0] === '@') { |
248
|
8 |
|
return $this->getEntryPoint($nameOrUrl); |
249
|
|
|
} |
250
|
|
|
//it's a URL |
251
|
|
|
return $nameOrUrl; |
252
|
|
|
} |
253
|
|
|
|
254
|
14 |
|
public function getEntryPoint($name) |
255
|
|
|
{ |
256
|
14 |
|
if (!is_array($this->entrypoint)) { |
257
|
14 |
|
$this->entrypoint = $this->loadEntrypointResource(); |
258
|
|
|
} else { |
259
|
|
|
$this->log->debug('using previously loaded entrypoint'); |
260
|
|
|
} |
261
|
|
|
|
262
|
8 |
|
if (!isset($this->entrypoint->_links->$name->href)) { |
263
|
2 |
|
throw new LogicException("Invalid LibLynx API entrypoint $name requested"); |
264
|
|
|
} |
265
|
|
|
|
266
|
6 |
|
return $this->entrypoint->_links->$name->href; |
267
|
|
|
} |
268
|
|
|
|
269
|
14 |
|
public function loadEntrypointResource() |
270
|
|
|
{ |
271
|
14 |
|
$entrypointResource = null; |
272
|
14 |
|
$key = 'entrypoint' . $this->clientId; |
273
|
|
|
|
274
|
14 |
|
$cache = $this->getCache(); |
275
|
12 |
|
if ($cache->has($key)) { |
276
|
2 |
|
$this->log->debug('loading entrypoint from persistent cache'); |
277
|
2 |
|
$entrypointResource = $cache->get($key); |
278
|
|
|
; |
279
|
|
|
} else { |
280
|
12 |
|
$this->log->debug('entrypoint not cached, requesting from API'); |
281
|
12 |
|
$client = $this->getClient(); |
282
|
|
|
|
283
|
10 |
|
$request = new Request('GET', 'api', [ |
284
|
10 |
|
'Content-Type' => 'application/json', |
285
|
|
|
'Accept' => 'application/json', |
286
|
|
|
]); |
287
|
|
|
|
288
|
|
|
try { |
289
|
10 |
|
$response = $client->send($request); |
290
|
|
|
|
291
|
8 |
|
$payload = json_decode($response->getBody()); |
292
|
8 |
|
if (is_object($payload) && isset($payload->_links)) { |
293
|
8 |
|
$this->log->info('entrypoint loaded from API and cached'); |
294
|
8 |
|
$entrypointResource = $payload; |
295
|
|
|
|
296
|
8 |
|
$cache->set($key, $payload, 86400); |
297
|
|
|
} |
298
|
2 |
|
} catch (GuzzleException $e) { |
299
|
2 |
|
throw new APIException("Unable to obtain LibLynx API entry point resource", 0, $e); |
300
|
|
|
} |
301
|
|
|
} |
302
|
8 |
|
return $entrypointResource; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
|
306
|
14 |
|
public function getCache() |
307
|
|
|
{ |
308
|
14 |
|
if (is_null($this->cache)) { |
309
|
2 |
|
throw new LogicException('LibLynx Connect Client requires a PSR-16 compatible cache'); |
310
|
|
|
} |
311
|
12 |
|
return $this->cache; |
312
|
|
|
} |
313
|
|
|
|
314
|
12 |
|
public function setCache(CacheInterface $cache) |
315
|
|
|
{ |
316
|
12 |
|
$this->cache = $cache; |
317
|
12 |
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Internal helper to provide an OAuth2 capable HTTP client |
321
|
|
|
*/ |
322
|
12 |
|
protected function getClient() |
323
|
|
|
{ |
324
|
12 |
|
if (empty($this->clientId)) { |
325
|
2 |
|
throw new LogicException('Cannot make API calls until setCredentials has been called'); |
326
|
|
|
} |
327
|
10 |
|
if (!is_object($this->guzzle)) { |
328
|
10 |
|
$this->guzzle = $this->httpClientFactory->create( |
329
|
10 |
|
$this->apiroot, |
330
|
10 |
|
$this->clientId, |
331
|
10 |
|
$this->clientSecret, |
332
|
10 |
|
$this->getCache() |
333
|
|
|
); |
334
|
|
|
} |
335
|
|
|
|
336
|
10 |
|
return $this->guzzle; |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.