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