Completed
Branch cleanup (5a622c)
by Paul
01:25
created

Client::authorize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

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