Completed
Push — master ( d594b8...7d4297 )
by Hector
02:28
created

TwitterAds::http()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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