GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#626)
by
unknown
01:41
created

TwitterOAuth   C

Complexity

Total Complexity 61

Size/Duplication

Total Lines 566
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 9
dl 0
loc 566
rs 6.018
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A uploadMediaChunked() 0 23 2
B __construct() 0 12 5
A setOauthToken() 0 5 1
A setBearer() 0 5 1
A getLastApiPath() 0 4 1
A getLastHttpCode() 0 4 1
A getLastXHeaders() 0 4 1
A getLastBody() 0 4 1
A resetLastResponse() 0 4 1
A resetAttemptsNumber() 0 4 1
A sleepIfNeeded() 0 6 3
A url() 0 8 1
A oauth() 0 17 2
A oauth2() 0 14 1
A get() 0 4 1
A post() 0 4 1
A delete() 0 4 1
A put() 0 4 1
A upload() 0 8 2
A uploadMediaNotChunked() 0 8 1
A mediaInitParameters() 0 16 3
A http() 0 9 1
A makeRequests() 0 13 2
A requestsAvailable() 0 4 3
A oAuthRequest() 0 21 4
B curlOptions() 0 31 4
C request() 0 44 8
A parseHeaders() 0 13 3
A encodeAppAuthorization() 0 7 1
A pharRunning() 0 4 2
A useCAFile() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like TwitterOAuth often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TwitterOAuth, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * The most popular PHP library for use with the Twitter OAuth REST API.
4
 *
5
 * @license MIT
6
 */
7
8
namespace Abraham\TwitterOAuth;
9
10
use Abraham\TwitterOAuth\Util\JsonDecoder;
11
12
/**
13
 * TwitterOAuth class for interacting with the Twitter API.
14
 *
15
 * @author Abraham Williams <[email protected]>
16
 */
17
class TwitterOAuth extends Config
18
{
19
    const API_VERSION = '1.1';
20
    const API_HOST = 'https://api.twitter.com';
21
    const UPLOAD_HOST = 'https://upload.twitter.com';
22
23
    /** @var Response details about the result of the last request */
24
    private $response;
25
    /** @var string|null Application bearer token */
26
    private $bearer;
27
    /** @var Consumer Twitter application details */
28
    private $consumer;
29
    /** @var Token|null User access token details */
30
    private $token;
31
    /** @var HmacSha1 OAuth 1 signature type used by Twitter */
32
    private $signatureMethod;
33
    /** @var int Number of attempts we made for the request */
34
    private $attempts = 0;
35
36
    /**
37
     * Constructor.
38
     *
39
     * @param string      $consumerKey      The Application Consumer Key
40
     * @param string      $consumerSecret   The Application Consumer Secret
41
     * @param string|null $oauthToken       The Client Token (optional)
42
     * @param string|null $oauthTokenSecret The Client Token Secret (optional)
43
     */
44
    public function __construct($consumerKey, $consumerSecret, $oauthToken = null, $oauthTokenSecret = null)
45
    {
46
        $this->resetLastResponse();
47
        $this->signatureMethod = new HmacSha1();
48
        $this->consumer = new Consumer($consumerKey, $consumerSecret);
49
        if (!empty($oauthToken) && !empty($oauthTokenSecret)) {
50
            $this->setOauthToken($oauthToken, $oauthTokenSecret);
51
        }
52
        if (empty($oauthToken) && !empty($oauthTokenSecret)) {
53
            $this->setBearer($oauthTokenSecret);
54
        }
55
    }
56
57
    /**
58
     * @param string $oauthToken
59
     * @param string $oauthTokenSecret
60
     */
61
    public function setOauthToken($oauthToken, $oauthTokenSecret)
62
    {
63
        $this->token = new Token($oauthToken, $oauthTokenSecret);
64
        $this->bearer = null;
65
    }
66
67
    /**
68
     * @param string $oauthTokenSecret
69
     */
70
    public function setBearer($oauthTokenSecret)
71
    {
72
        $this->bearer = $oauthTokenSecret;
73
        $this->token = null;
74
    }
75
76
    /**
77
     * @return string|null
78
     */
79
    public function getLastApiPath()
80
    {
81
        return $this->response->getApiPath();
82
    }
83
84
    /**
85
     * @return int
86
     */
87
    public function getLastHttpCode()
88
    {
89
        return $this->response->getHttpCode();
90
    }
91
92
    /**
93
     * @return array
94
     */
95
    public function getLastXHeaders()
96
    {
97
        return $this->response->getXHeaders();
98
    }
99
100
    /**
101
     * @return array|object|null
102
     */
103
    public function getLastBody()
104
    {
105
        return $this->response->getBody();
106
    }
107
108
    /**
109
     * Resets the last response cache.
110
     */
111
    public function resetLastResponse()
112
    {
113
        $this->response = new Response();
114
    }
115
116
    /**
117
     * Resets the attempts number.
118
     */
119
    private function resetAttemptsNumber()
120
    {
121
        $this->attempts = 0;
122
    }
123
124
    /**
125
     * Delays the retries when they're activated.
126
     */
127
    private function sleepIfNeeded()
128
    {
129
        if ($this->maxRetries && $this->attempts) {
130
            sleep($this->retriesDelay);
131
        }
132
    }
133
134
    /**
135
     * Make URLs for user browser navigation.
136
     *
137
     * @param string $path
138
     * @param array  $parameters
139
     *
140
     * @return string
141
     */
142
    public function url($path, array $parameters)
143
    {
144
        $this->resetLastResponse();
145
        $this->response->setApiPath($path);
146
        $query = http_build_query($parameters);
147
148
        return sprintf('%s/%s?%s', self::API_HOST, $path, $query);
149
    }
150
151
    /**
152
     * Make /oauth/* requests to the API.
153
     *
154
     * @param string $path
155
     * @param array  $parameters
156
     *
157
     * @throws TwitterOAuthException
158
     *
159
     * @return array
160
     */
161
    public function oauth($path, array $parameters = [])
162
    {
163
        $response = [];
164
        $this->resetLastResponse();
165
        $this->response->setApiPath($path);
166
        $url = sprintf('%s/%s', self::API_HOST, $path);
167
        $result = $this->oAuthRequest($url, 'POST', $parameters);
168
169
        if ($this->getLastHttpCode() != 200) {
170
            throw new TwitterOAuthException($result);
171
        }
172
173
        parse_str($result, $response);
174
        $this->response->setBody($response);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, Abraham\TwitterOAuth\Response::setBody() does only seem to accept array|object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
175
176
        return $response;
177
    }
178
179
    /**
180
     * Make /oauth2/* requests to the API.
181
     *
182
     * @param string $path
183
     * @param array  $parameters
184
     *
185
     * @return array|object
186
     */
187
    public function oauth2($path, array $parameters = [])
188
    {
189
        $method = 'POST';
190
        $this->resetLastResponse();
191
        $this->response->setApiPath($path);
192
        $url = sprintf('%s/%s', self::API_HOST, $path);
193
        $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
194
        $authorization = 'Authorization: Basic '.$this->encodeAppAuthorization($this->consumer);
195
        $result = $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
196
        $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
197
        $this->response->setBody($response);
198
199
        return $response;
200
    }
201
202
    /**
203
     * Make GET requests to the API.
204
     *
205
     * @param string $path
206
     * @param array  $parameters
207
     *
208
     * @return array|object
209
     */
210
    public function get($path, array $parameters = [])
211
    {
212
        return $this->http('GET', self::API_HOST, $path, $parameters);
213
    }
214
215
    /**
216
     * Make POST requests to the API.
217
     *
218
     * @param string $path
219
     * @param array  $parameters
220
     * @param string $host
221
     *
222
     * @return array|object
223
     */
224
    public function post($path, array $parameters = [], $host = self::API_HOST)
225
    {
226
        return $this->http('POST', $host, $path, $parameters);
227
    }
228
229
    /**
230
     * Make DELETE requests to the API.
231
     *
232
     * @param string $path
233
     * @param array  $parameters
234
     *
235
     * @return array|object
236
     */
237
    public function delete($path, array $parameters = [])
238
    {
239
        return $this->http('DELETE', self::API_HOST, $path, $parameters);
240
    }
241
242
    /**
243
     * Make PUT requests to the API.
244
     *
245
     * @param string $path
246
     * @param array  $parameters
247
     *
248
     * @return array|object
249
     */
250
    public function put($path, array $parameters = [])
251
    {
252
        return $this->http('PUT', self::API_HOST, $path, $parameters);
253
    }
254
255
    /**
256
     * Upload media to upload.twitter.com.
257
     *
258
     * @param string $path
259
     * @param array  $parameters
260
     * @param bool   $chunked
261
     *
262
     * @return array|object
263
     */
264
    public function upload($path, array $parameters = [], $chunked = false)
265
    {
266
        if ($chunked) {
267
            return $this->uploadMediaChunked($path, $parameters);
268
        } else {
269
            return $this->uploadMediaNotChunked($path, $parameters);
270
        }
271
    }
272
273
    /**
274
     * Private method to upload media (not chunked) to upload.twitter.com.
275
     *
276
     * @param string $path
277
     * @param array  $parameters
278
     *
279
     * @return array|object
280
     */
281
    private function uploadMediaNotChunked($path, array $parameters)
282
    {
283
        $file = file_get_contents($parameters['media']);
284
        $base = base64_encode($file);
285
        $parameters['media'] = $base;
286
287
        return $this->http('POST', self::UPLOAD_HOST, $path, $parameters);
288
    }
289
290
    /**
291
     * Private method to upload media (chunked) to upload.twitter.com.
292
     *
293
     * @param string $path
294
     * @param array  $parameters
295
     *
296
     * @return array|object
297
     */
298
    private function uploadMediaChunked($path, array $parameters)
299
    {
300
        $init = $this->http('POST', self::UPLOAD_HOST, $path, $this->mediaInitParameters($parameters));
301
        // Append
302
        $segmentIndex = 0;
303
        $media = fopen($parameters['media'], 'rb');
304
        while (!feof($media)) {
305
            $this->http('POST', self::UPLOAD_HOST, 'media/upload', [
306
                'command'       => 'APPEND',
307
                'media_id'      => $init->media_id_string,
308
                'segment_index' => $segmentIndex++,
309
                'media_data'    => base64_encode(fread($media, $this->chunkSize)),
310
            ]);
311
        }
312
        fclose($media);
313
        // Finalize
314
        $finalize = $this->http('POST', self::UPLOAD_HOST, 'media/upload', [
315
            'command'  => 'FINALIZE',
316
            'media_id' => $init->media_id_string,
317
        ]);
318
319
        return $finalize;
320
    }
321
322
    /**
323
     * Private method to get params for upload media chunked init.
324
     * Twitter docs: https://dev.twitter.com/rest/reference/post/media/upload-init.html.
325
     *
326
     * @param array $parameters
327
     *
328
     * @return array
329
     */
330
    private function mediaInitParameters(array $parameters)
331
    {
332
        $return = [
333
            'command'     => 'INIT',
334
            'media_type'  => $parameters['media_type'],
335
            'total_bytes' => filesize($parameters['media']),
336
        ];
337
        if (isset($parameters['additional_owners'])) {
338
            $return['additional_owners'] = $parameters['additional_owners'];
339
        }
340
        if (isset($parameters['media_category'])) {
341
            $return['media_category'] = $parameters['media_category'];
342
        }
343
344
        return $return;
345
    }
346
347
    /**
348
     * @param string $method
349
     * @param string $host
350
     * @param string $path
351
     * @param array  $parameters
352
     *
353
     * @return array|object
354
     */
355
    private function http($method, $host, $path, array $parameters)
356
    {
357
        $this->resetLastResponse();
358
        $this->resetAttemptsNumber();
359
        $url = sprintf('%s/%s/%s.json', $host, self::API_VERSION, $path);
360
        $this->response->setApiPath($path);
361
362
        return $this->makeRequests($url, $method, $parameters);
363
    }
364
365
    /**
366
     * Make requests and retry them (if enabled) in case of Twitter's problems.
367
     *
368
     * @param string $method
369
     * @param string $url
370
     * @param string $method
371
     * @param array  $parameters
372
     *
373
     * @return array|object
374
     */
375
    private function makeRequests($url, $method, array $parameters)
376
    {
377
        do {
378
            $this->sleepIfNeeded();
379
            $result = $this->oAuthRequest($url, $method, $parameters);
380
            $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
381
            $this->response->setBody($response);
382
            $this->attempts++;
383
            // Retry up to our $maxRetries number if we get errors greater than 500 (over capacity etc)
384
        } while ($this->requestsAvailable());
385
386
        return $response;
387
    }
388
389
    /**
390
     * Checks if we have to retry request if API is down.
391
     *
392
     * @return bool
393
     */
394
    private function requestsAvailable()
395
    {
396
        return $this->maxRetries && ($this->attempts <= $this->maxRetries) && $this->getLastHttpCode() >= 500;
397
    }
398
399
    /**
400
     * Format and sign an OAuth / API request.
401
     *
402
     * @param string $url
403
     * @param string $method
404
     * @param array  $parameters
405
     *
406
     * @throws TwitterOAuthException
407
     *
408
     * @return string
409
     */
410
    private function oAuthRequest($url, $method, array $parameters)
411
    {
412
        $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
413
        if (array_key_exists('oauth_callback', $parameters)) {
414
            // Twitter doesn't like oauth_callback as a parameter.
415
            unset($parameters['oauth_callback']);
416
        }
417
        if ($this->bearer === null) {
418
            $request->signRequest($this->signatureMethod, $this->consumer, $this->token);
419
            $authorization = $request->toHeader();
420
            if (array_key_exists('oauth_verifier', $parameters)) {
421
                // Twitter doesn't always work with oauth in the body and in the header
422
                // and it's already included in the $authorization header
423
                unset($parameters['oauth_verifier']);
424
            }
425
        } else {
426
            $authorization = 'Authorization: Bearer '.$this->bearer;
427
        }
428
429
        return $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
430
    }
431
432
    /**
433
     * Set Curl options.
434
     *
435
     * @return array
436
     */
437
    private function curlOptions()
438
    {
439
        $options = [
440
            // CURLOPT_VERBOSE => true,
441
            CURLOPT_CONNECTTIMEOUT => $this->connectionTimeout,
442
            CURLOPT_HEADER         => true,
443
            CURLOPT_RETURNTRANSFER => true,
444
            CURLOPT_SSL_VERIFYHOST => 2,
445
            CURLOPT_SSL_VERIFYPEER => true,
446
            CURLOPT_TIMEOUT        => $this->timeout,
447
            CURLOPT_USERAGENT      => $this->userAgent,
448
        ];
449
450
        if ($this->useCAFile()) {
451
            $options[CURLOPT_CAINFO] = __DIR__.DIRECTORY_SEPARATOR.'cacert.pem';
452
        }
453
454
        if ($this->gzipEncoding) {
455
            $options[CURLOPT_ENCODING] = 'gzip';
456
        }
457
458
        if (!empty($this->proxy)) {
459
            $options[CURLOPT_PROXY] = $this->proxy['CURLOPT_PROXY'];
460
            $options[CURLOPT_PROXYUSERPWD] = $this->proxy['CURLOPT_PROXYUSERPWD'];
461
            $options[CURLOPT_PROXYPORT] = $this->proxy['CURLOPT_PROXYPORT'];
462
            $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC;
463
            $options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
464
        }
465
466
        return $options;
467
    }
468
469
    /**
470
     * Make an HTTP request.
471
     *
472
     * @param string $url
473
     * @param string $method
474
     * @param string $authorization
475
     * @param array  $postfields
476
     *
477
     * @throws TwitterOAuthException
478
     *
479
     * @return string
480
     */
481
    private function request($url, $method, $authorization, array $postfields)
482
    {
483
        $options = $this->curlOptions();
484
        $options[CURLOPT_URL] = $url;
485
        $options[CURLOPT_HTTPHEADER] = ['Accept: application/json', $authorization, 'Expect:'];
486
487
        switch ($method) {
488
            case 'GET':
489
                break;
490
            case 'POST':
491
                $options[CURLOPT_POST] = true;
492
                $options[CURLOPT_POSTFIELDS] = Util::buildHttpQuery($postfields);
493
                break;
494
            case 'DELETE':
495
                $options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
496
                break;
497
            case 'PUT':
498
                $options[CURLOPT_CUSTOMREQUEST] = 'PUT';
499
                break;
500
        }
501
502
        if (in_array($method, ['GET', 'PUT', 'DELETE']) && !empty($postfields)) {
503
            $options[CURLOPT_URL] .= '?'.Util::buildHttpQuery($postfields);
504
        }
505
506
        $curlHandle = curl_init();
507
        curl_setopt_array($curlHandle, $options);
508
        $response = curl_exec($curlHandle);
509
510
        // Throw exceptions on cURL errors.
511
        if (curl_errno($curlHandle) > 0) {
512
            throw new TwitterOAuthException(curl_error($curlHandle), curl_errno($curlHandle));
513
        }
514
515
        $this->response->setHttpCode(curl_getinfo($curlHandle, CURLINFO_HTTP_CODE));
516
        $parts = explode("\r\n\r\n", $response);
517
        $responseBody = array_pop($parts);
518
        $responseHeader = array_pop($parts);
519
        $this->response->setHeaders($this->parseHeaders($responseHeader));
520
521
        curl_close($curlHandle);
522
523
        return $responseBody;
524
    }
525
526
    /**
527
     * Get the header info to store.
528
     *
529
     * @param string $header
530
     *
531
     * @return array
532
     */
533
    private function parseHeaders($header)
534
    {
535
        $headers = [];
536
        foreach (explode("\r\n", $header) as $line) {
537
            if (strpos($line, ':') !== false) {
538
                list($key, $value) = explode(': ', $line);
539
                $key = str_replace('-', '_', strtolower($key));
540
                $headers[$key] = trim($value);
541
            }
542
        }
543
544
        return $headers;
545
    }
546
547
    /**
548
     * Encode application authorization header with base64.
549
     *
550
     * @param Consumer $consumer
551
     *
552
     * @return string
553
     */
554
    private function encodeAppAuthorization(Consumer $consumer)
555
    {
556
        $key = rawurlencode($consumer->key);
557
        $secret = rawurlencode($consumer->secret);
558
559
        return base64_encode($key.':'.$secret);
560
    }
561
562
    /**
563
     * Is the code running from a Phar module.
564
     *
565
     * @return bool
566
     */
567
    private function pharRunning()
568
    {
569
        return class_exists('Phar') && \Phar::running(false) !== '';
570
    }
571
572
    /**
573
     * Use included CA file instead of OS provided list.
574
     *
575
     * @return bool
576
     */
577
    private function useCAFile()
578
    {
579
        /* Use CACert file when not in a PHAR file. */
580
        return !$this->pharRunning();
581
    }
582
}
583