Completed
Push — master ( 66735f...8b4718 )
by Oleg
02:40 queued 18s
created

getAccessTokenByRefreshToken()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 22
nc 5
nop 0
dl 0
loc 32
ccs 0
cts 23
cp 0
crap 30
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * @abstract API client for Optimizely.
4
 * @author Oleg Krivtsov <[email protected]>
5
 * @date 03 October 2016
6
 * @copyright (c) 2016, Web Marketing ROI
7
 */
8
namespace WebMarketingROI\OptimizelyPHP;
9
10
use WebMarketingROI\OptimizelyPHP\Exception;
11
use WebMarketingROI\OptimizelyPHP\Result;
12
13
/**
14
 * Client for Optimizely REST API v2.
15
 */
16
class OptimizelyApiClient 
17
{    
0 ignored issues
show
Coding Style introduced by
The opening class brace should be on a newline by itself.
Loading history...
18
    /**
19
     * Auth credentials.
20
     * @var array
21
     */
22
    private $authCredentials = array();
23
    
24
    /**
25
     * API version.
26
     * @var string
27
     */
28
    private $apiVersion;
29
    
30
    /**
31
     * CURL handle.
32
     * @var resource 
33
     */
34
    private $curlHandle;
35
    
36
    /**
37
     * Debugging information. Typically contains the last HTTP request/response.
38
     * @var type 
39
     */
40
    private $diagnosticsInfo = array();
41
    
42
    /**
43
     * Instantiated services (used internally).
44
     * @var array
45
     */
46
    private $services = array();
47
    
48
    /**
49
     * Constructor.
50
     * @param array $authCredentials Auth credentials.
51
     * @param string $apiVersion Optional. Currently supported 'v2' only.     
52
     */
53 8
    public function __construct($authCredentials, $apiVersion='v2')
54
    {
55 8
        if (!is_array($authCredentials)) {
56 1
            throw new Exception('Auth credentials must be an array');            
57
        }
58
        
59 7
        if ($apiVersion!='v2') {
60 1
            throw new Exception('Invalid API version passed');
61
        }
62
        
63 6
        $this->authCredentials = $authCredentials;
64 6
        $this->apiVersion = $apiVersion;
65
        
66 6
        $this->curlHandle = curl_init();
67 6
        if (!$this->curlHandle) {
68
            throw new Exception('Error initializing CURL',
69
                    Exception::CODE_CURL_ERROR);
70
        }
71 6
    }
72
    
73
    /**
74
     * Returns API version (currently it is always 'v2').
75
     * @return string
76
     */
77 1
    public function getApiVersion()
78
    {
79 1
        return $this->apiVersion;
80
    }
81
    
82
    /**
83
     * Sets API version. 
84
     * @param string $apiVersion Currently, 'v2' only.
85
     */
86 1
    public function setApiVersion($apiVersion)
87
    {
88 1
        if ($apiVersion!='v2') {
89
            throw new Exception('Invalid API version passed');
90
        }
91
        
92 1
        $this->apiVersion = $apiVersion;
93 1
    }
94
    
95
    /**
96
     * Returns auth credentials
97
     * @return array
98
     */
99 1
    public function getAuthCredentials()
100
    {
101 1
        return $this->authCredentials;
102
    }
103
    
104
    /**
105
     * Sets Auth credentials.
106
     * @param array $authCredentials
107
     */
108 1
    public function setAuthCredentials($authCredentials)
109
    {
110 1
        if (!is_array($authCredentials) || count($authCredentials)==0) {
111
            throw new Exception('Auth credentials must be an array');            
112
        }
113
        
114 1
        $this->authCredentials = $authCredentials;
115 1
    }
116
    
117
    /**
118
     * Returns access token information as array.
119
     * @return array
120
     */
121 1
    public function getAccessToken()
122
    {
123 1
        $accessToken = array();
124 1
        if (is_array($this->authCredentials)) {
125 1
            if (isset($this->authCredentials['access_token'])) {
126 1
                $accessToken['access_token'] = $this->authCredentials['access_token'];
127 1
            }
128
            
129 1
            if (isset($this->authCredentials['access_token_timestamp'])) {
130
                $accessToken['access_token_timestamp'] = $this->authCredentials['access_token_timestamp'];
131
            }
132
            
133 1
            if (isset($this->authCredentials['token_type'])) {
134
                $accessToken['token_type'] = $this->authCredentials['token_type'];
135
            }
136
            
137 1
            if (isset($this->authCredentials['expires_in'])) {
138
                $accessToken['expires_in'] = $this->authCredentials['expires_in'];
139
            }
140 1
        }
141
        
142 1
        return $accessToken;
143
    }
144
    
145
    /**
146
     * Returns refresh token.
147
     * @return string
148
     */
149 1
    public function getRefreshToken()
150
    {
151 1
        if (is_array($this->authCredentials)) {
152 1
            if (isset($this->authCredentials['refresh_token'])) {
153
                return $this->authCredentials['refresh_token'];
154
            }
155 1
        }
156
        
157 1
        return null;
158
    }
159
    
160
    /**
161
     * Sends an HTTP request to the given URL and returns response in form of array. 
162
     * @param string $url The URL of Optimizely endpoint (relative, without host and API version).
163
     * @param array $queryParams The list of query parameters.
164
     * @param string $method HTTP method (GET or POST).
165
     * @param array $postData Data send in request body (only for POST method).
166
     * @return array Optimizely response in form of array.
167
     * @throws Exception
168
     */
169 1
    public function sendApiRequest($url, $queryParams = array(), $method='GET', 
170
            $postData = array())
171
    {
172
        // If access token has expired, try to get another one with refresh token.
173 1
        if ($this->isAccessTokenExpired() && $this->getRefreshToken()!=null) {
174
            $this->getAccessTokenByRefreshToken();
175
        }
176
        
177
        // Produce absolute URL
178 1
        $url = 'https://api.optimizely.com/' . $this->apiVersion . $url;
179
        
180 1
        $result = $this->sendHttpRequest($url, $queryParams, $method, $postData);
181
        
182
        return $result;
183
    }
184
    
185
    /**
186
     * Sends an HTTP request to the given URL and returns response in form of array. 
187
     * @param string $url The URL of Optimizely endpoint.
188
     * @param array $queryParams The list of query parameters.
189
     * @param string $method HTTP method (GET or POST).
190
     * @param array $postData Data send in request body (only for POST method).
191
     * @return array Optimizely response in form of array.
192
     * @throws Exception
193
     */
194 1
    private function sendHttpRequest($url, $queryParams = array(), $method='GET', 
195
            $postData = array())
196
    {
197
        // Reset diagnostics info.
198 1
        $this->diagnosticsInfo = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type object<WebMarketingROI\OptimizelyPHP\type> of property $diagnosticsInfo.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
199
        
200
        // Check if CURL is initialized (it should have been initialized in 
201
        // constructor).
202 1
        if ($this->curlHandle==false) {
203
            throw new Exception('CURL is not initialized', 
204
                    Exception::CODE_CURL_ERROR);
205
        }
206
        
207 1
        if ($method!='GET' && $method!='POST' && $method!='PUT' && 
208 1
            $method!='PATCH' && $method!='DELETE') {
209
            throw new Exception('Invalid HTTP method passed: ' . $method);
210
        }
211
        
212 1
        if (!isset($this->authCredentials['access_token'])) {
213
            throw new Exception('OAuth access token is not set. You should pass ' . 
214
                    'it to the class constructor when initializing the Optimizely client.');
215
        }
216
                
217
        // Append query parameters to URL.
218 1
        if (count($queryParams)!=0) {            
219
            $query = http_build_query($queryParams);
220
            $url .= '?' . $query;
221
        }
222
        
223
        $headers = array(
224 1
            "Authorization: Bearer " . $this->authCredentials['access_token'],
225
            "Content-Type: application/json"
226 1
            );
227 1
        $content = '';
228 1
        if (count($postData)!=0) {
229
            $content = json_encode($postData);            
230
        }
231 1
        $headers[] = "Content-length:" . strlen($content);            
232
        
233
        // Set HTTP options.
234 1
        curl_setopt($this->curlHandle, CURLOPT_URL, $url);
235 1
        curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, $method);        
236 1
        if (count($postData)!=0) {
237
            curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, $content);            
238
        }
239 1
        curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
240 1
        curl_setopt($this->curlHandle, CURLOPT_HEADER, true);        
241 1
        curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, $headers);
242 1
        curl_setopt($this->curlHandle, CURLINFO_HEADER_OUT, true);
243
        
244
        // Save diagnostics info.
245 1
        $this->diagnosticsInfo['request']['method'] = $method;
246 1
        $this->diagnosticsInfo['request']['url'] = $url;
247 1
        $this->diagnosticsInfo['request']['headers'] = $headers;
248 1
        $this->diagnosticsInfo['request']['content'] = $content;        
249
        
250
        // Execute HTTP request and get response.
251 1
        $result = curl_exec($this->curlHandle);                        
252 1
        if ($result === false) {
253
            $code = curl_errno($this->curlHandle);
254
            $error = curl_error($this->curlHandle);
255
            throw new Exception("Failed to send HTTP request $method '$url', " . 
256
                    "the error code was $code, error message was: '$error'", 
257
                    Exception::CODE_CURL_ERROR, $code, $error);
0 ignored issues
show
Documentation introduced by
$code is of type integer, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Unused Code introduced by
The call to Exception::__construct() has too many arguments starting with $error.

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.

Loading history...
258
        }        
259
        
260
        // Split headers and body
261 1
        $headerSize = curl_getinfo($this->curlHandle, CURLINFO_HEADER_SIZE);
262 1
        $headers = substr($result, 0, $headerSize);
263 1
        $body = substr($result, $headerSize);
264
265
        // Parse headers
266 1
        $headers = explode("\n", $headers);
267 1
        $parsedHeaders = array();
268 1
        foreach ($headers as $i=>$header) {
269 1
            if ($i==0)
270 1
                continue; // Skip first line (http code).
271 1
            $pos = strpos($header, ':');
272 1
            if ($pos!=false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $pos of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
273 1
                $headerName = trim(strtolower(substr($header, 0, $pos)));
274 1
                $headerValue = trim(substr($header, $pos+1));
275 1
                $parsedHeaders[$headerName] = $headerValue;
276 1
            }
277 1
        }
278
        
279
        // Get HTTP code
280 1
        $info = curl_getinfo($this->curlHandle);    
281 1
        $httpCode = $info['http_code'];
282
        
283
        // Save diagnostics info.
284 1
        $this->diagnosticsInfo['response']['http_code'] = $httpCode;
285 1
        $this->diagnosticsInfo['response']['headers'] = $headers;
286 1
        $this->diagnosticsInfo['response']['content'] = $body;                       
287
        
288
        // Determine if we have rate limiting headers
289 1
        $rateLimit = null;
290 1
        $rateLimitRemaining = null;
291 1
        $rateLimitReset = null;
292 1
        if (isset($parsedHeaders['x-ratelimit-limit'])) {
293
            $rateLimit = $parsedHeaders['x-ratelimit-limit'];
294
        }
295
        
296 1
        if (isset($parsedHeaders['x-ratelimit-remaining'])) {
297
            $rateLimitRemaining = $parsedHeaders['x-ratelimit-remaining'];
298
        }
299
        
300 1
        if (isset($parsedHeaders['x-ratelimit-reset'])) {
301
            $rateLimitReset = $parsedHeaders['x-ratelimit-reset'];
302
        }        
303
        
304
        // JSON-decode payload.
305 1
        $decodedPayload = json_decode($body, true);
306 1
        if ($decodedPayload===false) {
307
            throw new Exception('Could not JSON-decode the Optimizely API response. Request was ' . 
308
                    $method . ' "' . $url . '". The response was: "' . $body . '"',
309
                    Exception::CODE_API_ERROR, array('http_code'=>$httpCode));
310
        }
311
        
312
        // Check HTTP response code.
313 1
        if ($httpCode<200 || $httpCode>299) {
314
            
315 1
            if (!isset($decodedPayload['message']) || 
316 1
                !isset($decodedPayload['code']) ||
317 1
                !isset($decodedPayload['uuid']) ||
318 1
                $httpCode != !isset($decodedPayload['code'])) {
319 1
                throw new Exception('Optimizely API responded with error code ' . $httpCode . 
320 1
                    ', but response format was unexpected. Request was ' . $method . ' "' . $url . '". Response was "' . $body . '"',
321 1
                    Exception::CODE_API_ERROR, array(
322 1
                        'http_code' => $httpCode,
323 1
                        'rate_limit' => $rateLimit,
324 1
                        'rate_limit_remaining' => $rateLimitRemaining,
325 1
                        'rate_limit_reset' => $rateLimitReset,
326 1
                    ));
327
            }
328
            
329
            throw new Exception($decodedPayload['message'], Exception::CODE_API_ERROR, 
330
                    array(
331
                        'http_code'=>$decodedPayload['code'], 
332
                        'uuid'=>$decodedPayload['uuid'],
333
                        'rate_limit' => $rateLimit,
334
                        'rate_limit_remaining' => $rateLimitRemaining,
335
                        'rate_limit_reset' => $rateLimitReset
336
                    ));
337
        }        
338
        
339
        // Create Result object
340
        $result = new Result($decodedPayload, $httpCode);
341
        $result->setRateLimit($rateLimit);
342
        $result->setRateLimitRemaining($rateLimitRemaining);
343
        $result->setRateLimitReset($rateLimitReset);
344
        
345
        // Determine if we have prev/next/last page headers.
346
        if (isset($parsedHeaders['link']))
347
        {
348
            // Parse LINK header
349
            $matched = preg_match_all('/<(.+)>;\s+rel=(\w+)(,|\z)/U', 
350
                    $parsedHeaders['link'], $matches, PREG_SET_ORDER);
351
            if (!$matched) {
352
                throw new Exception('Error parsing LINK header: ' . 
353
                        $parsedHeaders['link'], Exception::CODE_API_ERROR, $httpCode);
354
            }
355
            
356
            foreach ($matches as $match) {
357
                
358
                $url = $match[1];
359
                $rel = $match[2];
360
                
361
                $matched = preg_match('/page=(\d+)/U', $url, $pageMatches);
362 View Code Duplication
                if (!$matched || count($pageMatches)!=2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
363
                    throw new Exception('Error extracting page argument while parsing LINK header: ' . 
364
                            $parsedHeaders['link'], Exception::CODE_API_ERROR, 
365
                            array('http_code'=>$httpCode));
366
                }
367
                
368
                $pageNumber = $pageMatches[1];
369
                
370
                if ($rel=='prev') {
371
                    $result->setPrevPage($pageNumber);
372
                } else if ($rel=='next') {
373
                    $result->setNextPage($pageNumber);
374 View Code Duplication
                } else if ($rel=='last') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
375
                    $result->setLastPage($pageNumber);
376
                } else {
377
                    throw new Exception('Unexpected rel argument while parsing LINK header: ' . 
378
                            $parsedHeaders['link'], Exception::CODE_API_ERROR, 
379
                            array('http_code'=>$httpCode));
380
                }
381
            }
382
        }        
383
                
384
        // Return the result.
385
        return $result;
386
    }
387
    
388
    /**
389
     * Determines whether the access token has expired or not. Returns true if 
390
     * token has expired; false if token is valid.
391
     * @return boolean
392
     */
393 1
    private function isAccessTokenExpired() 
394
    {
395 1
        if(!isset($this->authCredentials['access_token'])) {
396
            return true; // We do not have access token.
397
        }
398
        
399 1
        if (!isset($this->authCredentials['expires_in']) || 
400 1
            !isset($this->authCredentials['access_token_timestamp'])) {
401 1
            return true; // Assume it has expired, since we can't tell for sure.
402
        } 
403
        
404
        $expiresIn = $this->authCredentials['expires_in'];
405
        $timestamp = $this->authCredentials['access_token_timestamp'];
406
        
407
        if ($timestamp + $expiresIn < time()) {
408
            // Access token has expired.
409
            return true;
410
        }
411
        
412
        // Access token is valid.
413
        return false;
414
    }
415
    
416
    /**
417
     * This method retrieves the access token by refresh token.
418
     * @return array
419
     * @throw Exception
420
     */
421
    private function getAccessTokenByRefreshToken()
422
    {
423
        if (!isset($this->authCredentials['client_id']))
424
            throw new Exception('OAuth 2.0 client ID is not set');
425
        
426
        if (!isset($this->authCredentials['client_secret']))
427
            throw new Exception('OAuth 2.0 client secret is not set');
428
        
429
        if (!isset($this->authCredentials['refresh_token']))
430
            throw new Exception('Refresh token is not set');
431
        
432
        $clientId = $this->authCredentials['client_id'];
433
        $clientSecret = $this->authCredentials['client_secret'];
434
        $refreshToken = $this->authCredentials['refresh_token'];
435
        
436
        $url = "https://app.optimizely.com/oauth2/token?refresh_token=$refreshToken" . 
437
                "&client_id=$clientId&client_secret=$clientSecret&grant_type=refresh_token";
438
        
439
        $response = $this->sendHttpRequest($url, array(), 'POST');
440
        $decodedJsonData = $response->getDecodedJsonData();
441
        
442
        if (!isset($decodedJsonData['access_token'])) {
443
            throw new Exception('Not found access token in response. Request URL was "' . 
444
                    $url. '". Response was "' . print_r(json_encode($decodedJsonData), true). '"',
445
                    Exception::CODE_API_ERROR, $response->getHttpCode());
0 ignored issues
show
Documentation introduced by
$response->getHttpCode() is of type integer, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
446
        }
447
        
448
        $this->authCredentials['access_token'] = $decodedJsonData['access_token'];
449
        $this->authCredentials['token_type'] = $decodedJsonData['token_type'];
450
        $this->authCredentials['expires_in'] = $decodedJsonData['expires_in'];
451
        $this->authCredentials['access_token_timestamp'] = time(); 
452
    }
453
    
454
    /**
455
     * Provides access to API services (experiments, campaigns, etc.)
456
     * @method Audiences audiences()
457
     * @method Campaigns campaigns()
458
     * @method Events events()
459
     * @method Experiment experiments()
460
     * @method Pages pages()
461
     * @method Projects projects()
462
     */
463 1
    public function __call($name, $arguments)
464
    {
465
        $allowedServiceNames = array(
466 1
            'audiences',
467 1
            'campaigns',
468 1
            'events',
469 1
            'experiments',
470 1
            'pages',
471
            'projects'
472 1
        );
473
        
474
        // Check if the service name is valid
475 1
        if (!in_array($name, $allowedServiceNames)) {
476
            throw new Exception("Unexpected service name: $name");
477
        }
478
        
479
        // Check if such service already instantiated
480 1
        if (isset($this->services[$this->apiVersion][$name])) {
481
            $service = $this->services[$this->apiVersion][$name];
482
        } else {
483
            // Instantiate the service
484 1
            $apiVersion = $this->apiVersion;
485 1
            $serviceName = ucwords($name);
486 1
            $className = "\\WebMarketingROI\\OptimizelyPHP\\Service\\$apiVersion\\$serviceName";
487 1
            $service = new $className($this); 
488 1
            $this->services[$apiVersion][$name] = $service;
489
        }
490
491 1
        return $service;
492
    }
493
    
494
    /**
495
     * Returns last HTTP request/response information (for diagnostics/debugging
496
     * purposes):
497
     *   - request
498
     *     - method
499
     *     - headers
500
     *     - content
501
     *   - response
502
     *     - http_code
503
     *     - headers
504
     *     - content
505
     * @return array
506
     */
507
    public function getDiagnosticsInfo()
508
    {
509
        return $this->diagnosticsInfo;
510
    }
511
}