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