Overview

Namespaces

  • Codebird

Classes

  • Codebird\Codebird
  • Overview
  • Namespace
  • Class
   1: <?php
   2: 
   3: namespace Codebird;
   4: 
   5: /**
   6:  * A Twitter library in PHP.
   7:  *
   8:  * @package   codebird
   9:  * @version   3.0.0-dev
  10:  * @author    Jublo Solutions <support@jublo.net>
  11:  * @copyright 2010-2015 Jublo Solutions <support@jublo.net>
  12:  * @license   http://opensource.org/licenses/GPL-3.0 GNU General Public License 3.0
  13:  * @link      https://github.com/jublonet/codebird-php
  14:  */
  15: 
  16: /**
  17:  * Define constants
  18:  */
  19: $constants = explode(' ', 'OBJECT ARRAY JSON');
  20: foreach ($constants as $i => $id) {
  21:   $id = 'CODEBIRD_RETURNFORMAT_' . $id;
  22:   defined($id) or define($id, $i);
  23: }
  24: $constants = [
  25:   'CURLE_SSL_CERTPROBLEM' => 58,
  26:   'CURLE_SSL_CACERT' => 60,
  27:   'CURLE_SSL_CACERT_BADFILE' => 77,
  28:   'CURLE_SSL_CRL_BADFILE' => 82,
  29:   'CURLE_SSL_ISSUER_ERROR' => 83
  30: ];
  31: foreach ($constants as $id => $i) {
  32:   defined($id) or define($id, $i);
  33: }
  34: unset($constants);
  35: unset($i);
  36: unset($id);
  37: 
  38: /**
  39:  * A Twitter library in PHP.
  40:  *
  41:  * @package codebird
  42:  * @subpackage codebird-php
  43:  */
  44: class Codebird
  45: {
  46:   /**
  47:    * The current singleton instance
  48:    */
  49:   private static $_instance = null;
  50: 
  51:   /**
  52:    * The OAuth consumer key of your registered app
  53:    */
  54:   protected static $_oauth_consumer_key = null;
  55: 
  56:   /**
  57:    * The corresponding consumer secret
  58:    */
  59:   protected static $_oauth_consumer_secret = null;
  60: 
  61:   /**
  62:    * The app-only bearer token. Used to authorize app-only requests
  63:    */
  64:   protected static $_oauth_bearer_token = null;
  65: 
  66:   /**
  67:    * The API endpoint to use
  68:    */
  69:   protected static $_endpoint = 'https://api.twitter.com/1.1/';
  70: 
  71:   /**
  72:    * The media API endpoint to use
  73:    */
  74:   protected static $_endpoint_media = 'https://upload.twitter.com/1.1/';
  75: 
  76:   /**
  77:    * The Streaming API endpoints to use
  78:    */
  79:   protected static $_endpoints_streaming = [
  80:     'public' => 'https://stream.twitter.com/1.1/',
  81:     'user'   => 'https://userstream.twitter.com/1.1/',
  82:     'site'   => 'https://sitestream.twitter.com/1.1/'
  83:   ];
  84: 
  85:   /**
  86:    * The TON API endpoint to use
  87:    */
  88:   protected static $_endpoint_ton = 'https://ton.twitter.com/1.1/';
  89: 
  90:   /**
  91:    * The Ads API endpoint to use
  92:    */
  93:   protected static $_endpoint_ads = 'https://ads-api.twitter.com/0/';
  94: 
  95:   /**
  96:    * The Ads Sandbox API endpoint to use
  97:    */
  98:   protected static $_endpoint_ads_sandbox = 'https://ads-api-sandbox.twitter.com/0/';
  99: 
 100:   /**
 101:    * The API endpoint base to use
 102:    */
 103:   protected static $_endpoint_oauth = 'https://api.twitter.com/';
 104: 
 105:   /**
 106:    * Possible file name parameters
 107:    */
 108:   protected static $_possible_files = [
 109:     // Tweets
 110:     'statuses/update_with_media' => ['media[]'],
 111:     'media/upload' => ['media'],
 112:     // Accounts
 113:     'account/update_profile_background_image' => ['image'],
 114:     'account/update_profile_image' => ['image'],
 115:     'account/update_profile_banner' => ['banner']
 116:   ];
 117: 
 118:   /**
 119:    * The Request or access token. Used to sign requests
 120:    */
 121:   protected $_oauth_token = null;
 122: 
 123:   /**
 124:    * The corresponding request or access token secret
 125:    */
 126:   protected $_oauth_token_secret = null;
 127: 
 128:   /**
 129:    * The format of data to return from API calls
 130:    */
 131:   protected $_return_format = CODEBIRD_RETURNFORMAT_OBJECT;
 132: 
 133:   /**
 134:    * The file formats that Twitter accepts as image uploads
 135:    */
 136:   protected $_supported_media_files = [
 137:     IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_BMP,
 138:     IMAGETYPE_GIF //, IMAGETYPE_WEBP
 139:   ];
 140: 
 141:   /**
 142:    * The callback to call with any new streaming messages
 143:    */
 144:   protected $_streaming_callback = null;
 145: 
 146:   /**
 147:    * The current Codebird version
 148:    */
 149:   protected $_version = '3.0.0-dev';
 150: 
 151:   /**
 152:    * Auto-detect cURL absence
 153:    */
 154:   protected $_use_curl = true;
 155: 
 156:   /**
 157:    * Request timeout
 158:    */
 159:   protected $_timeout = 10000;
 160: 
 161:   /**
 162:    * Connection timeout
 163:    */
 164:   protected $_connectionTimeout = 3000;
 165: 
 166:   /**
 167:    * Remote media download timeout
 168:    */
 169:   protected $_remoteDownloadTimeout = 5000;
 170: 
 171:   /**
 172:    * Proxy
 173:    */
 174:   protected $_proxy = [];
 175: 
 176:   /**
 177:    *
 178:    * Class constructor
 179:    *
 180:    */
 181:   public function __construct()
 182:   {
 183:     // Pre-define $_use_curl depending on cURL availability
 184:     $this->setUseCurl(function_exists('curl_init'));
 185:   }
 186: 
 187:   /**
 188:    * Returns singleton class instance
 189:    * Always use this method unless you're working with multiple authenticated users at once
 190:    *
 191:    * @return Codebird The instance
 192:    */
 193:   public static function getInstance()
 194:   {
 195:     if (self::$_instance === null) {
 196:       self::$_instance = new self;
 197:     }
 198:     return self::$_instance;
 199:   }
 200: 
 201:   /**
 202:    * Sets the OAuth consumer key and secret (App key)
 203:    *
 204:    * @param string $key    OAuth consumer key
 205:    * @param string $secret OAuth consumer secret
 206:    *
 207:    * @return void
 208:    */
 209:   public static function setConsumerKey($key, $secret)
 210:   {
 211:     self::$_oauth_consumer_key    = $key;
 212:     self::$_oauth_consumer_secret = $secret;
 213:   }
 214: 
 215:   /**
 216:    * Sets the OAuth2 app-only auth bearer token
 217:    *
 218:    * @param string $token OAuth2 bearer token
 219:    *
 220:    * @return void
 221:    */
 222:   public static function setBearerToken($token)
 223:   {
 224:     self::$_oauth_bearer_token = $token;
 225:   }
 226: 
 227:   /**
 228:    * Gets the current Codebird version
 229:    *
 230:    * @return string The version number
 231:    */
 232:   public function getVersion()
 233:   {
 234:     return $this->_version;
 235:   }
 236: 
 237:   /**
 238:    * Sets the OAuth request or access token and secret (User key)
 239:    *
 240:    * @param string $token  OAuth request or access token
 241:    * @param string $secret OAuth request or access token secret
 242:    *
 243:    * @return void
 244:    */
 245:   public function setToken($token, $secret)
 246:   {
 247:     $this->_oauth_token        = $token;
 248:     $this->_oauth_token_secret = $secret;
 249:   }
 250: 
 251:   /**
 252:    * Forgets the OAuth request or access token and secret (User key)
 253:    *
 254:    * @return bool
 255:    */
 256:   public function logout()
 257:   {
 258:     $this->_oauth_token =
 259:     $this->_oauth_token_secret = null;
 260: 
 261:     return true;
 262:   }
 263: 
 264:   /**
 265:    * Sets if codebird should use cURL
 266:    *
 267:    * @param bool $use_curl Request uses cURL or not
 268:    *
 269:    * @return void
 270:    */
 271:   public function setUseCurl($use_curl)
 272:   {
 273:     if ($use_curl && ! function_exists('curl_init')) {
 274:       throw new \Exception('To use cURL, the PHP curl extension must be available.');
 275:     }
 276: 
 277:     $this->_use_curl = (bool) $use_curl;
 278:   }
 279: 
 280:   /**
 281:    * Sets request timeout in milliseconds
 282:    *
 283:    * @param int $timeout Request timeout in milliseconds
 284:    *
 285:    * @return void
 286:    */
 287:   public function setTimeout($timeout)
 288:   {
 289:     $this->_timeout = (int) $timeout;
 290:   }
 291: 
 292:   /**
 293:    * Sets connection timeout in milliseconds
 294:    *
 295:    * @param int $timeout Connection timeout in milliseconds
 296:    *
 297:    * @return void
 298:    */
 299:   public function setConnectionTimeout($timeout)
 300:   {
 301:     $this->_connectionTimeout = (int) $timeout;
 302:   }
 303: 
 304:   /**
 305:    * Sets remote media download timeout in milliseconds
 306:    *
 307:    * @param int $timeout Remote media timeout in milliseconds
 308:    *
 309:    * @return void
 310:    */
 311:   public function setRemoteDownloadTimeout($timeout)
 312:   {
 313:     $this->_remoteDownloadTimeout = (int) $timeout;
 314:   }
 315: 
 316:   /**
 317:    * Sets the format for API replies
 318:    *
 319:    * @param int $return_format One of these:
 320:    *                           CODEBIRD_RETURNFORMAT_OBJECT (default)
 321:    *                           CODEBIRD_RETURNFORMAT_ARRAY
 322:    *
 323:    * @return void
 324:    */
 325:   public function setReturnFormat($return_format)
 326:   {
 327:     $this->_return_format = $return_format;
 328:   }
 329: 
 330:   /**
 331:    * Sets the proxy
 332:    *
 333:    * @param string $host Proxy host
 334:    * @param int    $port Proxy port
 335:    *
 336:    * @return void
 337:    */
 338:   public function setProxy($host, $port)
 339:   {
 340:     $this->_proxy['host'] = $host;
 341:     $this->_proxy['port'] = $port;
 342:   }
 343: 
 344:   /**
 345:    * Sets the proxy authentication
 346:    *
 347:    * @param string $authentication Proxy authentication
 348:    *
 349:    * @return void
 350:    */
 351:   public function setProxyAuthentication($authentication)
 352:   {
 353:     $this->_proxy['authentication'] = $authentication;
 354:   }
 355: 
 356:   /**
 357:    * Sets streaming callback
 358:    *
 359:    * @param callable $callback The streaming callback
 360:    *
 361:    * @return void
 362:    */
 363:   public function setStreamingCallback($callback)
 364:   {
 365:     if (!is_callable($callback)) {
 366:       throw new \Exception('This is not a proper callback.');
 367:     }
 368:     $this->_streaming_callback = $callback;
 369:   }
 370: 
 371:   /**
 372:    * Get allowed API methods, sorted by GET or POST
 373:    * Watch out for multiple-method API methods!
 374:    *
 375:    * @return array $apimethods
 376:    */
 377:   public function getApiMethods()
 378:   {
 379:     static $httpmethods = [
 380:       'GET' => [
 381:         'account/settings',
 382:         'account/verify_credentials',
 383:         'ads/accounts',
 384:         'ads/accounts/:account_id',
 385:         'ads/accounts/:account_id/app_event_provider_configurations',
 386:         'ads/accounts/:account_id/app_event_provider_configurations/:id',
 387:         'ads/accounts/:account_id/app_event_tags',
 388:         'ads/accounts/:account_id/app_event_tags/:id',
 389:         'ads/accounts/:account_id/app_lists',
 390:         'ads/accounts/:account_id/authenticated_user_access',
 391:         'ads/accounts/:account_id/campaigns',
 392:         'ads/accounts/:account_id/campaigns/:campaign_id',
 393:         'ads/accounts/:account_id/cards/app_download',
 394:         'ads/accounts/:account_id/cards/app_download/:card_id',
 395:         'ads/accounts/:account_id/cards/image_app_download',
 396:         'ads/accounts/:account_id/cards/image_app_download/:card_id',
 397:         'ads/accounts/:account_id/cards/image_conversation',
 398:         'ads/accounts/:account_id/cards/image_conversation/:card_id',
 399:         'ads/accounts/:account_id/cards/lead_gen',
 400:         'ads/accounts/:account_id/cards/lead_gen/:card_id',
 401:         'ads/accounts/:account_id/cards/video_app_download',
 402:         'ads/accounts/:account_id/cards/video_app_download/:id',
 403:         'ads/accounts/:account_id/cards/video_conversation',
 404:         'ads/accounts/:account_id/cards/video_conversation/:card_id',
 405:         'ads/accounts/:account_id/cards/website',
 406:         'ads/accounts/:account_id/cards/website/:card_id',
 407:         'ads/accounts/:account_id/features',
 408:         'ads/accounts/:account_id/funding_instruments',
 409:         'ads/accounts/:account_id/funding_instruments/:id',
 410:         'ads/accounts/:account_id/line_items',
 411:         'ads/accounts/:account_id/line_items/:line_item_id',
 412:         'ads/accounts/:account_id/promotable_users',
 413:         'ads/accounts/:account_id/promoted_accounts',
 414:         'ads/accounts/:account_id/promoted_tweets',
 415:         'ads/accounts/:account_id/reach_estimate',
 416:         'ads/accounts/:account_id/scoped_timeline',
 417:         'ads/accounts/:account_id/tailored_audience_changes',
 418:         'ads/accounts/:account_id/tailored_audience_changes/:id',
 419:         'ads/accounts/:account_id/tailored_audiences',
 420:         'ads/accounts/:account_id/tailored_audiences/:id',
 421:         'ads/accounts/:account_id/targeting_criteria',
 422:         'ads/accounts/:account_id/targeting_criteria/:id',
 423:         'ads/accounts/:account_id/targeting_suggestions',
 424:         'ads/accounts/:account_id/tweet/preview',
 425:         'ads/accounts/:account_id/tweet/preview/:tweet_id',
 426:         'ads/accounts/:account_id/videos',
 427:         'ads/accounts/:account_id/videos/:id',
 428:         'ads/accounts/:account_id/web_event_tags',
 429:         'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
 430:         'ads/bidding_rules',
 431:         'ads/iab_categories',
 432:         'ads/insights/accounts/:account_id',
 433:         'ads/insights/accounts/:account_id/available_audiences',
 434:         'ads/line_items/placements',
 435:         'ads/sandbox/accounts',
 436:         'ads/sandbox/accounts/:account_id',
 437:         'ads/sandbox/accounts/:account_id/app_event_provider_configurations',
 438:         'ads/sandbox/accounts/:account_id/app_event_provider_configurations/:id',
 439:         'ads/sandbox/accounts/:account_id/app_event_tags',
 440:         'ads/sandbox/accounts/:account_id/app_event_tags/:id',
 441:         'ads/sandbox/accounts/:account_id/app_lists',
 442:         'ads/sandbox/accounts/:account_id/authenticated_user_access',
 443:         'ads/sandbox/accounts/:account_id/campaigns',
 444:         'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
 445:         'ads/sandbox/accounts/:account_id/cards/app_download',
 446:         'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
 447:         'ads/sandbox/accounts/:account_id/cards/image_app_download',
 448:         'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
 449:         'ads/sandbox/accounts/:account_id/cards/image_conversation',
 450:         'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
 451:         'ads/sandbox/accounts/:account_id/cards/lead_gen',
 452:         'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
 453:         'ads/sandbox/accounts/:account_id/cards/video_app_download',
 454:         'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
 455:         'ads/sandbox/accounts/:account_id/cards/video_conversation',
 456:         'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
 457:         'ads/sandbox/accounts/:account_id/cards/website',
 458:         'ads/sandbox/accounts/:account_id/cards/website/:card_id',
 459:         'ads/sandbox/accounts/:account_id/features',
 460:         'ads/sandbox/accounts/:account_id/funding_instruments',
 461:         'ads/sandbox/accounts/:account_id/funding_instruments/:id',
 462:         'ads/sandbox/accounts/:account_id/line_items',
 463:         'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
 464:         'ads/sandbox/accounts/:account_id/promotable_users',
 465:         'ads/sandbox/accounts/:account_id/promoted_accounts',
 466:         'ads/sandbox/accounts/:account_id/promoted_tweets',
 467:         'ads/sandbox/accounts/:account_id/reach_estimate',
 468:         'ads/sandbox/accounts/:account_id/scoped_timeline',
 469:         'ads/sandbox/accounts/:account_id/tailored_audience_changes',
 470:         'ads/sandbox/accounts/:account_id/tailored_audience_changes/:id',
 471:         'ads/sandbox/accounts/:account_id/tailored_audiences',
 472:         'ads/sandbox/accounts/:account_id/tailored_audiences/:id',
 473:         'ads/sandbox/accounts/:account_id/targeting_criteria',
 474:         'ads/sandbox/accounts/:account_id/targeting_criteria/:id',
 475:         'ads/sandbox/accounts/:account_id/targeting_suggestions',
 476:         'ads/sandbox/accounts/:account_id/tweet/preview',
 477:         'ads/sandbox/accounts/:account_id/tweet/preview/:tweet_id',
 478:         'ads/sandbox/accounts/:account_id/videos',
 479:         'ads/sandbox/accounts/:account_id/videos/:id',
 480:         'ads/sandbox/accounts/:account_id/web_event_tags',
 481:         'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id',
 482:         'ads/sandbox/bidding_rules',
 483:         'ads/sandbox/iab_categories',
 484:         'ads/sandbox/insights/accounts/:account_id',
 485:         'ads/sandbox/insights/accounts/:account_id/available_audiences',
 486:         'ads/sandbox/line_items/placements',
 487:         'ads/sandbox/stats/accounts/:account_id',
 488:         'ads/sandbox/stats/accounts/:account_id/campaigns',
 489:         'ads/sandbox/stats/accounts/:account_id/campaigns/:id',
 490:         'ads/sandbox/stats/accounts/:account_id/funding_instruments',
 491:         'ads/sandbox/stats/accounts/:account_id/funding_instruments/:id',
 492:         'ads/sandbox/stats/accounts/:account_id/line_items',
 493:         'ads/sandbox/stats/accounts/:account_id/line_items/:id',
 494:         'ads/sandbox/stats/accounts/:account_id/promoted_accounts',
 495:         'ads/sandbox/stats/accounts/:account_id/promoted_accounts/:id',
 496:         'ads/sandbox/stats/accounts/:account_id/promoted_tweets',
 497:         'ads/sandbox/stats/accounts/:account_id/promoted_tweets/:id',
 498:         'ads/sandbox/stats/accounts/:account_id/reach/campaigns',
 499:         'ads/sandbox/targeting_criteria/app_store_categories',
 500:         'ads/sandbox/targeting_criteria/behavior_taxonomies',
 501:         'ads/sandbox/targeting_criteria/behaviors',
 502:         'ads/sandbox/targeting_criteria/devices',
 503:         'ads/sandbox/targeting_criteria/events',
 504:         'ads/sandbox/targeting_criteria/interests',
 505:         'ads/sandbox/targeting_criteria/languages',
 506:         'ads/sandbox/targeting_criteria/locations',
 507:         'ads/sandbox/targeting_criteria/network_operators',
 508:         'ads/sandbox/targeting_criteria/platform_versions',
 509:         'ads/sandbox/targeting_criteria/platforms',
 510:         'ads/sandbox/targeting_criteria/tv_channels',
 511:         'ads/sandbox/targeting_criteria/tv_genres',
 512:         'ads/sandbox/targeting_criteria/tv_markets',
 513:         'ads/sandbox/targeting_criteria/tv_shows',
 514:         'ads/stats/accounts/:account_id',
 515:         'ads/stats/accounts/:account_id/campaigns',
 516:         'ads/stats/accounts/:account_id/campaigns/:id',
 517:         'ads/stats/accounts/:account_id/funding_instruments',
 518:         'ads/stats/accounts/:account_id/funding_instruments/:id',
 519:         'ads/stats/accounts/:account_id/line_items',
 520:         'ads/stats/accounts/:account_id/line_items/:id',
 521:         'ads/stats/accounts/:account_id/promoted_accounts',
 522:         'ads/stats/accounts/:account_id/promoted_accounts/:id',
 523:         'ads/stats/accounts/:account_id/promoted_tweets',
 524:         'ads/stats/accounts/:account_id/promoted_tweets/:id',
 525:         'ads/stats/accounts/:account_id/reach/campaigns',
 526:         'ads/targeting_criteria/app_store_categories',
 527:         'ads/targeting_criteria/behavior_taxonomies',
 528:         'ads/targeting_criteria/behaviors',
 529:         'ads/targeting_criteria/devices',
 530:         'ads/targeting_criteria/events',
 531:         'ads/targeting_criteria/interests',
 532:         'ads/targeting_criteria/languages',
 533:         'ads/targeting_criteria/locations',
 534:         'ads/targeting_criteria/network_operators',
 535:         'ads/targeting_criteria/platform_versions',
 536:         'ads/targeting_criteria/platforms',
 537:         'ads/targeting_criteria/tv_channels',
 538:         'ads/targeting_criteria/tv_genres',
 539:         'ads/targeting_criteria/tv_markets',
 540:         'ads/targeting_criteria/tv_shows',
 541:         'application/rate_limit_status',
 542:         'blocks/ids',
 543:         'blocks/list',
 544:         'collections/entries',
 545:         'collections/list',
 546:         'collections/show',
 547:         'direct_messages',
 548:         'direct_messages/sent',
 549:         'direct_messages/show',
 550:         'favorites/list',
 551:         'followers/ids',
 552:         'followers/list',
 553:         'friends/ids',
 554:         'friends/list',
 555:         'friendships/incoming',
 556:         'friendships/lookup',
 557:         'friendships/lookup',
 558:         'friendships/no_retweets/ids',
 559:         'friendships/outgoing',
 560:         'friendships/show',
 561:         'geo/id/:place_id',
 562:         'geo/reverse_geocode',
 563:         'geo/search',
 564:         'geo/similar_places',
 565:         'help/configuration',
 566:         'help/languages',
 567:         'help/privacy',
 568:         'help/tos',
 569:         'lists/list',
 570:         'lists/members',
 571:         'lists/members/show',
 572:         'lists/memberships',
 573:         'lists/ownerships',
 574:         'lists/show',
 575:         'lists/statuses',
 576:         'lists/subscribers',
 577:         'lists/subscribers/show',
 578:         'lists/subscriptions',
 579:         'mutes/users/ids',
 580:         'mutes/users/list',
 581:         'oauth/authenticate',
 582:         'oauth/authorize',
 583:         'saved_searches/list',
 584:         'saved_searches/show/:id',
 585:         'search/tweets',
 586:         'site',
 587:         'statuses/firehose',
 588:         'statuses/home_timeline',
 589:         'statuses/mentions_timeline',
 590:         'statuses/oembed',
 591:         'statuses/retweeters/ids',
 592:         'statuses/retweets/:id',
 593:         'statuses/retweets_of_me',
 594:         'statuses/sample',
 595:         'statuses/show/:id',
 596:         'statuses/user_timeline',
 597:         'trends/available',
 598:         'trends/closest',
 599:         'trends/place',
 600:         'user',
 601:         'users/contributees',
 602:         'users/contributors',
 603:         'users/profile_banner',
 604:         'users/search',
 605:         'users/show',
 606:         'users/suggestions',
 607:         'users/suggestions/:slug',
 608:         'users/suggestions/:slug/members'
 609:       ],
 610:       'POST' => [
 611:         'account/remove_profile_banner',
 612:         'account/settings',
 613:         'account/update_delivery_device',
 614:         'account/update_profile',
 615:         'account/update_profile_background_image',
 616:         'account/update_profile_banner',
 617:         'account/update_profile_colors',
 618:         'account/update_profile_image',
 619:         'ads/accounts/:account_id/app_lists',
 620:         'ads/accounts/:account_id/campaigns',
 621:         'ads/accounts/:account_id/cards/app_download',
 622:         'ads/accounts/:account_id/cards/image_app_download',
 623:         'ads/accounts/:account_id/cards/image_conversation',
 624:         'ads/accounts/:account_id/cards/lead_gen',
 625:         'ads/accounts/:account_id/cards/video_app_download',
 626:         'ads/accounts/:account_id/cards/video_conversation',
 627:         'ads/accounts/:account_id/cards/website',
 628:         'ads/accounts/:account_id/line_items',
 629:         'ads/accounts/:account_id/promoted_accounts',
 630:         'ads/accounts/:account_id/promoted_tweets',
 631:         'ads/accounts/:account_id/tailored_audience_changes',
 632:         'ads/accounts/:account_id/tailored_audiences',
 633:         'ads/accounts/:account_id/targeting_criteria',
 634:         'ads/accounts/:account_id/tweet',
 635:         'ads/accounts/:account_id/videos',
 636:         'ads/accounts/:account_id/web_event_tags',
 637:         'ads/batch/accounts/:account_id/campaigns',
 638:         'ads/batch/accounts/:account_id/line_items',
 639:         'ads/sandbox/accounts/:account_id/app_lists',
 640:         'ads/sandbox/accounts/:account_id/campaigns',
 641:         'ads/sandbox/accounts/:account_id/cards/app_download',
 642:         'ads/sandbox/accounts/:account_id/cards/image_app_download',
 643:         'ads/sandbox/accounts/:account_id/cards/image_conversation',
 644:         'ads/sandbox/accounts/:account_id/cards/lead_gen',
 645:         'ads/sandbox/accounts/:account_id/cards/video_app_download',
 646:         'ads/sandbox/accounts/:account_id/cards/video_conversation',
 647:         'ads/sandbox/accounts/:account_id/cards/website',
 648:         'ads/sandbox/accounts/:account_id/line_items',
 649:         'ads/sandbox/accounts/:account_id/promoted_accounts',
 650:         'ads/sandbox/accounts/:account_id/promoted_tweets',
 651:         'ads/sandbox/accounts/:account_id/tailored_audience_changes',
 652:         'ads/sandbox/accounts/:account_id/tailored_audiences',
 653:         'ads/sandbox/accounts/:account_id/targeting_criteria',
 654:         'ads/sandbox/accounts/:account_id/tweet',
 655:         'ads/sandbox/accounts/:account_id/videos',
 656:         'ads/sandbox/accounts/:account_id/web_event_tags',
 657:         'ads/sandbox/batch/accounts/:account_id/campaigns',
 658:         'ads/sandbox/batch/accounts/:account_id/line_items',
 659:         'blocks/create',
 660:         'blocks/destroy',
 661:         'collections/create',
 662:         'collections/destroy',
 663:         'collections/entries/add',
 664:         'collections/entries/curate',
 665:         'collections/entries/move',
 666:         'collections/entries/remove',
 667:         'collections/update',
 668:         'direct_messages/destroy',
 669:         'direct_messages/new',
 670:         'favorites/create',
 671:         'favorites/destroy',
 672:         'friendships/create',
 673:         'friendships/destroy',
 674:         'friendships/update',
 675:         'lists/create',
 676:         'lists/destroy',
 677:         'lists/members/create',
 678:         'lists/members/create_all',
 679:         'lists/members/destroy',
 680:         'lists/members/destroy_all',
 681:         'lists/subscribers/create',
 682:         'lists/subscribers/destroy',
 683:         'lists/update',
 684:         'media/upload',
 685:         'mutes/users/create',
 686:         'mutes/users/destroy',
 687:         'oauth/access_token',
 688:         'oauth/request_token',
 689:         'oauth2/invalidate_token',
 690:         'oauth2/token',
 691:         'saved_searches/create',
 692:         'saved_searches/destroy/:id',
 693:         'statuses/destroy/:id',
 694:         'statuses/filter',
 695:         'statuses/lookup',
 696:         'statuses/retweet/:id',
 697:         'statuses/update',
 698:         'statuses/update_with_media', // deprecated, use media/upload
 699:         'ton/bucket/:bucket',
 700:         'ton/bucket/:bucket?resumable=true',
 701:         'users/lookup',
 702:         'users/report_spam'
 703:       ],
 704:       'PUT' => [
 705:         'ads/accounts/:account_id/campaigns/:campaign_id',
 706:         'ads/accounts/:account_id/cards/app_download/:card_id',
 707:         'ads/accounts/:account_id/cards/image_app_download/:card_id',
 708:         'ads/accounts/:account_id/cards/image_conversation/:card_id',
 709:         'ads/accounts/:account_id/cards/lead_gen/:card_id',
 710:         'ads/accounts/:account_id/cards/video_app_download/:id',
 711:         'ads/accounts/:account_id/cards/video_conversation/:card_id',
 712:         'ads/accounts/:account_id/cards/website/:card_id',
 713:         'ads/accounts/:account_id/line_items/:line_item_id',
 714:         'ads/accounts/:account_id/promoted_tweets/:id',
 715:         'ads/accounts/:account_id/tailored_audiences/global_opt_out',
 716:         'ads/accounts/:account_id/targeting_criteria',
 717:         'ads/accounts/:account_id/videos/:id',
 718:         'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
 719:         'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
 720:         'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
 721:         'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
 722:         'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
 723:         'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
 724:         'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
 725:         'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
 726:         'ads/sandbox/accounts/:account_id/cards/website/:card_id',
 727:         'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
 728:         'ads/sandbox/accounts/:account_id/promoted_tweets/:id',
 729:         'ads/sandbox/accounts/:account_id/tailored_audiences/global_opt_out',
 730:         'ads/sandbox/accounts/:account_id/targeting_criteria',
 731:         'ads/sandbox/accounts/:account_id/videos/:id',
 732:         'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id',
 733:         'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
 734:       ],
 735:       'DELETE' => [
 736:         'ads/accounts/:account_id/campaigns/:campaign_id',
 737:         'ads/accounts/:account_id/cards/app_download/:card_id',
 738:         'ads/accounts/:account_id/cards/image_app_download/:card_id',
 739:         'ads/accounts/:account_id/cards/image_conversation/:card_id',
 740:         'ads/accounts/:account_id/cards/lead_gen/:card_id',
 741:         'ads/accounts/:account_id/cards/video_app_download/:id',
 742:         'ads/accounts/:account_id/cards/video_conversation/:card_id',
 743:         'ads/accounts/:account_id/cards/website/:card_id',
 744:         'ads/accounts/:account_id/line_items/:line_item_id',
 745:         'ads/accounts/:account_id/promoted_tweets/:id',
 746:         'ads/accounts/:account_id/tailored_audiences/:id',
 747:         'ads/accounts/:account_id/targeting_criteria/:id',
 748:         'ads/accounts/:account_id/videos/:id',
 749:         'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
 750:         'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
 751:         'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
 752:         'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
 753:         'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
 754:         'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
 755:         'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
 756:         'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
 757:         'ads/sandbox/accounts/:account_id/cards/website/:card_id',
 758:         'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
 759:         'ads/sandbox/accounts/:account_id/promoted_tweets/:id',
 760:         'ads/sandbox/accounts/:account_id/tailored_audiences/:id',
 761:         'ads/sandbox/accounts/:account_id/targeting_criteria/:id',
 762:         'ads/sandbox/accounts/:account_id/videos/:id',
 763:         'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id'
 764:       ]
 765:     ];
 766:     return $httpmethods;
 767:   }
 768: 
 769:   /**
 770:    * Main API handler working on any requests you issue
 771:    *
 772:    * @param string $fn    The member function you called
 773:    * @param array $params The parameters you sent along
 774:    *
 775:    * @return string The API reply encoded in the set return_format
 776:    */
 777: 
 778:   public function __call($fn, $params)
 779:   {
 780:     // parse parameters
 781:     $apiparams = $this->_parseApiParams($params);
 782: 
 783:     // stringify null and boolean parameters
 784:     $apiparams = $this->_stringifyNullBoolParams($apiparams);
 785: 
 786:     $app_only_auth = false;
 787:     if (count($params) > 1) {
 788:       // convert app_only_auth param to bool
 789:       $app_only_auth = !! $params[1];
 790:     }
 791: 
 792:     // reset token when requesting a new token
 793:     // (causes 401 for signature error on subsequent requests)
 794:     if ($fn === 'oauth_requestToken') {
 795:       $this->setToken(null, null);
 796:     }
 797: 
 798:     // map function name to API method
 799:     list($method, $method_template) = $this->_mapFnToApiMethod($fn, $apiparams);
 800: 
 801:     $httpmethod = $this->_detectMethod($method_template, $apiparams);
 802:     $multipart  = $this->_detectMultipart($method_template);
 803: 
 804:     return $this->_callApi(
 805:       $httpmethod,
 806:       $method,
 807:       $method_template,
 808:       $apiparams,
 809:       $multipart,
 810:       $app_only_auth
 811:     );
 812:   }
 813: 
 814: 
 815:   /**
 816:    * __call() helpers
 817:    */
 818: 
 819:   /**
 820:    * Parse given params, detect query-style params
 821:    *
 822:    * @param array|string $params Parameters to parse
 823:    *
 824:    * @return array $apiparams
 825:    */
 826:   protected function _parseApiParams($params)
 827:   {
 828:     $apiparams = [];
 829:     if (count($params) === 0) {
 830:       return $apiparams;
 831:     }
 832: 
 833:     if (is_array($params[0])) {
 834:       // given parameters are array
 835:       $apiparams = $params[0];
 836:       return $apiparams;
 837:     }
 838: 
 839:     // user gave us query-style params
 840:     parse_str($params[0], $apiparams);
 841:     if (! is_array($apiparams)) {
 842:       $apiparams = [];
 843:     }
 844: 
 845:     return $apiparams;
 846:   }
 847: 
 848:   /**
 849:    * Replace null and boolean parameters with their string representations
 850:    *
 851:    * @param array $apiparams Parameter array to replace in
 852:    *
 853:    * @return array $apiparams
 854:    */
 855:   protected function _stringifyNullBoolParams($apiparams)
 856:   {
 857:     foreach ($apiparams as $key => $value) {
 858:       if (! is_scalar($value)) {
 859:         // no need to try replacing arrays
 860:         continue;
 861:       }
 862:       if (is_null($value)) {
 863:         $apiparams[$key] = 'null';
 864:       } elseif (is_bool($value)) {
 865:         $apiparams[$key] = $value ? 'true' : 'false';
 866:       }
 867:     }
 868: 
 869:     return $apiparams;
 870:   }
 871: 
 872:   /**
 873:    * Maps called PHP magic method name to Twitter API method
 874:    *
 875:    * @param string $fn              Function called
 876:    * @param array  $apiparams byref API parameters
 877:    *
 878:    * @return string[] (string method, string method_template)
 879:    */
 880:   protected function _mapFnToApiMethod($fn, &$apiparams)
 881:   {
 882:     // replace _ by /
 883:     $method = $this->_mapFnInsertSlashes($fn);
 884: 
 885:     // undo replacement for URL parameters
 886:     $method = $this->_mapFnRestoreParamUnderscores($method);
 887: 
 888:     // replace AA by URL parameters
 889:     $method_template = $method;
 890:     $match           = [];
 891:     if (preg_match_all('/[A-Z_]{2,}/', $method, $match)) {
 892:       foreach ($match[0] as $param) {
 893:         $param_l = strtolower($param);
 894:         if ($param_l === 'resumeid') {
 895:           $param_l = 'resumeId';
 896:         }
 897:         $method_template = str_replace($param, ':' . $param_l, $method_template);
 898:         if (! isset($apiparams[$param_l])) {
 899:           for ($i = 0; $i < 26; $i++) {
 900:             $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
 901:           }
 902:           throw new \Exception(
 903:             'To call the templated method "' . $method_template
 904:             . '", specify the parameter value for "' . $param_l . '".'
 905:           );
 906:         }
 907:         $method  = str_replace($param, $apiparams[$param_l], $method);
 908:         unset($apiparams[$param_l]);
 909:       }
 910:     }
 911: 
 912:     if (substr($method, 0, 4) !== 'ton/') {
 913:       // replace A-Z by _a-z
 914:       for ($i = 0; $i < 26; $i++) {
 915:         $method  = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method);
 916:         $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
 917:       }
 918:     }
 919: 
 920:     return [$method, $method_template];
 921:   }
 922: 
 923:   /**
 924:    * API method mapping: Replaces _ with / character
 925:    *
 926:    * @param string $fn Function called
 927:    *
 928:    * @return string API method to call
 929:    */
 930:   protected function _mapFnInsertSlashes($fn)
 931:   {
 932:     $path   = explode('_', $fn);
 933:     $method = implode('/', $path);
 934: 
 935:     return $method;
 936:   }
 937: 
 938:   /**
 939:    * API method mapping: Restore _ character in named parameters
 940:    *
 941:    * @param string $method API method to call
 942:    *
 943:    * @return string API method with restored underscores
 944:    */
 945:   protected function _mapFnRestoreParamUnderscores($method)
 946:   {
 947:     $url_parameters_with_underscore = [
 948:       'screen_name', 'place_id',
 949:       'account_id', 'campaign_id', 'card_id', 'line_item_id',
 950:       'tweet_id', 'web_event_tag_id'
 951:     ];
 952:     foreach ($url_parameters_with_underscore as $param) {
 953:       $param = strtoupper($param);
 954:       $replacement_was = str_replace('_', '/', $param);
 955:       $method = str_replace($replacement_was, $param, $method);
 956:     }
 957: 
 958:     return $method;
 959:   }
 960: 
 961: 
 962:   /**
 963:    * Uncommon API methods
 964:    */
 965: 
 966:   /**
 967:    * Gets the OAuth authenticate URL for the current request token
 968:    *
 969:    * @param optional bool   $force_login Whether to force the user to enter their login data
 970:    * @param optional string $screen_name Screen name to repopulate the user name with
 971:    * @param optional string $type        'authenticate' or 'authorize', to avoid duplicate code
 972:    *
 973:    * @return string The OAuth authenticate/authorize URL
 974:    */
 975:   public function oauth_authenticate($force_login = NULL, $screen_name = NULL, $type = 'authenticate')
 976:   {
 977:     if (! in_array($type, ['authenticate', 'authorize'])) {
 978:       throw new \Exception('To get the ' . $type . ' URL, use the correct third parameter, or omit it.');
 979:     }
 980:     if ($this->_oauth_token === null) {
 981:       throw new \Exception('To get the ' . $type . ' URL, the OAuth token must be set.');
 982:     }
 983:     $url = self::$_endpoint_oauth . 'oauth/' . $type . '?oauth_token=' . $this->_url($this->_oauth_token);
 984:     if ($force_login) {
 985:       $url .= "&force_login=1";
 986:     }
 987:     if ($screen_name) {
 988:       $url .= "&screen_name=" . $screen_name;
 989:     }
 990:     return $url;
 991:   }
 992: 
 993:   /**
 994:    * Gets the OAuth authorize URL for the current request token
 995:    * @param optional bool   $force_login Whether to force the user to enter their login data
 996:    * @param optional string $screen_name Screen name to repopulate the user name with
 997:    *
 998:    * @return string The OAuth authorize URL
 999:    */
1000:   public function oauth_authorize($force_login = NULL, $screen_name = NULL)
1001:   {
1002:     return $this->oauth_authenticate($force_login, $screen_name, 'authorize');
1003:   }
1004: 
1005:   /**
1006:    * Gets the OAuth bearer token
1007:    *
1008:    * @return string The OAuth bearer token
1009:    */
1010: 
1011:   public function oauth2_token()
1012:   {
1013:     if ($this->_use_curl) {
1014:       return $this->_oauth2TokenCurl();
1015:     }
1016:     return $this->_oauth2TokenNoCurl();
1017:   }
1018: 
1019:   /**
1020:    * Gets a cURL handle
1021:    * @param string $url the URL for the curl initialization
1022:    * @return resource handle
1023:    */
1024:   protected function getCurlInitialization($url)
1025:   {
1026:     $ch = curl_init($url);
1027: 
1028:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1029:     curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
1030:     curl_setopt($ch, CURLOPT_HEADER, 1);
1031:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
1032:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
1033:     curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem');
1034:     curl_setopt(
1035:       $ch, CURLOPT_USERAGENT,
1036:       'codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1037:     );
1038: 
1039:     if ($this->hasProxy()) {
1040:       curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
1041:       curl_setopt($ch, CURLOPT_PROXY, $this->getProxyHost());
1042:       curl_setopt($ch, CURLOPT_PROXYPORT, $this->getProxyPort());
1043: 
1044:       if ($this->hasProxyAuthentication()) {
1045:         curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
1046:         curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->getProxyAuthentication());
1047:       }
1048:     }
1049: 
1050:     return $ch;
1051:   }
1052: 
1053:   /**
1054:    * Gets a non cURL initialization
1055:    *
1056:    * @param string $url            the URL for the curl initialization
1057:    * @param array  $contextOptions the options for the stream context
1058:    * @param string $hostname       the hostname to verify the SSL FQDN for
1059:    *
1060:    * @return array the read data
1061:    */
1062:   protected function getNoCurlInitialization($url, $contextOptions, $hostname = '')
1063:   {
1064:     $httpOptions = [];
1065: 
1066:     $httpOptions['header'] = [
1067:       'User-Agent: codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1068:     ];
1069: 
1070:     $httpOptions['ssl'] = [
1071:       'verify_peer'  => true,
1072:       'cafile'       => __DIR__ . '/cacert.pem',
1073:       'verify_depth' => 5,
1074:       'peer_name'    => $hostname
1075:     ];
1076: 
1077:     if ($this->hasProxy()) {
1078:       $httpOptions['request_fulluri'] = true;
1079:       $httpOptions['proxy'] = $this->getProxyHost() . ':' . $this->getProxyPort();
1080: 
1081:       if ($this->hasProxyAuthentication()) {
1082:         $httpOptions['header'][] =
1083:           'Proxy-Authorization: Basic ' . base64_encode($this->getProxyAuthentication());
1084:       }
1085:     }
1086: 
1087:     // merge the http options with the context options
1088:     $options = array_merge_recursive(
1089:       $contextOptions,
1090:       ['http' => $httpOptions]
1091:     );
1092: 
1093:     // concatenate $options['http']['header']
1094:     $options['http']['header'] = implode("\r\n", $options['http']['header']);
1095: 
1096:     // silent the file_get_contents function
1097:     $content = @file_get_contents($url, false, stream_context_create($options));
1098: 
1099:     $headers = [];
1100:     // API is responding
1101:     if (isset($http_response_header)) {
1102:       $headers = $http_response_header;
1103:     }
1104: 
1105:     return [
1106:       $content,
1107:       $headers
1108:     ];
1109:   }
1110: 
1111:   protected function hasProxy()
1112:   {
1113:     if ($this->getProxyHost() === null) {
1114:       return false;
1115:     }
1116: 
1117:     if ($this->getProxyPort() === null) {
1118:       return false;
1119:     }
1120: 
1121:     return true;
1122:   }
1123: 
1124:   protected function hasProxyAuthentication()
1125:   {
1126:     if ($this->getProxyAuthentication() === null) {
1127:       return false;
1128:     }
1129: 
1130:     return true;
1131:   }
1132: 
1133:   /**
1134:    * Gets the proxy host
1135:    *
1136:    * @return string The proxy host
1137:    */
1138:   protected function getProxyHost()
1139:   {
1140:     return $this->getProxyData('host');
1141:   }
1142: 
1143:   /**
1144:    * Gets the proxy port
1145:    *
1146:    * @return string The proxy port
1147:    */
1148:   protected function getProxyPort()
1149:   {
1150:     return $this->getProxyData('port');
1151:   }
1152: 
1153:   /**
1154:    * Gets the proxy authentication
1155:    *
1156:    * @return string The proxy authentication
1157:    */
1158:   protected function getProxyAuthentication()
1159:   {
1160:     return $this->getProxyData('authentication');
1161:   }
1162: 
1163:   /**
1164:    * @param string $name
1165:    */
1166:   private function getProxyData($name)
1167:   {
1168:     if (empty($this->_proxy[$name])) {
1169:       return null;
1170:     }
1171: 
1172:     return $this->_proxy[$name];
1173:   }
1174: 
1175:   /**
1176:    * Gets the OAuth bearer token, using cURL
1177:    *
1178:    * @return string The OAuth bearer token
1179:    */
1180: 
1181:   protected function _oauth2TokenCurl()
1182:   {
1183:     if (self::$_oauth_consumer_key === null) {
1184:       throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1185:     }
1186:     $post_fields = [
1187:       'grant_type' => 'client_credentials'
1188:     ];
1189:     $url = self::$_endpoint_oauth . 'oauth2/token';
1190:     $ch = $this->getCurlInitialization($url);
1191:     curl_setopt($ch, CURLOPT_POST, 1);
1192:     curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
1193: 
1194:     curl_setopt($ch, CURLOPT_USERPWD, self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret);
1195:     curl_setopt($ch, CURLOPT_HTTPHEADER, [
1196:       'Expect:'
1197:     ]);
1198:     $result = curl_exec($ch);
1199: 
1200:     // catch request errors
1201:     if ($result === false) {
1202:       throw new \Exception('Request error for bearer token: ' . curl_error($ch));
1203:     }
1204: 
1205:     // certificate validation results
1206:     $validation_result = curl_errno($ch);
1207:     $this->_validateSslCertificate($validation_result);
1208: 
1209:     $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1210:     $reply = $this->_parseBearerReply($result, $httpstatus);
1211:     return $reply;
1212:   }
1213: 
1214:   /**
1215:    * Gets the OAuth bearer token, without cURL
1216:    *
1217:    * @return string The OAuth bearer token
1218:    */
1219: 
1220:   protected function _oauth2TokenNoCurl()
1221:   {
1222:     if (self::$_oauth_consumer_key == null) {
1223:       throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1224:     }
1225: 
1226:     $url      = self::$_endpoint_oauth . 'oauth2/token';
1227:     $hostname = parse_url($url, PHP_URL_HOST);
1228: 
1229:     if ($hostname === false) {
1230:       throw new \Exception('Incorrect API endpoint host.');
1231:     }
1232: 
1233:     $contextOptions = [
1234:       'http' => [
1235:         'method'           => 'POST',
1236:         'protocol_version' => '1.1',
1237:         'header'           => "Accept: */*\r\n"
1238:           . 'Authorization: Basic '
1239:           . base64_encode(
1240:             self::$_oauth_consumer_key
1241:             . ':'
1242:             . self::$_oauth_consumer_secret
1243:           ),
1244:         'timeout'          => $this->_timeout / 1000,
1245:         'content'          => 'grant_type=client_credentials',
1246:         'ignore_errors'    => true
1247:       ]
1248:     ];
1249:     list($reply, $headers) = $this->getNoCurlInitialization($url, $contextOptions, $hostname);
1250:     $result  = '';
1251:     foreach ($headers as $header) {
1252:       $result .= $header . "\r\n";
1253:     }
1254:     $result .= "\r\n" . $reply;
1255: 
1256:     // find HTTP status
1257:     $httpstatus = '500';
1258:     $match      = [];
1259:     if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
1260:       $httpstatus = $match[1];
1261:     }
1262: 
1263:     $reply = $this->_parseBearerReply($result, $httpstatus);
1264:     return $reply;
1265:   }
1266: 
1267: 
1268:   /**
1269:    * General helpers to avoid duplicate code
1270:    */
1271: 
1272:   /**
1273:    * Parse oauth2_token reply and set bearer token, if found
1274:    *
1275:    * @param string $result     Raw HTTP response
1276:    * @param int    $httpstatus HTTP status code
1277:    *
1278:    * @return string reply
1279:    */
1280:   protected function _parseBearerReply($result, $httpstatus)
1281:   {
1282:     list($headers, $reply) = $this->_parseApiHeaders($result);
1283:     $reply                 = $this->_parseApiReply($reply);
1284:     $rate                  = $this->_getRateLimitInfo($headers);
1285:     switch ($this->_return_format) {
1286:       case CODEBIRD_RETURNFORMAT_ARRAY:
1287:         $reply['httpstatus'] = $httpstatus;
1288:         $reply['rate']       = $rate;
1289:         if ($httpstatus === 200) {
1290:           self::setBearerToken($reply['access_token']);
1291:         }
1292:         break;
1293:       case CODEBIRD_RETURNFORMAT_JSON:
1294:         if ($httpstatus === 200) {
1295:           $parsed = json_decode($reply, false, 512, JSON_BIGINT_AS_STRING);
1296:           self::setBearerToken($parsed->access_token);
1297:         }
1298:         break;
1299:       case CODEBIRD_RETURNFORMAT_OBJECT:
1300:         $reply->httpstatus = $httpstatus;
1301:         $reply->rate       = $rate;
1302:         if ($httpstatus === 200) {
1303:           self::setBearerToken($reply->access_token);
1304:         }
1305:         break;
1306:     }
1307:     return $reply;
1308:   }
1309: 
1310:   /**
1311:    * Extract rate-limiting data from response headers
1312:    *
1313:    * @param array $headers The CURL response headers
1314:    *
1315:    * @return null|array The rate-limiting information
1316:    */
1317:   protected function _getRateLimitInfo($headers)
1318:   {
1319:     if (! isset($headers['x-rate-limit-limit'])) {
1320:       return null;
1321:     }
1322:     return [
1323:       'limit'     => $headers['x-rate-limit-limit'],
1324:       'remaining' => $headers['x-rate-limit-remaining'],
1325:       'reset'     => $headers['x-rate-limit-reset']
1326:     ];
1327:   }
1328: 
1329:   /**
1330:    * Check if there were any SSL certificate errors
1331:    *
1332:    * @param int $validation_result The curl error number
1333:    *
1334:    * @return void
1335:    */
1336:   protected function _validateSslCertificate($validation_result)
1337:   {
1338:     if (in_array(
1339:         $validation_result,
1340:         [
1341:           CURLE_SSL_CERTPROBLEM,
1342:           CURLE_SSL_CACERT,
1343:           CURLE_SSL_CACERT_BADFILE,
1344:           CURLE_SSL_CRL_BADFILE,
1345:           CURLE_SSL_ISSUER_ERROR
1346:         ]
1347:       )
1348:     ) {
1349:       throw new \Exception(
1350:         'Error ' . $validation_result
1351:         . ' while validating the Twitter API certificate.'
1352:       );
1353:     }
1354:   }
1355: 
1356:   /**
1357:    * Signing helpers
1358:    */
1359: 
1360:   /**
1361:    * URL-encodes the given data
1362:    *
1363:    * @param mixed $data
1364:    *
1365:    * @return mixed The encoded data
1366:    */
1367:   protected function _url($data)
1368:   {
1369:     if (is_array($data)) {
1370:       return array_map([
1371:         $this,
1372:         '_url'
1373:       ], $data);
1374:     } elseif (is_scalar($data)) {
1375:       return str_replace([
1376:         '+',
1377:         '!',
1378:         '*',
1379:         "'",
1380:         '(',
1381:         ')'
1382:       ], [
1383:         ' ',
1384:         '%21',
1385:         '%2A',
1386:         '%27',
1387:         '%28',
1388:         '%29'
1389:       ], rawurlencode($data));
1390:     } else {
1391:       return '';
1392:     }
1393:   }
1394: 
1395:   /**
1396:    * Gets the base64-encoded SHA1 hash for the given data
1397:    *
1398:    * @param string $data The data to calculate the hash from
1399:    *
1400:    * @return string The hash
1401:    */
1402:   protected function _sha1($data)
1403:   {
1404:     if (self::$_oauth_consumer_secret === null) {
1405:       throw new \Exception('To generate a hash, the consumer secret must be set.');
1406:     }
1407:     if (!function_exists('hash_hmac')) {
1408:       throw new \Exception('To generate a hash, the PHP hash extension must be available.');
1409:     }
1410:     return base64_encode(hash_hmac(
1411:       'sha1',
1412:       $data,
1413:       self::$_oauth_consumer_secret
1414:       . '&'
1415:       . ($this->_oauth_token_secret !== null
1416:         ? $this->_oauth_token_secret
1417:         : ''
1418:       ),
1419:       true
1420:     ));
1421:   }
1422: 
1423:   /**
1424:    * Generates a (hopefully) unique random string
1425:    *
1426:    * @param int optional $length The length of the string to generate
1427:    *
1428:    * @return string The random string
1429:    */
1430:   protected function _nonce($length = 8)
1431:   {
1432:     if ($length < 1) {
1433:       throw new \Exception('Invalid nonce length.');
1434:     }
1435:     return substr(md5(microtime(true)), 0, $length);
1436:   }
1437: 
1438:   /**
1439:    * Signature helper
1440:    *
1441:    * @param string $httpmethod   Usually either 'GET' or 'POST' or 'DELETE'
1442:    * @param string $method       The API method to call
1443:    * @param array  $base_params  The signature base parameters
1444:    *
1445:    * @return string signature
1446:    */
1447:   protected function _getSignature($httpmethod, $method, $base_params)
1448:   {
1449:     // convert params to string
1450:     $base_string = '';
1451:     foreach ($base_params as $key => $value) {
1452:       $base_string .= $key . '=' . $value . '&';
1453:     }
1454: 
1455:     // trim last ampersand
1456:     $base_string = substr($base_string, 0, -1);
1457: 
1458:     // hash it
1459:     return $this->_sha1(
1460:       $httpmethod . '&' .
1461:       $this->_url($method) . '&' .
1462:       $this->_url($base_string)
1463:     );
1464:   }
1465: 
1466:   /**
1467:    * Generates an OAuth signature
1468:    *
1469:    * @param string          $httpmethod   Usually either 'GET' or 'POST' or 'DELETE'
1470:    * @param string          $method       The API method to call
1471:    * @param array  optional $params       The API call parameters, associative
1472:    * @param bool   optional append_to_get Whether to append the OAuth params to GET
1473:    *
1474:    * @return string Authorization HTTP header
1475:    */
1476:   protected function _sign($httpmethod, $method, $params = [], $append_to_get = false)
1477:   {
1478:     if (self::$_oauth_consumer_key === null) {
1479:       throw new \Exception('To generate a signature, the consumer key must be set.');
1480:     }
1481:     $sign_base_params = array_map(
1482:       [$this, '_url'],
1483:       [
1484:         'oauth_consumer_key'     => self::$_oauth_consumer_key,
1485:         'oauth_version'          => '1.0',
1486:         'oauth_timestamp'        => time(),
1487:         'oauth_nonce'            => $this->_nonce(),
1488:         'oauth_signature_method' => 'HMAC-SHA1'
1489:       ]
1490:     );
1491:     if ($this->_oauth_token !== null) {
1492:       $sign_base_params['oauth_token'] = $this->_url($this->_oauth_token);
1493:     }
1494:     $oauth_params = $sign_base_params;
1495: 
1496:     // merge in the non-OAuth params
1497:     $sign_base_params = array_merge(
1498:       $sign_base_params,
1499:       array_map([$this, '_url'], $params)
1500:     );
1501:     ksort($sign_base_params);
1502: 
1503:     $signature = $this->_getSignature($httpmethod, $method, $sign_base_params);
1504: 
1505:     $params = $append_to_get ? $sign_base_params : $oauth_params;
1506:     $params['oauth_signature'] = $signature;
1507: 
1508:     ksort($params);
1509:     if ($append_to_get) {
1510:       $authorization = '';
1511:       foreach ($params as $key => $value) {
1512:         $authorization .= $key . '="' . $this->_url($value) . '", ';
1513:       }
1514:       return substr($authorization, 0, -1);
1515:     }
1516:     $authorization = 'OAuth ';
1517:     foreach ($params as $key => $value) {
1518:       $authorization .= $key . "=\"" . $this->_url($value) . "\", ";
1519:     }
1520:     return substr($authorization, 0, -2);
1521:   }
1522: 
1523:   /**
1524:    * Detects HTTP method to use for API call
1525:    *
1526:    * @param string      $method The API method to call
1527:    * @param array byref $params The parameters to send along
1528:    *
1529:    * @return string The HTTP method that should be used
1530:    */
1531:   protected function _detectMethod($method, &$params)
1532:   {
1533:     if (isset($params['httpmethod'])) {
1534:       $httpmethod = $params['httpmethod'];
1535:       unset($params['httpmethod']);
1536:       return $httpmethod;
1537:     }
1538:     $apimethods = $this->getApiMethods();
1539: 
1540:     // multi-HTTP method endpoints
1541:     switch ($method) {
1542:       case 'ads/accounts/:account_id/campaigns':
1543:       case 'ads/sandbox/accounts/:account_id/campaigns':
1544:         if (isset($params['funding_instrument_id'])) {
1545:           return 'POST';
1546:         }
1547:         break;
1548:       case 'ads/accounts/:account_id/line_items':
1549:       case 'ads/sandbox/accounts/:account_id/line_items':
1550:         if (isset($params['campaign_id'])) {
1551:           return 'POST';
1552:         }
1553:         break;
1554:       case 'ads/accounts/:account_id/targeting_criteria':
1555:       case 'ads/sandbox/accounts/:account_id/targeting_criteria':
1556:         if (isset($params['targeting_value'])) {
1557:           return 'POST';
1558:         }
1559:         break;
1560:       case 'ads/accounts/:account_id/app_lists':
1561:       case 'ads/accounts/:account_id/campaigns':
1562:       case 'ads/accounts/:account_id/cards/app_download':
1563:       case 'ads/accounts/:account_id/cards/image_app_download':
1564:       case 'ads/accounts/:account_id/cards/image_conversion':
1565:       case 'ads/accounts/:account_id/cards/lead_gen':
1566:       case 'ads/accounts/:account_id/cards/video_app_download':
1567:       case 'ads/accounts/:account_id/cards/video_conversation':
1568:       case 'ads/accounts/:account_id/cards/website':
1569:       case 'ads/accounts/:account_id/tailored_audiences':
1570:       case 'ads/accounts/:account_id/web_event_tags':
1571:       case 'ads/sandbox/accounts/:account_id/app_lists':
1572:       case 'ads/sandbox/accounts/:account_id/campaigns':
1573:       case 'ads/sandbox/accounts/:account_id/cards/app_download':
1574:       case 'ads/sandbox/accounts/:account_id/cards/image_app_download':
1575:       case 'ads/sandbox/accounts/:account_id/cards/image_conversion':
1576:       case 'ads/sandbox/accounts/:account_id/cards/lead_gen':
1577:       case 'ads/sandbox/accounts/:account_id/cards/video_app_download':
1578:       case 'ads/sandbox/accounts/:account_id/cards/video_conversation':
1579:       case 'ads/sandbox/accounts/:account_id/cards/website':
1580:       case 'ads/sandbox/accounts/:account_id/tailored_audiences':
1581:       case 'ads/sandbox/accounts/:account_id/web_event_tags':
1582:         if (isset($params['name'])) {
1583:           return 'POST';
1584:         }
1585:         break;
1586:       case 'ads/accounts/:account_id/promoted_accounts':
1587:       case 'ads/sandbox/accounts/:account_id/promoted_accounts':
1588:         if (isset($params['user_id'])) {
1589:           return 'POST';
1590:         }
1591:         break;
1592:       case 'ads/accounts/:account_id/promoted_tweets':
1593:       case 'ads/sandbox/accounts/:account_id/promoted_tweets':
1594:         if (isset($params['tweet_ids'])) {
1595:           return 'POST';
1596:         }
1597:         break;
1598:       case 'ads/accounts/:account_id/videos':
1599:       case 'ads/sandbox/accounts/:account_id/videos':
1600:         if (isset($params['video_media_id'])) {
1601:           return 'POST';
1602:         }
1603:         break;
1604:       case 'ads/accounts/:account_id/tailored_audience_changes':
1605:       case 'ads/sandbox/accounts/:account_id/tailored_audience_changes':
1606:         if (isset($params['tailored_audience_id'])) {
1607:           return 'POST';
1608:         }
1609:         break;
1610:       case 'ads/accounts/:account_id/cards/image_conversation/:card_id':
1611:       case 'ads/accounts/:account_id/cards/video_conversation/:card_id':
1612:       case 'ads/accounts/:account_id/cards/website/:card_id':
1613:       case 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id':
1614:       case 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id':
1615:       case 'ads/sandbox/accounts/:account_id/cards/website/:card_id':
1616:         if (isset($params['name'])) {
1617:           return 'PUT';
1618:         }
1619:         break;
1620:       default:
1621:         // prefer POST and PUT if parameters are set
1622:         if (count($params) > 0) {
1623:           if (isset($apimethods['POST'][$method])) {
1624:             return 'POST';
1625:           }
1626:           if (isset($apimethods['PUT'][$method])) {
1627:             return 'PUT';
1628:           }
1629:         }
1630:     }
1631: 
1632:     foreach ($apimethods as $httpmethod => $methods) {
1633:       if (in_array($method, $methods)) {
1634:         return $httpmethod;
1635:       }
1636:     }
1637:     throw new \Exception('Can\'t find HTTP method to use for "' . $method . '".');
1638:   }
1639: 
1640:   /**
1641:    * Detects if API call should use multipart/form-data
1642:    *
1643:    * @param string $method The API method to call
1644:    *
1645:    * @return bool Whether the method should be sent as multipart
1646:    */
1647:   protected function _detectMultipart($method)
1648:   {
1649:     $multiparts = [
1650:       // Tweets
1651:       'statuses/update_with_media',
1652:       'media/upload',
1653: 
1654:       // Users
1655:       // no multipart for these, for now:
1656:       //'account/update_profile_background_image',
1657:       //'account/update_profile_image',
1658:       //'account/update_profile_banner'
1659:     ];
1660:     return in_array($method, $multiparts);
1661:   }
1662: 
1663:   /**
1664:    * Merge multipart string from parameters array
1665:    *
1666:    * @param string $method_template The method template to call
1667:    * @param string $border          The multipart border
1668:    * @param array  $params          The parameters to send along
1669:    *
1670:    * @return string request
1671:    */
1672:   protected function _getMultipartRequestFromParams($method_template, $border, $params)
1673:   {
1674:     $request = '';
1675:     foreach ($params as $key => $value) {
1676:       // is it an array?
1677:       if (is_array($value)) {
1678:         throw new \Exception('Using URL-encoded parameters is not supported for uploading media.');
1679:       }
1680:       $request .=
1681:         '--' . $border . "\r\n"
1682:         . 'Content-Disposition: form-data; name="' . $key . '"';
1683: 
1684:       // check for filenames
1685:       $data = $this->_checkForFiles($method_template, $key, $value);
1686:       if ($data !== false) {
1687:         $value = $data;
1688:       }
1689: 
1690:       $request .= "\r\n\r\n" . $value . "\r\n";
1691:     }
1692: 
1693:     return $request;
1694:   }
1695: 
1696:   /**
1697:    * Check for files
1698:    *
1699:    * @param string $method_template The method template to call
1700:    * @param string $key             The parameter name
1701:    * @param array  $value           The possible file name or URL
1702:    *
1703:    * @return mixed
1704:    */
1705:   protected function _checkForFiles($method_template, $key, $value) {
1706:     if (!in_array($key, self::$_possible_files[$method_template])) {
1707:       return false;
1708:     }
1709:     if (// is it a file, a readable one?
1710:       @file_exists($value)
1711:       && @is_readable($value)
1712:     ) {
1713:       // is it a supported image format?
1714:       $data = @getimagesize($value);
1715:       if ((is_array($data) && in_array($data[2], $this->_supported_media_files))
1716:         || imagecreatefromwebp($data) // A WebP image! :-) —why won’t getimagesize support this?
1717:       ) {
1718:         // try to read the file
1719:         $data = @file_get_contents($value);
1720:         if ($data !== false && strlen($data) !== 0) {
1721:           return $data;
1722:         }
1723:       }
1724:     } elseif (// is it a remote file?
1725:       filter_var($value, FILTER_VALIDATE_URL)
1726:       && preg_match('/^https?:\/\//', $value)
1727:     ) {
1728:       $data = $this->_fetchRemoteFile($value);
1729:       if ($data !== false) {
1730:         return $data;
1731:       }
1732:     }
1733:     return false;
1734:   }
1735: 
1736: 
1737:   /**
1738:    * Detect filenames in upload parameters,
1739:    * build multipart request from upload params
1740:    *
1741:    * @param string $method  The API method to call
1742:    * @param array  $params  The parameters to send along
1743:    *
1744:    * @return null|string
1745:    */
1746:   protected function _buildMultipart($method, $params)
1747:   {
1748:     // well, files will only work in multipart methods
1749:     if (! $this->_detectMultipart($method)) {
1750:       return;
1751:     }
1752: 
1753:     // only check specific parameters
1754:     // method might have files?
1755:     if (! in_array($method, array_keys(self::$_possible_files))) {
1756:       return;
1757:     }
1758: 
1759:     $multipart_border = '--------------------' . $this->_nonce();
1760:     $multipart_request =
1761:       $this->_getMultipartRequestFromParams($method, $multipart_border, $params)
1762:       . '--' . $multipart_border . '--';
1763: 
1764:     return $multipart_request;
1765:   }
1766: 
1767:   /**
1768:    * Detect filenames in upload parameters
1769:    *
1770:    * @param mixed $input The data or file name to parse
1771:    *
1772:    * @return null|string
1773:    */
1774:   protected function _buildBinaryBody($input)
1775:   {
1776:     if (// is it a file, a readable one?
1777:       @file_exists($input)
1778:       && @is_readable($input)
1779:     ) {
1780:       // try to read the file
1781:       $data = @file_get_contents($input);
1782:       if ($data !== false && strlen($data) !== 0) {
1783:         return $data;
1784:       }
1785:     } elseif (// is it a remote file?
1786:       filter_var($input, FILTER_VALIDATE_URL)
1787:       && preg_match('/^https?:\/\//', $input)
1788:     ) {
1789:       $data = $this->_fetchRemoteFile($input);
1790:       if ($data !== false) {
1791:         return $data;
1792:       }
1793:     }
1794:     return $input;
1795:   }
1796: 
1797:   /**
1798:    * Fetches a remote file
1799:    *
1800:    * @param string $url The URL to download from
1801:    *
1802:    * @return mixed The file contents or FALSE
1803:    */
1804:   protected function _fetchRemoteFile($url)
1805:   {
1806:     // try to fetch the file
1807:     if ($this->_use_curl) {
1808:       $ch = $this->getCurlInitialization($url);
1809:       curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1810:       curl_setopt($ch, CURLOPT_HEADER, 0);
1811:       // no SSL validation for downloading media
1812:       curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
1813:       curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
1814:       // use hardcoded download timeouts for now
1815:       curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->_remoteDownloadTimeout);
1816:       curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->_remoteDownloadTimeout / 2);
1817:       // find files that have been redirected
1818:       curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1819:       // process compressed images
1820:       curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate,sdch');
1821:       $result = curl_exec($ch);
1822:       if ($result !== false) {
1823:         return $result;
1824:       }
1825:       return false;
1826:     }
1827:     // no cURL
1828:     $contextOptions = [
1829:       'http' => [
1830:         'method'           => 'GET',
1831:         'protocol_version' => '1.1',
1832:         'timeout'          => $this->_remoteDownloadTimeout
1833:       ],
1834:       'ssl' => [
1835:         'verify_peer'  => false
1836:       ]
1837:     ];
1838:     list($result) = $this->getNoCurlInitialization($url, $contextOptions);
1839:     if ($result !== false) {
1840:       return $result;
1841:     }
1842:     return false;
1843:   }
1844: 
1845:   /**
1846:    * Detects if API call should use media endpoint
1847:    *
1848:    * @param string $method The API method to call
1849:    *
1850:    * @return bool Whether the method is defined in media API
1851:    */
1852:   protected function _detectMedia($method) {
1853:     $medias = [
1854:       'media/upload'
1855:     ];
1856:     return in_array($method, $medias);
1857:   }
1858: 
1859:   /**
1860:    * Detects if API call should use JSON body
1861:    *
1862:    * @param string $method The API method to call
1863:    *
1864:    * @return bool Whether the method is defined as accepting JSON body
1865:    */
1866:   protected function _detectJsonBody($method) {
1867:     $json_bodies = [
1868:       'collections/entries/curate'
1869:     ];
1870:     return in_array($method, $json_bodies);
1871:   }
1872: 
1873:   /**
1874:    * Detects if API call should use binary body
1875:    *
1876:    * @param string $method_template The API method to call
1877:    *
1878:    * @return bool Whether the method is defined as accepting binary body
1879:    */
1880:   protected function _detectBinaryBody($method_template) {
1881:     $binary = [
1882:       'ton/bucket/:bucket',
1883:       'ton/bucket/:bucket?resumable=true',
1884:       'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
1885:     ];
1886:     return in_array($method_template, $binary);
1887:   }
1888: 
1889:   /**
1890:    * Detects if API call should use streaming endpoint, and if yes, which one
1891:    *
1892:    * @param string $method The API method to call
1893:    *
1894:    * @return string|false Variant of streaming API to be used
1895:    */
1896:   protected function _detectStreaming($method) {
1897:     $streamings = [
1898:       'public' => [
1899:         'statuses/sample',
1900:         'statuses/filter',
1901:         'statuses/firehose'
1902:       ],
1903:       'user' => ['user'],
1904:       'site' => ['site']
1905:     ];
1906:     foreach ($streamings as $key => $values) {
1907:       if (in_array($method, $values)) {
1908:         return $key;
1909:       }
1910:     }
1911: 
1912:     return false;
1913:   }
1914: 
1915:   /**
1916:    * Builds the complete API endpoint url
1917:    *
1918:    * @param string $method The API method to call
1919:    * @param string $method_template The API method to call
1920:    *
1921:    * @return string The URL to send the request to
1922:    */
1923:   protected function _getEndpoint($method, $method_template)
1924:   {
1925:     if (substr($method_template, 0, 5) === 'oauth') {
1926:       $url = self::$_endpoint_oauth . $method;
1927:     } elseif ($this->_detectMedia($method_template)) {
1928:       $url = self::$_endpoint_media . $method . '.json';
1929:     } elseif ($variant = $this->_detectStreaming($method_template)) {
1930:       $url = self::$_endpoints_streaming[$variant] . $method . '.json';
1931:     } elseif ($this->_detectBinaryBody($method_template)) {
1932:       $url = self::$_endpoint_ton . $method;
1933:     } elseif (substr($method_template, 0, 12) === 'ads/sandbox/') {
1934:       $url = self::$_endpoint_ads_sandbox . substr($method, 12);
1935:     } elseif (substr($method_template, 0, 4) === 'ads/') {
1936:       $url = self::$_endpoint_ads . substr($method, 4);
1937:     } else {
1938:       $url = self::$_endpoint . $method . '.json';
1939:     }
1940:     return $url;
1941:   }
1942: 
1943:   /**
1944:    * Calls the API
1945:    *
1946:    * @param string          $httpmethod      The HTTP method to use for making the request
1947:    * @param string          $method          The API method to call
1948:    * @param string          $method_template The API method template to call
1949:    * @param array  optional $params          The parameters to send along
1950:    * @param bool   optional $multipart       Whether to use multipart/form-data
1951:    * @param bool   optional $app_only_auth   Whether to use app-only bearer authentication
1952:    *
1953:    * @return string The API reply, encoded in the set return_format
1954:    */
1955: 
1956:   protected function _callApi($httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false)
1957:   {
1958:     if (! $app_only_auth
1959:       && $this->_oauth_token === null
1960:       && substr($method, 0, 5) !== 'oauth'
1961:     ) {
1962:         throw new \Exception('To call this API, the OAuth access token must be set.');
1963:     }
1964:     // use separate API access for streaming API
1965:     if ($this->_detectStreaming($method) !== false) {
1966:       return $this->_callApiStreaming($httpmethod, $method, $method_template, $params, $app_only_auth);
1967:     }
1968: 
1969:     if ($this->_use_curl) {
1970:       return $this->_callApiCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
1971:     }
1972:     return $this->_callApiNoCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
1973:   }
1974: 
1975:   /**
1976:    * Calls the API using cURL
1977:    *
1978:    * @param string          $httpmethod    The HTTP method to use for making the request
1979:    * @param string          $method        The API method to call
1980:    * @param string          $method_template The API method template to call
1981:    * @param array  optional $params        The parameters to send along
1982:    * @param bool   optional $multipart     Whether to use multipart/form-data
1983:    * @param bool   optional $app_only_auth Whether to use app-only bearer authentication
1984:    *
1985:    * @return string The API reply, encoded in the set return_format
1986:    */
1987: 
1988:   protected function _callApiCurl(
1989:     $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
1990:   )
1991:   {
1992:     list ($authorization, $url, $params, $request_headers)
1993:       = $this->_callApiPreparations(
1994:         $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
1995:       );
1996: 
1997:     $ch                = $this->getCurlInitialization($url);
1998:     $request_headers[] = 'Authorization: ' . $authorization;
1999:     $request_headers[] = 'Expect:';
2000: 
2001:     if ($httpmethod !== 'GET') {
2002:       curl_setopt($ch, CURLOPT_POST, 1);
2003:       curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
2004:       if (in_array($httpmethod, ['POST', 'PUT', 'DELETE'])) {
2005:         curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpmethod);
2006:       }
2007:     }
2008: 
2009:     curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers);
2010: 
2011:     if (isset($this->_timeout)) {
2012:       curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->_timeout);
2013:     }
2014: 
2015:     if (isset($this->_connectionTimeout)) {
2016:       curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->_connectionTimeout);
2017:     }
2018: 
2019:     $result = curl_exec($ch);
2020: 
2021:     // catch request errors
2022:     if ($result === false) {
2023:       throw new \Exception('Request error for API call: ' . curl_error($ch));
2024:     }
2025: 
2026:     // certificate validation results
2027:     $validation_result = curl_errno($ch);
2028:     $this->_validateSslCertificate($validation_result);
2029: 
2030:     $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
2031:     list($headers, $reply) = $this->_parseApiHeaders($result);
2032:     // TON API & redirects
2033:     $reply                 = $this->_parseApiReplyPrefillHeaders($headers, $reply);
2034:     $reply                 = $this->_parseApiReply($reply);
2035:     $rate                  = $this->_getRateLimitInfo($headers);
2036: 
2037:     switch ($this->_return_format) {
2038:       case CODEBIRD_RETURNFORMAT_ARRAY:
2039:         $reply['httpstatus'] = $httpstatus;
2040:         $reply['rate']       = $rate;
2041:         break;
2042:       case CODEBIRD_RETURNFORMAT_OBJECT:
2043:         $reply->httpstatus = $httpstatus;
2044:         $reply->rate       = $rate;
2045:         break;
2046:     }
2047:     return $reply;
2048:   }
2049: 
2050:   /**
2051:    * Calls the API without cURL
2052:    *
2053:    * @param string          $httpmethod      The HTTP method to use for making the request
2054:    * @param string          $method          The API method to call
2055:    * @param string          $method_template The API method template to call
2056:    * @param array  optional $params          The parameters to send along
2057:    * @param bool   optional $multipart       Whether to use multipart/form-data
2058:    * @param bool   optional $app_only_auth   Whether to use app-only bearer authentication
2059:    *
2060:    * @return string The API reply, encoded in the set return_format
2061:    */
2062: 
2063:   protected function _callApiNoCurl(
2064:     $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
2065:   )
2066:   {
2067:     list ($authorization, $url, $params, $request_headers)
2068:       = $this->_callApiPreparations(
2069:         $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2070:       );
2071: 
2072:     $hostname = parse_url($url, PHP_URL_HOST);
2073:     if ($hostname === false) {
2074:       throw new \Exception('Incorrect API endpoint host.');
2075:     }
2076: 
2077:     $request_headers[] = 'Authorization: ' . $authorization;
2078:     $request_headers[] = 'Accept: */*';
2079:     $request_headers[] = 'Connection: Close';
2080:     if ($httpmethod !== 'GET' && ! $multipart) {
2081:       $request_headers[]  = 'Content-Type: application/x-www-form-urlencoded';
2082:     }
2083: 
2084:     $contextOptions = [
2085:       'http' => [
2086:         'method'           => $httpmethod,
2087:         'protocol_version' => '1.1',
2088:         'header'           => implode("\r\n", $request_headers),
2089:         'timeout'          => $this->_timeout / 1000,
2090:         'content'          => in_array($httpmethod, ['POST', 'PUT']) ? $params : null,
2091:         'ignore_errors'    => true
2092:       ]
2093:     ];
2094: 
2095:     list($reply, $headers) = $this->getNoCurlInitialization($url, $contextOptions, $hostname);
2096:     $result  = '';
2097:     foreach ($headers as $header) {
2098:       $result .= $header . "\r\n";
2099:     }
2100:     $result .= "\r\n" . $reply;
2101: 
2102:     // find HTTP status
2103:     $httpstatus = '500';
2104:     $match      = [];
2105:     if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
2106:       $httpstatus = $match[1];
2107:     }
2108: 
2109:     list($headers, $reply) = $this->_parseApiHeaders($result);
2110:     // TON API & redirects
2111:     $reply                 = $this->_parseApiReplyPrefillHeaders($headers, $reply);
2112:     $reply                 = $this->_parseApiReply($reply);
2113:     $rate                  = $this->_getRateLimitInfo($headers);
2114:     switch ($this->_return_format) {
2115:       case CODEBIRD_RETURNFORMAT_ARRAY:
2116:         $reply['httpstatus'] = $httpstatus;
2117:         $reply['rate']       = $rate;
2118:         break;
2119:       case CODEBIRD_RETURNFORMAT_OBJECT:
2120:         $reply->httpstatus = $httpstatus;
2121:         $reply->rate       = $rate;
2122:         break;
2123:     }
2124:     return $reply;
2125:   }
2126: 
2127:   /**
2128:    * Do preparations to make the API GET call
2129:    *
2130:    * @param string  $httpmethod      The HTTP method to use for making the request
2131:    * @param string  $url             The URL to call
2132:    * @param array   $params          The parameters to send along
2133:    * @param bool    $app_only_auth   Whether to use app-only bearer authentication
2134:    *
2135:    * @return string[] (string authorization, string url)
2136:    */
2137:   protected function _callApiPreparationsGet(
2138:     $httpmethod, $url, $params, $app_only_auth
2139:   ) {
2140:     return [
2141:       $app_only_auth                ? null : $this->_sign($httpmethod, $url, $params),
2142:       json_encode($params) === '[]' ? $url : $url . '?' . http_build_query($params)
2143:     ];
2144:   }
2145: 
2146:   /**
2147:    * Do preparations to make the API POST call
2148:    *
2149:    * @param string  $httpmethod      The HTTP method to use for making the request
2150:    * @param string  $url             The URL to call
2151:    * @param string  $method          The API method to call
2152:    * @param string  $method_template The API method template to call
2153:    * @param array   $params          The parameters to send along
2154:    * @param bool    $multipart       Whether to use multipart/form-data
2155:    * @param bool    $app_only_auth   Whether to use app-only bearer authentication
2156:    *
2157:    * @return array (string authorization, array params, array request_headers)
2158:    */
2159:   protected function _callApiPreparationsPost(
2160:     $httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth
2161:   ) {
2162:     $authorization   = null;
2163:     $request_headers = [];
2164:     if ($multipart) {
2165:       if (! $app_only_auth) {
2166:         $authorization = $this->_sign($httpmethod, $url, []);
2167:       }
2168:       $params = $this->_buildMultipart($method, $params);
2169:       $first_newline      = strpos($params, "\r\n");
2170:       $multipart_boundary = substr($params, 2, $first_newline - 2);
2171:       $request_headers[]  = 'Content-Type: multipart/form-data; boundary='
2172:         . $multipart_boundary;
2173:     } elseif ($this->_detectJsonBody($method)) {
2174:       $authorization = $this->_sign($httpmethod, $url, []);
2175:       $params = json_encode($params);
2176:       $request_headers[] = 'Content-Type: application/json';
2177:     } elseif ($this->_detectBinaryBody($method_template)) {
2178:       // transform parametric headers to real headers
2179:       foreach ([
2180:           'Content-Type', 'X-TON-Content-Type',
2181:           'X-TON-Content-Length', 'Content-Range'
2182:         ] as $key) {
2183:         if (isset($params[$key])) {
2184:           $request_headers[] = $key . ': ' . $params[$key];
2185:           unset($params[$key]);
2186:         }
2187:       }
2188:       $sign_params = [];
2189:       parse_str(parse_url($method, PHP_URL_QUERY), $sign_params);
2190:       if ($sign_params === null) {
2191:         $sign_params = [];
2192:       }
2193:       $authorization = $this->_sign($httpmethod, $url, $sign_params);
2194:       if (isset($params['media'])) {
2195:         $params = $this->_buildBinaryBody($params['media']);
2196:       } else {
2197:         // resumable upload
2198:         $params = [];
2199:       }
2200:     } else {
2201:       // check for possible files in non-multipart methods
2202:       foreach ($params as $key => $value) {
2203:         $data = $this->_checkForFiles($method_template, $key, $value);
2204:         if ($data !== false) {
2205:           $params[$key] = base64_encode($data);
2206:         }
2207:       }
2208:       if (! $app_only_auth) {
2209:         $authorization = $this->_sign($httpmethod, $url, $params);
2210:       }
2211:       $params = http_build_query($params);
2212:     }
2213:     return [$authorization, $params, $request_headers];
2214:   }
2215: 
2216:   /**
2217:    * Get Bearer authorization string
2218:    *
2219:    * @return string authorization
2220:    */
2221:   protected function _getBearerAuthorization()
2222:   {
2223:     if (self::$_oauth_consumer_key === null
2224:       && self::$_oauth_bearer_token === null
2225:     ) {
2226:       throw new \Exception('To make an app-only auth API request, consumer key or bearer token must be set.');
2227:     }
2228:     // automatically fetch bearer token, if necessary
2229:     if (self::$_oauth_bearer_token === null) {
2230:       $this->oauth2_token();
2231:     }
2232:     return 'Bearer ' . self::$_oauth_bearer_token;
2233:   }
2234: 
2235:   /**
2236:    * Do preparations to make the API call
2237:    *
2238:    * @param string  $httpmethod      The HTTP method to use for making the request
2239:    * @param string  $method          The API method to call
2240:    * @param string  $method_template The API method template to call
2241:    * @param array   $params          The parameters to send along
2242:    * @param bool    $multipart       Whether to use multipart/form-data
2243:    * @param bool    $app_only_auth   Whether to use app-only bearer authentication
2244:    *
2245:    * @return array (string authorization, string url, array params, array request_headers)
2246:    */
2247:   protected function _callApiPreparations(
2248:     $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2249:   )
2250:   {
2251:     $url             = $this->_getEndpoint($method, $method_template);
2252:     $request_headers = [];
2253:     if ($httpmethod === 'GET') {
2254:       // GET
2255:       list ($authorization, $url) =
2256:         $this->_callApiPreparationsGet($httpmethod, $url, $params, $app_only_auth);
2257:     } else {
2258:       // POST
2259:       list ($authorization, $params, $request_headers) =
2260:         $this->_callApiPreparationsPost($httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth);
2261:     }
2262:     if ($app_only_auth) {
2263:       $authorization = $this->_getBearerAuthorization();
2264:     }
2265: 
2266:     return [
2267:       $authorization, $url, $params, $request_headers
2268:     ];
2269:   }
2270: 
2271:   /**
2272:    * Calls the streaming API
2273:    *
2274:    * @param string          $httpmethod      The HTTP method to use for making the request
2275:    * @param string          $method          The API method to call
2276:    * @param string          $method_template The API method template to call
2277:    * @param array  optional $params          The parameters to send along
2278:    * @param bool   optional $app_only_auth   Whether to use app-only bearer authentication
2279:    *
2280:    * @return void
2281:    */
2282: 
2283:   protected function _callApiStreaming(
2284:     $httpmethod, $method, $method_template, $params = [], $app_only_auth = false
2285:   )
2286:   {
2287:     if ($this->_streaming_callback === null) {
2288:       throw new \Exception('Set streaming callback before consuming a stream.');
2289:     }
2290: 
2291:     $params['delimited'] = 'length';
2292: 
2293:     list ($authorization, $url, $params, $request_headers)
2294:       = $this->_callApiPreparations(
2295:         $httpmethod, $method, $method_template, $params, false, $app_only_auth
2296:       );
2297: 
2298:     $hostname = parse_url($url, PHP_URL_HOST);
2299:     $path     = parse_url($url, PHP_URL_PATH);
2300:     $query    = parse_url($url, PHP_URL_QUERY);
2301:     if ($hostname === false) {
2302:       throw new \Exception('Incorrect API endpoint host.');
2303:     }
2304: 
2305:     $request_headers[] = 'Authorization: ' . $authorization;
2306:     $request_headers[] = 'Accept: */*';
2307:     if ($httpmethod !== 'GET') {
2308:       $request_headers[]  = 'Content-Type: application/x-www-form-urlencoded';
2309:       $request_headers[]  = 'Content-Length: ' . strlen($params);
2310:     }
2311: 
2312:     $errno   = 0;
2313:     $errstr  = '';
2314:     $ch = stream_socket_client(
2315:       'ssl://' . $hostname . ':443',
2316:       $errno, $errstr,
2317:       $this->_connectionTimeout,
2318:       STREAM_CLIENT_CONNECT
2319:     );
2320: 
2321:     // send request
2322:     $request = $httpmethod . ' '
2323:       . $path . ($query ? '?' . $query : '') . " HTTP/1.1\r\n"
2324:       . 'Host: ' . $hostname . "\r\n"
2325:       . implode("\r\n", $request_headers)
2326:       . "\r\n\r\n";
2327:     if ($httpmethod !== 'GET') {
2328:       $request .= $params;
2329:     }
2330:     fputs($ch, $request);
2331:     stream_set_blocking($ch, 0);
2332:     stream_set_timeout($ch, 0);
2333: 
2334:     // collect headers
2335:     do {
2336:       $result  = stream_get_line($ch, 1048576, "\r\n\r\n");
2337:     } while(!$result);
2338:     $headers = explode("\r\n", $result);
2339: 
2340:     // find HTTP status
2341:     $httpstatus = '500';
2342:     $match      = [];
2343:     if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
2344:       $httpstatus = $match[1];
2345:     }
2346: 
2347:     list($headers,) = $this->_parseApiHeaders($result);
2348:     $rate           = $this->_getRateLimitInfo($headers);
2349: 
2350:     if ($httpstatus !== '200') {
2351:       $reply = [
2352:         'httpstatus' => $httpstatus,
2353:         'rate'       => $rate
2354:       ];
2355:       switch ($this->_return_format) {
2356:         case CODEBIRD_RETURNFORMAT_ARRAY:
2357:           return $reply;
2358:         case CODEBIRD_RETURNFORMAT_OBJECT:
2359:           return (object) $reply;
2360:         case CODEBIRD_RETURNFORMAT_JSON:
2361:           return json_encode($reply);
2362:       }
2363:     }
2364: 
2365:     $signal_function = function_exists('pcntl_signal_dispatch');
2366:     $data            = '';
2367:     $last_message    = time();
2368:     $message_length  = 0;
2369: 
2370:     while (!feof($ch)) {
2371:       // call signal handlers, if any
2372:       if ($signal_function) {
2373:         pcntl_signal_dispatch();
2374:       }
2375:       $cha = [$ch];
2376:       $write = $except = null;
2377:       if (false === ($num_changed_streams = stream_select($cha, $write, $except, 0, 200000))) {
2378:         break;
2379:       } elseif ($num_changed_streams === 0) {
2380:         if (time() - $last_message >= 1) {
2381:           // deliver empty message, allow callback to cancel stream
2382:           $cancel_stream = $this->_deliverStreamingMessage(null);
2383:           if ($cancel_stream) {
2384:             break;
2385:           }
2386:           $last_message = time();
2387:         }
2388:         continue;
2389:       }
2390:       $chunk_length = fgets($ch, 10);
2391:       if ($chunk_length === '' || !$chunk_length = hexdec($chunk_length)) {
2392:         continue;
2393:       }
2394: 
2395:       $chunk = '';
2396:       do {
2397:         $chunk .= fread($ch, $chunk_length);
2398:         $chunk_length -= strlen($chunk);
2399:       } while($chunk_length > 0);
2400: 
2401:       if(0 === $message_length) {
2402:         $message_length = (int) strstr($chunk, "\r\n", true);
2403:         if ($message_length) {
2404:           $chunk = substr($chunk, strpos($chunk, "\r\n") + 2);
2405:         } else {
2406:           continue;
2407:         }
2408: 
2409:         $data = $chunk;
2410:       } else {
2411:         $data .= $chunk;
2412:       }
2413: 
2414:       if (strlen($data) < $message_length) {
2415:         continue;
2416:       }
2417: 
2418:       $reply = $this->_parseApiReply($data);
2419:       switch ($this->_return_format) {
2420:         case CODEBIRD_RETURNFORMAT_ARRAY:
2421:           $reply['httpstatus'] = $httpstatus;
2422:           $reply['rate']       = $rate;
2423:           break;
2424:         case CODEBIRD_RETURNFORMAT_OBJECT:
2425:           $reply->httpstatus = $httpstatus;
2426:           $reply->rate       = $rate;
2427:           break;
2428:       }
2429: 
2430:       $cancel_stream = $this->_deliverStreamingMessage($reply);
2431:       if ($cancel_stream) {
2432:         break;
2433:       }
2434: 
2435:       $data           = '';
2436:       $message_length = 0;
2437:       $last_message   = time();
2438:     }
2439: 
2440:     return;
2441:   }
2442: 
2443:   /**
2444:    * Calls streaming callback with received message
2445:    *
2446:    * @param string|array|object message
2447:    *
2448:    * @return bool Whether to cancel streaming
2449:    */
2450:   protected function _deliverStreamingMessage($message)
2451:   {
2452:     return call_user_func($this->_streaming_callback, $message);
2453:   }
2454: 
2455:   /**
2456:    * Parses the API reply to separate headers from the body
2457:    *
2458:    * @param string $reply The actual raw HTTP request reply
2459:    *
2460:    * @return array (headers, reply)
2461:    */
2462:   protected function _parseApiHeaders($reply) {
2463:     // split headers and body
2464:     $headers = [];
2465:     $reply = explode("\r\n\r\n", $reply, 4);
2466: 
2467:     // check if using proxy
2468:     $proxy_tester = strtolower(substr($reply[0], 0, 35));
2469:     if ($proxy_tester === 'http/1.0 200 connection established'
2470:       || $proxy_tester === 'http/1.1 200 connection established'
2471:     ) {
2472:       array_shift($reply);
2473:     } elseif (count($reply) > 2) {
2474:       $headers = array_shift($reply);
2475:       $reply = [
2476:         $headers,
2477:         implode("\r\n", $reply)
2478:       ];
2479:     }
2480: 
2481:     $headers_array = explode("\r\n", $reply[0]);
2482:     foreach ($headers_array as $header) {
2483:       $header_array = explode(': ', $header, 2);
2484:       $key = $header_array[0];
2485:       $value = '';
2486:       if (count($header_array) > 1) {
2487:         $value = $header_array[1];
2488:       }
2489:       $headers[$key] = $value;
2490:     }
2491: 
2492:     if (count($reply) > 1) {
2493:       $reply = $reply[1];
2494:     } else {
2495:       $reply = '';
2496:     }
2497: 
2498:     return [$headers, $reply];
2499:   }
2500: 
2501:   /**
2502:    * Parses the API headers to return Location and Ton API headers
2503:    *
2504:    * @param array  $headers The headers list
2505:    * @param string $reply   The actual HTTP body
2506:    *
2507:    * @return string $reply
2508:    */
2509:   protected function _parseApiReplyPrefillHeaders($headers, $reply)
2510:   {
2511:     if ($reply === '' && (isset($headers['Location']))) {
2512:       $reply = [
2513:         'Location' => $headers['Location']
2514:       ];
2515:       if (isset($headers['X-TON-Min-Chunk-Size'])) {
2516:         $reply['X-TON-Min-Chunk-Size'] = $headers['X-TON-Min-Chunk-Size'];
2517:       }
2518:       if (isset($headers['X-TON-Max-Chunk-Size'])) {
2519:         $reply['X-TON-Max-Chunk-Size'] = $headers['X-TON-Max-Chunk-Size'];
2520:       }
2521:       if (isset($headers['Range'])) {
2522:         $reply['Range'] = $headers['Range'];
2523:       }
2524:       $reply = json_encode($reply);
2525:     }
2526:     return $reply;
2527:   }
2528: 
2529:   /**
2530:    * Parses the API reply to encode it in the set return_format
2531:    *
2532:    * @param string $reply The actual HTTP body, JSON-encoded or URL-encoded
2533:    *
2534:    * @return array|string|object The parsed reply
2535:    */
2536:   protected function _parseApiReply($reply)
2537:   {
2538:     $need_array = $this->_return_format === CODEBIRD_RETURNFORMAT_ARRAY;
2539:     if ($reply === '[]') {
2540:       switch ($this->_return_format) {
2541:         case CODEBIRD_RETURNFORMAT_ARRAY:
2542:           return [];
2543:         case CODEBIRD_RETURNFORMAT_JSON:
2544:           return '{}';
2545:         case CODEBIRD_RETURNFORMAT_OBJECT:
2546:           return new \stdClass;
2547:       }
2548:     }
2549:     if (! $parsed = json_decode($reply, $need_array, 512, JSON_BIGINT_AS_STRING)) {
2550:       if ($reply) {
2551:         if (stripos($reply, '<' . '?xml version="1.0" encoding="UTF-8"?' . '>') === 0) {
2552:           // we received XML...
2553:           // since this only happens for errors,
2554:           // don't perform a full decoding
2555:           preg_match('/<request>(.*)<\/request>/', $reply, $request);
2556:           preg_match('/<error>(.*)<\/error>/', $reply, $error);
2557:           $parsed['request'] = htmlspecialchars_decode($request[1]);
2558:           $parsed['error'] = htmlspecialchars_decode($error[1]);
2559:         } else {
2560:           // assume query format
2561:           $reply = explode('&', $reply);
2562:           foreach ($reply as $element) {
2563:             if (stristr($element, '=')) {
2564:               list($key, $value) = explode('=', $element, 2);
2565:               $parsed[$key] = $value;
2566:             } else {
2567:               $parsed['message'] = $element;
2568:             }
2569:           }
2570:         }
2571:       }
2572:       $reply = json_encode($parsed);
2573:     }
2574:     switch ($this->_return_format) {
2575:       case CODEBIRD_RETURNFORMAT_ARRAY:
2576:         return $parsed;
2577:       case CODEBIRD_RETURNFORMAT_JSON:
2578:         return $reply;
2579:       case CODEBIRD_RETURNFORMAT_OBJECT:
2580:         return (object) $parsed;
2581:     }
2582:     return $parsed;
2583:   }
2584: }
2585: 
API documentation generated by ApiGen