TwitterOAuth::oAuthRequest()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 6
nop 3
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * The most popular PHP library for use with the Twitter OAuth REST API.
4
 *
5
 * @license MIT
6
 */
7
namespace Abraham\TwitterOAuth;
8
9
use Abraham\TwitterOAuth\Util\JsonDecoder;
10
11
/**
12
 * TwitterOAuth class for interacting with the Twitter API.
13
 *
14
 * @author Abraham Williams <[email protected]>
15
 */
16
class TwitterOAuth extends Config
17
{
18
    const API_VERSION = '1.1';
19
    const API_HOST = 'https://api.twitter.com';
20
    const UPLOAD_HOST = 'https://upload.twitter.com';
21
22
    /** @var Response details about the result of the last request */
23
    private $response;
24
    /** @var string|null Application bearer token */
25
    private $bearer;
26
    /** @var Consumer Twitter application details */
27
    private $consumer;
28
    /** @var Token|null User access token details */
29
    private $token;
30
    /** @var HmacSha1 OAuth 1 signature type used by Twitter */
31
    private $signatureMethod;
32
    /** @var int Number of attempts we made for the request */
33
    private $attempts = 0;
34
35
    /**
36
     * Constructor
37
     *
38
     * @param string      $consumerKey      The Application Consumer Key
39
     * @param string      $consumerSecret   The Application Consumer Secret
40
     * @param string|null $oauthToken       The Client Token (optional)
41
     * @param string|null $oauthTokenSecret The Client Token Secret (optional)
42
     */
43
    public function __construct($consumerKey, $consumerSecret, $oauthToken = null, $oauthTokenSecret = null)
44
    {
45
        $this->resetLastResponse();
46
        $this->signatureMethod = new HmacSha1();
47
        $this->consumer = new Consumer($consumerKey, $consumerSecret);
48
        if (!empty($oauthToken) && !empty($oauthTokenSecret)) {
49
            $this->setOauthToken($oauthToken, $oauthTokenSecret);
50
        }
51
        if (empty($oauthToken) && !empty($oauthTokenSecret)) {
52
            $this->setBearer($oauthTokenSecret);
53
        }
54
    }
55
56
    /**
57
     * @param string $oauthToken
58
     * @param string $oauthTokenSecret
59
     */
60
    public function setOauthToken($oauthToken, $oauthTokenSecret)
61
    {
62
        $this->token = new Token($oauthToken, $oauthTokenSecret);
63
        $this->bearer = null;
64
    }
65
66
    /**
67
     * @param string $oauthTokenSecret
68
     */
69
    public function setBearer($oauthTokenSecret)
70
    {
71
        $this->bearer = $oauthTokenSecret;
72
        $this->token = null;
73
    }
74
75
    /**
76
     * @return string|null
77
     */
78
    public function getLastApiPath()
79
    {
80
        return $this->response->getApiPath();
81
    }
82
83
    /**
84
     * @return int
85
     */
86
    public function getLastHttpCode()
87
    {
88
        return $this->response->getHttpCode();
89
    }
90
91
    /**
92
     * @return array
93
     */
94
    public function getLastXHeaders()
95
    {
96
        return $this->response->getXHeaders();
97
    }
98
99
    /**
100
     * @return array|object|null
101
     */
102
    public function getLastBody()
103
    {
104
        return $this->response->getBody();
105
    }
106
107
    /**
108
     * Resets the last response cache.
109
     */
110
    public function resetLastResponse()
111
    {
112
        $this->response = new Response();
113
    }
114
115
    /**
116
     * Resets the attempts number.
117
     */
118
    private function resetAttemptsNumber()
119
    {
120
        $this->attempts = 0;
121
    }
122
123
    /**
124
     * Delays the retries when they're activated.
125
     */
126
    private function sleepIfNeeded()
127
    {
128
        if ($this->maxRetries && $this->attempts) {
129
            sleep($this->retriesDelay);
130
        }
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
        return sprintf('%s/%s?%s', self::API_HOST, $path, $query);
148
    }
149
150
    /**
151
     * Make /oauth/* requests to the API.
152
     *
153
     * @param string $path
154
     * @param array  $parameters
155
     *
156
     * @return array
157
     * @throws TwitterOAuthException
158
     */
159
    public function oauth($path, array $parameters = [])
160
    {
161
        $response = [];
162
        $this->resetLastResponse();
163
        $this->response->setApiPath($path);
164
        $url = sprintf('%s/%s', self::API_HOST, $path);
165
        $result = $this->oAuthRequest($url, 'POST', $parameters);
166
167
        if ($this->getLastHttpCode() != 200) {
168
            throw new TwitterOAuthException($result);
169
        }
170
171
        parse_str($result, $response);
172
        $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...
173
174
        return $response;
175
    }
176
177
    /**
178
     * Make /oauth2/* requests to the API.
179
     *
180
     * @param string $path
181
     * @param array  $parameters
182
     *
183
     * @return array|object
184
     */
185
    public function oauth2($path, array $parameters = [])
186
    {
187
        $method = 'POST';
188
        $this->resetLastResponse();
189
        $this->response->setApiPath($path);
190
        $url = sprintf('%s/%s', self::API_HOST, $path);
191
        $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
192
        $authorization = 'Authorization: Basic ' . $this->encodeAppAuthorization($this->consumer);
193
        $result = $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
194
        $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
195
        $this->response->setBody($response);
196
        return $response;
197
    }
198
199
    /**
200
     * Make GET requests to the API.
201
     *
202
     * @param string $path
203
     * @param array  $parameters
204
     *
205
     * @return array|object
206
     */
207
    public function get($path, array $parameters = [])
208
    {
209
        return $this->http('GET', self::API_HOST, $path, $parameters);
210
    }
211
212
    /**
213
     * Make POST requests to the API.
214
     *
215
     * @param string $path
216
     * @param array  $parameters
217
     *
218
     * @return array|object
219
     */
220
    public function post($path, array $parameters = [])
221
    {
222
        return $this->http('POST', self::API_HOST, $path, $parameters);
223
    }
224
225
    /**
226
     * Make DELETE requests to the API.
227
     *
228
     * @param string $path
229
     * @param array  $parameters
230
     *
231
     * @return array|object
232
     */
233
    public function delete($path, array $parameters = [])
234
    {
235
        return $this->http('DELETE', self::API_HOST, $path, $parameters);
236
    }
237
238
    /**
239
     * Make PUT requests to the API.
240
     *
241
     * @param string $path
242
     * @param array  $parameters
243
     *
244
     * @return array|object
245
     */
246
    public function put($path, array $parameters = [])
247
    {
248
        return $this->http('PUT', self::API_HOST, $path, $parameters);
249
    }
250
251
    /**
252
     * Upload media to upload.twitter.com.
253
     *
254
     * @param string $path
255
     * @param array  $parameters
256
     * @param boolean  $chunked
257
     *
258
     * @return array|object
259
     */
260
    public function upload($path, array $parameters = [], $chunked = false)
261
    {
262
        if ($chunked) {
263
            return $this->uploadMediaChunked($path, $parameters);
264
        } else {
265
            return $this->uploadMediaNotChunked($path, $parameters);
266
        }
267
    }
268
269
    /**
270
     * Private method to upload media (not chunked) to upload.twitter.com.
271
     *
272
     * @param string $path
273
     * @param array  $parameters
274
     *
275
     * @return array|object
276
     */
277
    private function uploadMediaNotChunked($path, array $parameters)
278
    {
279
        $file = file_get_contents($parameters['media']);
280
        $base = base64_encode($file);
281
        $parameters['media'] = $base;
282
        return $this->http('POST', self::UPLOAD_HOST, $path, $parameters);
283
    }
284
285
    /**
286
     * Private method to upload media (chunked) to upload.twitter.com.
287
     *
288
     * @param string $path
289
     * @param array  $parameters
290
     *
291
     * @return array|object
292
     */
293
    private function uploadMediaChunked($path, array $parameters)
294
    {
295
        $init = $this->http('POST', self::UPLOAD_HOST, $path, $this->mediaInitParameters($parameters));
296
        // Append
297
        $segmentIndex = 0;
298
        $media = fopen($parameters['media'], 'rb');
299
        while (!feof($media)) {
300
            $this->http('POST', self::UPLOAD_HOST, 'media/upload', [
301
                'command' => 'APPEND',
302
                'media_id' => $init->media_id_string,
303
                'segment_index' => $segmentIndex++,
304
                'media_data' => base64_encode(fread($media, $this->chunkSize))
305
            ]);
306
        }
307
        fclose($media);
308
        // Finalize
309
        $finalize = $this->http('POST', self::UPLOAD_HOST, 'media/upload', [
310
            'command' => 'FINALIZE',
311
            'media_id' => $init->media_id_string
312
        ]);
313
        return $finalize;
314
    }
315
316
    /**
317
     * Private method to get params for upload media chunked init.
318
     * Twitter docs: https://dev.twitter.com/rest/reference/post/media/upload-init.html
319
     *
320
     * @param array  $parameters
321
     *
322
     * @return array
323
     */
324
    private function mediaInitParameters(array $parameters)
325
    {
326
        $return = [
327
            'command' => 'INIT',
328
            'media_type' => $parameters['media_type'],
329
            'total_bytes' => filesize($parameters['media'])
330
        ];
331
        if (isset($parameters['additional_owners'])) {
332
            $return['additional_owners'] = $parameters['additional_owners'];
333
        }
334
        if (isset($parameters['media_category'])) {
335
            $return['media_category'] = $parameters['media_category'];
336
        }
337
        return $return;
338
    }
339
340
    /**
341
     * @param string $method
342
     * @param string $host
343
     * @param string $path
344
     * @param array  $parameters
345
     *
346
     * @return array|object
347
     */
348
    private function http($method, $host, $path, array $parameters)
349
    {
350
        $this->resetLastResponse();
351
        $this->resetAttemptsNumber();
352
        $url = sprintf('%s/%s/%s.json', $host, self::API_VERSION, $path);
353
        $this->response->setApiPath($path);
354
        return $this->makeRequests($url, $method, $parameters);
355
    }
356
357
    /**
358
     *
359
     * Make requests and retry them (if enabled) in case of Twitter's problems.
360
     *
361
     * @param string $method
362
     * @param string $url
363
     * @param string $method
364
     * @param array  $parameters
365
     *
366
     * @return array|object
367
     */
368
    private function makeRequests($url, $method, array $parameters)
369
    {
370
        do {
371
            $this->sleepIfNeeded();
372
            $result = $this->oAuthRequest($url, $method, $parameters);
373
            $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
374
            $this->response->setBody($response);
375
            $this->attempts++;
376
            // Retry up to our $maxRetries number if we get errors greater than 500 (over capacity etc)
377
        } while ($this->requestsAvailable());
378
379
        return $response;
380
    }
381
382
    /**
383
     * Checks if we have to retry request if API is down.
384
     *
385
     * @return bool
386
     */
387
    private function requestsAvailable()
388
    {
389
        return ($this->maxRetries && ($this->attempts <= $this->maxRetries) && $this->getLastHttpCode() >= 500);
390
    }
391
392
    /**
393
     * Format and sign an OAuth / API request
394
     *
395
     * @param string $url
396
     * @param string $method
397
     * @param array  $parameters
398
     *
399
     * @return string
400
     * @throws TwitterOAuthException
401
     */
402
    private function oAuthRequest($url, $method, array $parameters)
403
    {
404
        $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
405
        if (array_key_exists('oauth_callback', $parameters)) {
406
            // Twitter doesn't like oauth_callback as a parameter.
407
            unset($parameters['oauth_callback']);
408
        }
409
        if ($this->bearer === null) {
410
            $request->signRequest($this->signatureMethod, $this->consumer, $this->token);
411
            $authorization = $request->toHeader();
412
            if (array_key_exists('oauth_verifier', $parameters)) {
413
                // Twitter doesn't always work with oauth in the body and in the header
414
                // and it's already included in the $authorization header
415
                unset($parameters['oauth_verifier']);
416
            }
417
        } else {
418
            $authorization = 'Authorization: Bearer ' . $this->bearer;
419
        }
420
        return $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
421
    }
422
423
    /**
424
     * Set Curl options.
425
     *
426
     * @return array
427
     */
428
    private function curlOptions()
429
    {
430
        $options = [
431
            // CURLOPT_VERBOSE => true,
432
            CURLOPT_CONNECTTIMEOUT => $this->connectionTimeout,
433
            CURLOPT_HEADER => true,
434
            CURLOPT_RETURNTRANSFER => true,
435
            CURLOPT_SSL_VERIFYHOST => 2,
436
            CURLOPT_SSL_VERIFYPEER => true,
437
            CURLOPT_TIMEOUT => $this->timeout,
438
            CURLOPT_USERAGENT => $this->userAgent,
439
        ];
440
441
        if ($this->useCAFile()) {
442
            $options[CURLOPT_CAINFO] = __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem';
443
        }
444
445
        if ($this->gzipEncoding) {
446
            $options[CURLOPT_ENCODING] = 'gzip';
447
        }
448
449
        if (!empty($this->proxy)) {
450
            $options[CURLOPT_PROXY] = $this->proxy['CURLOPT_PROXY'];
451
            $options[CURLOPT_PROXYUSERPWD] = $this->proxy['CURLOPT_PROXYUSERPWD'];
452
            $options[CURLOPT_PROXYPORT] = $this->proxy['CURLOPT_PROXYPORT'];
453
            $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC;
454
            $options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
455
        }
456
457
        return $options;
458
    }
459
460
    /**
461
     * Make an HTTP request
462
     *
463
     * @param string $url
464
     * @param string $method
465
     * @param string $authorization
466
     * @param array $postfields
467
     *
468
     * @return string
469
     * @throws TwitterOAuthException
470
     */
471
    private function request($url, $method, $authorization, array $postfields)
472
    {
473
        $options = $this->curlOptions();
474
        $options[CURLOPT_URL] = $url;
475
        $options[CURLOPT_HTTPHEADER] = ['Accept: application/json', $authorization, 'Expect:'];
476
477
        switch ($method) {
478
            case 'GET':
479
                break;
480
            case 'POST':
481
                $options[CURLOPT_POST] = true;
482
                $options[CURLOPT_POSTFIELDS] = Util::buildHttpQuery($postfields);
483
                break;
484
            case 'DELETE':
485
                $options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
486
                break;
487
            case 'PUT':
488
                $options[CURLOPT_CUSTOMREQUEST] = 'PUT';
489
                break;
490
        }
491
492
        if (in_array($method, ['GET', 'PUT', 'DELETE']) && !empty($postfields)) {
493
            $options[CURLOPT_URL] .= '?' . Util::buildHttpQuery($postfields);
494
        }
495
496
497
        $curlHandle = curl_init();
498
        curl_setopt_array($curlHandle, $options);
499
        $response = curl_exec($curlHandle);
500
501
        // Throw exceptions on cURL errors.
502
        if (curl_errno($curlHandle) > 0) {
503
            throw new TwitterOAuthException(curl_error($curlHandle), curl_errno($curlHandle));
504
        }
505
506
        $this->response->setHttpCode(curl_getinfo($curlHandle, CURLINFO_HTTP_CODE));
507
        $parts = explode("\r\n\r\n", $response);
508
        $responseBody = array_pop($parts);
509
        $responseHeader = array_pop($parts);
510
        $this->response->setHeaders($this->parseHeaders($responseHeader));
511
512
        curl_close($curlHandle);
513
514
        return $responseBody;
515
    }
516
517
    /**
518
     * Get the header info to store.
519
     *
520
     * @param string $header
521
     *
522
     * @return array
523
     */
524
    private function parseHeaders($header)
525
    {
526
        $headers = [];
527
        foreach (explode("\r\n", $header) as $line) {
528
            if (strpos($line, ':') !== false) {
529
                list ($key, $value) = explode(': ', $line);
530
                $key = str_replace('-', '_', strtolower($key));
531
                $headers[$key] = trim($value);
532
            }
533
        }
534
        return $headers;
535
    }
536
537
    /**
538
     * Encode application authorization header with base64.
539
     *
540
     * @param Consumer $consumer
541
     *
542
     * @return string
543
     */
544
    private function encodeAppAuthorization(Consumer $consumer)
545
    {
546
        $key = rawurlencode($consumer->key);
547
        $secret = rawurlencode($consumer->secret);
548
        return base64_encode($key . ':' . $secret);
549
    }
550
551
    /**
552
     * Is the code running from a Phar module.
553
     *
554
     * @return boolean
555
     */
556
    private function pharRunning()
557
    {
558
        return class_exists('Phar') && \Phar::running(false) !== '';
559
    }
560
561
    /**
562
     * Use included CA file instead of OS provided list.
563
     *
564
     * @return boolean
565
     */
566
    private function useCAFile()
567
    {
568
        /* Use CACert file when not in a PHAR file. */
569
        return !$this->pharRunning();
570
    }
571
}
572