├── LICENSE
├── README.md
├── config.php
├── index.php
└── lib
├── DatumboxAPI.php
├── TwitterSentimentAnalysis.php
├── ca-chain-bundle.crt
└── twitter-client.php
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Datumbox
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Twitter-Sentiment-Analysis
2 | ==========================
3 |
4 | This tool is written in PHP and it performs Sentiment Analysis on Twitter messages by using the Datumbox API 1.0v.
5 |
6 | To read more about how it works, how it should be configured etc check out the original blog post:
7 | http://blog.datumbox.com/how-to-build-your-own-twitter-sentiment-analysis-tool/
8 |
9 | Useful Links
10 | ============
11 |
12 | Download API Documentation and Code Samples: http://www.datumbox.com/machine-learning-api/
13 |
14 | Sign-up for free API Key: http://www.datumbox.com/users/register/
15 |
16 | View your API Key: http://www.datumbox.com/apikeys/view/
17 |
18 | PHP Twitter API Client: https://github.com/timwhitlock/php-twitter-api
--------------------------------------------------------------------------------
/config.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Datumbox Twitter Sentiment Analysis Demo
8 |
9 |
10 | Datumbox Twitter Sentiment Analysis
11 | Type your keyword below to perform Sentiment Analysis on Twitter Results:
12 |
16 |
17 | $_GET['q'],
28 | 'lang'=>'en',
29 | 'count'=>10,
30 | );
31 | $results=$TwitterSentimentAnalysis->sentimentAnalysis($twitterSearchParams);
32 |
33 |
34 | ?>
35 | Results for ""
36 |
37 |
38 | Id |
39 | User |
40 | Text |
41 | Twitter Link |
42 | Sentiment |
43 |
44 |
58 |
59 | |
60 | |
61 | |
62 | View |
63 | |
64 |
65 |
68 |
69 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/lib/DatumboxAPI.php:
--------------------------------------------------------------------------------
1 | api_key=$api_key;
22 | }
23 |
24 | /**
25 | * Calls the Web Service of Datumbox
26 | *
27 | * @param string $api_method
28 | * @param array $POSTparameters
29 | *
30 | * @return string $jsonreply
31 | */
32 | protected function CallWebService($api_method,$POSTparameters) {
33 | $POSTparameters['api_key']=$this->api_key;
34 |
35 | $ch = curl_init();
36 | curl_setopt($ch, CURLOPT_URL, 'http://api.datumbox.com/'.self::version.'/'.$api_method.'.json');
37 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
38 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
39 | curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
40 | curl_setopt($ch, CURLOPT_POST, true );
41 | curl_setopt($ch, CURLOPT_POSTFIELDS, $POSTparameters);
42 |
43 | $jsonreply = curl_exec ($ch);
44 | curl_close ($ch);
45 | unset($ch);
46 |
47 | return $jsonreply;
48 | }
49 |
50 | /**
51 | * Parses the API Reply
52 | *
53 | * @param mixed $jsonreply
54 | *
55 | * @return mixed
56 | */
57 | protected function ParseReply($jsonreply) {
58 | $jsonreply=json_decode($jsonreply,true);
59 |
60 | if(isset($jsonreply['output']['status']) && $jsonreply['output']['status']==1) {
61 | return $jsonreply['output']['result'];
62 | }
63 |
64 | if(isset($jsonreply['error']['ErrorCode']) && isset($jsonreply['error']['ErrorMessage'])) {
65 | echo $jsonreply['error']['ErrorMessage'].' (ErrorCode: '.$jsonreply['error']['ErrorCode'].')';
66 | }
67 |
68 | return false;
69 | }
70 |
71 | /**
72 | * Performs Sentiment Analysis.
73 | *
74 | * @param string $text The clear text (no HTML tags) that we evaluate.
75 | *
76 | * @return string|false It returns "positive", "negative" or "neutral" on success and false on fail.
77 | */
78 | public function SentimentAnalysis($text) {
79 | $parameters=array(
80 | 'text'=>$text,
81 | );
82 |
83 | $jsonreply=$this->CallWebService('SentimentAnalysis',$parameters);
84 |
85 | return $this->ParseReply($jsonreply);
86 | }
87 |
88 | /**
89 | * Performs Sentiment Analysis on Twitter.
90 | *
91 | * @param string $text The text of the tweet that we evaluate.
92 | *
93 | * @return string|false It returns "positive", "negative" or "neutral" on success and false on fail.
94 | */
95 | public function TwitterSentimentAnalysis($text) {
96 | $parameters=array(
97 | 'text'=>$text,
98 | );
99 |
100 | $jsonreply=$this->CallWebService('TwitterSentimentAnalysis',$parameters);
101 |
102 | return $this->ParseReply($jsonreply);
103 | }
104 |
105 | /**
106 | * Performs Subjectivity Analysis.
107 | *
108 | * @param string $text The clear text (no HTML tags) that we evaluate.
109 | *
110 | * @return string|false. It returns "objective" or "subjective" on success and false on fail.
111 | */
112 | public function SubjectivityAnalysis($text) {
113 | $parameters=array(
114 | 'text'=>$text,
115 | );
116 |
117 | $jsonreply=$this->CallWebService('SubjectivityAnalysis',$parameters);
118 |
119 | return $this->ParseReply($jsonreply);
120 | }
121 |
122 | /**
123 | * Performs Topic Classification.
124 | *
125 | * @param string $text The clear text (no HTML tags) that we evaluate.
126 | *
127 | * @return string|false. It returns "Arts", "Business & Economy", "Computers & Technology", "Health", "Home & Domestic Life", "News", "Recreation & Activities", "Reference & Education", "Science", "Shopping", "Society", "Sports" on success and false on fail.
128 | */
129 | public function TopicClassification($text) {
130 | $parameters=array(
131 | 'text'=>$text,
132 | );
133 |
134 | $jsonreply=$this->CallWebService('TopicClassification',$parameters);
135 |
136 | return $this->ParseReply($jsonreply);
137 | }
138 |
139 | /**
140 | * Performs Spam Detection.
141 | *
142 | * @param string $text The clear text (no HTML tags) that we evaluate.
143 | *
144 | * @return string|false It returns "spam" or "nospam" on success and false on fail.
145 | */
146 | public function SpamDetection($text) {
147 | $parameters=array(
148 | 'text'=>$text,
149 | );
150 |
151 | $jsonreply=$this->CallWebService('SpamDetection',$parameters);
152 |
153 | return $this->ParseReply($jsonreply);
154 | }
155 |
156 | /**
157 | * Performs Adult Content Detection.
158 | *
159 | * @param string $text The clear text (no HTML tags) that we evaluate.
160 | *
161 | * @return string|false It returns "adult" or "noadult" on success and false on fail.
162 | */
163 | public function AdultContentDetection($text) {
164 | $parameters=array(
165 | 'text'=>$text,
166 | );
167 |
168 | $jsonreply=$this->CallWebService('AdultContentDetection',$parameters);
169 |
170 | return $this->ParseReply($jsonreply);
171 | }
172 |
173 | /**
174 | * Performs Readability Assessment.
175 | *
176 | * @param string $text The clear text (no HTML tags) that we evaluate.
177 | *
178 | * @return string|false It returns "basic", "intermediate" or "advanced" on success and false on fail.
179 | */
180 | public function ReadabilityAssessment($text) {
181 | $parameters=array(
182 | 'text'=>$text,
183 | );
184 |
185 | $jsonreply=$this->CallWebService('ReadabilityAssessment',$parameters);
186 |
187 | return $this->ParseReply($jsonreply);
188 | }
189 |
190 | /**
191 | * Performs Language Detection.
192 | *
193 | * @param string $text The clear text (no HTML tags) that we evaluate.
194 | *
195 | * @return string|false It returns the ISO639-1 two-letter language code (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) on success and false on fail.
196 | */
197 | public function LanguageDetection($text) {
198 | $parameters=array(
199 | 'text'=>$text,
200 | );
201 |
202 | $jsonreply=$this->CallWebService('LanguageDetection',$parameters);
203 |
204 | return $this->ParseReply($jsonreply);
205 | }
206 |
207 | /**
208 | * Performs Commercial Detection.
209 | *
210 | * @param string $text The clear text (no HTML tags) that we evaluate.
211 | *
212 | * @return string|false It returns "commercial" or "noncommercial" on success and false on fail.
213 | */
214 | public function CommercialDetection($text) {
215 | $parameters=array(
216 | 'text'=>$text,
217 | );
218 |
219 | $jsonreply=$this->CallWebService('CommercialDetection',$parameters);
220 |
221 | return $this->ParseReply($jsonreply);
222 | }
223 |
224 | /**
225 | * Performs Educational Detection.
226 | *
227 | * @param string $text The clear text (no HTML tags) that we evaluate.
228 | *
229 | * @return string|false It returns "educational" or "noneducational" on success and false on fail.
230 | */
231 | public function EducationalDetection($text) {
232 | $parameters=array(
233 | 'text'=>$text,
234 | );
235 |
236 | $jsonreply=$this->CallWebService('EducationalDetection',$parameters);
237 |
238 | return $this->ParseReply($jsonreply);
239 | }
240 |
241 | /**
242 | * Performs Gender Detection.
243 | *
244 | * @param string $text The clear text (no HTML tags) that we evaluate.
245 | *
246 | * @return string|false It returns "male" or "female" on success and false on fail.
247 | */
248 | public function GenderDetection($text) {
249 | $parameters=array(
250 | 'text'=>$text,
251 | );
252 |
253 | $jsonreply=$this->CallWebService('GenderDetection',$parameters);
254 |
255 | return $this->ParseReply($jsonreply);
256 | }
257 |
258 | /**
259 | * Performs Text Extraction. It extracts the important information (clear text) from a given webpage.
260 | *
261 | * @param string $text The HTML of the webpage.
262 | *
263 | * @return string|false It returns the clear text of the document on success and false on fail.
264 | */
265 | public function TextExtraction($text) {
266 | $parameters=array(
267 | 'text'=>$text,
268 | );
269 |
270 | $jsonreply=$this->CallWebService('TextExtraction',$parameters);
271 |
272 | return $this->ParseReply($jsonreply);
273 | }
274 |
275 | /**
276 | * Performs Keyword Extraction. It extracts the keywords and keywords combinations from a text.
277 | *
278 | * @param string $text The clear text (no HTML tags) that we analyze.
279 | * @param integer $n It is a number from 1 to 5 which denotes the number of Keyword combinations that we want to get.
280 | *
281 | * @return array|false It returns an array with the keywords of the document on success and false on fail.
282 | */
283 | public function KeywordExtraction($text,$n) {
284 | $parameters=array(
285 | 'text'=>$text,
286 | 'n'=>$n,
287 | );
288 |
289 | $jsonreply=$this->CallWebService('KeywordExtraction',$parameters);
290 |
291 | return $this->ParseReply($jsonreply);
292 | }
293 |
294 | /**
295 | * Evaluates the Document Similarity between 2 documents.
296 | *
297 | * @param string $original The first clear text (no HTML tags) that we compare.
298 | * @param string $copy The second clear text (no HTML tags) that we compare.
299 | *
300 | * @return array|false It returns an array with similarity metrics for the two documents on success and false on fail.
301 | */
302 | public function DocumentSimilarity($original,$copy) {
303 | $parameters=array(
304 | 'original'=>$original,
305 | 'copy'=>$copy,
306 | );
307 |
308 | $jsonreply=$this->CallWebService('DocumentSimilarity',$parameters);
309 |
310 | return $this->ParseReply($jsonreply);
311 | }
312 |
313 |
314 | }
315 |
316 |
--------------------------------------------------------------------------------
/lib/TwitterSentimentAnalysis.php:
--------------------------------------------------------------------------------
1 | datumbox_api_key=$datumbox_api_key;
26 |
27 | $this->consumer_key=$consumer_key;
28 | $this->consumer_secret=$consumer_secret;
29 | $this->access_key=$access_key;
30 | $this->access_secret=$access_secret;
31 | }
32 |
33 | /**
34 | * This function fetches the twitter list and evaluates their sentiment
35 | *
36 | * @param array $twitterSearchParams The Twitter Search Parameters that are passed to Twitter API. Read more here https://dev.twitter.com/docs/api/1.1/get/search/tweets
37 | *
38 | * @return array
39 | */
40 | public function sentimentAnalysis($twitterSearchParams) {
41 | $tweets=$this->getTweets($twitterSearchParams);
42 |
43 | return $this->findSentiment($tweets);
44 | }
45 |
46 | /**
47 | * Calls the Search/tweets method of the Twitter API for particular Twitter Search Parameters and returns the list of tweets that match the search criteria.
48 | *
49 | * @param mixed $twitterSearchParams The Twitter Search Parameters that are passed to Twitter API. Read more here https://dev.twitter.com/docs/api/1.1/get/search/tweets
50 | *
51 | * @return array $tweets
52 | */
53 | protected function getTweets($twitterSearchParams) {
54 | $Client = new TwitterApiClient(); //Use the TwitterAPIClient
55 | $Client->set_oauth ($this->consumer_key, $this->consumer_secret, $this->access_key, $this->access_secret);
56 |
57 | $tweets = $Client->call('search/tweets', $twitterSearchParams, 'GET' ); //call the service and get the list of tweets
58 |
59 | unset($Client);
60 |
61 | return $tweets;
62 | }
63 |
64 | protected function findSentiment($tweets) {
65 | $DatumboxAPI = new DatumboxAPI($this->datumbox_api_key); //initialize the DatumboxAPI client
66 |
67 | $results=array();
68 | foreach($tweets['statuses'] as $tweet) { //foreach of the tweets that we received
69 | if(isset($tweet['metadata']['iso_language_code']) && $tweet['metadata']['iso_language_code']=='en') { //perform sentiment analysis only for the English Tweets
70 | $sentiment=$DatumboxAPI->TwitterSentimentAnalysis($tweet['text']); //call Datumbox service to get the sentiment
71 |
72 | if($sentiment!=false) { //if the sentiment is not false, the API call was successful.
73 | $results[]=array( //add the tweet message in the results
74 | 'id'=>$tweet['id_str'],
75 | 'user'=>$tweet['user']['name'],
76 | 'text'=>$tweet['text'],
77 | 'url'=>'https://twitter.com/'.$tweet['user']['name'].'/status/'.$tweet['id_str'],
78 |
79 | 'sentiment'=>$sentiment,
80 | );
81 | }
82 | }
83 |
84 | }
85 |
86 | unset($tweets);
87 | unset($DatumboxAPI);
88 |
89 | return $results;
90 | }
91 | }
92 |
93 |
94 |
--------------------------------------------------------------------------------
/lib/twitter-client.php:
--------------------------------------------------------------------------------
1 | cache_ttl = (int) $ttl;
81 | $this->cache_ns = $namespace;
82 | return $this;
83 | }
84 | trigger_error( 'Cannot enable Twitter API cache without APC extension' );
85 | return $this->disable_cache();
86 | }
87 |
88 | /**
89 | * Disable caching for susequent API calls
90 | * @return TwitterApiClient
91 | */
92 | public function disable_cache(){
93 | $this->cache_ttl = null;
94 | $this->cache_ns = null;
95 | return $this;
96 | }
97 |
98 | /**
99 | * Test whether the client has full authentication data.
100 | * Warning: does not validate credentials
101 | * @return bool
102 | */
103 | public function has_auth(){
104 | return $this->AccessToken instanceof TwitterOAuthToken && $this->AccessToken->secret;
105 | }
106 |
107 | /**
108 | * Unset all logged in credentials - useful in error situations
109 | * @return TwitterApiClient
110 | */
111 | public function deauthorize(){
112 | $this->AccessToken = null;
113 | return $this;
114 | }
115 |
116 |
117 | /**
118 | * Set currently logged in user's OAuth access token
119 | * @param string consumer api key
120 | * @param string consumer secret
121 | * @param string access token
122 | * @param string access token secret
123 | * @return TwitterApiClient
124 | */
125 | public function set_oauth( $consumer_key, $consumer_secret, $access_key = '', $access_secret = '' ){
126 | $this->deauthorize();
127 | $this->Consumer = new TwitterOAuthToken( $consumer_key, $consumer_secret );
128 | if( $access_key && $access_secret ){
129 | $this->AccessToken = new TwitterOAuthToken( $access_key, $access_secret );
130 | }
131 | return $this;
132 | }
133 |
134 |
135 | /**
136 | * Set consumer oauth token by object
137 | * @param TwitterOAuthToken
138 | * @return TwitterApiClient
139 | */
140 | public function set_oauth_consumer( TwitterOAuthToken $token ){
141 | $this->Consumer = $token;
142 | return $this;
143 | }
144 |
145 |
146 | /**
147 | * Set access oauth token by object
148 | * @param TwitterOAuthToken
149 | * @return TwitterApiClient
150 | */
151 | public function set_oauth_access( TwitterOAuthToken $token ){
152 | $this->AccessToken = $token;
153 | return $this;
154 | }
155 |
156 |
157 |
158 | /**
159 | * Contact Twitter for a request token, which will be exchanged for an access token later.
160 | * @param string callback URL or "oob" for desktop apps (out of bounds)
161 | * @return TwitterOAuthToken Request token
162 | */
163 | public function get_oauth_request_token( $oauth_callback = 'oob' ){
164 | $params = $this->oauth_exchange( TWITTER_OAUTH_REQUEST_TOKEN_URL, compact('oauth_callback') );
165 | return new TwitterOAuthToken( $params['oauth_token'], $params['oauth_token_secret'] );
166 | }
167 |
168 |
169 |
170 | /**
171 | * Exchange request token for an access token after authentication/authorization by user
172 | * @param string verifier passed back from Twitter or copied out of browser window
173 | * @return TwitterOAuthToken Access token
174 | */
175 | public function get_oauth_access_token( $oauth_verifier ){
176 | $params = $this->oauth_exchange( TWITTER_OAUTH_ACCESS_TOKEN_URL, compact('oauth_verifier') );
177 | $token = new TwitterOAuthToken( $params['oauth_token'], $params['oauth_token_secret'] );
178 | $token->user = array (
179 | 'id' => $params['user_id'],
180 | 'screen_name' => $params['screen_name'],
181 | );
182 | return $token;
183 | }
184 |
185 |
186 |
187 | /**
188 | * Basic sanitation of api request arguments
189 | * @param array original params passed by client code
190 | * @return array sanitized params that we'll serialize
191 | */
192 | private function sanitize_args( array $_args ){
193 | // transform some arguments and ensure strings
194 | // no further validation is performed
195 | $args = array();
196 | foreach( $_args as $key => $val ){
197 | if( is_string($val) ){
198 | $args[$key] = $val;
199 | }
200 | else if( true === $val ){
201 | $args[$key] = 'true';
202 | }
203 | else if( false === $val || null === $val ){
204 | $args[$key] = 'false';
205 | }
206 | else if( ! is_scalar($val) ){
207 | throw new TwitterApiException( 'Invalid Twitter parameter ('.gettype($val).') '.$key, -1 );
208 | }
209 | else {
210 | $args[$key] = (string) $val;
211 | }
212 | }
213 | return $args;
214 | }
215 |
216 |
217 |
218 | /**
219 | * Call API method over HTTP and return serialized data
220 | * @param string API method, e.g. "users/show"
221 | * @param array method arguments
222 | * @param string http request method
223 | * @return array unserialized data returned from Twitter
224 | * @throws TwitterApiException
225 | */
226 | public function call( $path, array $args = array(), $http_method = 'GET' ){
227 | $args = $this->sanitize_args( $args );
228 | // Fetch response from cache if possible / allowed / enabled
229 | if( $http_method === 'GET' && isset($this->cache_ttl) ){
230 | $cachekey = $this->cache_ns.$path.'_'.md5( serialize($args) );
231 | if( preg_match('/^(\d+)-/', $this->AccessToken->key, $reg ) ){
232 | $cachekey .= '_'.$reg[1];
233 | }
234 | $data = apc_fetch( $cachekey );
235 | if( is_array($data) ){
236 | return $data;
237 | }
238 | }
239 | $http = $this->rest_request( $path, $args, $http_method );
240 | // Deserialize response
241 | $status = $http['status'];
242 | $data = json_decode( $http['body'], true );
243 | // unserializable array assumed to be serious error
244 | if( ! is_array($data) ){
245 | $err = array(
246 | 'message' => $http['error'],
247 | 'code' => -1
248 | );
249 | TwitterApiException::chuck( $err, $status );
250 | }
251 | // else could be well-formed error
252 | if( isset( $data['errors'] ) ) {
253 | while( $err = array_shift($data['errors']) ){
254 | $err['message'] = $err['message'];
255 | if( $data['errors'] ){
256 | $message = sprintf('Twitter error #%d', $err['code'] ).' "'.$err['message'].'"';
257 | trigger_error( $message, E_USER_WARNING );
258 | }
259 | else {
260 | TwitterApiException::chuck( $err, $status );
261 | }
262 | }
263 | }
264 | if( isset($cachekey) ){
265 | apc_store( $cachekey, $data, $this->cache_ttl );
266 | }
267 | return $data;
268 | }
269 |
270 |
271 |
272 | /**
273 | * Call API method over HTTP and return raw response data without caching
274 | * @param string API method, e.g. "users/show"
275 | * @param array method arguments
276 | * @param string http request method
277 | * @return array structure from http_request
278 | * @throws TwitterApiException
279 | */
280 | public function raw( $path, array $args = array(), $http_method = 'GET' ){
281 | $args = $this->sanitize_args( $args );
282 | return $this->rest_request( $path, $args, $http_method );
283 | }
284 |
285 |
286 |
287 | /**
288 | * Perform an OAuth request - these differ somewhat from regular API calls
289 | * @internal
290 | */
291 | private function oauth_exchange( $endpoint, array $args ){
292 | // build a post request and authenticate via OAuth header
293 | $params = new TwitterOAuthParams( $args );
294 | $params->set_consumer( $this->Consumer );
295 | if( $this->AccessToken ){
296 | $params->set_token( $this->AccessToken );
297 | }
298 | $params->sign_hmac( 'POST', $endpoint );
299 | $conf = array (
300 | 'method' => 'POST',
301 | 'headers' => array( 'Authorization' => $params->oauth_header() ),
302 | );
303 | $http = self::http_request( $endpoint, $conf );
304 | $body = trim( $http['body'] );
305 | $stat = $http['status'];
306 | if( 200 !== $stat ){
307 | // Twitter might respond as XML, but with an HTML content type for some reason
308 | if( 0 === strpos($body, '') ){
309 | $xml = simplexml_load_string($body);
310 | $body = (string) $xml->error;
311 | }
312 | throw new TwitterApiException( $body, -1, $stat );
313 | }
314 | parse_str( $body, $params );
315 | if( ! is_array($params) || ! isset($params['oauth_token']) || ! isset($params['oauth_token_secret']) ){
316 | throw new TwitterApiException( 'Malformed response from Twitter', -1, $stat );
317 | }
318 | return $params;
319 | }
320 |
321 |
322 |
323 |
324 | /**
325 | * Sign and execute REST API call
326 | * @return array
327 | */
328 | private function rest_request( $path, array $args, $http_method ){
329 | // all calls must be authenticated in API 1.1
330 | if( ! $this->has_auth() ){
331 | throw new TwitterApiException( 'Twitter client not authenticated', 0, 401 );
332 | }
333 | // prepare HTTP request config
334 | $conf = array (
335 | 'method' => $http_method,
336 | );
337 | // build signed URL and request parameters
338 | $endpoint = TWITTER_API_BASE.'/'.$path.'.json';
339 | $params = new TwitterOAuthParams( $args );
340 | $params->set_consumer( $this->Consumer );
341 | $params->set_token( $this->AccessToken );
342 | $params->sign_hmac( $http_method, $endpoint );
343 | if( 'GET' === $http_method ){
344 | $endpoint .= '?'.$params->serialize();
345 | }
346 | else {
347 | $conf['body'] = $params->serialize();
348 | }
349 | $http = self::http_request( $endpoint, $conf );
350 | // remember current rate limits for this endpoint
351 | $this->last_call = $path;
352 | if( isset($http['headers']['x-rate-limit-limit']) ) {
353 | $this->last_rate[$path] = array (
354 | 'limit' => (int) $http['headers']['x-rate-limit-limit'],
355 | 'remaining' => (int) $http['headers']['x-rate-limit-remaining'],
356 | 'reset' => (int) $http['headers']['x-rate-limit-reset'],
357 | );
358 | }
359 | return $http;
360 | }
361 |
362 |
363 |
364 | /**
365 | * Abstract HTTP call, currently just uses cURL extension
366 | * @return array e.g. { body: '', error: '', status: 200, headers: {} }
367 | */
368 | public static function http_request( $endpoint, array $conf ){
369 | $conf += array(
370 | 'body' => '',
371 | 'method' => 'GET',
372 | 'headers' => array(),
373 | );
374 |
375 | $ch = curl_init();
376 | curl_setopt( $ch, CURLOPT_URL, $endpoint );
377 | curl_setopt( $ch, CURLOPT_TIMEOUT, TWITTER_API_TIMEOUT );
378 | curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, TWITTER_API_TIMEOUT );
379 | curl_setopt( $ch, CURLOPT_USERAGENT, TWITTER_API_USERAGENT );
380 | curl_setopt( $ch, CURLOPT_HEADER, true );
381 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
382 |
383 | switch ( $conf['method'] ) {
384 | case 'GET':
385 | break;
386 | case 'POST':
387 | curl_setopt( $ch, CURLOPT_POST, true );
388 | curl_setopt( $ch, CURLOPT_POSTFIELDS, $conf['body'] );
389 | break;
390 | default:
391 | throw new TwitterApiException('Unsupported method '.$conf['method'] );
392 | }
393 |
394 | foreach( $conf['headers'] as $key => $val ){
395 | $headers[] = $key.': '.$val;
396 | }
397 | if( isset($headers) ) {
398 | curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
399 | }
400 |
401 | // execute and parse response
402 | $response = curl_exec( $ch );
403 | if ( 60 === curl_errno($ch) ) { // CURLE_SSL_CACERT
404 | curl_setopt( $ch, CURLOPT_CAINFO, __DIR__.'/ca-chain-bundle.crt');
405 | $response = curl_exec($ch);
406 | }
407 | $status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
408 | $headers = array();
409 | $body = '';
410 | if( $response && $status ){
411 | list( $header, $body ) = preg_split('/\r\n\r\n/', $response, 2 );
412 | if( preg_match_all('/^(Content[\w\-]+|X-Rate[^:]+):\s*(.+)/mi', $header, $r, PREG_SET_ORDER ) ){
413 | foreach( $r as $match ){
414 | $headers[ strtolower($match[1]) ] = $match[2];
415 | }
416 | }
417 | curl_close($ch);
418 | }
419 | else {
420 | $error = curl_error( $ch ) or
421 | $error = 'No response from Twitter';
422 | is_resource($ch) and curl_close($ch);
423 | throw new TwitterApiException( $error );
424 | }
425 | return array (
426 | 'body' => $body,
427 | 'status' => $status,
428 | 'headers' => $headers,
429 | );
430 | }
431 |
432 |
433 |
434 | /**
435 | * Get current rate limit, if known. does not look it up
436 | */
437 | public function last_rate_limit_data( $func = '' ){
438 | $func or $func = $this->last_call;
439 | return isset($this->last_rate[$func]) ? $this->last_rate[$func] : array( 'limit' => 0 );
440 | }
441 |
442 |
443 | /**
444 | * Get rate limit allowance for last endpoint request
445 | */
446 | public function last_rate_limit_allowance( $func = '' ){
447 | $data = $this->last_rate_limit_data($func);
448 | return isset($data['limit']) ? $data['limit'] : null;
449 | }
450 |
451 |
452 | /**
453 | * Get number of requests remaining this period for last endpoint request
454 | */
455 | public function last_rate_limit_remaining( $func = '' ){
456 | $data = $this->last_rate_limit_data($func);
457 | return isset($data['remaining']) ? $data['remaining'] : null;
458 | }
459 |
460 |
461 | /**
462 | * Get rate limit reset time for last endpoint request
463 | */
464 | public function last_rate_limit_reset( $func = '' ){
465 | $data = $this->last_rate_limit_data($func);
466 | return isset($data['reset']) ? $data['reset'] : null;
467 | }
468 |
469 | }
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 | /**
478 | * Simple token class that holds key and secret
479 | * @internal
480 | */
481 | class TwitterOAuthToken {
482 |
483 | public $key;
484 | public $secret;
485 | public $verifier;
486 | public $user;
487 |
488 | public function __construct( $key, $secret = '' ){
489 | if( ! $key ){
490 | throw new Exception( 'Invalid OAuth token - Key required even if secret is empty' );
491 | }
492 | $this->key = $key;
493 | $this->secret = $secret;
494 | $this->verifier = '';
495 | }
496 |
497 | public function get_authorization_url(){
498 | return TWITTER_OAUTH_AUTHORIZE_URL.'?oauth_token='.rawurlencode($this->key);
499 | }
500 |
501 | public function get_authentication_url(){
502 | return TWITTER_OAUTH_AUTHENTICATE_URL.'?oauth_token='.rawurlencode($this->key);
503 | }
504 |
505 | }
506 |
507 |
508 |
509 |
510 |
511 | /**
512 | * Class for compiling, signing and serializing OAuth parameters
513 | * @internal
514 | */
515 | class TwitterOAuthParams {
516 |
517 | private $args;
518 | private $consumer_secret;
519 | private $token_secret;
520 |
521 | private static function urlencode( $val ){
522 | return str_replace( '%7E', '~', rawurlencode($val) );
523 | }
524 |
525 | public function __construct( array $args = array() ){
526 | $this->args = $args + array (
527 | 'oauth_version' => '1.0',
528 | );
529 | }
530 |
531 | public function set_consumer( TwitterOAuthToken $Consumer ){
532 | $this->consumer_secret = $Consumer->secret;
533 | $this->args['oauth_consumer_key'] = $Consumer->key;
534 | }
535 |
536 | public function set_token( TwitterOAuthToken $Token ){
537 | $this->token_secret = $Token->secret;
538 | $this->args['oauth_token'] = $Token->key;
539 | }
540 |
541 | private function normalize(){
542 | $flags = SORT_STRING | SORT_ASC;
543 | ksort( $this->args, $flags );
544 | foreach( $this->args as $k => $a ){
545 | if( is_array($a) ){
546 | sort( $this->args[$k], $flags );
547 | }
548 | }
549 | return $this->args;
550 | }
551 |
552 | public function serialize(){
553 | $str = http_build_query( $this->args );
554 | // PHP_QUERY_RFC3986 requires PHP >= 5.4
555 | $str = str_replace( array('+','%7E'), array('%20','~'), $str );
556 | return $str;
557 | }
558 |
559 | public function sign_hmac( $http_method, $http_rsc ){
560 | $this->args['oauth_signature_method'] = 'HMAC-SHA1';
561 | $this->args['oauth_timestamp'] = sprintf('%u', time() );
562 | $this->args['oauth_nonce'] = sprintf('%f', microtime(true) );
563 | unset( $this->args['oauth_signature'] );
564 | $this->normalize();
565 | $str = $this->serialize();
566 | $str = strtoupper($http_method).'&'.self::urlencode($http_rsc).'&'.self::urlencode($str);
567 | $key = self::urlencode($this->consumer_secret).'&'.self::urlencode($this->token_secret);
568 | $this->args['oauth_signature'] = base64_encode( hash_hmac( 'sha1', $str, $key, true ) );
569 | return $this->args;
570 | }
571 |
572 | public function oauth_header(){
573 | $lines = array();
574 | foreach( $this->args as $key => $val ){
575 | $lines[] = self::urlencode($key).'="'.self::urlencode($val).'"';
576 | }
577 | return 'OAuth '.implode( ",\n ", $lines );
578 | }
579 |
580 | }
581 |
582 |
583 |
584 |
585 |
586 |
587 | /**
588 | * HTTP status codes with some overridden for Twitter-related messages.
589 | * Note these do not replace error text from Twitter, they're for complete API failures.
590 | * @param int HTTP status code
591 | * @return string HTTP status text
592 | */
593 | function _twitter_api_http_status_text( $s ){
594 | static $codes = array (
595 | 100 => 'Continue',
596 | 101 => 'Switching Protocols',
597 |
598 | 200 => 'OK',
599 | 201 => 'Created',
600 | 202 => 'Accepted',
601 | 203 => 'Non-Authoritative Information',
602 | 204 => 'No Content',
603 | 205 => 'Reset Content',
604 | 206 => 'Partial Content',
605 |
606 | 300 => 'Multiple Choices',
607 | 301 => 'Moved Permanently',
608 | 302 => 'Found',
609 | 303 => 'See Other',
610 | 304 => 'Not Modified',
611 | 305 => 'Use Proxy',
612 | 307 => 'Temporary Redirect',
613 |
614 | 400 => 'Bad Request',
615 | 401 => 'Authorization Required',
616 | 402 => 'Payment Required',
617 | 403 => 'Forbidden',
618 | 404 => 'Not Found',
619 | 405 => 'Method Not Allowed',
620 | 406 => 'Not Acceptable',
621 | 407 => 'Proxy Authentication Required',
622 | 408 => 'Request Time-out',
623 | 409 => 'Conflict',
624 | 410 => 'Gone',
625 | 411 => 'Length Required',
626 | 412 => 'Precondition Failed',
627 | 413 => 'Request Entity Too Large',
628 | 414 => 'Request-URI Too Large',
629 | 415 => 'Unsupported Media Type',
630 | 416 => 'Requested range not satisfiable',
631 | 417 => 'Expectation Failed',
632 | // ..
633 | 429 => 'Twitter API rate limit exceeded',
634 |
635 | 500 => 'Twitter server error',
636 | 501 => 'Not Implemented',
637 | 502 => 'Twitter is not responding',
638 | 503 => 'Twitter is too busy to respond',
639 | 504 => 'Gateway Time-out',
640 | 505 => 'HTTP Version not supported',
641 | );
642 | return isset($codes[$s]) ? $codes[$s] : sprintf('Status %u from Twitter', $s );
643 | }
644 |
645 |
646 |
647 |
648 |
649 | /**
650 | * Exception for throwing when Twitter responds with something unpleasant
651 | */
652 | class TwitterApiException extends Exception {
653 |
654 | /**
655 | * HTTP Status of error
656 | * @var int
657 | */
658 | protected $status = 0;
659 |
660 |
661 | /**
662 | * Throw appropriate exception type according to HTTP status code
663 | * @param array Twitter error data from their response
664 | */
665 | public static function chuck( array $err, $status ){
666 | $code = isset($err['code']) ? (int) $err['code'] : -1;
667 | $mess = isset($err['message']) ? trim($err['message']) : '';
668 | static $classes = array (
669 | 404 => 'TwitterApiNotFoundException',
670 | 429 => 'TwitterApiRateLimitException',
671 | );
672 | $eclass = isset($classes[$status]) ? $classes[$status] : __CLASS__;
673 | throw new $eclass( $mess, $code, $status );
674 | }
675 |
676 |
677 | /**
678 | * Construct TwitterApiException with addition of HTTP status code.
679 | * @overload
680 | */
681 | public function __construct( $message, $code = 0 ){
682 | if( 2 < func_num_args() ){
683 | $this->status = (int) func_get_arg(2);
684 | }
685 | if( ! $message ){
686 | $message = _twitter_api_http_status_text($this->status);
687 | }
688 | parent::__construct( $message, $code );
689 | }
690 |
691 |
692 | /**
693 | * Get HTTP status of error
694 | * @return int
695 | */
696 | public function getStatus(){
697 | return $this->status;
698 | }
699 |
700 | }
701 |
702 |
703 | /** 404 */
704 | class TwitterApiNotFoundException extends TwitterApiException {
705 |
706 | }
707 |
708 |
709 | /** 429 */
710 | class TwitterApiRateLimitException extends TwitterApiException {
711 |
712 | }
713 |
--------------------------------------------------------------------------------