1<?php
2
3namespace Codebird;
4
5/**
6 * A Twitter library in PHP.
7 *
8 * @package codebird
9 * @version 2.4.1
10 * @author Jublo IT Solutions &lt;support@jublo.net&gt;
11 * @copyright 2010-2014 Jublo IT Solutions &lt;support@jublo.net&gt;
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');
31foreach ($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);
42foreach ($constants as $id => $i) {
43    defined($id) or define($id, $i);
44}
45unset($constants);
46unset($i);
47unset($id);
48
49/**
50 * A Twitter library in PHP.
51 *
52 * @package codebird
53 * @subpackage codebird-php
54 */
55class 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 StringHelper $key    OAuth consumer key
130     * @param StringHelper $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 StringHelper $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 StringHelper 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 StringHelper $token  OAuth request or access token
166     * @param StringHelper $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 StringHelper $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 StringHelper 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 StringHelper 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 StringHelper 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 StringHelper $data The data to calculate the hash from
460     *
461     * @return StringHelper 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 StringHelper 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 StringHelper          $httpmethod Usually either 'GET' or 'POST' or 'DELETE'
494     * @param StringHelper          $method     The API method to call
495     * @param array  optional $params     The API call parameters, associative
496     *
497     * @return StringHelper 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 StringHelper $method The API method to call
545     * @param array  $params The parameters to send along
546     *
547     * @return StringHelper 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 StringHelper $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 StringHelper $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 StringHelper $method           The API method to call
824     * @param StringHelper $method_template  The API method template to call
825     *
826     * @return StringHelper 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 StringHelper          $httpmethod      The HTTP method to use for making the request
842     * @param StringHelper          $method          The API method to call
843     * @param StringHelper          $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 StringHelper $method The method that has been called
940     * @param StringHelper $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?>
1027