6 * A Twitter library in PHP.
10 * @author Jublo Solutions <support@jublo.net>
11 * @copyright 2010-2016 Jublo Solutions <support@jublo.net>
12 * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License 3.0
13 * @link https://github.com/jublonet/codebird-php
19 $constants = explode(' ', 'OBJECT ARRAY JSON');
20 foreach ($constants as $i => $id) {
21 $id = 'CODEBIRD_RETURNFORMAT_' . $id;
22 defined($id) or define($id, $i);
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
31 foreach ($constants as $id => $i) {
32 defined($id) or define($id, $i);
39 * A Twitter library in PHP.
42 * @subpackage codebird-php
47 * The current singleton instance
49 private static $_instance = null;
52 * The OAuth consumer key of your registered app
54 protected static $_consumer_key = null;
57 * The corresponding consumer secret
59 protected static $_consumer_secret = null;
62 * The app-only bearer token. Used to authorize app-only requests
64 protected static $_bearer_token = null;
67 * The API endpoints to use
69 protected static $_endpoints = [
71 'production' => 'https://ads-api.twitter.com/0/',
72 'sandbox' => 'https://ads-api-sandbox.twitter.com/0/'
74 'media' => 'https://upload.twitter.com/1.1/',
75 'oauth' => 'https://api.twitter.com/',
76 'rest' => 'https://api.twitter.com/1.1/',
78 'public' => 'https://stream.twitter.com/1.1/',
79 'user' => 'https://userstream.twitter.com/1.1/',
80 'site' => 'https://sitestream.twitter.com/1.1/'
82 'ton' => 'https://ton.twitter.com/1.1/'
86 * Supported API methods
88 protected static $_api_methods = [
91 'account/verify_credentials',
93 'ads/accounts/:account_id',
94 'ads/accounts/:account_id/account_media',
95 'ads/accounts/:account_id/app_event_provider_configurations',
96 'ads/accounts/:account_id/app_event_provider_configurations/:id',
97 'ads/accounts/:account_id/app_event_tags',
98 'ads/accounts/:account_id/app_event_tags/:id',
99 'ads/accounts/:account_id/app_lists',
100 'ads/accounts/:account_id/authenticated_user_access',
101 'ads/accounts/:account_id/campaigns',
102 'ads/accounts/:account_id/campaigns/:campaign_id',
103 'ads/accounts/:account_id/cards/app_download',
104 'ads/accounts/:account_id/cards/app_download/:card_id',
105 'ads/accounts/:account_id/cards/image_app_download',
106 'ads/accounts/:account_id/cards/image_app_download/:card_id',
107 'ads/accounts/:account_id/cards/image_conversation',
108 'ads/accounts/:account_id/cards/image_conversation/:card_id',
109 'ads/accounts/:account_id/cards/lead_gen',
110 'ads/accounts/:account_id/cards/lead_gen/:card_id',
111 'ads/accounts/:account_id/cards/video_app_download',
112 'ads/accounts/:account_id/cards/video_app_download/:id',
113 'ads/accounts/:account_id/cards/video_conversation',
114 'ads/accounts/:account_id/cards/video_conversation/:card_id',
115 'ads/accounts/:account_id/cards/website',
116 'ads/accounts/:account_id/cards/website/:card_id',
117 'ads/accounts/:account_id/features',
118 'ads/accounts/:account_id/funding_instruments',
119 'ads/accounts/:account_id/funding_instruments/:id',
120 'ads/accounts/:account_id/line_items',
121 'ads/accounts/:account_id/line_items/:line_item_id',
122 'ads/accounts/:account_id/media_creatives',
123 'ads/accounts/:account_id/media_creatives/:id',
124 'ads/accounts/:account_id/promotable_users',
125 'ads/accounts/:account_id/promoted_accounts',
126 'ads/accounts/:account_id/promoted_tweets',
127 'ads/accounts/:account_id/reach_estimate',
128 'ads/accounts/:account_id/scoped_timeline',
129 'ads/accounts/:account_id/tailored_audience_changes',
130 'ads/accounts/:account_id/tailored_audience_changes/:id',
131 'ads/accounts/:account_id/tailored_audiences',
132 'ads/accounts/:account_id/tailored_audiences/:id',
133 'ads/accounts/:account_id/targeting_criteria',
134 'ads/accounts/:account_id/targeting_criteria/:id',
135 'ads/accounts/:account_id/targeting_suggestions',
136 'ads/accounts/:account_id/tweet/preview',
137 'ads/accounts/:account_id/tweet/preview/:tweet_id',
138 'ads/accounts/:account_id/videos',
139 'ads/accounts/:account_id/videos/:id',
140 'ads/accounts/:account_id/web_event_tags',
141 'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
143 'ads/iab_categories',
144 'ads/insights/accounts/:account_id',
145 'ads/insights/accounts/:account_id/available_audiences',
146 'ads/insights/keywords/search',
147 'ads/line_items/placements',
148 'ads/sandbox/accounts',
149 'ads/sandbox/accounts/:account_id',
150 'ads/sandbox/accounts/:account_id/account_media',
151 'ads/sandbox/accounts/:account_id/app_event_provider_configurations',
152 'ads/sandbox/accounts/:account_id/app_event_provider_configurations/:id',
153 'ads/sandbox/accounts/:account_id/app_event_tags',
154 'ads/sandbox/accounts/:account_id/app_event_tags/:id',
155 'ads/sandbox/accounts/:account_id/app_lists',
156 'ads/sandbox/accounts/:account_id/authenticated_user_access',
157 'ads/sandbox/accounts/:account_id/campaigns',
158 'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
159 'ads/sandbox/accounts/:account_id/cards/app_download',
160 'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
161 'ads/sandbox/accounts/:account_id/cards/image_app_download',
162 'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
163 'ads/sandbox/accounts/:account_id/cards/image_conversation',
164 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
165 'ads/sandbox/accounts/:account_id/cards/lead_gen',
166 'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
167 'ads/sandbox/accounts/:account_id/cards/video_app_download',
168 'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
169 'ads/sandbox/accounts/:account_id/cards/video_conversation',
170 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
171 'ads/sandbox/accounts/:account_id/cards/website',
172 'ads/sandbox/accounts/:account_id/cards/website/:card_id',
173 'ads/sandbox/accounts/:account_id/features',
174 'ads/sandbox/accounts/:account_id/funding_instruments',
175 'ads/sandbox/accounts/:account_id/funding_instruments/:id',
176 'ads/sandbox/accounts/:account_id/line_items',
177 'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
178 'ads/sandbox/accounts/:account_id/media_creatives',
179 'ads/sandbox/accounts/:account_id/media_creatives/:id',
180 'ads/sandbox/accounts/:account_id/promotable_users',
181 'ads/sandbox/accounts/:account_id/promoted_accounts',
182 'ads/sandbox/accounts/:account_id/promoted_tweets',
183 'ads/sandbox/accounts/:account_id/reach_estimate',
184 'ads/sandbox/accounts/:account_id/scoped_timeline',
185 'ads/sandbox/accounts/:account_id/tailored_audience_changes',
186 'ads/sandbox/accounts/:account_id/tailored_audience_changes/:id',
187 'ads/sandbox/accounts/:account_id/tailored_audiences',
188 'ads/sandbox/accounts/:account_id/tailored_audiences/:id',
189 'ads/sandbox/accounts/:account_id/targeting_criteria',
190 'ads/sandbox/accounts/:account_id/targeting_criteria/:id',
191 'ads/sandbox/accounts/:account_id/targeting_suggestions',
192 'ads/sandbox/accounts/:account_id/tweet/preview',
193 'ads/sandbox/accounts/:account_id/tweet/preview/:tweet_id',
194 'ads/sandbox/accounts/:account_id/videos',
195 'ads/sandbox/accounts/:account_id/videos/:id',
196 'ads/sandbox/accounts/:account_id/web_event_tags',
197 'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id',
198 'ads/sandbox/bidding_rules',
199 'ads/sandbox/iab_categories',
200 'ads/sandbox/insights/accounts/:account_id',
201 'ads/sandbox/insights/accounts/:account_id/available_audiences',
202 'ads/sandbox/insights/keywords/search',
203 'ads/sandbox/line_items/placements',
204 'ads/sandbox/stats/accounts/:account_id',
205 'ads/sandbox/stats/accounts/:account_id/campaigns',
206 'ads/sandbox/stats/accounts/:account_id/campaigns/:id',
207 'ads/sandbox/stats/accounts/:account_id/funding_instruments',
208 'ads/sandbox/stats/accounts/:account_id/funding_instruments/:id',
209 'ads/sandbox/stats/accounts/:account_id/line_items',
210 'ads/sandbox/stats/accounts/:account_id/line_items/:id',
211 'ads/sandbox/stats/accounts/:account_id/promoted_accounts',
212 'ads/sandbox/stats/accounts/:account_id/promoted_accounts/:id',
213 'ads/sandbox/stats/accounts/:account_id/promoted_tweets',
214 'ads/sandbox/stats/accounts/:account_id/promoted_tweets/:id',
215 'ads/sandbox/stats/accounts/:account_id/reach/campaigns',
216 'ads/sandbox/targeting_criteria/app_store_categories',
217 'ads/sandbox/targeting_criteria/behavior_taxonomies',
218 'ads/sandbox/targeting_criteria/behaviors',
219 'ads/sandbox/targeting_criteria/devices',
220 'ads/sandbox/targeting_criteria/events',
221 'ads/sandbox/targeting_criteria/interests',
222 'ads/sandbox/targeting_criteria/languages',
223 'ads/sandbox/targeting_criteria/locations',
224 'ads/sandbox/targeting_criteria/network_operators',
225 'ads/sandbox/targeting_criteria/platform_versions',
226 'ads/sandbox/targeting_criteria/platforms',
227 'ads/sandbox/targeting_criteria/tv_channels',
228 'ads/sandbox/targeting_criteria/tv_genres',
229 'ads/sandbox/targeting_criteria/tv_markets',
230 'ads/sandbox/targeting_criteria/tv_shows',
231 'ads/stats/accounts/:account_id',
232 'ads/stats/accounts/:account_id/campaigns',
233 'ads/stats/accounts/:account_id/campaigns/:id',
234 'ads/stats/accounts/:account_id/funding_instruments',
235 'ads/stats/accounts/:account_id/funding_instruments/:id',
236 'ads/stats/accounts/:account_id/line_items',
237 'ads/stats/accounts/:account_id/line_items/:id',
238 'ads/stats/accounts/:account_id/promoted_accounts',
239 'ads/stats/accounts/:account_id/promoted_accounts/:id',
240 'ads/stats/accounts/:account_id/promoted_tweets',
241 'ads/stats/accounts/:account_id/promoted_tweets/:id',
242 'ads/stats/accounts/:account_id/reach/campaigns',
243 'ads/targeting_criteria/app_store_categories',
244 'ads/targeting_criteria/behavior_taxonomies',
245 'ads/targeting_criteria/behaviors',
246 'ads/targeting_criteria/devices',
247 'ads/targeting_criteria/events',
248 'ads/targeting_criteria/interests',
249 'ads/targeting_criteria/languages',
250 'ads/targeting_criteria/locations',
251 'ads/targeting_criteria/network_operators',
252 'ads/targeting_criteria/platform_versions',
253 'ads/targeting_criteria/platforms',
254 'ads/targeting_criteria/tv_channels',
255 'ads/targeting_criteria/tv_genres',
256 'ads/targeting_criteria/tv_markets',
257 'ads/targeting_criteria/tv_shows',
258 'application/rate_limit_status',
261 'collections/entries',
265 'direct_messages/sent',
266 'direct_messages/show',
272 'friendships/incoming',
273 'friendships/lookup',
274 'friendships/lookup',
275 'friendships/no_retweets/ids',
276 'friendships/outgoing',
279 'geo/reverse_geocode',
281 'geo/similar_places',
282 'help/configuration',
288 'lists/members/show',
294 'lists/subscribers/show',
295 'lists/subscriptions',
298 'oauth/authenticate',
300 'saved_searches/list',
301 'saved_searches/show/:id',
305 'statuses/home_timeline',
306 'statuses/mentions_timeline',
308 'statuses/retweeters/ids',
309 'statuses/retweets/:id',
310 'statuses/retweets_of_me',
313 'statuses/user_timeline',
318 'users/contributees',
319 'users/contributors',
320 'users/profile_banner',
324 'users/suggestions/:slug',
325 'users/suggestions/:slug/members'
328 'account/remove_profile_banner',
330 'account/update_delivery_device',
331 'account/update_profile',
332 'account/update_profile_background_image',
333 'account/update_profile_banner',
334 'account/update_profile_colors',
335 'account/update_profile_image',
336 'ads/accounts/:account_id/account_media',
337 'ads/accounts/:account_id/app_lists',
338 'ads/accounts/:account_id/campaigns',
339 'ads/accounts/:account_id/cards/app_download',
340 'ads/accounts/:account_id/cards/image_app_download',
341 'ads/accounts/:account_id/cards/image_conversation',
342 'ads/accounts/:account_id/cards/lead_gen',
343 'ads/accounts/:account_id/cards/video_app_download',
344 'ads/accounts/:account_id/cards/video_conversation',
345 'ads/accounts/:account_id/cards/website',
346 'ads/accounts/:account_id/line_items',
347 'ads/accounts/:account_id/media_creatives',
348 'ads/accounts/:account_id/promoted_accounts',
349 'ads/accounts/:account_id/promoted_tweets',
350 'ads/accounts/:account_id/tailored_audience_changes',
351 'ads/accounts/:account_id/tailored_audiences',
352 'ads/accounts/:account_id/targeting_criteria',
353 'ads/accounts/:account_id/tweet',
354 'ads/accounts/:account_id/videos',
355 'ads/accounts/:account_id/web_event_tags',
356 'ads/batch/accounts/:account_id/campaigns',
357 'ads/batch/accounts/:account_id/line_items',
358 'ads/batch/accounts/:account_id/tailored_audiences',
359 'ads/batch/accounts/:account_id/targeting_criteria',
360 'ads/sandbox/accounts/:account_id/account_media',
361 'ads/sandbox/accounts/:account_id/app_lists',
362 'ads/sandbox/accounts/:account_id/campaigns',
363 'ads/sandbox/accounts/:account_id/cards/app_download',
364 'ads/sandbox/accounts/:account_id/cards/image_app_download',
365 'ads/sandbox/accounts/:account_id/cards/image_conversation',
366 'ads/sandbox/accounts/:account_id/cards/lead_gen',
367 'ads/sandbox/accounts/:account_id/cards/video_app_download',
368 'ads/sandbox/accounts/:account_id/cards/video_conversation',
369 'ads/sandbox/accounts/:account_id/cards/website',
370 'ads/sandbox/accounts/:account_id/line_items',
371 'ads/sandbox/accounts/:account_id/media_creatives',
372 'ads/sandbox/accounts/:account_id/promoted_accounts',
373 'ads/sandbox/accounts/:account_id/promoted_tweets',
374 'ads/sandbox/accounts/:account_id/tailored_audience_changes',
375 'ads/sandbox/accounts/:account_id/tailored_audiences',
376 'ads/sandbox/accounts/:account_id/targeting_criteria',
377 'ads/sandbox/accounts/:account_id/tweet',
378 'ads/sandbox/accounts/:account_id/videos',
379 'ads/sandbox/accounts/:account_id/web_event_tags',
380 'ads/sandbox/batch/accounts/:account_id/campaigns',
381 'ads/sandbox/batch/accounts/:account_id/line_items',
382 'ads/sandbox/batch/accounts/:account_id/tailored_audiences',
383 'ads/sandbox/batch/accounts/:account_id/targeting_criteria',
386 'collections/create',
387 'collections/destroy',
388 'collections/entries/add',
389 'collections/entries/curate',
390 'collections/entries/move',
391 'collections/entries/remove',
392 'collections/update',
393 'direct_messages/destroy',
394 'direct_messages/new',
397 'friendships/create',
398 'friendships/destroy',
399 'friendships/update',
402 'lists/members/create',
403 'lists/members/create_all',
404 'lists/members/destroy',
405 'lists/members/destroy_all',
406 'lists/subscribers/create',
407 'lists/subscribers/destroy',
410 'mutes/users/create',
411 'mutes/users/destroy',
412 'oauth/access_token',
413 'oauth/request_token',
414 'oauth2/invalidate_token',
416 'saved_searches/create',
417 'saved_searches/destroy/:id',
418 'statuses/destroy/:id',
421 'statuses/retweet/:id',
422 'statuses/unretweet/:id',
424 'statuses/update_with_media', // deprecated, use media/upload
425 'ton/bucket/:bucket',
426 'ton/bucket/:bucket?resumable=true',
431 'ads/accounts/:account_id/campaigns/:campaign_id',
432 'ads/accounts/:account_id/cards/app_download/:card_id',
433 'ads/accounts/:account_id/cards/image_app_download/:card_id',
434 'ads/accounts/:account_id/cards/image_conversation/:card_id',
435 'ads/accounts/:account_id/cards/lead_gen/:card_id',
436 'ads/accounts/:account_id/cards/video_app_download/:id',
437 'ads/accounts/:account_id/cards/video_conversation/:card_id',
438 'ads/accounts/:account_id/cards/website/:card_id',
439 'ads/accounts/:account_id/line_items/:line_item_id',
440 'ads/accounts/:account_id/promoted_tweets/:id',
441 'ads/accounts/:account_id/tailored_audiences/global_opt_out',
442 'ads/accounts/:account_id/targeting_criteria',
443 'ads/accounts/:account_id/videos/:id',
444 'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
445 'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
446 'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
447 'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
448 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
449 'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
450 'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
451 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
452 'ads/sandbox/accounts/:account_id/cards/website/:card_id',
453 'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
454 'ads/sandbox/accounts/:account_id/promoted_tweets/:id',
455 'ads/sandbox/accounts/:account_id/tailored_audiences/global_opt_out',
456 'ads/sandbox/accounts/:account_id/targeting_criteria',
457 'ads/sandbox/accounts/:account_id/videos/:id',
458 'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id',
459 'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
462 'ads/accounts/:account_id/campaigns/:campaign_id',
463 'ads/accounts/:account_id/cards/app_download/:card_id',
464 'ads/accounts/:account_id/cards/image_app_download/:card_id',
465 'ads/accounts/:account_id/cards/image_conversation/:card_id',
466 'ads/accounts/:account_id/cards/lead_gen/:card_id',
467 'ads/accounts/:account_id/cards/video_app_download/:id',
468 'ads/accounts/:account_id/cards/video_conversation/:card_id',
469 'ads/accounts/:account_id/cards/website/:card_id',
470 'ads/accounts/:account_id/line_items/:line_item_id',
471 'ads/accounts/:account_id/media_creatives/:id',
472 'ads/accounts/:account_id/promoted_tweets/:id',
473 'ads/accounts/:account_id/tailored_audiences/:id',
474 'ads/accounts/:account_id/targeting_criteria/:id',
475 'ads/accounts/:account_id/videos/:id',
476 'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
477 'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
478 'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
479 'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
480 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
481 'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
482 'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
483 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
484 'ads/sandbox/accounts/:account_id/cards/website/:card_id',
485 'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
486 'ads/sandbox/accounts/:account_id/media_creatives/:id',
487 'ads/sandbox/accounts/:account_id/promoted_tweets/:id',
488 'ads/sandbox/accounts/:account_id/tailored_audiences/:id',
489 'ads/sandbox/accounts/:account_id/targeting_criteria/:id',
490 'ads/sandbox/accounts/:account_id/videos/:id',
491 'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id'
496 * Possible file name parameters
498 protected static $_possible_files = [
500 'statuses/update_with_media' => ['media[]'],
501 'media/upload' => ['media'],
503 'account/update_profile_background_image' => ['image'],
504 'account/update_profile_image' => ['image'],
505 'account/update_profile_banner' => ['banner']
509 * The current Codebird version
511 protected static $_version = '3.1.0';
514 * The Request or access token. Used to sign requests
516 protected $_oauth_token = null;
519 * The corresponding request or access token secret
521 protected $_oauth_token_secret = null;
524 * The format of data to return from API calls
526 protected $_return_format = CODEBIRD_RETURNFORMAT_OBJECT;
529 * The callback to call with any new streaming messages
531 protected $_streaming_callback = null;
534 * Auto-detect cURL absence
536 protected $_use_curl = true;
541 protected $_timeouts = [
550 protected $_proxy = [];
557 public function __construct()
559 // Pre-define $_use_curl depending on cURL availability
560 $this->setUseCurl(function_exists('curl_init'));
564 * Returns singleton class instance
565 * Always use this method unless you're working with multiple authenticated users at once
567 * @return Codebird The instance
569 public static function getInstance()
571 if (self::$_instance === null) {
572 self::$_instance = new self;
574 return self::$_instance;
578 * Sets the OAuth consumer key and secret (App key)
580 * @param string $key OAuth consumer key
581 * @param string $secret OAuth consumer secret
585 public static function setConsumerKey($key, $secret)
587 self::$_consumer_key = $key;
588 self::$_consumer_secret = $secret;
592 * Sets the OAuth2 app-only auth bearer token
594 * @param string $token OAuth2 bearer token
598 public static function setBearerToken($token)
600 self::$_bearer_token = $token;
604 * Gets the current Codebird version
606 * @return string The version number
608 public function getVersion()
610 return self::$_version;
614 * Sets the OAuth request or access token and secret (User key)
616 * @param string $token OAuth request or access token
617 * @param string $secret OAuth request or access token secret
621 public function setToken($token, $secret)
623 $this->_oauth_token = $token;
624 $this->_oauth_token_secret = $secret;
628 * Forgets the OAuth request or access token and secret (User key)
632 public function logout()
634 $this->_oauth_token =
635 $this->_oauth_token_secret = null;
641 * Sets if codebird should use cURL
643 * @param bool $use_curl Request uses cURL or not
648 public function setUseCurl($use_curl)
650 if ($use_curl && ! function_exists('curl_init')) {
651 throw new \Exception('To use cURL, the PHP curl extension must be available.');
654 $this->_use_curl = (bool) $use_curl;
658 * Sets request timeout in milliseconds
660 * @param int $timeout Request timeout in milliseconds
664 public function setTimeout($timeout)
669 $this->_timeouts['request'] = (int) $timeout;
673 * Sets connection timeout in milliseconds
675 * @param int $timeout Connection timeout in milliseconds
679 public function setConnectionTimeout($timeout)
684 $this->_timeouts['connect'] = (int) $timeout;
688 * Sets remote media download timeout in milliseconds
690 * @param int $timeout Remote media timeout in milliseconds
694 public function setRemoteDownloadTimeout($timeout)
699 $this->_timeouts['remote'] = (int) $timeout;
703 * Sets the format for API replies
705 * @param int $return_format One of these:
706 * CODEBIRD_RETURNFORMAT_OBJECT (default)
707 * CODEBIRD_RETURNFORMAT_ARRAY
708 * CODEBIRD_RETURNFORMAT_JSON
712 public function setReturnFormat($return_format)
714 $this->_return_format = $return_format;
720 * @param string $host Proxy host
721 * @param int $port Proxy port
722 * @param int optional $type Proxy type, defaults to HTTP
727 public function setProxy($host, $port, $type = CURLPROXY_HTTP)
729 static $types_str = [
730 'HTTP', 'SOCKS4', 'SOCKS5', 'SOCKS4A', 'SOCKS5_HOSTNAME'
733 foreach ($types_str as $type_str) {
734 if (defined('CURLPROXY_' . $type_str)) {
735 $types[] = constant('CURLPROXY_' . $type_str);
738 if (! in_array($type, $types)) {
739 throw new \Exception('Invalid proxy type specified.');
742 $this->_proxy['host'] = $host;
743 $this->_proxy['port'] = (int) $port;
744 $this->_proxy['type'] = $type;
748 * Sets the proxy authentication
750 * @param string $authentication Proxy authentication
754 public function setProxyAuthentication($authentication)
756 $this->_proxy['authentication'] = $authentication;
760 * Sets streaming callback
762 * @param callable $callback The streaming callback
767 public function setStreamingCallback($callback)
769 if (!is_callable($callback)) {
770 throw new \Exception('This is not a proper callback.');
772 $this->_streaming_callback = $callback;
776 * Get allowed API methods, sorted by HTTP method
777 * Watch out for multiple-method API methods!
779 * @return array $apimethods
781 public function getApiMethods()
783 return self::$_api_methods;
787 * Main API handler working on any requests you issue
789 * @param string $function The member function you called
790 * @param array $params The parameters you sent along
792 * @return string The API reply encoded in the set return_format
795 public function __call($function, $params)
798 if (substr($function, 0, 6) === '_curl_'
799 || $function === '_time'
800 || $function === '_microtime'
802 return call_user_func_array(substr($function, 1), $params);
806 $apiparams = $this->_parseApiParams($params);
808 // stringify null and boolean parameters
809 $apiparams = $this->_stringifyNullBoolParams($apiparams);
811 $app_only_auth = false;
812 if (count($params) > 1) {
813 // convert app_only_auth param to bool
814 $app_only_auth = !! $params[1];
817 // reset token when requesting a new token
818 // (causes 401 for signature error on subsequent requests)
819 if ($function === 'oauth_requestToken') {
820 $this->setToken(null, null);
823 // map function name to API method
824 list($method, $method_template) = $this->_mapFnToApiMethod($function, $apiparams);
826 $httpmethod = $this->_detectMethod($method_template, $apiparams);
827 $multipart = $this->_detectMultipart($method_template);
829 return $this->_callApi(
845 * Parse given params, detect query-style params
847 * @param array|string $params Parameters to parse
849 * @return array $apiparams
851 protected function _parseApiParams($params)
854 if (count($params) === 0) {
858 if (is_array($params[0])) {
859 // given parameters are array
860 $apiparams = $params[0];
864 // user gave us query-style params
865 parse_str($params[0], $apiparams);
866 if (! is_array($apiparams)) {
874 * Replace null and boolean parameters with their string representations
876 * @param array $apiparams Parameter array to replace in
878 * @return array $apiparams
880 protected function _stringifyNullBoolParams($apiparams)
882 foreach ($apiparams as $key => $value) {
883 if (! is_scalar($value)) {
884 // no need to try replacing arrays
887 if (is_null($value)) {
888 $apiparams[$key] = 'null';
889 } elseif (is_bool($value)) {
890 $apiparams[$key] = $value ? 'true' : 'false';
898 * Maps called PHP magic method name to Twitter API method
900 * @param string $function Function called
901 * @param array $apiparams byref API parameters
903 * @return string[] (string method, string method_template)
905 protected function _mapFnToApiMethod($function, &$apiparams)
908 $method = $this->_mapFnInsertSlashes($function);
910 // undo replacement for URL parameters
911 $method = $this->_mapFnRestoreParamUnderscores($method);
913 // replace AA by URL parameters
914 list ($method, $method_template) = $this->_mapFnInlineParams($method, $apiparams);
916 if (substr($method, 0, 4) !== 'ton/') {
917 // replace A-Z by _a-z
918 for ($i = 0; $i < 26; $i++) {
919 $method = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method);
920 $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
924 return [$method, $method_template];
928 * API method mapping: Replaces _ with / character
930 * @param string $function Function called
932 * @return string API method to call
934 protected function _mapFnInsertSlashes($function)
936 return str_replace('_', '/', $function);
940 * API method mapping: Restore _ character in named parameters
942 * @param string $method API method to call
944 * @return string API method with restored underscores
946 protected function _mapFnRestoreParamUnderscores($method)
949 'screen_name', 'place_id',
950 'account_id', 'campaign_id', 'card_id', 'line_item_id',
951 'tweet_id', 'web_event_tag_id'
953 foreach ($params as $param) {
954 $param = strtoupper($param);
955 $replacement_was = str_replace('_', '/', $param);
956 $method = str_replace($replacement_was, $param, $method);
963 * Inserts inline parameters into the method name
965 * @param string $method The method to call
966 * @param array byref $apiparams The parameters to send along
968 * @return string[] (string method, string method_template)
971 protected function _mapFnInlineParams($method, &$apiparams)
973 $method_template = $method;
975 if (preg_match_all('/[A-Z_]{2,}/', $method, $match)) {
976 foreach ($match[0] as $param) {
977 $param_l = strtolower($param);
978 if ($param_l === 'resumeid') {
979 $param_l = 'resumeId';
981 $method_template = str_replace($param, ':' . $param_l, $method_template);
982 if (! isset($apiparams[$param_l])) {
983 for ($i = 0; $i < 26; $i++) {
984 $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
986 throw new \Exception(
987 'To call the templated method "' . $method_template
988 . '", specify the parameter value for "' . $param_l . '".'
991 $method = str_replace($param, $apiparams[$param_l], $method);
992 unset($apiparams[$param_l]);
996 return [$method, $method_template];
1000 * Avoids any JSON_BIGINT_AS_STRING errors
1002 * @param string $data JSON data to decode
1003 * @param int optional $need_array Decode as array, otherwise as object
1005 * @return array|object The decoded object
1007 protected function _json_decode($data, $need_array = false)
1009 if (!(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
1010 return json_decode($data, $need_array, 512, JSON_BIGINT_AS_STRING);
1012 $max_int_length = strlen((string) PHP_INT_MAX) - 1;
1013 $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $data);
1014 $obj = json_decode($json_without_bigints, $need_array);
1019 * Uncommon API methods
1023 * Gets the OAuth authenticate URL for the current request token
1025 * @param optional bool $force_login Whether to force the user to enter their login data
1026 * @param optional string $screen_name Screen name to repopulate the user name with
1027 * @param optional string $type 'authenticate' or 'authorize', to avoid duplicate code
1029 * @return string The OAuth authenticate/authorize URL
1030 * @throws \Exception
1032 public function oauth_authenticate($force_login = NULL, $screen_name = NULL, $type = 'authenticate')
1034 if (! in_array($type, ['authenticate', 'authorize'])) {
1035 throw new \Exception('To get the ' . $type . ' URL, use the correct third parameter, or omit it.');
1037 if ($this->_oauth_token === null) {
1038 throw new \Exception('To get the ' . $type . ' URL, the OAuth token must be set.');
1040 $url = self::$_endpoints['oauth'] . 'oauth/' . $type . '?oauth_token=' . $this->_url($this->_oauth_token);
1042 $url .= "&force_login=1";
1045 $url .= "&screen_name=" . $screen_name;
1051 * Gets the OAuth authorize URL for the current request token
1052 * @param optional bool $force_login Whether to force the user to enter their login data
1053 * @param optional string $screen_name Screen name to repopulate the user name with
1055 * @return string The OAuth authorize URL
1057 public function oauth_authorize($force_login = NULL, $screen_name = NULL)
1059 return $this->oauth_authenticate($force_login, $screen_name, 'authorize');
1063 * Gets the OAuth bearer token
1065 * @return string The OAuth bearer token
1068 public function oauth2_token()
1070 if ($this->_use_curl) {
1071 return $this->_oauth2TokenCurl();
1073 return $this->_oauth2TokenNoCurl();
1077 * Gets a cURL handle
1078 * @param string $url the URL for the curl initialization
1079 * @return resource handle
1081 protected function _getCurlInitialization($url)
1083 $connection = $this->_curl_init($url);
1085 $this->_curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1);
1086 $this->_curl_setopt($connection, CURLOPT_FOLLOWLOCATION, 0);
1087 $this->_curl_setopt($connection, CURLOPT_HEADER, 1);
1088 $this->_curl_setopt($connection, CURLOPT_SSL_VERIFYPEER, 1);
1089 $this->_curl_setopt($connection, CURLOPT_SSL_VERIFYHOST, 2);
1090 $this->_curl_setopt($connection, CURLOPT_CAINFO, __DIR__ . '/cacert.pem');
1091 $this->_curl_setopt(
1092 $connection, CURLOPT_USERAGENT,
1093 'codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1096 if ($this->_hasProxy()) {
1097 $this->_curl_setopt($connection, CURLOPT_PROXYTYPE, $this->_getProxyType());
1098 $this->_curl_setopt($connection, CURLOPT_PROXY, $this->_getProxyHost());
1099 $this->_curl_setopt($connection, CURLOPT_PROXYPORT, $this->_getProxyPort());
1101 if ($this->_getProxyAuthentication()) {
1102 $this->_curl_setopt($connection, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
1103 $this->_curl_setopt($connection, CURLOPT_PROXYUSERPWD, $this->_getProxyAuthentication());
1111 * Gets a non cURL initialization
1113 * @param string $url the URL for the curl initialization
1114 * @param array $contextOptions the options for the stream context
1115 * @param string $hostname the hostname to verify the SSL FQDN for
1117 * @return array the read data
1119 protected function _getNoCurlInitialization($url, $contextOptions, $hostname = '')
1123 $httpOptions['header'] = [
1124 'User-Agent: codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1127 $httpOptions['ssl'] = [
1128 'verify_peer' => true,
1129 'cafile' => __DIR__ . '/cacert.pem',
1130 'verify_depth' => 5,
1131 'peer_name' => $hostname
1134 if ($this->_hasProxy()) {
1135 $httpOptions['request_fulluri'] = true;
1136 $httpOptions['proxy'] = $this->_getProxyHost() . ':' . $this->_getProxyPort();
1138 if ($this->_getProxyAuthentication()) {
1139 $httpOptions['header'][] =
1140 'Proxy-Authorization: Basic ' . base64_encode($this->_getProxyAuthentication());
1144 // merge the http options with the context options
1145 $options = array_merge_recursive(
1147 ['http' => $httpOptions]
1150 // concatenate $options['http']['header']
1151 $options['http']['header'] = implode("\r\n", $options['http']['header']);
1153 // silent the file_get_contents function
1154 $content = @file_get_contents($url, false, stream_context_create($options));
1157 // API is responding
1158 if (isset($http_response_header)) {
1159 $headers = $http_response_header;
1168 protected function _hasProxy()
1170 return isset($this->_proxy['host']) || isset($this->_proxy['port']);
1174 * Gets the proxy host
1176 * @return string The proxy host
1178 protected function _getProxyHost()
1180 return $this->_getProxyData('host');
1184 * Gets the proxy port
1186 * @return string The proxy port
1188 protected function _getProxyPort()
1190 return $this->_getProxyData('port');
1194 * Gets the proxy authentication
1196 * @return string The proxy authentication
1198 protected function _getProxyAuthentication()
1200 return $this->_getProxyData('authentication');
1204 * Gets the proxy type
1206 * @return string The proxy type
1208 protected function _getProxyType()
1210 return $this->_getProxyData('type');
1214 * Gets data from the proxy configuration
1216 * @param string $name
1218 private function _getProxyData($name)
1220 return empty($this->_proxy[$name]) ? null : $this->_proxy[$name];
1224 * Gets the OAuth bearer token, using cURL
1226 * @return string The OAuth bearer token
1227 * @throws \Exception
1230 protected function _oauth2TokenCurl()
1232 if (self::$_consumer_key === null) {
1233 throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1236 'grant_type' => 'client_credentials'
1238 $url = self::$_endpoints['oauth'] . 'oauth2/token';
1239 $connection = $this->_getCurlInitialization($url);
1240 $this->_curl_setopt($connection, CURLOPT_POST, 1);
1241 $this->_curl_setopt($connection, CURLOPT_POSTFIELDS, $post_fields);
1243 $this->_curl_setopt($connection, CURLOPT_USERPWD, self::$_consumer_key . ':' . self::$_consumer_secret);
1244 $this->_curl_setopt($connection, CURLOPT_HTTPHEADER, [
1247 $result = $this->_curl_exec($connection);
1249 // catch request errors
1250 if ($result === false) {
1251 throw new \Exception('Request error for bearer token: ' . $this->_curl_error($connection));
1254 // certificate validation results
1255 $validation_result = $this->_curl_errno($connection);
1256 $this->_validateSslCertificate($validation_result);
1258 $httpstatus = $this->_curl_getinfo($connection, CURLINFO_HTTP_CODE);
1259 $reply = $this->_parseBearerReply($result, $httpstatus);
1264 * Gets the OAuth bearer token, without cURL
1266 * @return string The OAuth bearer token
1267 * @throws \Exception
1270 protected function _oauth2TokenNoCurl()
1272 if (self::$_consumer_key == null) {
1273 throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1276 $url = self::$_endpoints['oauth'] . 'oauth2/token';
1277 $hostname = parse_url($url, PHP_URL_HOST);
1279 if ($hostname === false) {
1280 throw new \Exception('Incorrect API endpoint host.');
1286 'protocol_version' => '1.1',
1287 'header' => "Accept: */*\r\n"
1288 . 'Authorization: Basic '
1290 self::$_consumer_key
1292 . self::$_consumer_secret
1294 'timeout' => $this->_timeouts['request'] / 1000,
1295 'content' => 'grant_type=client_credentials',
1296 'ignore_errors' => true
1299 list($reply, $headers) = $this->_getNoCurlInitialization($url, $contextOptions, $hostname);
1301 foreach ($headers as $header) {
1302 $result .= $header . "\r\n";
1304 $result .= "\r\n" . $reply;
1307 $httpstatus = $this->_getHttpStatusFromHeaders($headers);
1308 $reply = $this->_parseBearerReply($result, $httpstatus);
1314 * General helpers to avoid duplicate code
1318 * Extract HTTP status code from headers
1320 * @param array $headers The headers to parse
1322 * @return string The HTTP status code
1324 protected function _getHttpStatusFromHeaders($headers)
1326 $httpstatus = '500';
1328 if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
1329 $httpstatus = $match[1];
1335 * Parse oauth2_token reply and set bearer token, if found
1337 * @param string $result Raw HTTP response
1338 * @param int $httpstatus HTTP status code
1340 * @return string reply
1342 protected function _parseBearerReply($result, $httpstatus)
1344 list($headers, $reply) = $this->_parseApiHeaders($result);
1345 $reply = $this->_parseApiReply($reply);
1346 $rate = $this->_getRateLimitInfo($headers);
1347 switch ($this->_return_format) {
1348 case CODEBIRD_RETURNFORMAT_ARRAY:
1349 $reply['httpstatus'] = $httpstatus;
1350 $reply['rate'] = $rate;
1351 if ($httpstatus === 200) {
1352 self::setBearerToken($reply['access_token']);
1355 case CODEBIRD_RETURNFORMAT_JSON:
1356 if ($httpstatus === 200) {
1357 $parsed = $this->_json_decode($reply);
1358 self::setBearerToken($parsed->access_token);
1361 case CODEBIRD_RETURNFORMAT_OBJECT:
1362 $reply->httpstatus = $httpstatus;
1363 $reply->rate = $rate;
1364 if ($httpstatus === 200) {
1365 self::setBearerToken($reply->access_token);
1373 * Extract rate-limiting data from response headers
1375 * @param array $headers The CURL response headers
1377 * @return null|array|object The rate-limiting information
1379 protected function _getRateLimitInfo($headers)
1381 if (! isset($headers['x-rate-limit-limit'])) {
1385 'limit' => $headers['x-rate-limit-limit'],
1386 'remaining' => $headers['x-rate-limit-remaining'],
1387 'reset' => $headers['x-rate-limit-reset']
1389 if ($this->_return_format === CODEBIRD_RETURNFORMAT_OBJECT) {
1390 return (object) $rate;
1396 * Check if there were any SSL certificate errors
1398 * @param int $validation_result The curl error number
1401 * @throws \Exception
1403 protected function _validateSslCertificate($validation_result)
1408 CURLE_SSL_CERTPROBLEM,
1410 CURLE_SSL_CACERT_BADFILE,
1411 CURLE_SSL_CRL_BADFILE,
1412 CURLE_SSL_ISSUER_ERROR
1416 throw new \Exception(
1417 'Error ' . $validation_result
1418 . ' while validating the Twitter API certificate.'
1428 * URL-encodes the given data
1430 * @param mixed $data
1432 * @return mixed The encoded data
1434 protected function _url($data)
1436 if (is_array($data)) {
1441 } elseif (is_scalar($data)) {
1442 return str_replace([
1456 ], rawurlencode($data));
1462 * Gets the base64-encoded SHA1 hash for the given data
1464 * @param string $data The data to calculate the hash from
1466 * @return string The hash
1467 * @throws \Exception
1469 protected function _sha1($data)
1471 if (self::$_consumer_secret === null) {
1472 throw new \Exception('To generate a hash, the consumer secret must be set.');
1474 if (!function_exists('hash_hmac')) {
1475 throw new \Exception('To generate a hash, the PHP hash extension must be available.');
1477 return base64_encode(hash_hmac(
1480 self::$_consumer_secret
1482 . ($this->_oauth_token_secret !== null
1483 ? $this->_oauth_token_secret
1491 * Generates a (hopefully) unique random string
1493 * @param int optional $length The length of the string to generate
1495 * @return string The random string
1496 * @throws \Exception
1498 protected function _nonce($length = 8)
1501 throw new \Exception('Invalid nonce length.');
1503 return substr(md5($this->_microtime(true)), 0, $length);
1509 * @param string $httpmethod Usually either 'GET' or 'POST' or 'DELETE'
1510 * @param string $method The API method to call
1511 * @param array $base_params The signature base parameters
1513 * @return string signature
1515 protected function _getSignature($httpmethod, $method, $base_params)
1517 // convert params to string
1519 foreach ($base_params as $key => $value) {
1520 $base_string .= $key . '=' . $value . '&';
1523 // trim last ampersand
1524 $base_string = substr($base_string, 0, -1);
1527 return $this->_sha1(
1529 $this->_url($method) . '&' .
1530 $this->_url($base_string)
1535 * Generates an OAuth signature
1537 * @param string $httpmethod Usually either 'GET' or 'POST' or 'DELETE'
1538 * @param string $method The API method to call
1539 * @param array optional $params The API call parameters, associative
1541 * @return string Authorization HTTP header
1542 * @throws \Exception
1544 protected function _sign($httpmethod, $method, $params = [])
1546 if (self::$_consumer_key === null) {
1547 throw new \Exception('To generate a signature, the consumer key must be set.');
1549 $sign_base_params = array_map(
1552 'oauth_consumer_key' => self::$_consumer_key,
1553 'oauth_version' => '1.0',
1554 'oauth_timestamp' => $this->_time(),
1555 'oauth_nonce' => $this->_nonce(),
1556 'oauth_signature_method' => 'HMAC-SHA1'
1559 if ($this->_oauth_token !== null) {
1560 $sign_base_params['oauth_token'] = $this->_url($this->_oauth_token);
1562 $oauth_params = $sign_base_params;
1564 // merge in the non-OAuth params
1565 $sign_base_params = array_merge(
1567 array_map([$this, '_url'], $params)
1569 ksort($sign_base_params);
1571 $signature = $this->_getSignature($httpmethod, $method, $sign_base_params);
1573 $params = $oauth_params;
1574 $params['oauth_signature'] = $signature;
1577 $authorization = 'OAuth ';
1578 foreach ($params as $key => $value) {
1579 $authorization .= $key . "=\"" . $this->_url($value) . "\", ";
1581 return substr($authorization, 0, -2);
1585 * Detects HTTP method to use for API call
1587 * @param string $method The API method to call
1588 * @param array byref $params The parameters to send along
1590 * @return string The HTTP method that should be used
1592 protected function _detectMethod($method, &$params)
1594 if (isset($params['httpmethod'])) {
1595 $httpmethod = $params['httpmethod'];
1596 unset($params['httpmethod']);
1599 $apimethods = $this->getApiMethods();
1601 // multi-HTTP method API methods
1602 // parameter-based detection
1603 $httpmethods_by_param = [
1606 'ads/accounts/:account_id/line_items',
1607 'ads/sandbox/accounts/:account_id/line_items'
1610 'ads/accounts/:account_id/account_media',
1611 'ads/sandbox/accounts/:account_id/account_media'
1614 'ads/accounts/:account_id/app_lists',
1615 'ads/accounts/:account_id/campaigns',
1616 'ads/accounts/:account_id/cards/app_download',
1617 'ads/accounts/:account_id/cards/image_app_download',
1618 'ads/accounts/:account_id/cards/image_conversation',
1619 'ads/accounts/:account_id/cards/lead_gen',
1620 'ads/accounts/:account_id/cards/video_app_download',
1621 'ads/accounts/:account_id/cards/video_conversation',
1622 'ads/accounts/:account_id/cards/website',
1623 'ads/accounts/:account_id/tailored_audiences',
1624 'ads/accounts/:account_id/web_event_tags',
1625 'ads/sandbox/accounts/:account_id/app_lists',
1626 'ads/sandbox/accounts/:account_id/campaigns',
1627 'ads/sandbox/accounts/:account_id/cards/app_download',
1628 'ads/sandbox/accounts/:account_id/cards/image_app_download',
1629 'ads/sandbox/accounts/:account_id/cards/image_conversation',
1630 'ads/sandbox/accounts/:account_id/cards/lead_gen',
1631 'ads/sandbox/accounts/:account_id/cards/video_app_download',
1632 'ads/sandbox/accounts/:account_id/cards/video_conversation',
1633 'ads/sandbox/accounts/:account_id/cards/website',
1634 'ads/sandbox/accounts/:account_id/tailored_audiences',
1635 'ads/sandbox/accounts/:account_id/web_event_tags'
1637 'tailored_audience_id' => [
1638 'ads/accounts/:account_id/tailored_audience_changes',
1639 'ads/sandbox/accounts/:account_id/tailored_audience_changes'
1641 'targeting_value' => [
1642 'ads/accounts/:account_id/targeting_criteria',
1643 'ads/sandbox/accounts/:account_id/targeting_criteria'
1646 'ads/accounts/:account_id/promoted_tweets',
1647 'ads/sandbox/accounts/:account_id/promoted_tweets'
1650 'ads/accounts/:account_id/promoted_accounts',
1651 'ads/sandbox/accounts/:account_id/promoted_accounts'
1653 'video_media_id' => [
1654 'ads/accounts/:account_id/videos',
1655 'ads/sandbox/accounts/:account_id/videos'
1660 'ads/accounts/:account_id/cards/image_conversation/:card_id',
1661 'ads/accounts/:account_id/cards/video_conversation/:card_id',
1662 'ads/accounts/:account_id/cards/website/:card_id',
1663 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
1664 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
1665 'ads/sandbox/accounts/:account_id/cards/website/:card_id'
1669 foreach ($httpmethods_by_param as $httpmethod => $methods_by_param) {
1670 foreach ($methods_by_param as $param => $methods) {
1671 if (in_array($method, $methods) && isset($params[$param])) {
1677 // prefer POST and PUT if parameters are set
1678 if (count($params) > 0) {
1679 if (in_array($method, $apimethods['POST'])) {
1682 if (in_array($method, $apimethods['PUT'])) {
1687 foreach ($apimethods as $httpmethod => $methods) {
1688 if (in_array($method, $methods)) {
1692 throw new \Exception('Can\'t find HTTP method to use for "' . $method . '".');
1696 * Detects if API call should use multipart/form-data
1698 * @param string $method The API method to call
1700 * @return bool Whether the method should be sent as multipart
1702 protected function _detectMultipart($method)
1706 'statuses/update_with_media',
1710 // no multipart for these, for now:
1711 //'account/update_profile_background_image',
1712 //'account/update_profile_image',
1713 //'account/update_profile_banner'
1715 return in_array($method, $multiparts);
1719 * Merge multipart string from parameters array
1721 * @param string $method_template The method template to call
1722 * @param string $border The multipart border
1723 * @param array $params The parameters to send along
1725 * @return string request
1726 * @throws \Exception
1728 protected function _getMultipartRequestFromParams($method_template, $border, $params)
1731 foreach ($params as $key => $value) {
1733 if (is_array($value)) {
1734 throw new \Exception('Using URL-encoded parameters is not supported for uploading media.');
1737 '--' . $border . "\r\n"
1738 . 'Content-Disposition: form-data; name="' . $key . '"';
1740 // check for filenames
1741 $data = $this->_checkForFiles($method_template, $key, $value);
1742 if ($data !== false) {
1746 $request .= "\r\n\r\n" . $value . "\r\n";
1755 * @param string $method_template The method template to call
1756 * @param string $key The parameter name
1757 * @param string $value The possible file name or URL
1761 protected function _checkForFiles($method_template, $key, $value) {
1762 if (!array_key_exists($method_template, self::$_possible_files)
1763 || !in_array($key, self::$_possible_files[$method_template])
1767 $data = $this->_buildBinaryBody($value);
1768 if ($data === $value) {
1776 * Detect filenames in upload parameters,
1777 * build multipart request from upload params
1779 * @param string $method The API method to call
1780 * @param array $params The parameters to send along
1782 * @return null|string
1784 protected function _buildMultipart($method, $params)
1786 // well, files will only work in multipart methods
1787 if (! $this->_detectMultipart($method)) {
1791 // only check specific parameters
1792 // method might have files?
1793 if (! in_array($method, array_keys(self::$_possible_files))) {
1797 $multipart_border = '--------------------' . $this->_nonce();
1798 $multipart_request =
1799 $this->_getMultipartRequestFromParams($method, $multipart_border, $params)
1800 . '--' . $multipart_border . '--';
1802 return $multipart_request;
1806 * Detect filenames in upload parameters
1808 * @param mixed $input The data or file name to parse
1810 * @return null|string
1812 protected function _buildBinaryBody($input)
1814 if (// is it a file, a readable one?
1815 @file_exists($input)
1816 && @is_readable($input)
1818 // try to read the file
1819 $data = @file_get_contents($input);
1820 if ($data !== false && strlen($data) !== 0) {
1823 } elseif (// is it a remote file?
1824 filter_var($input, FILTER_VALIDATE_URL)
1825 && preg_match('/^https?:\/\//', $input)
1827 $data = $this->_fetchRemoteFile($input);
1828 if ($data !== false) {
1836 * Fetches a remote file
1838 * @param string $url The URL to download from
1840 * @return mixed The file contents or FALSE
1841 * @throws \Exception
1843 protected function _fetchRemoteFile($url)
1845 // try to fetch the file
1846 if ($this->_use_curl) {
1847 $connection = $this->_getCurlInitialization($url);
1848 $this->_curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1);
1849 $this->_curl_setopt($connection, CURLOPT_HEADER, 0);
1850 // no SSL validation for downloading media
1851 $this->_curl_setopt($connection, CURLOPT_SSL_VERIFYPEER, 1);
1852 $this->_curl_setopt($connection, CURLOPT_SSL_VERIFYHOST, 2);
1853 $this->_curl_setopt($connection, CURLOPT_TIMEOUT_MS, $this->_timeouts['remote']);
1854 $this->_curl_setopt($connection, CURLOPT_CONNECTTIMEOUT_MS, $this->_timeouts['remote'] / 2);
1855 // find files that have been redirected
1856 $this->_curl_setopt($connection, CURLOPT_FOLLOWLOCATION, true);
1857 // process compressed images
1858 $this->_curl_setopt($connection, CURLOPT_ENCODING, 'gzip,deflate,sdch');
1859 $result = $this->_curl_exec($connection);
1860 if ($result !== false
1861 && $this->_curl_getinfo($connection, CURLINFO_HTTP_CODE) === 200
1865 throw new \Exception('Downloading a remote media file failed.');
1872 'protocol_version' => '1.1',
1873 'timeout' => $this->_timeouts['remote']
1876 'verify_peer' => false
1879 list($result, $headers) = $this->_getNoCurlInitialization($url, $contextOptions);
1880 if ($result !== false
1881 && preg_match('/^HTTP\/\d\.\d 200 OK$/', $headers[0])
1885 throw new \Exception('Downloading a remote media file failed.');
1890 * Detects if API call should use media endpoint
1892 * @param string $method The API method to call
1894 * @return bool Whether the method is defined in media API
1896 protected function _detectMedia($method) {
1900 return in_array($method, $medias);
1904 * Detects if API call should use JSON body
1906 * @param string $method The API method to call
1908 * @return bool Whether the method is defined as accepting JSON body
1910 protected function _detectJsonBody($method) {
1912 'collections/entries/curate'
1914 return in_array($method, $json_bodies);
1918 * Detects if API call should use binary body
1920 * @param string $method_template The API method to call
1922 * @return bool Whether the method is defined as accepting binary body
1924 protected function _detectBinaryBody($method_template) {
1926 'ton/bucket/:bucket',
1927 'ton/bucket/:bucket?resumable=true',
1928 'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
1930 return in_array($method_template, $binary);
1934 * Detects if API call should use streaming endpoint, and if yes, which one
1936 * @param string $method The API method to call
1938 * @return string|false Variant of streaming API to be used
1940 protected function _detectStreaming($method) {
1950 foreach ($streamings as $key => $values) {
1951 if (in_array($method, $values)) {
1960 * Builds the complete API endpoint url
1962 * @param string $method The API method to call
1963 * @param string $method_template The API method to call
1965 * @return string The URL to send the request to
1967 protected function _getEndpoint($method, $method_template)
1969 $url = self::$_endpoints['rest'] . $method . '.json';
1970 if (substr($method_template, 0, 5) === 'oauth') {
1971 $url = self::$_endpoints['oauth'] . $method;
1972 } elseif ($this->_detectMedia($method_template)) {
1973 $url = self::$_endpoints['media'] . $method . '.json';
1974 } elseif ($variant = $this->_detectStreaming($method_template)) {
1975 $url = self::$_endpoints['streaming'][$variant] . $method . '.json';
1976 } elseif ($this->_detectBinaryBody($method_template)) {
1977 $url = self::$_endpoints['ton'] . $method;
1978 } elseif (substr($method_template, 0, 12) === 'ads/sandbox/') {
1979 $url = self::$_endpoints['ads']['sandbox'] . substr($method, 12);
1980 } elseif (substr($method_template, 0, 4) === 'ads/') {
1981 $url = self::$_endpoints['ads']['production'] . substr($method, 4);
1989 * @param string $httpmethod The HTTP method to use for making the request
1990 * @param string $method The API method to call
1991 * @param string $method_template The API method template to call
1992 * @param array optional $params The parameters to send along
1993 * @param bool optional $multipart Whether to use multipart/form-data
1994 * @param bool optional $app_only_auth Whether to use app-only bearer authentication
1996 * @return string The API reply, encoded in the set return_format
1997 * @throws \Exception
2000 protected function _callApi($httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false)
2002 if (! $app_only_auth
2003 && $this->_oauth_token === null
2004 && substr($method, 0, 5) !== 'oauth'
2006 throw new \Exception('To call this API, the OAuth access token must be set.');
2008 // use separate API access for streaming API
2009 if ($this->_detectStreaming($method) !== false) {
2010 return $this->_callApiStreaming($httpmethod, $method, $method_template, $params, $app_only_auth);
2013 if ($this->_use_curl) {
2014 return $this->_callApiCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
2016 return $this->_callApiNoCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
2020 * Calls the API using cURL
2022 * @param string $httpmethod The HTTP method to use for making the request
2023 * @param string $method The API method to call
2024 * @param string $method_template The API method template to call
2025 * @param array optional $params The parameters to send along
2026 * @param bool optional $multipart Whether to use multipart/form-data
2027 * @param bool optional $app_only_auth Whether to use app-only bearer authentication
2029 * @return string The API reply, encoded in the set return_format
2030 * @throws \Exception
2033 protected function _callApiCurl(
2034 $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
2037 list ($authorization, $url, $params, $request_headers)
2038 = $this->_callApiPreparations(
2039 $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2042 $connection = $this->_getCurlInitialization($url);
2043 $request_headers[] = 'Authorization: ' . $authorization;
2044 $request_headers[] = 'Expect:';
2046 if ($httpmethod !== 'GET') {
2047 $this->_curl_setopt($connection, CURLOPT_POST, 1);
2048 $this->_curl_setopt($connection, CURLOPT_POSTFIELDS, $params);
2049 if (in_array($httpmethod, ['POST', 'PUT', 'DELETE'])) {
2050 $this->_curl_setopt($connection, CURLOPT_CUSTOMREQUEST, $httpmethod);
2054 $this->_curl_setopt($connection, CURLOPT_HTTPHEADER, $request_headers);
2055 $this->_curl_setopt($connection, CURLOPT_TIMEOUT_MS, $this->_timeouts['request']);
2056 $this->_curl_setopt($connection, CURLOPT_CONNECTTIMEOUT_MS, $this->_timeouts['connect']);
2058 $result = $this->_curl_exec($connection);
2060 // catch request errors
2061 if ($result === false) {
2062 throw new \Exception('Request error for API call: ' . $this->_curl_error($connection));
2065 // certificate validation results
2066 $validation_result = $this->_curl_errno($connection);
2067 $this->_validateSslCertificate($validation_result);
2069 $httpstatus = $this->_curl_getinfo($connection, CURLINFO_HTTP_CODE);
2070 list($headers, $reply) = $this->_parseApiHeaders($result);
2071 // TON API & redirects
2072 $reply = $this->_parseApiReplyPrefillHeaders($headers, $reply);
2073 $reply = $this->_parseApiReply($reply);
2074 $rate = $this->_getRateLimitInfo($headers);
2076 $reply = $this->_appendHttpStatusAndRate($reply, $httpstatus, $rate);
2081 * Calls the API without cURL
2083 * @param string $httpmethod The HTTP method to use for making the request
2084 * @param string $method The API method to call
2085 * @param string $method_template The API method template to call
2086 * @param array optional $params The parameters to send along
2087 * @param bool optional $multipart Whether to use multipart/form-data
2088 * @param bool optional $app_only_auth Whether to use app-only bearer authentication
2090 * @return string The API reply, encoded in the set return_format
2091 * @throws \Exception
2094 protected function _callApiNoCurl(
2095 $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
2098 list ($authorization, $url, $params, $request_headers)
2099 = $this->_callApiPreparations(
2100 $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2103 $hostname = parse_url($url, PHP_URL_HOST);
2104 if ($hostname === false) {
2105 throw new \Exception('Incorrect API endpoint host.');
2108 $request_headers[] = 'Authorization: ' . $authorization;
2109 $request_headers[] = 'Accept: */*';
2110 $request_headers[] = 'Connection: Close';
2111 if ($httpmethod !== 'GET' && ! $multipart) {
2112 $request_headers[] = 'Content-Type: application/x-www-form-urlencoded';
2117 'method' => $httpmethod,
2118 'protocol_version' => '1.1',
2119 'header' => implode("\r\n", $request_headers),
2120 'timeout' => $this->_timeouts['request'] / 1000,
2121 'content' => in_array($httpmethod, ['POST', 'PUT']) ? $params : null,
2122 'ignore_errors' => true
2126 list($reply, $headers) = $this->_getNoCurlInitialization($url, $contextOptions, $hostname);
2128 foreach ($headers as $header) {
2129 $result .= $header . "\r\n";
2131 $result .= "\r\n" . $reply;
2134 $httpstatus = $this->_getHttpStatusFromHeaders($headers);
2135 list($headers, $reply) = $this->_parseApiHeaders($result);
2136 // TON API & redirects
2137 $reply = $this->_parseApiReplyPrefillHeaders($headers, $reply);
2138 $reply = $this->_parseApiReply($reply);
2139 $rate = $this->_getRateLimitInfo($headers);
2141 $reply = $this->_appendHttpStatusAndRate($reply, $httpstatus, $rate);
2146 * Do preparations to make the API GET call
2148 * @param string $httpmethod The HTTP method to use for making the request
2149 * @param string $url The URL to call
2150 * @param array $params The parameters to send along
2151 * @param bool $app_only_auth Whether to use app-only bearer authentication
2153 * @return string[] (string authorization, string url)
2155 protected function _callApiPreparationsGet(
2156 $httpmethod, $url, $params, $app_only_auth
2159 $app_only_auth ? null : $this->_sign($httpmethod, $url, $params),
2160 json_encode($params) === '[]' ? $url : $url . '?' . http_build_query($params)
2165 * Do preparations to make the API POST call
2167 * @param string $httpmethod The HTTP method to use for making the request
2168 * @param string $url The URL to call
2169 * @param string $method The API method to call
2170 * @param string $method_template The API method template to call
2171 * @param array $params The parameters to send along
2172 * @param bool $multipart Whether to use multipart/form-data
2173 * @param bool $app_only_auth Whether to use app-only bearer authentication
2175 * @return array (string authorization, array params, array request_headers)
2177 protected function _callApiPreparationsPost(
2178 $httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth
2180 $authorization = null;
2181 $request_headers = [];
2183 if (! $app_only_auth) {
2184 $authorization = $this->_sign($httpmethod, $url, []);
2186 $params = $this->_buildMultipart($method, $params);
2187 $first_newline = strpos($params, "\r\n");
2188 $multipart_boundary = substr($params, 2, $first_newline - 2);
2189 $request_headers[] = 'Content-Type: multipart/form-data; boundary='
2190 . $multipart_boundary;
2191 } elseif ($this->_detectJsonBody($method)) {
2192 $authorization = $this->_sign($httpmethod, $url, []);
2193 $params = json_encode($params);
2194 $request_headers[] = 'Content-Type: application/json';
2195 } elseif ($this->_detectBinaryBody($method_template)) {
2196 // transform parametric headers to real headers
2198 'Content-Type', 'X-TON-Content-Type',
2199 'X-TON-Content-Length', 'Content-Range'
2201 if (isset($params[$key])) {
2202 $request_headers[] = $key . ': ' . $params[$key];
2203 unset($params[$key]);
2207 parse_str(parse_url($method, PHP_URL_QUERY), $sign_params);
2208 if ($sign_params === null) {
2211 $authorization = $this->_sign($httpmethod, $url, $sign_params);
2212 if (isset($params['media'])) {
2213 $params = $this->_buildBinaryBody($params['media']);
2219 // check for possible files in non-multipart methods
2220 foreach ($params as $key => $value) {
2221 $data = $this->_checkForFiles($method_template, $key, $value);
2222 if ($data !== false) {
2223 $params[$key] = base64_encode($data);
2226 if (! $app_only_auth) {
2227 $authorization = $this->_sign($httpmethod, $url, $params);
2229 $params = http_build_query($params);
2231 return [$authorization, $params, $request_headers];
2235 * Appends HTTP status and rate limiting info to the reply
2237 * @param array|object|string $reply The reply to append to
2238 * @param string $httpstatus The HTTP status code to append
2239 * @param mixed $rate The rate limiting info to append
2241 protected function _appendHttpStatusAndRate($reply, $httpstatus, $rate)
2243 switch ($this->_return_format) {
2244 case CODEBIRD_RETURNFORMAT_ARRAY:
2245 $reply['httpstatus'] = $httpstatus;
2246 $reply['rate'] = $rate;
2248 case CODEBIRD_RETURNFORMAT_OBJECT:
2249 $reply->httpstatus = $httpstatus;
2250 $reply->rate = $rate;
2252 case CODEBIRD_RETURNFORMAT_JSON:
2253 $reply = $this->_json_decode($reply);
2254 $reply->httpstatus = $httpstatus;
2255 $reply->rate = $rate;
2256 $reply = json_encode($reply);
2263 * Get Bearer authorization string
2265 * @return string authorization
2266 * @throws \Exception
2268 protected function _getBearerAuthorization()
2270 if (self::$_consumer_key === null
2271 && self::$_bearer_token === null
2273 throw new \Exception('To make an app-only auth API request, consumer key or bearer token must be set.');
2275 // automatically fetch bearer token, if necessary
2276 if (self::$_bearer_token === null) {
2277 $this->oauth2_token();
2279 return 'Bearer ' . self::$_bearer_token;
2283 * Do preparations to make the API call
2285 * @param string $httpmethod The HTTP method to use for making the request
2286 * @param string $method The API method to call
2287 * @param string $method_template The API method template to call
2288 * @param array $params The parameters to send along
2289 * @param bool $multipart Whether to use multipart/form-data
2290 * @param bool $app_only_auth Whether to use app-only bearer authentication
2292 * @return array (string authorization, string url, array params, array request_headers)
2294 protected function _callApiPreparations(
2295 $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2298 $url = $this->_getEndpoint($method, $method_template);
2299 $request_headers = [];
2300 if ($httpmethod === 'GET') {
2302 list ($authorization, $url) =
2303 $this->_callApiPreparationsGet($httpmethod, $url, $params, $app_only_auth);
2306 list ($authorization, $params, $request_headers) =
2307 $this->_callApiPreparationsPost($httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth);
2309 if ($app_only_auth) {
2310 $authorization = $this->_getBearerAuthorization();
2314 $authorization, $url, $params, $request_headers
2319 * Calls the streaming API
2321 * @param string $httpmethod The HTTP method to use for making the request
2322 * @param string $method The API method to call
2323 * @param string $method_template The API method template to call
2324 * @param array optional $params The parameters to send along
2325 * @param bool optional $app_only_auth Whether to use app-only bearer authentication
2328 * @throws \Exception
2331 protected function _callApiStreaming(
2332 $httpmethod, $method, $method_template, $params = [], $app_only_auth = false
2335 if ($this->_streaming_callback === null) {
2336 throw new \Exception('Set streaming callback before consuming a stream.');
2339 $params['delimited'] = 'length';
2341 list ($authorization, $url, $params, $request_headers)
2342 = $this->_callApiPreparations(
2343 $httpmethod, $method, $method_template, $params, false, $app_only_auth
2346 $hostname = parse_url($url, PHP_URL_HOST);
2347 $path = parse_url($url, PHP_URL_PATH);
2348 $query = parse_url($url, PHP_URL_QUERY);
2349 if ($hostname === false) {
2350 throw new \Exception('Incorrect API endpoint host.');
2353 $request_headers[] = 'Authorization: ' . $authorization;
2354 $request_headers[] = 'Accept: */*';
2355 if ($httpmethod !== 'GET') {
2356 $request_headers[] = 'Content-Type: application/x-www-form-urlencoded';
2357 $request_headers[] = 'Content-Length: ' . strlen($params);
2362 $connection = stream_socket_client(
2363 'ssl://' . $hostname . ':443',
2365 $this->_timeouts['connect'],
2366 STREAM_CLIENT_CONNECT
2370 $request = $httpmethod . ' '
2371 . $path . ($query ? '?' . $query : '') . " HTTP/1.1\r\n"
2372 . 'Host: ' . $hostname . "\r\n"
2373 . implode("\r\n", $request_headers)
2375 if ($httpmethod !== 'GET') {
2376 $request .= $params;
2378 fputs($connection, $request);
2379 stream_set_blocking($connection, 0);
2380 stream_set_timeout($connection, 0);
2384 $result = stream_get_line($connection, 1048576, "\r\n\r\n");
2386 $headers = explode("\r\n", $result);
2389 $httpstatus = $this->_getHttpStatusFromHeaders($headers);
2390 list($headers,) = $this->_parseApiHeaders($result);
2391 $rate = $this->_getRateLimitInfo($headers);
2393 if ($httpstatus !== '200') {
2395 'httpstatus' => $httpstatus,
2398 switch ($this->_return_format) {
2399 case CODEBIRD_RETURNFORMAT_ARRAY:
2401 case CODEBIRD_RETURNFORMAT_OBJECT:
2402 return (object) $reply;
2403 case CODEBIRD_RETURNFORMAT_JSON:
2404 return json_encode($reply);
2408 $signal_function = function_exists('pcntl_signal_dispatch');
2410 $last_message = $this->_time();
2411 $message_length = 0;
2413 while (!feof($connection)) {
2414 // call signal handlers, if any
2415 if ($signal_function) {
2416 pcntl_signal_dispatch();
2418 $connection_array = [$connection];
2419 $write = $except = null;
2420 if (false === ($num_changed_streams = stream_select($connection_array, $write, $except, 0, 200000))) {
2422 } elseif ($num_changed_streams === 0) {
2423 if ($this->_time() - $last_message >= 1) {
2424 // deliver empty message, allow callback to cancel stream
2425 $cancel_stream = $this->_deliverStreamingMessage(null);
2426 if ($cancel_stream) {
2429 $last_message = $this->_time();
2433 $chunk_length = fgets($connection, 10);
2434 if ($chunk_length === '' || !$chunk_length = hexdec($chunk_length)) {
2440 $chunk .= fread($connection, $chunk_length);
2441 $chunk_length -= strlen($chunk);
2442 } while($chunk_length > 0);
2444 if(0 === $message_length) {
2445 $message_length = (int) strstr($chunk, "\r\n", true);
2446 if ($message_length) {
2447 $chunk = substr($chunk, strpos($chunk, "\r\n") + 2);
2457 if (strlen($data) < $message_length) {
2461 $reply = $this->_parseApiReply($data);
2462 switch ($this->_return_format) {
2463 case CODEBIRD_RETURNFORMAT_ARRAY:
2464 $reply['httpstatus'] = $httpstatus;
2465 $reply['rate'] = $rate;
2467 case CODEBIRD_RETURNFORMAT_OBJECT:
2468 $reply->httpstatus = $httpstatus;
2469 $reply->rate = $rate;
2473 $cancel_stream = $this->_deliverStreamingMessage($reply);
2474 if ($cancel_stream) {
2475 fclose($connection);
2480 $message_length = 0;
2481 $last_message = $this->_time();
2488 * Calls streaming callback with received message
2490 * @param string|array|object message
2492 * @return bool Whether to cancel streaming
2494 protected function _deliverStreamingMessage($message)
2496 return call_user_func($this->_streaming_callback, $message);
2500 * Parses the API reply to separate headers from the body
2502 * @param string $reply The actual raw HTTP request reply
2504 * @return array (headers, reply)
2506 protected function _parseApiHeaders($reply) {
2507 // split headers and body
2509 $reply = explode("\r\n\r\n", $reply, 4);
2511 // check if using proxy
2512 $proxy_tester = strtolower(substr($reply[0], 0, 35));
2513 if ($proxy_tester === 'http/1.0 200 connection established'
2514 || $proxy_tester === 'http/1.1 200 connection established'
2516 array_shift($reply);
2519 $headers_array = explode("\r\n", $reply[0]);
2520 foreach ($headers_array as $header) {
2521 $header_array = explode(': ', $header, 2);
2522 $key = $header_array[0];
2524 if (count($header_array) > 1) {
2525 $value = $header_array[1];
2527 $headers[$key] = $value;
2530 if (count($reply) > 1) {
2536 return [$headers, $reply];
2540 * Parses the API headers to return Location and Ton API headers
2542 * @param array $headers The headers list
2543 * @param string $reply The actual HTTP body
2545 * @return string $reply
2547 protected function _parseApiReplyPrefillHeaders($headers, $reply)
2549 if ($reply === '' && (isset($headers['Location']))) {
2551 'Location' => $headers['Location']
2553 if (isset($headers['X-TON-Min-Chunk-Size'])) {
2554 $reply['X-TON-Min-Chunk-Size'] = $headers['X-TON-Min-Chunk-Size'];
2556 if (isset($headers['X-TON-Max-Chunk-Size'])) {
2557 $reply['X-TON-Max-Chunk-Size'] = $headers['X-TON-Max-Chunk-Size'];
2559 if (isset($headers['Range'])) {
2560 $reply['Range'] = $headers['Range'];
2562 $reply = json_encode($reply);
2568 * Parses the API reply to encode it in the set return_format
2570 * @param string $reply The actual HTTP body, JSON-encoded or URL-encoded
2572 * @return array|string|object The parsed reply
2574 protected function _parseApiReply($reply)
2576 $need_array = $this->_return_format === CODEBIRD_RETURNFORMAT_ARRAY;
2577 if ($reply === '[]') {
2578 switch ($this->_return_format) {
2579 case CODEBIRD_RETURNFORMAT_ARRAY:
2581 case CODEBIRD_RETURNFORMAT_JSON:
2583 case CODEBIRD_RETURNFORMAT_OBJECT:
2584 return new \stdClass;
2587 if (! $parsed = $this->_json_decode($reply, $need_array)) {
2589 // assume query format
2590 $reply = explode('&', $reply);
2591 foreach ($reply as $element) {
2592 if (stristr($element, '=')) {
2593 list($key, $value) = explode('=', $element, 2);
2594 $parsed[$key] = $value;
2596 $parsed['message'] = $element;
2600 $reply = json_encode($parsed);
2602 switch ($this->_return_format) {
2603 case CODEBIRD_RETURNFORMAT_ARRAY:
2605 case CODEBIRD_RETURNFORMAT_JSON:
2607 case CODEBIRD_RETURNFORMAT_OBJECT:
2608 return (object) $parsed;