OSDN Git Service

https://github.com/rahuldottech/Dabr
[embrj/master.git] / i / common / codebird.php
1 <?php
2
3 namespace Codebird;
4
5 /**
6  * A Twitter library in PHP.
7  *
8  * @package   codebird
9  * @version   3.1.0
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
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 $_consumer_key = null;
55
56   /**
57    * The corresponding consumer secret
58    */
59   protected static $_consumer_secret = null;
60
61   /**
62    * The app-only bearer token. Used to authorize app-only requests
63    */
64   protected static $_bearer_token = null;
65
66   /**
67    * The API endpoints to use
68    */
69   protected static $_endpoints = [
70     'ads'          => [
71       'production' => 'https://ads-api.twitter.com/0/',
72       'sandbox'    => 'https://ads-api-sandbox.twitter.com/0/'
73     ],
74     'media'        => 'https://upload.twitter.com/1.1/',
75     'oauth'        => 'https://api.twitter.com/',
76     'rest'         => 'https://api.twitter.com/1.1/',
77     'streaming'    => [
78       'public'     => 'https://stream.twitter.com/1.1/',
79       'user'       => 'https://userstream.twitter.com/1.1/',
80       'site'       => 'https://sitestream.twitter.com/1.1/'
81     ],
82     'ton'          => 'https://ton.twitter.com/1.1/'
83   ];
84
85   /**
86    * Supported API methods
87    */
88   protected static $_api_methods = [
89     'GET' => [
90       'account/settings',
91       'account/verify_credentials',
92       'ads/accounts',
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',
142       'ads/bidding_rules',
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',
259       'blocks/ids',
260       'blocks/list',
261       'collections/entries',
262       'collections/list',
263       'collections/show',
264       'direct_messages',
265       'direct_messages/sent',
266       'direct_messages/show',
267       'favorites/list',
268       'followers/ids',
269       'followers/list',
270       'friends/ids',
271       'friends/list',
272       'friendships/incoming',
273       'friendships/lookup',
274       'friendships/lookup',
275       'friendships/no_retweets/ids',
276       'friendships/outgoing',
277       'friendships/show',
278       'geo/id/:place_id',
279       'geo/reverse_geocode',
280       'geo/search',
281       'geo/similar_places',
282       'help/configuration',
283       'help/languages',
284       'help/privacy',
285       'help/tos',
286       'lists/list',
287       'lists/members',
288       'lists/members/show',
289       'lists/memberships',
290       'lists/ownerships',
291       'lists/show',
292       'lists/statuses',
293       'lists/subscribers',
294       'lists/subscribers/show',
295       'lists/subscriptions',
296       'mutes/users/ids',
297       'mutes/users/list',
298       'oauth/authenticate',
299       'oauth/authorize',
300       'saved_searches/list',
301       'saved_searches/show/:id',
302       'search/tweets',
303       'site',
304       'statuses/firehose',
305       'statuses/home_timeline',
306       'statuses/mentions_timeline',
307       'statuses/oembed',
308       'statuses/retweeters/ids',
309       'statuses/retweets/:id',
310       'statuses/retweets_of_me',
311       'statuses/sample',
312       'statuses/show/:id',
313       'statuses/user_timeline',
314       'trends/available',
315       'trends/closest',
316       'trends/place',
317       'user',
318       'users/contributees',
319       'users/contributors',
320       'users/profile_banner',
321       'users/search',
322       'users/show',
323       'users/suggestions',
324       'users/suggestions/:slug',
325       'users/suggestions/:slug/members'
326     ],
327     'POST' => [
328       'account/remove_profile_banner',
329       'account/settings',
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',
384       'blocks/create',
385       'blocks/destroy',
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',
395       'favorites/create',
396       'favorites/destroy',
397       'friendships/create',
398       'friendships/destroy',
399       'friendships/update',
400       'lists/create',
401       'lists/destroy',
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',
408       'lists/update',
409       'media/upload',
410       'mutes/users/create',
411       'mutes/users/destroy',
412       'oauth/access_token',
413       'oauth/request_token',
414       'oauth2/invalidate_token',
415       'oauth2/token',
416       'saved_searches/create',
417       'saved_searches/destroy/:id',
418       'statuses/destroy/:id',
419       'statuses/filter',
420       'statuses/lookup',
421       'statuses/retweet/:id',
422       'statuses/unretweet/:id',
423       'statuses/update',
424       'statuses/update_with_media', // deprecated, use media/upload
425       'ton/bucket/:bucket',
426       'ton/bucket/:bucket?resumable=true',
427       'users/lookup',
428       'users/report_spam'
429     ],
430     'PUT' => [
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'
460     ],
461     'DELETE' => [
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'
492     ]
493   ];
494
495   /**
496    * Possible file name parameters
497    */
498   protected static $_possible_files = [
499     // Tweets
500     'statuses/update_with_media' => ['media[]'],
501     'media/upload' => ['media'],
502     // Accounts
503     'account/update_profile_background_image' => ['image'],
504     'account/update_profile_image' => ['image'],
505     'account/update_profile_banner' => ['banner']
506   ];
507
508   /**
509    * The current Codebird version
510    */
511   protected static $_version = '3.1.0';
512
513   /**
514    * The Request or access token. Used to sign requests
515    */
516   protected $_oauth_token = null;
517
518   /**
519    * The corresponding request or access token secret
520    */
521   protected $_oauth_token_secret = null;
522
523   /**
524    * The format of data to return from API calls
525    */
526   protected $_return_format = CODEBIRD_RETURNFORMAT_OBJECT;
527
528   /**
529    * The callback to call with any new streaming messages
530    */
531   protected $_streaming_callback = null;
532
533   /**
534    * Auto-detect cURL absence
535    */
536   protected $_use_curl = true;
537
538   /**
539    * Timeouts
540    */
541   protected $_timeouts = [
542     'request' => 10000,
543     'connect' => 3000,
544     'remote'  => 5000
545   ];
546
547   /**
548    * Proxy
549    */
550   protected $_proxy = [];
551
552   /**
553    *
554    * Class constructor
555    *
556    */
557   public function __construct()
558   {
559     // Pre-define $_use_curl depending on cURL availability
560     $this->setUseCurl(function_exists('curl_init'));
561   }
562
563   /**
564    * Returns singleton class instance
565    * Always use this method unless you're working with multiple authenticated users at once
566    *
567    * @return Codebird The instance
568    */
569   public static function getInstance()
570   {
571     if (self::$_instance === null) {
572       self::$_instance = new self;
573     }
574     return self::$_instance;
575   }
576
577   /**
578    * Sets the OAuth consumer key and secret (App key)
579    *
580    * @param string $key    OAuth consumer key
581    * @param string $secret OAuth consumer secret
582    *
583    * @return void
584    */
585   public static function setConsumerKey($key, $secret)
586   {
587     self::$_consumer_key    = $key;
588     self::$_consumer_secret = $secret;
589   }
590
591   /**
592    * Sets the OAuth2 app-only auth bearer token
593    *
594    * @param string $token OAuth2 bearer token
595    *
596    * @return void
597    */
598   public static function setBearerToken($token)
599   {
600     self::$_bearer_token = $token;
601   }
602
603   /**
604    * Gets the current Codebird version
605    *
606    * @return string The version number
607    */
608   public function getVersion()
609   {
610     return self::$_version;
611   }
612
613   /**
614    * Sets the OAuth request or access token and secret (User key)
615    *
616    * @param string $token  OAuth request or access token
617    * @param string $secret OAuth request or access token secret
618    *
619    * @return void
620    */
621   public function setToken($token, $secret)
622   {
623     $this->_oauth_token        = $token;
624     $this->_oauth_token_secret = $secret;
625   }
626
627   /**
628    * Forgets the OAuth request or access token and secret (User key)
629    *
630    * @return bool
631    */
632   public function logout()
633   {
634     $this->_oauth_token =
635     $this->_oauth_token_secret = null;
636
637     return true;
638   }
639
640   /**
641    * Sets if codebird should use cURL
642    *
643    * @param bool $use_curl Request uses cURL or not
644    *
645    * @return void
646    * @throws \Exception
647    */
648   public function setUseCurl($use_curl)
649   {
650     if ($use_curl && ! function_exists('curl_init')) {
651       throw new \Exception('To use cURL, the PHP curl extension must be available.');
652     }
653
654     $this->_use_curl = (bool) $use_curl;
655   }
656
657   /**
658    * Sets request timeout in milliseconds
659    *
660    * @param int $timeout Request timeout in milliseconds
661    *
662    * @return void
663    */
664   public function setTimeout($timeout)
665   {
666     if ($timeout < 0) {
667       $timeout = 0;
668     }
669     $this->_timeouts['request'] = (int) $timeout;
670   }
671
672   /**
673    * Sets connection timeout in milliseconds
674    *
675    * @param int $timeout Connection timeout in milliseconds
676    *
677    * @return void
678    */
679   public function setConnectionTimeout($timeout)
680   {
681     if ($timeout < 0) {
682       $timeout = 0;
683     }
684     $this->_timeouts['connect'] = (int) $timeout;
685   }
686
687   /**
688    * Sets remote media download timeout in milliseconds
689    *
690    * @param int $timeout Remote media timeout in milliseconds
691    *
692    * @return void
693    */
694   public function setRemoteDownloadTimeout($timeout)
695   {
696     if ($timeout < 0) {
697       $timeout = 0;
698     }
699     $this->_timeouts['remote'] = (int) $timeout;
700   }
701
702   /**
703    * Sets the format for API replies
704    *
705    * @param int $return_format One of these:
706    *                           CODEBIRD_RETURNFORMAT_OBJECT (default)
707    *                           CODEBIRD_RETURNFORMAT_ARRAY
708    *                           CODEBIRD_RETURNFORMAT_JSON
709    *
710    * @return void
711    */
712   public function setReturnFormat($return_format)
713   {
714     $this->_return_format = $return_format;
715   }
716
717   /**
718    * Sets the proxy
719    *
720    * @param string       $host Proxy host
721    * @param int          $port Proxy port
722    * @param int optional $type Proxy type, defaults to HTTP
723    *
724    * @return void
725    * @throws \Exception
726    */
727   public function setProxy($host, $port, $type = CURLPROXY_HTTP)
728   {
729     static $types_str = [
730       'HTTP', 'SOCKS4', 'SOCKS5', 'SOCKS4A', 'SOCKS5_HOSTNAME'
731     ];
732     $types = [];
733     foreach ($types_str as $type_str) {
734       if (defined('CURLPROXY_' . $type_str)) {
735         $types[] = constant('CURLPROXY_' . $type_str);
736       }
737     }
738     if (! in_array($type, $types)) {
739       throw new \Exception('Invalid proxy type specified.');
740     }
741
742     $this->_proxy['host'] = $host;
743     $this->_proxy['port'] = (int) $port;
744     $this->_proxy['type'] = $type;
745   }
746
747   /**
748    * Sets the proxy authentication
749    *
750    * @param string $authentication Proxy authentication
751    *
752    * @return void
753    */
754   public function setProxyAuthentication($authentication)
755   {
756     $this->_proxy['authentication'] = $authentication;
757   }
758
759   /**
760    * Sets streaming callback
761    *
762    * @param callable $callback The streaming callback
763    *
764    * @return void
765    * @throws \Exception
766    */
767   public function setStreamingCallback($callback)
768   {
769     if (!is_callable($callback)) {
770       throw new \Exception('This is not a proper callback.');
771     }
772     $this->_streaming_callback = $callback;
773   }
774
775   /**
776    * Get allowed API methods, sorted by HTTP method
777    * Watch out for multiple-method API methods!
778    *
779    * @return array $apimethods
780    */
781   public function getApiMethods()
782   {
783     return self::$_api_methods;
784   }
785
786   /**
787    * Main API handler working on any requests you issue
788    *
789    * @param string $function The member function you called
790    * @param array  $params   The parameters you sent along
791    *
792    * @return string The API reply encoded in the set return_format
793    */
794
795   public function __call($function, $params)
796   {
797     // cURL function?
798     if (substr($function, 0, 6) === '_curl_'
799       || $function === '_time'
800       || $function === '_microtime'
801     ) {
802       return call_user_func_array(substr($function, 1), $params);
803     }
804
805     // parse parameters
806     $apiparams = $this->_parseApiParams($params);
807
808     // stringify null and boolean parameters
809     $apiparams = $this->_stringifyNullBoolParams($apiparams);
810
811     $app_only_auth = false;
812     if (count($params) > 1) {
813       // convert app_only_auth param to bool
814       $app_only_auth = !! $params[1];
815     }
816
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);
821     }
822
823     // map function name to API method
824     list($method, $method_template) = $this->_mapFnToApiMethod($function, $apiparams);
825
826     $httpmethod = $this->_detectMethod($method_template, $apiparams);
827     $multipart  = $this->_detectMultipart($method_template);
828
829     return $this->_callApi(
830       $httpmethod,
831       $method,
832       $method_template,
833       $apiparams,
834       $multipart,
835       $app_only_auth
836     );
837   }
838
839
840   /**
841    * __call() helpers
842    */
843
844   /**
845    * Parse given params, detect query-style params
846    *
847    * @param array|string $params Parameters to parse
848    *
849    * @return array $apiparams
850    */
851   protected function _parseApiParams($params)
852   {
853     $apiparams = [];
854     if (count($params) === 0) {
855       return $apiparams;
856     }
857
858     if (is_array($params[0])) {
859       // given parameters are array
860       $apiparams = $params[0];
861       return $apiparams;
862     }
863
864     // user gave us query-style params
865     parse_str($params[0], $apiparams);
866     if (! is_array($apiparams)) {
867       $apiparams = [];
868     }
869
870     return $apiparams;
871   }
872
873   /**
874    * Replace null and boolean parameters with their string representations
875    *
876    * @param array $apiparams Parameter array to replace in
877    *
878    * @return array $apiparams
879    */
880   protected function _stringifyNullBoolParams($apiparams)
881   {
882     foreach ($apiparams as $key => $value) {
883       if (! is_scalar($value)) {
884         // no need to try replacing arrays
885         continue;
886       }
887       if (is_null($value)) {
888         $apiparams[$key] = 'null';
889       } elseif (is_bool($value)) {
890         $apiparams[$key] = $value ? 'true' : 'false';
891       }
892     }
893
894     return $apiparams;
895   }
896
897   /**
898    * Maps called PHP magic method name to Twitter API method
899    *
900    * @param string $function        Function called
901    * @param array  $apiparams byref API parameters
902    *
903    * @return string[] (string method, string method_template)
904    */
905   protected function _mapFnToApiMethod($function, &$apiparams)
906   {
907     // replace _ by /
908     $method = $this->_mapFnInsertSlashes($function);
909
910     // undo replacement for URL parameters
911     $method = $this->_mapFnRestoreParamUnderscores($method);
912
913     // replace AA by URL parameters
914     list ($method, $method_template) = $this->_mapFnInlineParams($method, $apiparams);
915
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);
921       }
922     }
923
924     return [$method, $method_template];
925   }
926
927   /**
928    * API method mapping: Replaces _ with / character
929    *
930    * @param string $function Function called
931    *
932    * @return string API method to call
933    */
934   protected function _mapFnInsertSlashes($function)
935   {
936     return str_replace('_', '/', $function);
937   }
938
939   /**
940    * API method mapping: Restore _ character in named parameters
941    *
942    * @param string $method API method to call
943    *
944    * @return string API method with restored underscores
945    */
946   protected function _mapFnRestoreParamUnderscores($method)
947   {
948     $params = [
949       'screen_name', 'place_id',
950       'account_id', 'campaign_id', 'card_id', 'line_item_id',
951       'tweet_id', 'web_event_tag_id'
952     ];
953     foreach ($params as $param) {
954       $param = strtoupper($param);
955       $replacement_was = str_replace('_', '/', $param);
956       $method = str_replace($replacement_was, $param, $method);
957     }
958
959     return $method;
960   }
961
962   /**
963    * Inserts inline parameters into the method name
964    *
965    * @param string      $method    The method to call
966    * @param array byref $apiparams The parameters to send along
967    *
968    * @return string[] (string method, string method_template)
969    * @throws \Exception
970    */
971   protected function _mapFnInlineParams($method, &$apiparams)
972   {
973     $method_template = $method;
974     $match           = [];
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';
980         }
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);
985           }
986           throw new \Exception(
987             'To call the templated method "' . $method_template
988             . '", specify the parameter value for "' . $param_l . '".'
989           );
990         }
991         $method  = str_replace($param, $apiparams[$param_l], $method);
992         unset($apiparams[$param_l]);
993       }
994     }
995
996     return [$method, $method_template];
997   }
998
999   /**
1000    * Avoids any JSON_BIGINT_AS_STRING errors
1001    *
1002    * @param string       $data       JSON data to decode
1003    * @param int optional $need_array Decode as array, otherwise as object
1004    *
1005    * @return array|object The decoded object
1006    */
1007   protected function _json_decode($data, $need_array = false)
1008   {
1009     if (!(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
1010       return json_decode($data, $need_array, 512, JSON_BIGINT_AS_STRING);
1011     }
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);
1015     return $obj;
1016   }
1017
1018   /**
1019    * Uncommon API methods
1020    */
1021
1022   /**
1023    * Gets the OAuth authenticate URL for the current request token
1024    *
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
1028    *
1029    * @return string The OAuth authenticate/authorize URL
1030    * @throws \Exception
1031    */
1032   public function oauth_authenticate($force_login = NULL, $screen_name = NULL, $type = 'authenticate')
1033   {
1034     if (! in_array($type, ['authenticate', 'authorize'])) {
1035       throw new \Exception('To get the ' . $type . ' URL, use the correct third parameter, or omit it.');
1036     }
1037     if ($this->_oauth_token === null) {
1038       throw new \Exception('To get the ' . $type . ' URL, the OAuth token must be set.');
1039     }
1040     $url = self::$_endpoints['oauth'] . 'oauth/' . $type . '?oauth_token=' . $this->_url($this->_oauth_token);
1041     if ($force_login) {
1042       $url .= "&force_login=1";
1043     }
1044     if ($screen_name) {
1045       $url .= "&screen_name=" . $screen_name;
1046     }
1047     return $url;
1048   }
1049
1050   /**
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
1054    *
1055    * @return string The OAuth authorize URL
1056    */
1057   public function oauth_authorize($force_login = NULL, $screen_name = NULL)
1058   {
1059     return $this->oauth_authenticate($force_login, $screen_name, 'authorize');
1060   }
1061
1062   /**
1063    * Gets the OAuth bearer token
1064    *
1065    * @return string The OAuth bearer token
1066    */
1067
1068   public function oauth2_token()
1069   {
1070     if ($this->_use_curl) {
1071       return $this->_oauth2TokenCurl();
1072     }
1073     return $this->_oauth2TokenNoCurl();
1074   }
1075
1076   /**
1077    * Gets a cURL handle
1078    * @param string $url the URL for the curl initialization
1079    * @return resource handle
1080    */
1081   protected function _getCurlInitialization($url)
1082   {
1083     $connection = $this->_curl_init($url);
1084
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'
1094     );
1095
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());
1100
1101       if ($this->_getProxyAuthentication()) {
1102         $this->_curl_setopt($connection, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
1103         $this->_curl_setopt($connection, CURLOPT_PROXYUSERPWD, $this->_getProxyAuthentication());
1104       }
1105     }
1106
1107     return $connection;
1108   }
1109
1110   /**
1111    * Gets a non cURL initialization
1112    *
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
1116    *
1117    * @return array the read data
1118    */
1119   protected function _getNoCurlInitialization($url, $contextOptions, $hostname = '')
1120   {
1121     $httpOptions = [];
1122
1123     $httpOptions['header'] = [
1124       'User-Agent: codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1125     ];
1126
1127     $httpOptions['ssl'] = [
1128       'verify_peer'  => true,
1129       'cafile'       => __DIR__ . '/cacert.pem',
1130       'verify_depth' => 5,
1131       'peer_name'    => $hostname
1132     ];
1133
1134     if ($this->_hasProxy()) {
1135       $httpOptions['request_fulluri'] = true;
1136       $httpOptions['proxy'] = $this->_getProxyHost() . ':' . $this->_getProxyPort();
1137
1138       if ($this->_getProxyAuthentication()) {
1139         $httpOptions['header'][] =
1140           'Proxy-Authorization: Basic ' . base64_encode($this->_getProxyAuthentication());
1141       }
1142     }
1143
1144     // merge the http options with the context options
1145     $options = array_merge_recursive(
1146       $contextOptions,
1147       ['http' => $httpOptions]
1148     );
1149
1150     // concatenate $options['http']['header']
1151     $options['http']['header'] = implode("\r\n", $options['http']['header']);
1152
1153     // silent the file_get_contents function
1154     $content = @file_get_contents($url, false, stream_context_create($options));
1155
1156     $headers = [];
1157     // API is responding
1158     if (isset($http_response_header)) {
1159       $headers = $http_response_header;
1160     }
1161
1162     return [
1163       $content,
1164       $headers
1165     ];
1166   }
1167
1168   protected function _hasProxy()
1169   {
1170     return isset($this->_proxy['host']) || isset($this->_proxy['port']);
1171   }
1172
1173   /**
1174    * Gets the proxy host
1175    *
1176    * @return string The proxy host
1177    */
1178   protected function _getProxyHost()
1179   {
1180     return $this->_getProxyData('host');
1181   }
1182
1183   /**
1184    * Gets the proxy port
1185    *
1186    * @return string The proxy port
1187    */
1188   protected function _getProxyPort()
1189   {
1190     return $this->_getProxyData('port');
1191   }
1192
1193   /**
1194    * Gets the proxy authentication
1195    *
1196    * @return string The proxy authentication
1197    */
1198   protected function _getProxyAuthentication()
1199   {
1200     return $this->_getProxyData('authentication');
1201   }
1202
1203   /**
1204    * Gets the proxy type
1205    *
1206    * @return string The proxy type
1207    */
1208   protected function _getProxyType()
1209   {
1210     return $this->_getProxyData('type');
1211   }
1212
1213   /**
1214    * Gets data from the proxy configuration
1215    *
1216    * @param string $name
1217    */
1218   private function _getProxyData($name)
1219   {
1220     return empty($this->_proxy[$name]) ? null : $this->_proxy[$name];
1221   }
1222
1223   /**
1224    * Gets the OAuth bearer token, using cURL
1225    *
1226    * @return string The OAuth bearer token
1227    * @throws \Exception
1228    */
1229
1230   protected function _oauth2TokenCurl()
1231   {
1232     if (self::$_consumer_key === null) {
1233       throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1234     }
1235     $post_fields = [
1236       'grant_type' => 'client_credentials'
1237     ];
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);
1242
1243     $this->_curl_setopt($connection, CURLOPT_USERPWD, self::$_consumer_key . ':' . self::$_consumer_secret);
1244     $this->_curl_setopt($connection, CURLOPT_HTTPHEADER, [
1245       'Expect:'
1246     ]);
1247     $result = $this->_curl_exec($connection);
1248
1249     // catch request errors
1250     if ($result === false) {
1251       throw new \Exception('Request error for bearer token: ' . $this->_curl_error($connection));
1252     }
1253
1254     // certificate validation results
1255     $validation_result = $this->_curl_errno($connection);
1256     $this->_validateSslCertificate($validation_result);
1257
1258     $httpstatus = $this->_curl_getinfo($connection, CURLINFO_HTTP_CODE);
1259     $reply = $this->_parseBearerReply($result, $httpstatus);
1260     return $reply;
1261   }
1262
1263   /**
1264    * Gets the OAuth bearer token, without cURL
1265    *
1266    * @return string The OAuth bearer token
1267    * @throws \Exception
1268    */
1269
1270   protected function _oauth2TokenNoCurl()
1271   {
1272     if (self::$_consumer_key == null) {
1273       throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1274     }
1275
1276     $url      = self::$_endpoints['oauth'] . 'oauth2/token';
1277     $hostname = parse_url($url, PHP_URL_HOST);
1278
1279     if ($hostname === false) {
1280       throw new \Exception('Incorrect API endpoint host.');
1281     }
1282
1283     $contextOptions = [
1284       'http' => [
1285         'method'           => 'POST',
1286         'protocol_version' => '1.1',
1287         'header'           => "Accept: */*\r\n"
1288           . 'Authorization: Basic '
1289           . base64_encode(
1290             self::$_consumer_key
1291             . ':'
1292             . self::$_consumer_secret
1293           ),
1294         'timeout'          => $this->_timeouts['request'] / 1000,
1295         'content'          => 'grant_type=client_credentials',
1296         'ignore_errors'    => true
1297       ]
1298     ];
1299     list($reply, $headers) = $this->_getNoCurlInitialization($url, $contextOptions, $hostname);
1300     $result  = '';
1301     foreach ($headers as $header) {
1302       $result .= $header . "\r\n";
1303     }
1304     $result .= "\r\n" . $reply;
1305
1306     // find HTTP status
1307     $httpstatus = $this->_getHttpStatusFromHeaders($headers);
1308     $reply      = $this->_parseBearerReply($result, $httpstatus);
1309     return $reply;
1310   }
1311
1312
1313   /**
1314    * General helpers to avoid duplicate code
1315    */
1316
1317   /**
1318    * Extract HTTP status code from headers
1319    *
1320    * @param array $headers The headers to parse
1321    *
1322    * @return string The HTTP status code
1323    */
1324   protected function _getHttpStatusFromHeaders($headers)
1325   {
1326     $httpstatus = '500';
1327     $match      = [];
1328     if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
1329       $httpstatus = $match[1];
1330     }
1331     return $httpstatus;
1332   }
1333
1334   /**
1335    * Parse oauth2_token reply and set bearer token, if found
1336    *
1337    * @param string $result     Raw HTTP response
1338    * @param int    $httpstatus HTTP status code
1339    *
1340    * @return string reply
1341    */
1342   protected function _parseBearerReply($result, $httpstatus)
1343   {
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']);
1353         }
1354         break;
1355       case CODEBIRD_RETURNFORMAT_JSON:
1356         if ($httpstatus === 200) {
1357           $parsed = $this->_json_decode($reply);
1358           self::setBearerToken($parsed->access_token);
1359         }
1360         break;
1361       case CODEBIRD_RETURNFORMAT_OBJECT:
1362         $reply->httpstatus = $httpstatus;
1363         $reply->rate       = $rate;
1364         if ($httpstatus === 200) {
1365           self::setBearerToken($reply->access_token);
1366         }
1367         break;
1368     }
1369     return $reply;
1370   }
1371
1372   /**
1373    * Extract rate-limiting data from response headers
1374    *
1375    * @param array $headers The CURL response headers
1376    *
1377    * @return null|array|object The rate-limiting information
1378    */
1379   protected function _getRateLimitInfo($headers)
1380   {
1381     if (! isset($headers['x-rate-limit-limit'])) {
1382       return null;
1383     }
1384     $rate = [
1385       'limit'     => $headers['x-rate-limit-limit'],
1386       'remaining' => $headers['x-rate-limit-remaining'],
1387       'reset'     => $headers['x-rate-limit-reset']
1388     ];
1389     if ($this->_return_format === CODEBIRD_RETURNFORMAT_OBJECT) {
1390       return (object) $rate;
1391     }
1392     return $rate;
1393   }
1394
1395   /**
1396    * Check if there were any SSL certificate errors
1397    *
1398    * @param int $validation_result The curl error number
1399    *
1400    * @return void
1401    * @throws \Exception
1402    */
1403   protected function _validateSslCertificate($validation_result)
1404   {
1405     if (in_array(
1406         $validation_result,
1407         [
1408           CURLE_SSL_CERTPROBLEM,
1409           CURLE_SSL_CACERT,
1410           CURLE_SSL_CACERT_BADFILE,
1411           CURLE_SSL_CRL_BADFILE,
1412           CURLE_SSL_ISSUER_ERROR
1413         ]
1414       )
1415     ) {
1416       throw new \Exception(
1417         'Error ' . $validation_result
1418         . ' while validating the Twitter API certificate.'
1419       );
1420     }
1421   }
1422
1423   /**
1424    * Signing helpers
1425    */
1426
1427   /**
1428    * URL-encodes the given data
1429    *
1430    * @param mixed $data
1431    *
1432    * @return mixed The encoded data
1433    */
1434   protected function _url($data)
1435   {
1436     if (is_array($data)) {
1437       return array_map([
1438         $this,
1439         '_url'
1440       ], $data);
1441     } elseif (is_scalar($data)) {
1442       return str_replace([
1443         '+',
1444         '!',
1445         '*',
1446         "'",
1447         '(',
1448         ')'
1449       ], [
1450         ' ',
1451         '%21',
1452         '%2A',
1453         '%27',
1454         '%28',
1455         '%29'
1456       ], rawurlencode($data));
1457     }
1458     return '';
1459   }
1460
1461   /**
1462    * Gets the base64-encoded SHA1 hash for the given data
1463    *
1464    * @param string $data The data to calculate the hash from
1465    *
1466    * @return string The hash
1467    * @throws \Exception
1468    */
1469   protected function _sha1($data)
1470   {
1471     if (self::$_consumer_secret === null) {
1472       throw new \Exception('To generate a hash, the consumer secret must be set.');
1473     }
1474     if (!function_exists('hash_hmac')) {
1475       throw new \Exception('To generate a hash, the PHP hash extension must be available.');
1476     }
1477     return base64_encode(hash_hmac(
1478       'sha1',
1479       $data,
1480       self::$_consumer_secret
1481       . '&'
1482       . ($this->_oauth_token_secret !== null
1483         ? $this->_oauth_token_secret
1484         : ''
1485       ),
1486       true
1487     ));
1488   }
1489
1490   /**
1491    * Generates a (hopefully) unique random string
1492    *
1493    * @param int optional $length The length of the string to generate
1494    *
1495    * @return string The random string
1496    * @throws \Exception
1497    */
1498   protected function _nonce($length = 8)
1499   {
1500     if ($length < 1) {
1501       throw new \Exception('Invalid nonce length.');
1502     }
1503     return substr(md5($this->_microtime(true)), 0, $length);
1504   }
1505
1506   /**
1507    * Signature helper
1508    *
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
1512    *
1513    * @return string signature
1514    */
1515   protected function _getSignature($httpmethod, $method, $base_params)
1516   {
1517     // convert params to string
1518     $base_string = '';
1519     foreach ($base_params as $key => $value) {
1520       $base_string .= $key . '=' . $value . '&';
1521     }
1522
1523     // trim last ampersand
1524     $base_string = substr($base_string, 0, -1);
1525
1526     // hash it
1527     return $this->_sha1(
1528       $httpmethod . '&' .
1529       $this->_url($method) . '&' .
1530       $this->_url($base_string)
1531     );
1532   }
1533
1534   /**
1535    * Generates an OAuth signature
1536    *
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
1540    *
1541    * @return string Authorization HTTP header
1542    * @throws \Exception
1543    */
1544   protected function _sign($httpmethod, $method, $params = [])
1545   {
1546     if (self::$_consumer_key === null) {
1547       throw new \Exception('To generate a signature, the consumer key must be set.');
1548     }
1549     $sign_base_params = array_map(
1550       [$this, '_url'],
1551       [
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'
1557       ]
1558     );
1559     if ($this->_oauth_token !== null) {
1560       $sign_base_params['oauth_token'] = $this->_url($this->_oauth_token);
1561     }
1562     $oauth_params = $sign_base_params;
1563
1564     // merge in the non-OAuth params
1565     $sign_base_params = array_merge(
1566       $sign_base_params,
1567       array_map([$this, '_url'], $params)
1568     );
1569     ksort($sign_base_params);
1570
1571     $signature = $this->_getSignature($httpmethod, $method, $sign_base_params);
1572
1573     $params = $oauth_params;
1574     $params['oauth_signature'] = $signature;
1575
1576     ksort($params);
1577     $authorization = 'OAuth ';
1578     foreach ($params as $key => $value) {
1579       $authorization .= $key . "=\"" . $this->_url($value) . "\", ";
1580     }
1581     return substr($authorization, 0, -2);
1582   }
1583
1584   /**
1585    * Detects HTTP method to use for API call
1586    *
1587    * @param string      $method The API method to call
1588    * @param array byref $params The parameters to send along
1589    *
1590    * @return string The HTTP method that should be used
1591    */
1592   protected function _detectMethod($method, &$params)
1593   {
1594     if (isset($params['httpmethod'])) {
1595       $httpmethod = $params['httpmethod'];
1596       unset($params['httpmethod']);
1597       return $httpmethod;
1598     }
1599     $apimethods = $this->getApiMethods();
1600
1601     // multi-HTTP method API methods
1602     // parameter-based detection
1603     $httpmethods_by_param = [
1604       'POST' => [
1605         'campaign_id' => [
1606           'ads/accounts/:account_id/line_items',
1607           'ads/sandbox/accounts/:account_id/line_items'
1608         ],
1609         'media_id' => [
1610           'ads/accounts/:account_id/account_media',
1611           'ads/sandbox/accounts/:account_id/account_media'
1612         ],
1613         'name' => [
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'
1636         ],
1637         'tailored_audience_id' => [
1638           'ads/accounts/:account_id/tailored_audience_changes',
1639           'ads/sandbox/accounts/:account_id/tailored_audience_changes'
1640         ],
1641         'targeting_value' => [
1642           'ads/accounts/:account_id/targeting_criteria',
1643           'ads/sandbox/accounts/:account_id/targeting_criteria'
1644         ],
1645         'tweet_ids' => [
1646           'ads/accounts/:account_id/promoted_tweets',
1647           'ads/sandbox/accounts/:account_id/promoted_tweets'
1648         ],
1649         'user_id' => [
1650           'ads/accounts/:account_id/promoted_accounts',
1651           'ads/sandbox/accounts/:account_id/promoted_accounts'
1652         ],
1653         'video_media_id' => [
1654           'ads/accounts/:account_id/videos',
1655           'ads/sandbox/accounts/:account_id/videos'
1656         ]
1657       ],
1658       'PUT' => [
1659         'name' => [
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'
1666         ]
1667       ]
1668     ];
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])) {
1672           return $httpmethod;
1673         }
1674       }
1675     }
1676
1677     // prefer POST and PUT if parameters are set
1678     if (count($params) > 0) {
1679       if (in_array($method, $apimethods['POST'])) {
1680         return 'POST';
1681       }
1682       if (in_array($method, $apimethods['PUT'])) {
1683         return 'PUT';
1684       }
1685     }
1686
1687     foreach ($apimethods as $httpmethod => $methods) {
1688       if (in_array($method, $methods)) {
1689         return $httpmethod;
1690       }
1691     }
1692     throw new \Exception('Can\'t find HTTP method to use for "' . $method . '".');
1693   }
1694
1695   /**
1696    * Detects if API call should use multipart/form-data
1697    *
1698    * @param string $method The API method to call
1699    *
1700    * @return bool Whether the method should be sent as multipart
1701    */
1702   protected function _detectMultipart($method)
1703   {
1704     $multiparts = [
1705       // Tweets
1706       'statuses/update_with_media',
1707       'media/upload',
1708
1709       // Users
1710       // no multipart for these, for now:
1711       //'account/update_profile_background_image',
1712       //'account/update_profile_image',
1713       //'account/update_profile_banner'
1714     ];
1715     return in_array($method, $multiparts);
1716   }
1717
1718   /**
1719    * Merge multipart string from parameters array
1720    *
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
1724    *
1725    * @return string request
1726    * @throws \Exception
1727    */
1728   protected function _getMultipartRequestFromParams($method_template, $border, $params)
1729   {
1730     $request = '';
1731     foreach ($params as $key => $value) {
1732       // is it an array?
1733       if (is_array($value)) {
1734         throw new \Exception('Using URL-encoded parameters is not supported for uploading media.');
1735       }
1736       $request .=
1737         '--' . $border . "\r\n"
1738         . 'Content-Disposition: form-data; name="' . $key . '"';
1739
1740       // check for filenames
1741       $data = $this->_checkForFiles($method_template, $key, $value);
1742       if ($data !== false) {
1743         $value = $data;
1744       }
1745
1746       $request .= "\r\n\r\n" . $value . "\r\n";
1747     }
1748
1749     return $request;
1750   }
1751
1752   /**
1753    * Check for files
1754    *
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
1758    *
1759    * @return mixed
1760    */
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])
1764     ) {
1765       return false;
1766     }
1767     $data = $this->_buildBinaryBody($value);
1768     if ($data === $value) {
1769       return false;
1770     }
1771     return $data;
1772   }
1773
1774
1775   /**
1776    * Detect filenames in upload parameters,
1777    * build multipart request from upload params
1778    *
1779    * @param string $method  The API method to call
1780    * @param array  $params  The parameters to send along
1781    *
1782    * @return null|string
1783    */
1784   protected function _buildMultipart($method, $params)
1785   {
1786     // well, files will only work in multipart methods
1787     if (! $this->_detectMultipart($method)) {
1788       return;
1789     }
1790
1791     // only check specific parameters
1792     // method might have files?
1793     if (! in_array($method, array_keys(self::$_possible_files))) {
1794       return;
1795     }
1796
1797     $multipart_border = '--------------------' . $this->_nonce();
1798     $multipart_request =
1799       $this->_getMultipartRequestFromParams($method, $multipart_border, $params)
1800       . '--' . $multipart_border . '--';
1801
1802     return $multipart_request;
1803   }
1804
1805   /**
1806    * Detect filenames in upload parameters
1807    *
1808    * @param mixed $input The data or file name to parse
1809    *
1810    * @return null|string
1811    */
1812   protected function _buildBinaryBody($input)
1813   {
1814     if (// is it a file, a readable one?
1815       @file_exists($input)
1816       && @is_readable($input)
1817     ) {
1818       // try to read the file
1819       $data = @file_get_contents($input);
1820       if ($data !== false && strlen($data) !== 0) {
1821         return $data;
1822       }
1823     } elseif (// is it a remote file?
1824       filter_var($input, FILTER_VALIDATE_URL)
1825       && preg_match('/^https?:\/\//', $input)
1826     ) {
1827       $data = $this->_fetchRemoteFile($input);
1828       if ($data !== false) {
1829         return $data;
1830       }
1831     }
1832     return $input;
1833   }
1834
1835   /**
1836    * Fetches a remote file
1837    *
1838    * @param string $url The URL to download from
1839    *
1840    * @return mixed The file contents or FALSE
1841    * @throws \Exception
1842    */
1843   protected function _fetchRemoteFile($url)
1844   {
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
1862       ) {
1863         return $result;
1864       }
1865       throw new \Exception('Downloading a remote media file failed.');
1866       return false;
1867     }
1868     // no cURL
1869     $contextOptions = [
1870       'http' => [
1871         'method'           => 'GET',
1872         'protocol_version' => '1.1',
1873         'timeout'          => $this->_timeouts['remote']
1874       ],
1875       'ssl' => [
1876         'verify_peer'  => false
1877       ]
1878     ];
1879     list($result, $headers) = $this->_getNoCurlInitialization($url, $contextOptions);
1880     if ($result !== false
1881       && preg_match('/^HTTP\/\d\.\d 200 OK$/', $headers[0])
1882     ) {
1883       return $result;
1884     }
1885     throw new \Exception('Downloading a remote media file failed.');
1886     return false;
1887   }
1888
1889   /**
1890    * Detects if API call should use media endpoint
1891    *
1892    * @param string $method The API method to call
1893    *
1894    * @return bool Whether the method is defined in media API
1895    */
1896   protected function _detectMedia($method) {
1897     $medias = [
1898       'media/upload'
1899     ];
1900     return in_array($method, $medias);
1901   }
1902
1903   /**
1904    * Detects if API call should use JSON body
1905    *
1906    * @param string $method The API method to call
1907    *
1908    * @return bool Whether the method is defined as accepting JSON body
1909    */
1910   protected function _detectJsonBody($method) {
1911     $json_bodies = [
1912       'collections/entries/curate'
1913     ];
1914     return in_array($method, $json_bodies);
1915   }
1916
1917   /**
1918    * Detects if API call should use binary body
1919    *
1920    * @param string $method_template The API method to call
1921    *
1922    * @return bool Whether the method is defined as accepting binary body
1923    */
1924   protected function _detectBinaryBody($method_template) {
1925     $binary = [
1926       'ton/bucket/:bucket',
1927       'ton/bucket/:bucket?resumable=true',
1928       'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
1929     ];
1930     return in_array($method_template, $binary);
1931   }
1932
1933   /**
1934    * Detects if API call should use streaming endpoint, and if yes, which one
1935    *
1936    * @param string $method The API method to call
1937    *
1938    * @return string|false Variant of streaming API to be used
1939    */
1940   protected function _detectStreaming($method) {
1941     $streamings = [
1942       'public' => [
1943         'statuses/sample',
1944         'statuses/filter',
1945         'statuses/firehose'
1946       ],
1947       'user' => ['user'],
1948       'site' => ['site']
1949     ];
1950     foreach ($streamings as $key => $values) {
1951       if (in_array($method, $values)) {
1952         return $key;
1953       }
1954     }
1955
1956     return false;
1957   }
1958
1959   /**
1960    * Builds the complete API endpoint url
1961    *
1962    * @param string $method The API method to call
1963    * @param string $method_template The API method to call
1964    *
1965    * @return string The URL to send the request to
1966    */
1967   protected function _getEndpoint($method, $method_template)
1968   {
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);
1982     }
1983     return $url;
1984   }
1985
1986   /**
1987    * Calls the API
1988    *
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
1995    *
1996    * @return string The API reply, encoded in the set return_format
1997    * @throws \Exception
1998    */
1999
2000   protected function _callApi($httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false)
2001   {
2002     if (! $app_only_auth
2003       && $this->_oauth_token === null
2004       && substr($method, 0, 5) !== 'oauth'
2005     ) {
2006         throw new \Exception('To call this API, the OAuth access token must be set.');
2007     }
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);
2011     }
2012
2013     if ($this->_use_curl) {
2014       return $this->_callApiCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
2015     }
2016     return $this->_callApiNoCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
2017   }
2018
2019   /**
2020    * Calls the API using cURL
2021    *
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
2028    *
2029    * @return string The API reply, encoded in the set return_format
2030    * @throws \Exception
2031    */
2032
2033   protected function _callApiCurl(
2034     $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
2035   )
2036   {
2037     list ($authorization, $url, $params, $request_headers)
2038       = $this->_callApiPreparations(
2039         $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2040       );
2041
2042     $connection        = $this->_getCurlInitialization($url);
2043     $request_headers[] = 'Authorization: ' . $authorization;
2044     $request_headers[] = 'Expect:';
2045
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);
2051       }
2052     }
2053
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']);
2057
2058     $result = $this->_curl_exec($connection);
2059
2060     // catch request errors
2061     if ($result === false) {
2062       throw new \Exception('Request error for API call: ' . $this->_curl_error($connection));
2063     }
2064
2065     // certificate validation results
2066     $validation_result = $this->_curl_errno($connection);
2067     $this->_validateSslCertificate($validation_result);
2068
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);
2075
2076     $reply = $this->_appendHttpStatusAndRate($reply, $httpstatus, $rate);
2077     return $reply;
2078   }
2079
2080   /**
2081    * Calls the API without cURL
2082    *
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
2089    *
2090    * @return string The API reply, encoded in the set return_format
2091    * @throws \Exception
2092    */
2093
2094   protected function _callApiNoCurl(
2095     $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
2096   )
2097   {
2098     list ($authorization, $url, $params, $request_headers)
2099       = $this->_callApiPreparations(
2100         $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2101       );
2102
2103     $hostname = parse_url($url, PHP_URL_HOST);
2104     if ($hostname === false) {
2105       throw new \Exception('Incorrect API endpoint host.');
2106     }
2107
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';
2113     }
2114
2115     $contextOptions = [
2116       'http' => [
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
2123       ]
2124     ];
2125
2126     list($reply, $headers) = $this->_getNoCurlInitialization($url, $contextOptions, $hostname);
2127     $result  = '';
2128     foreach ($headers as $header) {
2129       $result .= $header . "\r\n";
2130     }
2131     $result .= "\r\n" . $reply;
2132
2133     // find HTTP status
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);
2140
2141     $reply = $this->_appendHttpStatusAndRate($reply, $httpstatus, $rate);
2142     return $reply;
2143   }
2144
2145   /**
2146    * Do preparations to make the API GET call
2147    *
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
2152    *
2153    * @return string[] (string authorization, string url)
2154    */
2155   protected function _callApiPreparationsGet(
2156     $httpmethod, $url, $params, $app_only_auth
2157   ) {
2158     return [
2159       $app_only_auth                ? null : $this->_sign($httpmethod, $url, $params),
2160       json_encode($params) === '[]' ? $url : $url . '?' . http_build_query($params)
2161     ];
2162   }
2163
2164   /**
2165    * Do preparations to make the API POST call
2166    *
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
2174    *
2175    * @return array (string authorization, array params, array request_headers)
2176    */
2177   protected function _callApiPreparationsPost(
2178     $httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth
2179   ) {
2180     $authorization   = null;
2181     $request_headers = [];
2182     if ($multipart) {
2183       if (! $app_only_auth) {
2184         $authorization = $this->_sign($httpmethod, $url, []);
2185       }
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
2197       foreach ([
2198           'Content-Type', 'X-TON-Content-Type',
2199           'X-TON-Content-Length', 'Content-Range'
2200         ] as $key) {
2201         if (isset($params[$key])) {
2202           $request_headers[] = $key . ': ' . $params[$key];
2203           unset($params[$key]);
2204         }
2205       }
2206       $sign_params = [];
2207       parse_str(parse_url($method, PHP_URL_QUERY), $sign_params);
2208       if ($sign_params === null) {
2209         $sign_params = [];
2210       }
2211       $authorization = $this->_sign($httpmethod, $url, $sign_params);
2212       if (isset($params['media'])) {
2213         $params = $this->_buildBinaryBody($params['media']);
2214       } else {
2215         // resumable upload
2216         $params = [];
2217       }
2218     } else {
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);
2224         }
2225       }
2226       if (! $app_only_auth) {
2227         $authorization = $this->_sign($httpmethod, $url, $params);
2228       }
2229       $params = http_build_query($params);
2230     }
2231     return [$authorization, $params, $request_headers];
2232   }
2233
2234   /**
2235    * Appends HTTP status and rate limiting info to the reply
2236    *
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
2240    */
2241   protected function _appendHttpStatusAndRate($reply, $httpstatus, $rate)
2242   {
2243     switch ($this->_return_format) {
2244       case CODEBIRD_RETURNFORMAT_ARRAY:
2245         $reply['httpstatus'] = $httpstatus;
2246         $reply['rate']       = $rate;
2247         break;
2248       case CODEBIRD_RETURNFORMAT_OBJECT:
2249         $reply->httpstatus = $httpstatus;
2250         $reply->rate       = $rate;
2251         break;
2252       case CODEBIRD_RETURNFORMAT_JSON:
2253         $reply             = $this->_json_decode($reply);
2254         $reply->httpstatus = $httpstatus;
2255         $reply->rate       = $rate;
2256         $reply             = json_encode($reply);
2257         break;
2258     }
2259     return $reply;
2260   }
2261
2262   /**
2263    * Get Bearer authorization string
2264    *
2265    * @return string authorization
2266    * @throws \Exception
2267    */
2268   protected function _getBearerAuthorization()
2269   {
2270     if (self::$_consumer_key === null
2271       && self::$_bearer_token === null
2272     ) {
2273       throw new \Exception('To make an app-only auth API request, consumer key or bearer token must be set.');
2274     }
2275     // automatically fetch bearer token, if necessary
2276     if (self::$_bearer_token === null) {
2277       $this->oauth2_token();
2278     }
2279     return 'Bearer ' . self::$_bearer_token;
2280   }
2281
2282   /**
2283    * Do preparations to make the API call
2284    *
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
2291    *
2292    * @return array (string authorization, string url, array params, array request_headers)
2293    */
2294   protected function _callApiPreparations(
2295     $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2296   )
2297   {
2298     $url             = $this->_getEndpoint($method, $method_template);
2299     $request_headers = [];
2300     if ($httpmethod === 'GET') {
2301       // GET
2302       list ($authorization, $url) =
2303         $this->_callApiPreparationsGet($httpmethod, $url, $params, $app_only_auth);
2304     } else {
2305       // POST
2306       list ($authorization, $params, $request_headers) =
2307         $this->_callApiPreparationsPost($httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth);
2308     }
2309     if ($app_only_auth) {
2310       $authorization = $this->_getBearerAuthorization();
2311     }
2312
2313     return [
2314       $authorization, $url, $params, $request_headers
2315     ];
2316   }
2317
2318   /**
2319    * Calls the streaming API
2320    *
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
2326    *
2327    * @return void
2328    * @throws \Exception
2329    */
2330
2331   protected function _callApiStreaming(
2332     $httpmethod, $method, $method_template, $params = [], $app_only_auth = false
2333   )
2334   {
2335     if ($this->_streaming_callback === null) {
2336       throw new \Exception('Set streaming callback before consuming a stream.');
2337     }
2338
2339     $params['delimited'] = 'length';
2340
2341     list ($authorization, $url, $params, $request_headers)
2342       = $this->_callApiPreparations(
2343         $httpmethod, $method, $method_template, $params, false, $app_only_auth
2344       );
2345
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.');
2351     }
2352
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);
2358     }
2359
2360     $errno   = 0;
2361     $errstr  = '';
2362     $connection = stream_socket_client(
2363       'ssl://' . $hostname . ':443',
2364       $errno, $errstr,
2365       $this->_timeouts['connect'],
2366       STREAM_CLIENT_CONNECT
2367     );
2368
2369     // send request
2370     $request = $httpmethod . ' '
2371       . $path . ($query ? '?' . $query : '') . " HTTP/1.1\r\n"
2372       . 'Host: ' . $hostname . "\r\n"
2373       . implode("\r\n", $request_headers)
2374       . "\r\n\r\n";
2375     if ($httpmethod !== 'GET') {
2376       $request .= $params;
2377     }
2378     fputs($connection, $request);
2379     stream_set_blocking($connection, 0);
2380     stream_set_timeout($connection, 0);
2381
2382     // collect headers
2383     do {
2384       $result  = stream_get_line($connection, 1048576, "\r\n\r\n");
2385     } while(!$result);
2386     $headers = explode("\r\n", $result);
2387
2388     // find HTTP status
2389     $httpstatus     = $this->_getHttpStatusFromHeaders($headers);
2390     list($headers,) = $this->_parseApiHeaders($result);
2391     $rate           = $this->_getRateLimitInfo($headers);
2392
2393     if ($httpstatus !== '200') {
2394       $reply = [
2395         'httpstatus' => $httpstatus,
2396         'rate'       => $rate
2397       ];
2398       switch ($this->_return_format) {
2399         case CODEBIRD_RETURNFORMAT_ARRAY:
2400           return $reply;
2401         case CODEBIRD_RETURNFORMAT_OBJECT:
2402           return (object) $reply;
2403         case CODEBIRD_RETURNFORMAT_JSON:
2404           return json_encode($reply);
2405       }
2406     }
2407
2408     $signal_function = function_exists('pcntl_signal_dispatch');
2409     $data            = '';
2410     $last_message    = $this->_time();
2411     $message_length  = 0;
2412
2413     while (!feof($connection)) {
2414       // call signal handlers, if any
2415       if ($signal_function) {
2416         pcntl_signal_dispatch();
2417       }
2418       $connection_array = [$connection];
2419       $write            = $except = null;
2420       if (false === ($num_changed_streams = stream_select($connection_array, $write, $except, 0, 200000))) {
2421         break;
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) {
2427             break;
2428           }
2429           $last_message = $this->_time();
2430         }
2431         continue;
2432       }
2433       $chunk_length = fgets($connection, 10);
2434       if ($chunk_length === '' || !$chunk_length = hexdec($chunk_length)) {
2435         continue;
2436       }
2437
2438       $chunk = '';
2439       do {
2440         $chunk .= fread($connection, $chunk_length);
2441         $chunk_length -= strlen($chunk);
2442       } while($chunk_length > 0);
2443
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);
2448         } else {
2449           continue;
2450         }
2451
2452         $data = $chunk;
2453       } else {
2454         $data .= $chunk;
2455       }
2456
2457       if (strlen($data) < $message_length) {
2458         continue;
2459       }
2460
2461       $reply = $this->_parseApiReply($data);
2462       switch ($this->_return_format) {
2463         case CODEBIRD_RETURNFORMAT_ARRAY:
2464           $reply['httpstatus'] = $httpstatus;
2465           $reply['rate']       = $rate;
2466           break;
2467         case CODEBIRD_RETURNFORMAT_OBJECT:
2468           $reply->httpstatus = $httpstatus;
2469           $reply->rate       = $rate;
2470           break;
2471       }
2472
2473       $cancel_stream = $this->_deliverStreamingMessage($reply);
2474       if ($cancel_stream) {
2475         fclose($connection);
2476         break;
2477       }
2478
2479       $data           = '';
2480       $message_length = 0;
2481       $last_message   = $this->_time();
2482     }
2483
2484     return;
2485   }
2486
2487   /**
2488    * Calls streaming callback with received message
2489    *
2490    * @param string|array|object message
2491    *
2492    * @return bool Whether to cancel streaming
2493    */
2494   protected function _deliverStreamingMessage($message)
2495   {
2496     return call_user_func($this->_streaming_callback, $message);
2497   }
2498
2499   /**
2500    * Parses the API reply to separate headers from the body
2501    *
2502    * @param string $reply The actual raw HTTP request reply
2503    *
2504    * @return array (headers, reply)
2505    */
2506   protected function _parseApiHeaders($reply) {
2507     // split headers and body
2508     $headers = [];
2509     $reply = explode("\r\n\r\n", $reply, 4);
2510
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'
2515     ) {
2516       array_shift($reply);
2517     }
2518
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];
2523       $value = '';
2524       if (count($header_array) > 1) {
2525         $value = $header_array[1];
2526       }
2527       $headers[$key] = $value;
2528     }
2529
2530     if (count($reply) > 1) {
2531       $reply = $reply[1];
2532     } else {
2533       $reply = '';
2534     }
2535
2536     return [$headers, $reply];
2537   }
2538
2539   /**
2540    * Parses the API headers to return Location and Ton API headers
2541    *
2542    * @param array  $headers The headers list
2543    * @param string $reply   The actual HTTP body
2544    *
2545    * @return string $reply
2546    */
2547   protected function _parseApiReplyPrefillHeaders($headers, $reply)
2548   {
2549     if ($reply === '' && (isset($headers['Location']))) {
2550       $reply = [
2551         'Location' => $headers['Location']
2552       ];
2553       if (isset($headers['X-TON-Min-Chunk-Size'])) {
2554         $reply['X-TON-Min-Chunk-Size'] = $headers['X-TON-Min-Chunk-Size'];
2555       }
2556       if (isset($headers['X-TON-Max-Chunk-Size'])) {
2557         $reply['X-TON-Max-Chunk-Size'] = $headers['X-TON-Max-Chunk-Size'];
2558       }
2559       if (isset($headers['Range'])) {
2560         $reply['Range'] = $headers['Range'];
2561       }
2562       $reply = json_encode($reply);
2563     }
2564     return $reply;
2565   }
2566
2567   /**
2568    * Parses the API reply to encode it in the set return_format
2569    *
2570    * @param string $reply The actual HTTP body, JSON-encoded or URL-encoded
2571    *
2572    * @return array|string|object The parsed reply
2573    */
2574   protected function _parseApiReply($reply)
2575   {
2576     $need_array = $this->_return_format === CODEBIRD_RETURNFORMAT_ARRAY;
2577     if ($reply === '[]') {
2578       switch ($this->_return_format) {
2579         case CODEBIRD_RETURNFORMAT_ARRAY:
2580           return [];
2581         case CODEBIRD_RETURNFORMAT_JSON:
2582           return '{}';
2583         case CODEBIRD_RETURNFORMAT_OBJECT:
2584           return new \stdClass;
2585       }
2586     }
2587     if (! $parsed = $this->_json_decode($reply, $need_array)) {
2588       if ($reply) {
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;
2595           } else {
2596             $parsed['message'] = $element;
2597           }
2598         }
2599       }
2600       $reply = json_encode($parsed);
2601     }
2602     switch ($this->_return_format) {
2603       case CODEBIRD_RETURNFORMAT_ARRAY:
2604         return $parsed;
2605       case CODEBIRD_RETURNFORMAT_JSON:
2606         return $reply;
2607       case CODEBIRD_RETURNFORMAT_OBJECT:
2608         return (object) $parsed;
2609     }
2610     return $parsed;
2611   }
2612 }