Completed
Push — master ( 7ebb95...8f8be8 )
by Hector
02:52
created

TwitterAds::uploadMediaChunked()   C

Complexity

Conditions 8
Paths 24

Size

Total Lines 66
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 66
rs 6.7081
cc 8
eloc 45
nc 24
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
12
use Hborras\TwitterAdsSDK\TwitterAds\Cursor;
13
use Hborras\TwitterAdsSDK\TwitterAds\Errors\BadRequest;
14
use Hborras\TwitterAdsSDK\TwitterAds\Errors\Forbidden;
15
use Hborras\TwitterAdsSDK\TwitterAds\Errors\NotAuthorized;
16
use Hborras\TwitterAdsSDK\TwitterAds\Errors\NotFound;
17
use Hborras\TwitterAdsSDK\TwitterAds\Errors\RateLimit;
18
use Hborras\TwitterAdsSDK\TwitterAds\Errors\ServerError;
19
use Hborras\TwitterAdsSDK\TwitterAds\Errors\ServiceUnavailable;
20
use Hborras\TwitterAdsSDK\Util\JsonDecoder;
21
use GuzzleHttp;
22
23
/**
24
 * TwitterAds class for interacting with the Twitter API.
25
 *
26
 * @author Hector Borras <[email protected]>
27
 */
28
class TwitterAds extends Config
29
{
30
    const API_VERSION      = '1';
31
    const API_REST_VERSION = '1.1';
32
    const API_HOST         = 'https://ads-api.twitter.com';
33
    const API_HOST_SANDBOX = 'https://ads-api-sandbox.twitter.com';
34
    const API_HOST_OAUTH   = 'https://api.twitter.com';
35
    const UPLOAD_HOST      = 'https://upload.twitter.com';
36
    const UPLOAD_PATH      = 'media/upload.json';
37
    const UPLOAD_CHUNK     = 40960; // 1024 * 40
38
39
    /** @var  string Method used for the request */
40
    private $method;
41
    /** @var  string Resource used for the request */
42
    private $resource;
43
    /** @var Response details about the result of the last request */
44
    private $response;
45
    /** @var string|null Application bearer token */
46
    private $bearer;
47
    /** @var Consumer Twitter application details */
48
    private $consumer;
49
    /** @var Token|null User access token details */
50
    private $token;
51
    /** @var HmacSha1 OAuth 1 signature type used by Twitter */
52
    private $signatureMethod;
53
    /** @var  bool Sandbox allows to make requests thought sandbox environment */
54
    private $sandbox;
55
56
    /**
57
     * Constructor.
58
     *
59
     * @param string $consumerKey The Application Consumer Key
60
     * @param string $consumerSecret The Application Consumer Secret
61
     * @param string|null $oauthToken The Client Token (optional)
62
     * @param string|null $oauthTokenSecret The Client Token Secret (optional)
63
     * @param bool $sandbox The Sandbox environment (optional)
64
     */
65
    public function __construct($consumerKey, $consumerSecret, $oauthToken = null, $oauthTokenSecret = null, $sandbox = false)
66
    {
67
        $this->resetLastResponse();
68
        $this->signatureMethod = new HmacSha1();
69
        $this->consumer = new Consumer($consumerKey, $consumerSecret);
70
        if (!empty($oauthToken) && !empty($oauthTokenSecret)) {
71
            $this->token = new Token($oauthToken, $oauthTokenSecret);
72
        }
73
        if (empty($oauthToken) && !empty($oauthTokenSecret)) {
74
            $this->bearer = $oauthTokenSecret;
75
        }
76
        $this->sandbox = $sandbox;
77
    }
78
79
    /**
80
     * @param string $accountId
81
     *
82
     * @return Account|Cursor
83
     */
84
    public function getAccounts($accountId = '')
85
    {
86
        $account = new Account($this);
87
88
        return $accountId ? $account->load($accountId) : $account->all();
89
    }
90
91
    /**
92
     * @param string $oauthToken
93
     * @param string $oauthTokenSecret
94
     */
95
    public function setOauthToken($oauthToken, $oauthTokenSecret)
96
    {
97
        $this->token = new Token($oauthToken, $oauthTokenSecret);
98
    }
99
100
    /**
101
     * @return string|null
102
     */
103
    public function getLastApiPath()
104
    {
105
        return $this->response->getApiPath();
106
    }
107
108
    /**
109
     * @return int
110
     */
111
    public function getLastHttpCode()
112
    {
113
        return $this->response->getHttpCode();
114
    }
115
116
    /**
117
     * @return array
118
     */
119
    public function getLastXHeaders()
120
    {
121
        return $this->response->getXHeaders();
122
    }
123
124
    /**
125
     * @return array|object|null
126
     */
127
    public function getLastBody()
128
    {
129
        return $this->response->getBody();
130
    }
131
132
    /**
133
     * Resets the last response cache.
134
     */
135
    public function resetLastResponse()
136
    {
137
        $this->response = new Response();
138
    }
139
140
    /**
141
     * Make URLs for user browser navigation.
142
     *
143
     * @param string $path
144
     * @param array $parameters
145
     *
146
     * @return string
147
     */
148
    public function url($path, array $parameters)
149
    {
150
        $this->resetLastResponse();
151
        $this->response->setApiPath($path);
152
        $query = http_build_query($parameters);
153
154
        return sprintf('%s/%s?%s', self::API_HOST_OAUTH, $path, $query);
155
    }
156
157
    /**
158
     * Make /oauth/* requests to the API.
159
     *
160
     * @param string $path
161
     * @param array $parameters
162
     * @return array
163
     * @throws Exception
164
     */
165
    public function oauth($path, array $parameters = [])
166
    {
167
        $response = [];
168
        $this->resetLastResponse();
169
        $this->response->setApiPath($path);
170
        $url = sprintf('%s/%s', self::API_HOST_OAUTH, $path);
171
        $result = $this->oAuthRequest($url, 'POST', $parameters);
172
173
        if ($this->getLastHttpCode() != 200) {
174
            throw new TwitterAdsException($result, 500, null, $result);
175
        }
176
177
        parse_str($result, $response);
178
        $this->response->setBody($response);
179
180
        return $response;
181
    }
182
183
    /**
184
     * Make /oauth2/* requests to the API.
185
     *
186
     * @param string $path
187
     * @param array $parameters
188
     *
189
     * @return array|object
190
     */
191
    public function oauth2($path, array $parameters = [])
192
    {
193
        $method = 'POST';
194
        $this->resetLastResponse();
195
        $this->response->setApiPath($path);
196
        $url = sprintf('%s/%s', self::API_HOST_OAUTH, $path);
197
        $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
198
        $authorization = 'Authorization: Basic '.$this->encodeAppAuthorization($this->consumer);
199
        $result = $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
200
        $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
201
        $this->response->setBody($response);
202
203
        return $response;
204
    }
205
206
    public function verifyCredentials($parameters = [])
207
    {
208
        return $this->http('GET', self::API_HOST_OAUTH, 'account/verify_credentials', $parameters);
209
    }
210
211
    /**
212
     * Make GET requests to the API.
213
     *
214
     * @param string $path
215
     * @param array $parameters
216
     *
217
     * @return Response
218
     */
219
    public function get($path, array $parameters = [])
220
    {
221
        return $this->http('GET', !$this->sandbox ? self::API_HOST : self::API_HOST_SANDBOX, $path, $parameters);
222
    }
223
224
    /**
225
     * Make POST requests to the API.
226
     *
227
     * @param string $path
228
     * @param array $parameters
229
     *
230
     * @param array $headers
231
     * @return Response
232
     */
233
    public function post($path, array $parameters = [], array $headers = [])
234
    {
235
        return $this->http('POST', !$this->sandbox ? self::API_HOST : self::API_HOST_SANDBOX, $path, $parameters, $headers);
236
    }
237
238
    /**
239
     * Make DELETE requests to the API.
240
     *
241
     * @param string $path
242
     * @param array $parameters
243
     *
244
     * @return Response
245
     */
246
    public function delete($path, array $parameters = [])
247
    {
248
        return $this->http('DELETE', !$this->sandbox ? self::API_HOST : self::API_HOST_SANDBOX, $path, $parameters);
249
    }
250
251
    /**
252
     * Make PUT requests to the API.
253
     *
254
     * @param string $path
255
     * @param array $parameters
256
     *
257
     * @return Response
258
     */
259
    public function put($path, array $parameters = [], array $headers = [])
260
    {
261
        return $this->http('PUT', !$this->sandbox ? self::API_HOST : self::API_HOST_SANDBOX, $path, $parameters, $headers);
262
    }
263
264
    /**
265
     * Upload media to upload.twitter.com.
266
     *
267
     * @param array $parameters
268
     * @param bool $chunked
269
     *
270
     * @return array|object
271
     */
272
    public function upload(array $parameters = [], $chunked = false)
273
    {
274
        if ($chunked) {
275
            return $this->uploadMediaChunked(self::UPLOAD_PATH, $parameters);
276
        } else {
277
            return $this->uploadMediaNotChunked(self::UPLOAD_PATH, $parameters)->getBody();
278
        }
279
    }
280
281
    /**
282
     * Private method to upload media (not chunked) to upload.twitter.com.
283
     *
284
     * @param string $path
285
     * @param array $parameters
286
     *
287
     * @return array|object
288
     */
289
    private function uploadMediaNotChunked($path, $parameters)
290
    {
291
        $file = file_get_contents($parameters['media']);
292
        $base = base64_encode($file);
293
        $parameters['media'] = $base;
294
295
        return $this->http('POST', self::UPLOAD_HOST, $path, $parameters);
296
    }
297
298
    /**
299
     * Private method to upload media (chunked) to upload.twitter.com.
300
     *
301
     * @param string $path
302
     * @param array $parameters
303
     *
304
     * @return array|object
305
     */
306
    private function uploadMediaChunked($path, $parameters)
307
    {
308
        if ($parameters['media_type'] == 'video/mp4') {
309
            $parameters['media_category'] = "amplify_video";
310
        } elseif($parameters['media_type'] == 'image/gif'){
311
            $parameters['media_category'] = 'tweet_gif';
312
        }
313
314
        // Init
315
        $init = $this->http(
316
            'POST',
317
            self::UPLOAD_HOST,
318
            $path,
319
            [
320
                'command' => 'INIT',
321
                'media_type' => $parameters['media_type'],
322
                'media_category' => $parameters['media_category'],
323
                'total_bytes' => filesize($parameters['media'])
324
            ]
325
        )->getBody();
326
327
        // Append
328
        $segment_index = 0;
329
        $media = fopen($parameters['media'], 'rb');
330
        while (!feof($media)) {
331
            $this->http(
0 ignored issues
show
Unused Code introduced by
The call to the method Hborras\TwitterAdsSDK\Response::getBody() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
332
                'POST',
333
                self::UPLOAD_HOST,
334
                $path,
335
                [
336
                    'command' => 'APPEND',
337
                    'media_id' => $init->media_id_string,
338
                    'segment_index' => $segment_index++,
339
                    'media_data' => base64_encode(fread($media, self::UPLOAD_CHUNK)),
340
                ]
341
            )->getBody();
342
        }
343
        fclose($media);
344
        // Finalize
345
        $finalize = $this->http(
346
            'POST',
347
            self::UPLOAD_HOST,
348
            $path,
349
            [
350
                'command' => 'FINALIZE',
351
                'media_id' => $init->media_id_string,
352
            ]
353
        )->getBody();
354
355
        while (isset($finalize->processing_info)) {
356
            if (isset($finalize->processing_info->check_after_secs)) {
357
                sleep($finalize->processing_info->check_after_secs);
358
                $data = array(
359
                    'command' => 'STATUS',
360
                    'media_id' => $finalize->media_id
361
                );
362
                $finalize = $this->http('GET', self::UPLOAD_HOST, $path, $data)->getBody();
363
            } else {
364
                if (isset($finalize->processing_info->state) && $finalize->processing_info->state == 'succeeded') {
365
                    break;
366
                }
367
            }
368
        }
369
370
        return $finalize;
371
    }
372
373
    /**
374
     * @param string $method
375
     * @param string $host
376
     * @param string $path
377
     * @param array $parameters
378
     * @param array $headers
379
     * @return Response
380
     * @throws BadRequest
381
     * @throws Forbidden
382
     * @throws NotAuthorized
383
     * @throws NotFound
384
     * @throws RateLimit
385
     * @throws ServerError
386
     * @throws ServiceUnavailable
387
     */
388
    private function http($method, $host, $path, array $parameters, $headers = [])
389
    {
390
        $this->method = $method;
391
        $this->resource = $path;
392
        $this->resetLastResponse();
393
        if(strpos($path, TONUpload::DEFAULT_DOMAIN) === 0) {
394
            $url = $path;
395
        } else {
396
            if($host == self::UPLOAD_HOST){
397
                $url = sprintf('%s/%s/%s', $host, self::API_REST_VERSION, $path);
398
            } else {
399
                $url = sprintf('%s/%s/%s', $host, self::API_VERSION, $path);
400
            }
401
        }
402
403
        $this->response->setApiPath($path);
404
        $result = $this->oAuthRequest($url, $method, $parameters, $headers);
405
        $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
406
        $this->response->setBody($response);
407
        if ($this->getLastHttpCode() > 399) {
408
            $this->manageErrors($response);
409
        }
410
        return $this->response;
411
    }
412
413
    /**
414
     * @param $response
415
     * @throws BadRequest
416
     * @throws Forbidden
417
     * @throws NotAuthorized
418
     * @throws NotFound
419
     * @throws RateLimit
420
     * @throws ServerError
421
     * @throws ServiceUnavailable
422
     */
423
    public function manageErrors($response){
424
        switch ($this->getLastHttpCode()) {
425
            case 400:
426
                throw new BadRequest(TwitterAdsException::BAD_REQUEST, 400, null, $response->errors);
427
            case 401:
428
                throw new NotAuthorized(TwitterAdsException::NOT_AUTHORIZED, 401, null, $response->errors);
429
            case 403:
430
                throw new Forbidden(TwitterAdsException::FORBIDDEN, 403, null, $response->errors);
431
            case 404:
432
                throw new NotFound(TwitterAdsException::NOT_FOUND, 404, null, $response->errors);
433
            case 429:
434
                throw new RateLimit(TwitterAdsException::RATE_LIMIT, 429, null, $response->errors, $this->response->getsHeaders());
435
            case 500:
436
                throw new ServerError(TwitterAdsException::SERVER_ERROR, 500, null, $response->errors);
437
            case 503:
438
                throw new ServiceUnavailable(TwitterAdsException::SERVICE_UNAVAILABLE, 503, null, $response->errors, $this->response->getsHeaders());
439
            default:
440
                throw new ServerError(TwitterAdsException::SERVER_ERROR, 500, null, $response->errors);
441
        }
442
    }
443
444
    /**
445
     * Format and sign an OAuth / API request.
446
     *
447
     * @param string $url
448
     * @param string $method
449
     * @param array $parameters
450
     *
451
     * @param array $headers
452
     * @return string
453
     * @throws TwitterAdsException
454
     * @throws TwitterOAuthException
455
     */
456
    private function oAuthRequest($url, $method, array $parameters, $headers = [])
457
    {
458
        $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
459
        if (array_key_exists('oauth_callback', $parameters)) {
460
            // Twitter doesn't like oauth_callback as a parameter.
461
            unset($parameters['oauth_callback']);
462
        }
463
        if ($this->bearer === null) {
464
            $request->signRequest($this->signatureMethod, $this->consumer, $this->token);
465
            $authorization = $request->toHeader();
466
        } else {
467
            $authorization = 'Authorization: Bearer '.$this->bearer;
468
        }
469
        if(strpos($url, TONUpload::DEFAULT_DOMAIN) === 0) {
470
            return $this->request($url, $method, $authorization, $parameters, $headers);
471
        } else {
472
            return $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters, $headers);
473
        }
474
475
    }
476
477
    /**
478
     * Make an HTTP request.
479
     *
480
     * @param string $url
481
     * @param string $method
482
     * @param string $authorization
483
     * @param array $postfields
484
     *
485
     * @param array $headers
486
     * @return string
487
     * @throws TwitterAdsException
488
     */
489
    private function request($url, $method, $authorization, $postfields, $headers = [])
490
    {
491
        /* Curl settings */
492
        $options = [
493
            CURLOPT_VERBOSE => false,
494
            CURLOPT_CAINFO => __DIR__.DIRECTORY_SEPARATOR.'cacert.pem',
495
            CURLOPT_CONNECTTIMEOUT => $this->connectionTimeout,
496
            CURLOPT_HEADER => true,
497
            CURLOPT_HTTPHEADER => array_merge(['Accept: */*', $authorization, 'Expect:'],$headers,['Connection: close']),
498
            CURLOPT_RETURNTRANSFER => true,
499
            CURLOPT_SSL_VERIFYHOST => 2,
500
            CURLOPT_SSL_VERIFYPEER => true,
501
            CURLOPT_TIMEOUT => $this->timeout,
502
            CURLOPT_URL => $url,
503
            CURLOPT_USERAGENT => $this->userAgent,
504
            CURLOPT_ENCODING => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
505
        ];
506
507
        if (!empty($this->proxy)) {
508
            $options[CURLOPT_PROXY] = $this->proxy['CURLOPT_PROXY'];
509
            $options[CURLOPT_PROXYUSERPWD] = $this->proxy['CURLOPT_PROXYUSERPWD'];
510
            $options[CURLOPT_PROXYPORT] = $this->proxy['CURLOPT_PROXYPORT'];
511
            $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC;
512
            $options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
513
        }
514
515
        switch ($method) {
516
            case 'GET':
517
                break;
518
            case 'POST':
519
                $options[CURLOPT_POST] = true;
520
                if(isset($postfields['raw'])){
521
                    $options[CURLOPT_POSTFIELDS] = $postfields['raw'];
522
                } else {
523
                    $options[CURLOPT_POSTFIELDS] = Util::buildHttpQuery($postfields);
524
                }
525
526
                break;
527
            case 'DELETE':
528
                $options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
529
                break;
530
            case 'PUT':
531
                $options[CURLOPT_CUSTOMREQUEST] = 'PUT';
532
                if(isset($postfields['raw'])){
533
                    $options[CURLOPT_POSTFIELDS] = $postfields['raw'];
534
                }
535
                break;
536
        }
537
538
        if (in_array($method, ['GET', 'PUT', 'DELETE']) && !empty($postfields) && !isset($postfields['raw'])) {
539
            $options[CURLOPT_URL] .= '?'.Util::buildHttpQuery($postfields);
540
        }
541
542
        $curlHandle = curl_init();
543
        curl_setopt_array($curlHandle, $options);
544
        $response = curl_exec($curlHandle);
545
        // Throw exceptions on cURL errors.
546
        if (curl_errno($curlHandle) > 0) {
547
            throw new TwitterAdsException(curl_error($curlHandle), curl_errno($curlHandle), null, null);
548
        }
549
        $this->response->setHttpCode(curl_getinfo($curlHandle, CURLINFO_HTTP_CODE));
550
        $parts = explode("\r\n\r\n", $response);
551
        $responseBody = array_pop($parts);
552
        $responseHeader = array_pop($parts);
553
        $this->response->setHeaders($this->parseHeaders($responseHeader));
554
        curl_close($curlHandle);
555
        return $responseBody;
556
    }
557
558
    /**
559
     * Get the header info to store.
560
     *
561
     * @param string $header
562
     *
563
     * @return array
564
     */
565
    private function parseHeaders($header)
566
    {
567
        $headers = [];
568
        foreach (explode("\r\n", $header) as $line) {
569
            if (strpos($line, ':') !== false) {
570
                list($key, $value) = explode(': ', $line);
571
                $key = str_replace('-', '_', strtolower($key));
572
                $headers[$key] = trim($value);
573
            }
574
        }
575
576
        return $headers;
577
    }
578
579
    /**
580
     * Encode application authorization header with base64.
581
     *
582
     * @param Consumer $consumer
583
     *
584
     * @return string
585
     */
586
    private function encodeAppAuthorization($consumer)
587
    {
588
        // TODO: key and secret should be rfc 1738 encoded
589
        $key = $consumer->key;
590
        $secret = $consumer->secret;
591
592
        return base64_encode($key.':'.$secret);
593
    }
594
595
    /**
596
     * Return current response. Allows inheritance.
597
     *
598
     * @return Response
599
     */
600
    public function getResponse()
601
    {
602
        return $this->response;
603
    }
604
605
    /**
606
     * @return string
607
     */
608
    public function getMethod()
609
    {
610
        return $this->method;
611
    }
612
613
    /**
614
     * @return string
615
     */
616
    public function getResource()
617
    {
618
        return $this->resource;
619
    }
620
}
621