]> git.mxchange.org Git - friendica-addons.git/blob - twitter/codebird.php
Merge pull request #462 from MrPetovan/task/3942-move-user-to-src
[friendica-addons.git] / twitter / codebird.php
1 <?php
2
3 namespace Codebird;
4
5 /**
6  * A Twitter library in PHP.
7  *
8  * @package codebird
9  * @version 2.4.1
10  * @author J.M. <me@mynetx.net>
11  * @copyright 2010-2013 J.M. <me@mynetx.net>
12  *
13  * This program is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation, either version 3 of the License, or
16  * (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26
27 /**
28  * Define constants
29  */
30 $constants = explode(' ', 'OBJECT ARRAY JSON');
31 foreach ($constants as $i => $id) {
32     $id = 'CODEBIRD_RETURNFORMAT_' . $id;
33     defined($id) or define($id, $i);
34 }
35 $constants = array(
36     'CURLE_SSL_CERTPROBLEM' => 58,
37     'CURLE_SSL_CACERT' => 60,
38     'CURLE_SSL_CACERT_BADFILE' => 77,
39     'CURLE_SSL_CRL_BADFILE' => 82,
40     'CURLE_SSL_ISSUER_ERROR' => 83
41 );
42 foreach ($constants as $id => $i) {
43     defined($id) or define($id, $i);
44 }
45 unset($constants);
46 unset($i);
47 unset($id);
48
49 /**
50  * A Twitter library in PHP.
51  *
52  * @package codebird
53  * @subpackage codebird-php
54  */
55 class Codebird
56 {
57     /**
58      * The current singleton instance
59      */
60     private static $_instance = null;
61
62     /**
63      * The OAuth consumer key of your registered app
64      */
65     protected static $_oauth_consumer_key = null;
66
67     /**
68      * The corresponding consumer secret
69      */
70     protected static $_oauth_consumer_secret = null;
71
72     /**
73      * The app-only bearer token. Used to authorize app-only requests
74      */
75     protected static $_oauth_bearer_token = null;
76
77     /**
78      * The API endpoint to use
79      */
80     protected static $_endpoint = 'https://api.twitter.com/1.1/';
81
82     /**
83      * The API endpoint to use for OAuth requests
84      */
85     protected static $_endpoint_oauth = 'https://api.twitter.com/';
86
87     /**
88      * The Request or access token. Used to sign requests
89      */
90     protected $_oauth_token = null;
91
92     /**
93      * The corresponding request or access token secret
94      */
95     protected $_oauth_token_secret = null;
96
97     /**
98      * The format of data to return from API calls
99      */
100     protected $_return_format = CODEBIRD_RETURNFORMAT_OBJECT;
101
102     /**
103      * The file formats that Twitter accepts as image uploads
104      */
105     protected $_supported_media_files = array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG);
106
107     /**
108      * The current Codebird version
109      */
110     protected $_version = '2.4.1';
111
112     /**
113      * Returns singleton class instance
114      * Always use this method unless you're working with multiple authenticated users at once
115      *
116      * @return Codebird The instance
117      */
118     public static function getInstance()
119     {
120         if (self::$_instance == null) {
121             self::$_instance = new self;
122         }
123         return self::$_instance;
124     }
125
126     /**
127      * Sets the OAuth consumer key and secret (App key)
128      *
129      * @param string $key    OAuth consumer key
130      * @param string $secret OAuth consumer secret
131      *
132      * @return void
133      */
134     public static function setConsumerKey($key, $secret)
135     {
136         self::$_oauth_consumer_key    = $key;
137         self::$_oauth_consumer_secret = $secret;
138     }
139
140     /**
141      * Sets the OAuth2 app-only auth bearer token
142      *
143      * @param string $token OAuth2 bearer token
144      *
145      * @return void
146      */
147     public static function setBearerToken($token)
148     {
149         self::$_oauth_bearer_token = $token;
150     }
151
152     /**
153      * Gets the current Codebird version
154      *
155      * @return string The version number
156      */
157     public function getVersion()
158     {
159         return $this->_version;
160     }
161
162     /**
163      * Sets the OAuth request or access token and secret (User key)
164      *
165      * @param string $token  OAuth request or access token
166      * @param string $secret OAuth request or access token secret
167      *
168      * @return void
169      */
170     public function setToken($token, $secret)
171     {
172         $this->_oauth_token        = $token;
173         $this->_oauth_token_secret = $secret;
174     }
175
176     /**
177      * Sets the format for API replies
178      *
179      * @param int $return_format One of these:
180      *                           CODEBIRD_RETURNFORMAT_OBJECT (default)
181      *                           CODEBIRD_RETURNFORMAT_ARRAY
182      *
183      * @return void
184      */
185     public function setReturnFormat($return_format)
186     {
187         $this->_return_format = $return_format;
188     }
189
190     /**
191      * Main API handler working on any requests you issue
192      *
193      * @param string $fn    The member function you called
194      * @param array $params The parameters you sent along
195      *
196      * @return mixed The API reply encoded in the set return_format
197      */
198
199     public function __call($fn, $params)
200     {
201         // parse parameters
202         $apiparams = array();
203         if (count($params) > 0) {
204             if (is_array($params[0])) {
205                 $apiparams = $params[0];
206             } else {
207                 parse_str($params[0], $apiparams);
208                 // remove auto-added slashes if on magic quotes steroids
209                 if (get_magic_quotes_gpc()) {
210                     foreach($apiparams as $key => $value) {
211                         if (is_array($value)) {
212                             $apiparams[$key] = array_map('stripslashes', $value);
213                         } else {
214                             $apiparams[$key] = stripslashes($value);
215                         }
216                     }
217                 }
218             }
219         }
220
221         // stringify null and boolean parameters
222         foreach ($apiparams as $key => $value) {
223             if (! is_scalar($value)) {
224                 continue;
225             }
226             if (is_null($value)) {
227                 $apiparams[$key] = 'null';
228             } elseif (is_bool($value)) {
229                 $apiparams[$key] = $value ? 'true' : 'false';
230             }
231         }
232
233         $app_only_auth = false;
234         if (count($params) > 1) {
235             $app_only_auth = !! $params[1];
236         }
237
238         // map function name to API method
239         $method = '';
240
241         // replace _ by /
242         $path = explode('_', $fn);
243         for ($i = 0; $i < count($path); $i++) {
244             if ($i > 0) {
245                 $method .= '/';
246             }
247             $method .= $path[$i];
248         }
249         // undo replacement for URL parameters
250         $url_parameters_with_underscore = array('screen_name');
251         foreach ($url_parameters_with_underscore as $param) {
252             $param = strtoupper($param);
253             $replacement_was = str_replace('_', '/', $param);
254             $method = str_replace($replacement_was, $param, $method);
255         }
256
257         // replace AA by URL parameters
258         $method_template = $method;
259         $match   = array();
260         if (preg_match('/[A-Z_]{2,}/', $method, $match)) {
261             foreach ($match as $param) {
262                 $param_l = strtolower($param);
263                 $method_template = str_replace($param, ':' . $param_l, $method_template);
264                 if (!isset($apiparams[$param_l])) {
265                     for ($i = 0; $i < 26; $i++) {
266                         $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
267                     }
268                     throw new \Exception(
269                         'To call the templated method "' . $method_template
270                         . '", specify the parameter value for "' . $param_l . '".'
271                     );
272                 }
273                 $method  = str_replace($param, $apiparams[$param_l], $method);
274                 unset($apiparams[$param_l]);
275             }
276         }
277
278         // replace A-Z by _a-z
279         for ($i = 0; $i < 26; $i++) {
280             $method  = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method);
281             $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
282         }
283
284         $httpmethod = $this->_detectMethod($method_template, $apiparams);
285         $multipart  = $this->_detectMultipart($method_template);
286
287         return $this->_callApi(
288             $httpmethod,
289             $method,
290             $method_template,
291             $apiparams,
292             $multipart,
293             $app_only_auth
294         );
295     }
296
297     /**
298      * Uncommon API methods
299      */
300
301     /**
302      * Gets the OAuth authenticate URL for the current request token
303      *
304      * @return string The OAuth authenticate URL
305      */
306     public function oauth_authenticate($force_login = NULL, $screen_name = NULL)
307     {
308         if ($this->_oauth_token == null) {
309             throw new \Exception('To get the authenticate URL, the OAuth token must be set.');
310         }
311         $url = self::$_endpoint_oauth . 'oauth/authenticate?oauth_token=' . $this->_url($this->_oauth_token);
312         if ($force_login) {
313             $url .= "&force_login=1";
314         }
315         if ($screen_name) {
316             $url .= "&screen_name=" . $screen_name;
317         }
318         return $url;
319     }
320
321     /**
322      * Gets the OAuth authorize URL for the current request token
323      *
324      * @return string The OAuth authorize URL
325      */
326     public function oauth_authorize($force_login = NULL, $screen_name = NULL)
327     {
328         if ($this->_oauth_token == null) {
329             throw new \Exception('To get the authorize URL, the OAuth token must be set.');
330         }
331         $url = self::$_endpoint_oauth . 'oauth/authorize?oauth_token=' . $this->_url($this->_oauth_token);
332         if ($force_login) {
333             $url .= "&force_login=1";
334         }
335         if ($screen_name) {
336             $url .= "&screen_name=" . $screen_name;
337         }
338         return $url;
339     }
340
341     /**
342      * Gets the OAuth bearer token
343      *
344      * @return string The OAuth bearer token
345      */
346
347     public function oauth2_token()
348     {
349         if (! function_exists('curl_init')) {
350             throw new \Exception('To make API requests, the PHP curl extension must be available.');
351         }
352         if (self::$_oauth_consumer_key == null) {
353             throw new \Exception('To obtain a bearer token, the consumer key must be set.');
354         }
355         $ch  = false;
356         $post_fields = array(
357             'grant_type' => 'client_credentials'
358         );
359         $url = self::$_endpoint_oauth . 'oauth2/token';
360         $ch = curl_init($url);
361         curl_setopt($ch, CURLOPT_POST, 1);
362         curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
363         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
364         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
365         curl_setopt($ch, CURLOPT_HEADER, 1);
366         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
367         curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
368         curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem');
369
370         curl_setopt($ch, CURLOPT_USERPWD, self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret);
371         curl_setopt($ch, CURLOPT_HTTPHEADER, array(
372             'Expect:'
373         ));
374         $reply = curl_exec($ch);
375
376         // certificate validation results
377         $validation_result = curl_errno($ch);
378         if (in_array(
379                 $validation_result,
380                 array(
381                     CURLE_SSL_CERTPROBLEM,
382                     CURLE_SSL_CACERT,
383                     CURLE_SSL_CACERT_BADFILE,
384                     CURLE_SSL_CRL_BADFILE,
385                     CURLE_SSL_ISSUER_ERROR
386                 )
387             )
388         ) {
389             throw new \Exception('Error ' . $validation_result . ' while validating the Twitter API certificate.');
390         }
391
392         $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
393         $reply = $this->_parseApiReply('oauth2/token', $reply);
394         switch ($this->_return_format) {
395             case CODEBIRD_RETURNFORMAT_ARRAY:
396                 $reply['httpstatus'] = $httpstatus;
397                 if ($httpstatus == 200) {
398                     self::setBearerToken($reply['access_token']);
399                 }
400                 break;
401             case CODEBIRD_RETURNFORMAT_JSON:
402                 if ($httpstatus == 200) {
403                     $parsed = json_decode($reply);
404                     self::setBearerToken($parsed->access_token);
405                 }
406                 break;
407             case CODEBIRD_RETURNFORMAT_OBJECT:
408                 $reply->httpstatus = $httpstatus;
409                 if ($httpstatus == 200) {
410                     self::setBearerToken($reply->access_token);
411                 }
412                 break;
413         }
414         return $reply;
415     }
416
417     /**
418      * Signing helpers
419      */
420
421     /**
422      * URL-encodes the given data
423      *
424      * @param mixed $data
425      *
426      * @return mixed The encoded data
427      */
428     private function _url($data)
429     {
430         if (is_array($data)) {
431             return array_map(array(
432                 $this,
433                 '_url'
434             ), $data);
435         } elseif (is_scalar($data)) {
436             return str_replace(array(
437                 '+',
438                 '!',
439                 '*',
440                 "'",
441                 '(',
442                 ')'
443             ), array(
444                 ' ',
445                 '%21',
446                 '%2A',
447                 '%27',
448                 '%28',
449                 '%29'
450             ), rawurlencode($data));
451         } else {
452             return '';
453         }
454     }
455
456     /**
457      * Gets the base64-encoded SHA1 hash for the given data
458      *
459      * @param string $data The data to calculate the hash from
460      *
461      * @return string The hash
462      */
463     private function _sha1($data)
464     {
465         if (self::$_oauth_consumer_secret == null) {
466             throw new \Exception('To generate a hash, the consumer secret must be set.');
467         }
468         if (!function_exists('hash_hmac')) {
469             throw new \Exception('To generate a hash, the PHP hash extension must be available.');
470         }
471         return base64_encode(hash_hmac('sha1', $data, self::$_oauth_consumer_secret . '&'
472             . ($this->_oauth_token_secret != null ? $this->_oauth_token_secret : ''), true));
473     }
474
475     /**
476      * Generates a (hopefully) unique random string
477      *
478      * @param int optional $length The length of the string to generate
479      *
480      * @return string The random string
481      */
482     protected function _nonce($length = 8)
483     {
484         if ($length < 1) {
485             throw new \Exception('Invalid nonce length.');
486         }
487         return substr(md5(microtime(true)), 0, $length);
488     }
489
490     /**
491      * Generates an OAuth signature
492      *
493      * @param string          $httpmethod Usually either 'GET' or 'POST' or 'DELETE'
494      * @param string          $method     The API method to call
495      * @param array  optional $params     The API call parameters, associative
496      *
497      * @return string Authorization HTTP header
498      */
499     protected function _sign($httpmethod, $method, $params = array())
500     {
501         if (self::$_oauth_consumer_key == null) {
502             throw new \Exception('To generate a signature, the consumer key must be set.');
503         }
504         $sign_params      = array(
505             'consumer_key' => self::$_oauth_consumer_key,
506             'version' => '1.0',
507             'timestamp' => time(),
508             'nonce' => $this->_nonce(),
509             'signature_method' => 'HMAC-SHA1'
510         );
511         $sign_base_params = array();
512         foreach ($sign_params as $key => $value) {
513             $sign_base_params['oauth_' . $key] = $this->_url($value);
514         }
515         if ($this->_oauth_token != null) {
516             $sign_base_params['oauth_token'] = $this->_url($this->_oauth_token);
517         }
518         $oauth_params = $sign_base_params;
519         foreach ($params as $key => $value) {
520             $sign_base_params[$key] = $this->_url($value);
521         }
522         ksort($sign_base_params);
523         $sign_base_string = '';
524         foreach ($sign_base_params as $key => $value) {
525             $sign_base_string .= $key . '=' . $value . '&';
526         }
527         $sign_base_string = substr($sign_base_string, 0, -1);
528         $signature        = $this->_sha1($httpmethod . '&' . $this->_url($method) . '&' . $this->_url($sign_base_string));
529
530         $params = array_merge($oauth_params, array(
531             'oauth_signature' => $signature
532         ));
533         ksort($params);
534         $authorization = 'Authorization: OAuth ';
535         foreach ($params as $key => $value) {
536             $authorization .= $key . '="' . $this->_url($value) . '", ';
537         }
538         return substr($authorization, 0, -2);
539     }
540
541     /**
542      * Detects HTTP method to use for API call
543      *
544      * @param string $method The API method to call
545      * @param array  $params The parameters to send along
546      *
547      * @return string The HTTP method that should be used
548      */
549     protected function _detectMethod($method, $params)
550     {
551         // multi-HTTP method endpoints
552         switch($method) {
553             case 'account/settings':
554                 $method = count($params) > 0 ? $method . '__post' : $method;
555                 break;
556         }
557
558         $httpmethods         = array();
559         $httpmethods['GET']  = array(
560             // Timelines
561             'statuses/mentions_timeline',
562             'statuses/user_timeline',
563             'statuses/home_timeline',
564             'statuses/retweets_of_me',
565
566             // Tweets
567             'statuses/retweets/:id',
568             'statuses/show/:id',
569             'statuses/oembed',
570
571             // Search
572             'search/tweets',
573
574             // Direct Messages
575             'direct_messages',
576             'direct_messages/sent',
577             'direct_messages/show',
578
579             // Friends & Followers
580             'friendships/no_retweets/ids',
581             'friends/ids',
582             'followers/ids',
583             'friendships/lookup',
584             'friendships/incoming',
585             'friendships/outgoing',
586             'friendships/show',
587             'friends/list',
588             'followers/list',
589
590             // Users
591             'account/settings',
592             'account/verify_credentials',
593             'blocks/list',
594             'blocks/ids',
595             'users/lookup',
596             'users/show',
597             'users/search',
598             'users/contributees',
599             'users/contributors',
600             'users/profile_banner',
601
602             // Suggested Users
603             'users/suggestions/:slug',
604             'users/suggestions',
605             'users/suggestions/:slug/members',
606
607             // Favorites
608             'favorites/list',
609
610             // Lists
611             'lists/list',
612             'lists/statuses',
613             'lists/memberships',
614             'lists/subscribers',
615             'lists/subscribers/show',
616             'lists/members/show',
617             'lists/members',
618             'lists/show',
619             'lists/subscriptions',
620
621             // Saved searches
622             'saved_searches/list',
623             'saved_searches/show/:id',
624
625             // Places & Geo
626             'geo/id/:place_id',
627             'geo/reverse_geocode',
628             'geo/search',
629             'geo/similar_places',
630
631             // Trends
632             'trends/place',
633             'trends/available',
634             'trends/closest',
635
636             // OAuth
637             'oauth/authenticate',
638             'oauth/authorize',
639
640             // Help
641             'help/configuration',
642             'help/languages',
643             'help/privacy',
644             'help/tos',
645             'application/rate_limit_status'
646         );
647         $httpmethods['POST'] = array(
648             // Tweets
649             'statuses/destroy/:id',
650             'statuses/update',
651             'statuses/retweet/:id',
652             'statuses/update_with_media',
653
654             // Direct Messages
655             'direct_messages/destroy',
656             'direct_messages/new',
657
658             // Friends & Followers
659             'friendships/create',
660             'friendships/destroy',
661             'friendships/update',
662
663             // Users
664             'account/settings__post',
665             'account/update_delivery_device',
666             'account/update_profile',
667             'account/update_profile_background_image',
668             'account/update_profile_colors',
669             'account/update_profile_image',
670             'blocks/create',
671             'blocks/destroy',
672             'account/update_profile_banner',
673             'account/remove_profile_banner',
674
675             // Favorites
676             'favorites/destroy',
677             'favorites/create',
678
679             // Lists
680             'lists/members/destroy',
681             'lists/subscribers/create',
682             'lists/subscribers/destroy',
683             'lists/members/create_all',
684             'lists/members/create',
685             'lists/destroy',
686             'lists/update',
687             'lists/create',
688             'lists/members/destroy_all',
689
690             // Saved Searches
691             'saved_searches/create',
692             'saved_searches/destroy/:id',
693
694             // Places & Geo
695             'geo/place',
696
697             // Spam Reporting
698             'users/report_spam',
699
700             // OAuth
701             'oauth/access_token',
702             'oauth/request_token',
703             'oauth2/token',
704             'oauth2/invalidate_token'
705         );
706         foreach ($httpmethods as $httpmethod => $methods) {
707             if (in_array($method, $methods)) {
708                 return $httpmethod;
709             }
710         }
711         throw new \Exception('Can\'t find HTTP method to use for "' . $method . '".');
712     }
713
714     /**
715      * Detects if API call should use multipart/form-data
716      *
717      * @param string $method The API method to call
718      *
719      * @return bool Whether the method should be sent as multipart
720      */
721     protected function _detectMultipart($method)
722     {
723         $multiparts = array(
724             // Tweets
725             'statuses/update_with_media',
726
727             // Users
728             'account/update_profile_background_image',
729             'account/update_profile_image',
730             'account/update_profile_banner'
731         );
732         return in_array($method, $multiparts);
733     }
734
735     /**
736      * Detect filenames in upload parameters,
737      * build multipart request from upload params
738      *
739      * @param string $method  The API method to call
740      * @param array  $params  The parameters to send along
741      *
742      * @return void
743      */
744     protected function _buildMultipart($method, $params)
745     {
746         // well, files will only work in multipart methods
747         if (! $this->_detectMultipart($method)) {
748             return;
749         }
750
751         // only check specific parameters
752         $possible_files = array(
753             // Tweets
754             'statuses/update_with_media' => 'media[]',
755             // Accounts
756             'account/update_profile_background_image' => 'image',
757             'account/update_profile_image' => 'image',
758             'account/update_profile_banner' => 'banner'
759         );
760         // method might have files?
761         if (! in_array($method, array_keys($possible_files))) {
762             return;
763         }
764
765         $possible_files = explode(' ', $possible_files[$method]);
766
767         $multipart_border = '--------------------' . $this->_nonce();
768         $multipart_request = '';
769
770         foreach ($params as $key => $value) {
771             // is it an array?
772             if (is_array($value)) {
773                 throw new \Exception('Using URL-encoded parameters is not supported for uploading media.');
774                 continue;
775             }
776             $multipart_request .=
777                 '--' . $multipart_border . "\r\n"
778                 . 'Content-Disposition: form-data; name="' . $key . '"';
779
780             // check for filenames
781             if (in_array($key, $possible_files)) {
782                 if (// is it a file, a readable one?
783                     @file_exists($value)
784                     && @is_readable($value)
785
786                     // is it a valid image?
787                     && $data = @getimagesize($value)
788                 ) {
789                     if (// is it a supported image format?
790                         in_array($data[2], $this->_supported_media_files)
791                     ) {
792                         // try to read the file
793                         ob_start();
794                         readfile($value);
795                         $data = ob_get_contents();
796                         ob_end_clean();
797                         if (strlen($data) == 0) {
798                             continue;
799                         }
800                         $value = $data;
801                     }
802                 }
803
804                 /*
805                 $multipart_request .=
806                     "\r\nContent-Transfer-Encoding: base64";
807                 $value = base64_encode($value);
808                 */
809             }
810
811             $multipart_request .=
812                 "\r\n\r\n" . $value . "\r\n";
813         }
814         $multipart_request .= '--' . $multipart_border . '--';
815
816         return $multipart_request;
817     }
818
819
820     /**
821      * Builds the complete API endpoint url
822      *
823      * @param string $method           The API method to call
824      * @param string $method_template  The API method template to call
825      *
826      * @return string The URL to send the request to
827      */
828     protected function _getEndpoint($method, $method_template)
829     {
830         if (substr($method, 0, 5) == 'oauth') {
831             $url = self::$_endpoint_oauth . $method;
832         } else {
833             $url = self::$_endpoint . $method . '.json';
834         }
835         return $url;
836     }
837
838     /**
839      * Calls the API using cURL
840      *
841      * @param string          $httpmethod      The HTTP method to use for making the request
842      * @param string          $method          The API method to call
843      * @param string          $method_template The templated API method to call
844      * @param array  optional $params          The parameters to send along
845      * @param bool   optional $multipart       Whether to use multipart/form-data
846      * @param bool   optional $app_only_auth   Whether to use app-only bearer authentication
847      *
848      * @return mixed The API reply, encoded in the set return_format
849      */
850
851     protected function _callApi($httpmethod, $method, $method_template, $params = array(), $multipart = false, $app_only_auth = false)
852     {
853         if (! function_exists('curl_init')) {
854             throw new \Exception('To make API requests, the PHP curl extension must be available.');
855         }
856         $url = $this->_getEndpoint($method, $method_template);
857         $ch  = false;
858         if ($httpmethod == 'GET') {
859             $url_with_params = $url;
860             if (count($params) > 0) {
861                 $url_with_params .= '?' . http_build_query($params);
862             }
863             $authorization = $this->_sign($httpmethod, $url, $params);
864             $ch = curl_init($url_with_params);
865         } else {
866             if ($multipart) {
867                 $authorization = $this->_sign($httpmethod, $url, array());
868                 $params        = $this->_buildMultipart($method_template, $params);
869             } else {
870                 $authorization = $this->_sign($httpmethod, $url, $params);
871                 $params        = http_build_query($params);
872             }
873             $ch = curl_init($url);
874             curl_setopt($ch, CURLOPT_POST, 1);
875             curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
876         }
877         if ($app_only_auth) {
878             if (self::$_oauth_consumer_key == null) {
879                 throw new \Exception('To make an app-only auth API request, the consumer key must be set.');
880             }
881             // automatically fetch bearer token, if necessary
882             if (self::$_oauth_bearer_token == null) {
883                 $this->oauth2_token();
884             }
885             $authorization = 'Authorization: Bearer ' . self::$_oauth_bearer_token;
886         }
887         $request_headers = array();
888         if (isset($authorization)) {
889             $request_headers[] = $authorization;
890             $request_headers[] = 'Expect:';
891         }
892         if ($multipart) {
893             $first_newline      = strpos($params, "\r\n");
894             $multipart_boundary = substr($params, 2, $first_newline - 2);
895             $request_headers[]  = 'Content-Length: ' . strlen($params);
896             $request_headers[]  = 'Content-Type: multipart/form-data; boundary='
897                 . $multipart_boundary;
898         }
899
900         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
901         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
902         curl_setopt($ch, CURLOPT_HEADER, 1);
903         curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers);
904         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
905         curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
906         curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem');
907
908         $reply = curl_exec($ch);
909
910         // certificate validation results
911         $validation_result = curl_errno($ch);
912         if (in_array(
913                 $validation_result,
914                 array(
915                     CURLE_SSL_CERTPROBLEM,
916                     CURLE_SSL_CACERT,
917                     CURLE_SSL_CACERT_BADFILE,
918                     CURLE_SSL_CRL_BADFILE,
919                     CURLE_SSL_ISSUER_ERROR
920                 )
921             )
922         ) {
923             throw new \Exception('Error ' . $validation_result . ' while validating the Twitter API certificate.');
924         }
925
926         $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
927         $reply = $this->_parseApiReply($method_template, $reply);
928         if ($this->_return_format == CODEBIRD_RETURNFORMAT_OBJECT) {
929             $reply->httpstatus = $httpstatus;
930         } elseif ($this->_return_format == CODEBIRD_RETURNFORMAT_ARRAY) {
931             $reply['httpstatus'] = $httpstatus;
932         }
933         return $reply;
934     }
935
936     /**
937      * Parses the API reply to encode it in the set return_format
938      *
939      * @param string $method The method that has been called
940      * @param string $reply  The actual reply, JSON-encoded or URL-encoded
941      *
942      * @return array|object The parsed reply
943      */
944     protected function _parseApiReply($method, $reply)
945     {
946         // split headers and body
947         $headers = array();
948         $reply = explode("\r\n\r\n", $reply, 4);
949
950         // check if using proxy
951         if (substr($reply[0], 0, 35) === 'HTTP/1.1 200 Connection Established') {
952             array_shift($reply);
953         } elseif (count($reply) > 2) {
954             $headers = array_shift($reply);
955             $reply = array(
956                 $headers,
957                 implode("\r\n", $reply)
958             );
959         }
960
961         $headers_array = explode("\r\n", $reply[0]);
962         foreach ($headers_array as $header) {
963             $header_array = explode(': ', $header, 2);
964             $key = $header_array[0];
965             $value = '';
966             if (count($header_array) > 1) {
967                 $value = $header_array[1];
968             }
969             $headers[$key] = $value;
970         }
971         if (count($reply) > 1) {
972             $reply = $reply[1];
973         } else {
974             $reply = '';
975         }
976
977         $need_array = $this->_return_format == CODEBIRD_RETURNFORMAT_ARRAY;
978         if ($reply == '[]') {
979             switch ($this->_return_format) {
980                 case CODEBIRD_RETURNFORMAT_ARRAY:
981                     return array();
982                 case CODEBIRD_RETURNFORMAT_JSON:
983                     return '{}';
984                 case CODEBIRD_RETURNFORMAT_OBJECT:
985                     return new \stdClass;
986             }
987         }
988         $parsed = array();
989         if (! $parsed = json_decode($reply, $need_array)) {
990             if ($reply) {
991                 if (stripos($reply, '<' . '?xml version="1.0" encoding="UTF-8"?' . '>') === 0) {
992                     // we received XML...
993                     // since this only happens for errors,
994                     // don't perform a full decoding
995                     preg_match('/<request>(.*)<\/request>/', $reply, $request);
996                     preg_match('/<error>(.*)<\/error>/', $reply, $error);
997                     $parsed['request'] = htmlspecialchars_decode($request[1]);
998                     $parsed['error'] = htmlspecialchars_decode($error[1]);
999                 } else {
1000                     // assume query format
1001                     $reply = explode('&', $reply);
1002                     foreach ($reply as $element) {
1003                         if (stristr($element, '=')) {
1004                             list($key, $value) = explode('=', $element);
1005                             $parsed[$key] = $value;
1006                         } else {
1007                             $parsed['message'] = $element;
1008                         }
1009                     }
1010                 }
1011             }
1012             $reply = json_encode($parsed);
1013         }
1014         switch ($this->_return_format) {
1015             case CODEBIRD_RETURNFORMAT_ARRAY:
1016                 return $parsed;
1017             case CODEBIRD_RETURNFORMAT_JSON:
1018                 return $reply;
1019             case CODEBIRD_RETURNFORMAT_OBJECT:
1020                 return (object) $parsed;
1021         }
1022         return $parsed;
1023     }
1024 }
1025
1026 ?>