Completed
Push — master ( 20aef1...24a9f7 )
by Stephan
03:51 queued 01:49
created

Connection::setExactClientSecret()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 1
nc 1
nop 1
1
<?php
2
3
namespace Picqer\Financials\Exact;
4
5
use Exception;
6
use GuzzleHttp\Client;
7
use GuzzleHttp\Exception\BadResponseException;
8
use GuzzleHttp\HandlerStack;
9
use GuzzleHttp\Psr7\Request;
10
use GuzzleHttp\Psr7\Response;
11
use GuzzleHttp\Psr7;
12
13
/**
14
 * Class Connection
15
 *
16
 * @package Picqer\Financials\Exact
17
 *
18
 */
19
class Connection
20
{
21
    /**
22
     * @var string
23
     */
24
    private $baseUrl = 'https://start.exactonline.nl';
25
26
    /**
27
     * @var string
28
     */
29
    private $apiUrl = '/api/v1';
30
31
    /**
32
     * @var string
33
     */
34
    private $authUrl = '/api/oauth2/auth';
35
36
    /**
37
     * @var string
38
     */
39
    private $tokenUrl = '/api/oauth2/token';
40
41
    /**
42
     * @var mixed
43
     */
44
    private $exactClientId;
45
46
    /**
47
     * @var mixed
48
     */
49
    private $exactClientSecret;
50
51
    /**
52
     * @var mixed
53
     */
54
    private $authorizationCode;
55
56
    /**
57
     * @var mixed
58
     */
59
    private $accessToken;
60
61
    /**
62
     * @var int The Unix timestamp at which the access token expires.
63
     */
64
    private $tokenExpires;
65
66
    /**
67
     * @var mixed
68
     */
69
    private $refreshToken;
70
71
    /**
72
     * @var mixed
73
     */
74
    private $redirectUrl;
75
76
    /**
77
     * @var mixed
78
     */
79
    private $division;
80
81
    /**
82
     * @var Client|null
83
     */
84
    private $client;
85
86
    /**
87
     * @var callable(Connection)
88
     */
89
    private $tokenUpdateCallback;
90
91
    /**
92
     * @var callable[]
93
     */
94
    protected $middleWares = [];
95
96
    /**
97
    * @var string|null
98
    */
99
    public $nextUrl = null;
100
101
    /**
102
     * @return Client
103
     */
104
    private function client()
105
    {
106
        if ($this->client) {
107
            return $this->client;
108
        }
109
110
        $handlerStack = HandlerStack::create();
111
        foreach ($this->middleWares as $middleWare) {
112
            $handlerStack->push($middleWare);
113
        }
114
115
        $this->client = new Client([
116
            'http_errors' => true,
117
            'handler' => $handlerStack,
118
            'expect' => false,
119
        ]);
120
121
        return $this->client;
122
    }
123
124
    public function insertMiddleWare($middleWare)
125
    {
126
        $this->middleWares[] = $middleWare;
127
    }
128
129
    public function connect()
130
    {
131
        // Redirect for authorization if needed (no access token or refresh token given)
132
        if ($this->needsAuthentication()) {
133
            $this->redirectForAuthorization();
134
        }
135
136
        // If access token is not set or token has expired, acquire new token
137
        if (empty($this->accessToken) || $this->tokenHasExpired()) {
138
            $this->acquireAccessToken();
139
        }
140
141
        $client = $this->client();
142
143
        return $client;
144
    }
145
146
    /**
147
     * @param string $method
148
     * @param string $endpoint
149
     * @param mixed $body
150
     * @param array $params
151
     * @param array $headers
152
     * @return Request
153
     */
154
    private function createRequest($method = 'GET', $endpoint, $body = null, array $params = [], array $headers = [])
155
    {
156
        // Add default json headers to the request
157
        $headers = array_merge($headers, [
158
            'Accept' => 'application/json',
159
            'Content-Type' => 'application/json',
160
            'Prefer' => 'return=representation'
161
        ]);
162
163
        // If access token is not set or token has expired, acquire new token
164
        if (empty($this->accessToken) || $this->tokenHasExpired()) {
165
            $this->acquireAccessToken();
166
        }
167
168
        // If we have a token, sign the request
169
        if (!$this->needsAuthentication() && !empty($this->accessToken)) {
170
            $headers['Authorization'] = 'Bearer ' . $this->accessToken;
171
        }
172
173
        // Create param string
174
        if (!empty($params)) {
175
            $endpoint .= '?' . http_build_query($params);
176
        }
177
178
        // Create the request
179
        $request = new Request($method, $endpoint, $headers, $body);
180
181
        return $request;
182
    }
183
184
    /**
185
     * @param string $url
186
     * @param array $params
187
     * @param array $headers
188
     * @return mixed
189
     * @throws ApiException
190
     */
191
    public function get($url, array $params = [], array $headers = [])
192
    {
193
        $url = $this->formatUrl($url, $url !== 'current/Me', $url == $this->nextUrl);
194
195
        try {
196
            $request = $this->createRequest('GET', $url, null, $params, $headers);
197
            $response = $this->client()->send($request);
198
199
            return $this->parseResponse($response, $url != $this->nextUrl);
200
        } catch (Exception $e) {
201
            $this->parseExceptionForErrorMessages($e);
202
        }
203
        
204
        return null;
205
    }
206
207
    /**
208
     * @param string $url
209
     * @param mixed $body
210
     * @return mixed
211
     * @throws ApiException
212
     */
213
    public function post($url, $body)
214
    {
215
        $url = $this->formatUrl($url);
216
217
        try {
218
            $request  = $this->createRequest('POST', $url, $body);
219
            $response = $this->client()->send($request);
220
221
            return $this->parseResponse($response);
222
        } catch (Exception $e) {
223
            $this->parseExceptionForErrorMessages($e);
224
        }
225
226
        return null;
227
    }
228
229
    /**
230
     * @param string $url
231
     * @param mixed $body
232
     * @return mixed
233
     * @throws ApiException
234
     */
235
    public function put($url, $body)
236
    {
237
        $url = $this->formatUrl($url);
238
239
        try {
240
            $request  = $this->createRequest('PUT', $url, $body);
241
            $response = $this->client()->send($request);
242
243
            return $this->parseResponse($response);
244
        } catch (Exception $e) {
245
            $this->parseExceptionForErrorMessages($e);
246
        }
247
248
        return null;
249
    }
250
251
    /**
252
     * @param string $url
253
     * @return mixed
254
     * @throws ApiException
255
     */
256
    public function delete($url)
257
    {
258
        $url = $this->formatUrl($url);
259
260
        try {
261
            $request  = $this->createRequest('DELETE', $url);
262
            $response = $this->client()->send($request);
263
264
            return $this->parseResponse($response);
265
        } catch (Exception $e) {
266
            $this->parseExceptionForErrorMessages($e);
267
        }
268
269
        return null;
270
    }
271
272
    /**
273
     * @return string
274
     */
275
    public function getAuthUrl()
276
    {
277
        return $this->baseUrl . $this->authUrl . '?' . http_build_query(array(
278
            'client_id' => $this->exactClientId,
279
            'redirect_uri' => $this->redirectUrl,
280
            'response_type' => 'code'
281
        ));
282
    }
283
284
    /**
285
     * @param mixed $exactClientId
286
     */
287
    public function setExactClientId($exactClientId)
288
    {
289
        $this->exactClientId = $exactClientId;
290
    }
291
292
    /**
293
     * @param mixed $exactClientSecret
294
     */
295
    public function setExactClientSecret($exactClientSecret)
296
    {
297
        $this->exactClientSecret = $exactClientSecret;
298
    }
299
300
    /**
301
     * @param mixed $authorizationCode
302
     */
303
    public function setAuthorizationCode($authorizationCode)
304
    {
305
        $this->authorizationCode = $authorizationCode;
306
    }
307
308
    /**
309
     * @param mixed $accessToken
310
     */
311
    public function setAccessToken($accessToken)
312
    {
313
        $this->accessToken = $accessToken;
314
    }
315
316
    /**
317
     * @param mixed $refreshToken
318
     */
319
    public function setRefreshToken($refreshToken)
320
    {
321
        $this->refreshToken = $refreshToken;
322
    }
323
324
    /**
325
     *
326
     */
327
    public function redirectForAuthorization()
328
    {
329
        $authUrl = $this->getAuthUrl();
330
        header('Location: ' . $authUrl);
331
        exit;
332
    }
333
334
    /**
335
     * @param mixed $redirectUrl
336
     */
337
    public function setRedirectUrl($redirectUrl)
338
    {
339
        $this->redirectUrl = $redirectUrl;
340
    }
341
342
    /**
343
     * @return bool
344
     */
345
    public function needsAuthentication()
346
    {
347
        return empty($this->refreshToken) && empty($this->authorizationCode);
348
    }
349
350
    /**
351
     * @param Response $response
352
     * @param bool $returnSingleIfPossible
353
     * @return mixed
354
     * @throws ApiException
355
     */
356
    private function parseResponse(Response $response, $returnSingleIfPossible = true)
357
    {
358
        try {
359
360
            if ($response->getStatusCode() === 204) {
361
                return [];
362
            }
363
364
            Psr7\rewind_body($response);
0 ignored issues
show
Bug introduced by
The function rewind_body was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

364
            /** @scrutinizer ignore-call */ 
365
            Psr7\rewind_body($response);
Loading history...
365
            $json = json_decode($response->getBody()->getContents(), true);
366
            if (array_key_exists('d', $json)) {
367
                if (array_key_exists('__next', $json['d'])) {
368
                    $this->nextUrl = $json['d']['__next'];
369
                }
370
                else {
371
                    $this->nextUrl = null;
372
                }
373
374
                if (array_key_exists('results', $json['d'])) {
375
                    if ($returnSingleIfPossible && count($json['d']['results']) == 1) {
376
                        return $json['d']['results'][0];
377
                    }
378
379
                    return $json['d']['results'];
380
                }
381
382
                return $json['d'];
383
            }
384
385
            return $json;
386
        } catch (\RuntimeException $e) {
387
            throw new ApiException($e->getMessage());
388
        }
389
    }
390
391
    /**
392
     * @return mixed
393
     */
394
    private function getCurrentDivisionNumber()
395
    {
396
        if (empty($this->division)) {
397
            $me             = new Me($this);
398
            $this->division = $me->find()->CurrentDivision;
399
        }
400
401
        return $this->division;
402
    }
403
404
    /**
405
     * @return mixed
406
     */
407
    public function getRefreshToken()
408
    {
409
        return $this->refreshToken;
410
    }
411
412
    /**
413
     * @return mixed
414
     */
415
    public function getAccessToken()
416
    {
417
        return $this->accessToken;
418
    }
419
420
    private function acquireAccessToken()
421
    {
422
        // If refresh token not yet acquired, do token request
423
        if (empty($this->refreshToken)) {
424
            $body = [
425
                'form_params' => [
426
                    'redirect_uri' => $this->redirectUrl,
427
                    'grant_type' => 'authorization_code',
428
                    'client_id' => $this->exactClientId,
429
                    'client_secret' => $this->exactClientSecret,
430
                    'code' => $this->authorizationCode
431
                ]
432
            ];
433
        } else { // else do refresh token request
434
            $body = [
435
                'form_params' => [
436
                    'refresh_token' => $this->refreshToken,
437
                    'grant_type' => 'refresh_token',
438
                    'client_id' => $this->exactClientId,
439
                    'client_secret' => $this->exactClientSecret,
440
                ]
441
            ];
442
        }
443
444
445
        try {
446
            $response = $this->client()->post($this->getTokenUrl(), $body);
447
448
            Psr7\rewind_body($response);
0 ignored issues
show
Bug introduced by
The function rewind_body was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

448
            /** @scrutinizer ignore-call */ 
449
            Psr7\rewind_body($response);
Loading history...
449
            $body = json_decode($response->getBody()->getContents(), true);
450
451
            if (json_last_error() === JSON_ERROR_NONE) {
452
                $this->accessToken  = $body['access_token'];
453
                $this->refreshToken = $body['refresh_token'];
454
                $this->tokenExpires = $this->getTimestampFromExpiresIn($body['expires_in']);
455
456
                if (is_callable($this->tokenUpdateCallback)) {
457
                    call_user_func($this->tokenUpdateCallback, $this);
458
                }
459
            } else {
460
                throw new ApiException('Could not acquire tokens, json decode failed. Got response: ' . $response->getBody()->getContents());
461
            }
462
        } catch (BadResponseException $ex) {
463
            throw new ApiException('Could not acquire or refresh tokens [http ' . $ex->getResponse()->getStatusCode() . ']', 0, $ex);
464
        }
465
    }
466
467
    /**
468
     * Translates expires_in to a Unix timestamp.
469
     * @param string $expiresIn Number of seconds until the token expires.
470
     * @return int
471
     */
472
    private function getTimestampFromExpiresIn($expiresIn)
473
    {
474
        if (!ctype_digit($expiresIn)) {
475
            throw new \InvalidArgumentException('Function requires a numeric expires value');
476
        }
477
478
        return time() + $expiresIn;
479
    }
480
481
    /**
482
     * @return int The Unix timestamp at which the access token expires.
483
     */
484
    public function getTokenExpires()
485
    {
486
        return $this->tokenExpires;
487
    }
488
489
    /**
490
     * @param int $tokenExpires The Unix timestamp at which the access token expires.
491
     */
492
    public function setTokenExpires($tokenExpires)
493
    {
494
        $this->tokenExpires = $tokenExpires;
495
    }
496
497
    private function tokenHasExpired()
498
    {
499
        if (empty($this->tokenExpires)) {
500
            return true;
501
        }
502
503
        return $this->tokenExpires <= time() + 10;
504
    }
505
506
    private function formatUrl($endPoint, $includeDivision = true, $formatNextUrl = false)
507
    {
508
        if ($formatNextUrl) {
509
            return $endPoint;
510
        }
511
512
        if ($includeDivision) {
513
            return implode('/', [
514
                $this->getApiUrl(),
515
                $this->getCurrentDivisionNumber(),
516
                $endPoint
517
            ]);
518
        }
519
520
        return implode('/', [
521
            $this->getApiUrl(),
522
            $endPoint
523
        ]);
524
    }
525
526
527
    /**
528
     * @return mixed
529
     */
530
    public function getDivision()
531
    {
532
        return $this->division;
533
    }
534
535
536
    /**
537
     * @param mixed $division
538
     */
539
    public function setDivision($division)
540
    {
541
        $this->division = $division;
542
    }
543
544
    /**
545
     * @param callable $callback
546
     */
547
    public function setTokenUpdateCallback($callback) {
548
        $this->tokenUpdateCallback = $callback;
549
    }
550
551
    /**
552
     * Parse the reponse in the Exception to return the Exact error messages
553
     * @param Exception $e
554
     * @throws ApiException
555
     */
556
    private function parseExceptionForErrorMessages(Exception $e)
557
    {
558
        if (! $e instanceof BadResponseException) {
559
            throw new ApiException($e->getMessage());
560
        }
561
562
        $response = $e->getResponse();
563
        Psr7\rewind_body($response);
0 ignored issues
show
Bug introduced by
The function rewind_body was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

563
        /** @scrutinizer ignore-call */ 
564
        Psr7\rewind_body($response);
Loading history...
564
        $responseBody = $response->getBody()->getContents();
565
        $decodedResponseBody = json_decode($responseBody, true);
566
567
        if (! is_null($decodedResponseBody) && isset($decodedResponseBody['error']['message']['value'])) {
568
            $errorMessage = $decodedResponseBody['error']['message']['value'];
569
        } else {
570
            $errorMessage = $responseBody;
571
        }
572
573
        throw new ApiException('Error ' . $response->getStatusCode() .': ' . $errorMessage);
574
    }
575
576
    /**
577
     * @return string
578
     */
579
    protected function getBaseUrl()
580
    {
581
        return $this->baseUrl;
582
    }
583
584
    /**
585
     * @return string
586
     */
587
    private function getApiUrl()
588
    {
589
        return $this->baseUrl . $this->apiUrl;
590
    }
591
592
    /**
593
     * @return string
594
     */
595
    private function getTokenUrl()
596
    {
597
        return $this->baseUrl . $this->tokenUrl;
598
    }
599
600
    /**
601
     * Set base URL for different countries according to
602
     * https://developers.exactonline.com/#Exact%20Online%20sites.html
603
     *
604
     * @param string $baseUrl
605
     */
606
    public function setBaseUrl($baseUrl)
607
    {
608
        $this->baseUrl = $baseUrl;
609
    }
610
611
    /**
612
     * @param string $apiUrl
613
     */
614
    public function setApiUrl($apiUrl)
615
    {
616
        $this->apiUrl = $apiUrl;
617
    }
618
619
    /**
620
     * @param string $authUrl
621
     */
622
    public function setAuthUrl($authUrl)
623
    {
624
        $this->authUrl = $authUrl;
625
    }
626
627
    /**
628
     * @param string $tokenUrl
629
     */
630
    public function setTokenUrl($tokenUrl)
631
    {
632
        $this->tokenUrl = $tokenUrl;
633
    }
634
}
635