├── index.php
├── README.md
├── Twitteroauth.php
├── TwitterSearch.php
├── TweetMunger.php
└── OAuth.php
/index.php:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | :" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is "false"
74 | * @var boolean
75 | */
76 | var $show_user = false;
77 |
78 | /**
79 | * @param string $query optional
80 | */
81 | function TwitterSearch($query=false) {
82 | $this->query = $query;
83 | }
84 |
85 | /**
86 | * Find tweets from a user
87 | * @param string $user required
88 | * @return object
89 | */
90 | function from($user) {
91 | $this->query .= ' from:'.str_replace('@', '', $user);
92 | return $this;
93 | }
94 |
95 | /**
96 | * Find tweets to a user
97 | * @param string $user required
98 | * @return object
99 | */
100 | function to($user) {
101 | $this->query .= ' to:'.str_replace('@', '', $user);
102 | return $this;
103 | }
104 |
105 | /**
106 | * Find tweets referencing a user
107 | * @param string $user required
108 | * @return object
109 | */
110 | function about($user) {
111 | $this->query .= ' @'.str_replace('@', '', $user);
112 | return $this;
113 | }
114 |
115 | /**
116 | * Find tweets containing a hashtag
117 | * @param string $user required
118 | * @return object
119 | */
120 | function with($hashtag) {
121 | $this->query .= ' #'.str_replace('#', '', $hashtag);
122 | return $this;
123 | }
124 |
125 | /**
126 | * Find tweets containing a word
127 | * @param string $user required
128 | * @return object
129 | */
130 | function contains($word) {
131 | $this->query .= ' '.$word;
132 | return $this;
133 | }
134 |
135 | /**
136 | * Set show_user to true
137 | * @return object
138 | */
139 | function show_user() {
140 | $this->show_user = true;
141 | return $this;
142 | }
143 |
144 | /**
145 | * @param int $since_id required
146 | * @return object
147 | */
148 | function since($since_id) {
149 | $this->since = $since_id;
150 | return $this;
151 | }
152 |
153 | /**
154 | * @param int $language required
155 | * @return object
156 | */
157 | function lang($language) {
158 | $this->lang = $language;
159 | return $this;
160 | }
161 |
162 | /**
163 | * @param int $n required
164 | * @return object
165 | */
166 | function rpp($n) {
167 | $this->rpp = $n;
168 | return $this;
169 | }
170 |
171 | /**
172 | * @param int $n required
173 | * @return object
174 | */
175 | function page($n) {
176 | $this->page = $n;
177 | return $this;
178 | }
179 |
180 | /**
181 | * @param float $lat required. lattitude
182 | * @param float $long required. longitude
183 | * @param int $radius required.
184 | * @param string optional. mi|km
185 | * @return object
186 | */
187 | function geocode($lat, $long, $radius, $units='mi') {
188 | $this->geocode = $lat.','.$long.','.$radius.$units;
189 | return $this;
190 | }
191 |
192 | /**
193 | * Build and perform the query, return the results.
194 | * @param $reset_query boolean optional.
195 | * @return object
196 | */
197 | function results($reset_query=true) {
198 | $request = 'http://search.twitter.com/search.'.$this->type;
199 | $request .= '?q='.urlencode($this->query);
200 |
201 | if(isset($this->rpp)) {
202 | $request .= '&rpp='.$this->rpp;
203 | }
204 |
205 | if(isset($this->page)) {
206 | $request .= '&page='.$this->page;
207 | }
208 |
209 | if(isset($this->lang)) {
210 | $request .= '&lang='.$this->lang;
211 | }
212 |
213 | if(isset($this->since)) {
214 | $request .= '&since_id='.$this->since;
215 | }
216 |
217 | if($this->show_user) {
218 | $request .= '&show_user=true';
219 | }
220 |
221 | if(isset($this->geocode)) {
222 | $request .= '&geocode='.$this->geocode;
223 | }
224 |
225 | if($reset_query) {
226 | $this->query = '';
227 | }
228 |
229 | return $this->objectify($this->process($request))->results;
230 | }
231 |
232 | /**
233 | * Returns the top ten queries that are currently trending on Twitter.
234 | * @return object
235 | */
236 | function trends() {
237 | $request = 'http://search.twitter.com/trends.json';
238 |
239 | return $this->objectify($this->process($request));
240 | }
241 |
242 | /**
243 | * Internal function where all the juicy curl fun takes place
244 | * this should not be called by anything external unless you are
245 | * doing something else completely then knock youself out.
246 | * @access private
247 | * @param string $url Required. API URL to request
248 | * @param string $postargs Optional. Urlencoded query string to append to the $url
249 | */
250 | function process($url, $postargs=false) {
251 | $ch = curl_init($url);
252 | if($postargs !== false) {
253 | curl_setopt ($ch, CURLOPT_POST, true);
254 | curl_setopt ($ch, CURLOPT_POSTFIELDS, $postargs);
255 | }
256 |
257 | curl_setopt($ch, CURLOPT_VERBOSE, 1);
258 | curl_setopt($ch, CURLOPT_NOBODY, 0);
259 | curl_setopt($ch, CURLOPT_HEADER, 0);
260 | curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent);
261 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
262 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
263 | curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
264 |
265 | $response = curl_exec($ch);
266 |
267 | $this->responseInfo=curl_getinfo($ch);
268 | curl_close($ch);
269 |
270 | if( intval( $this->responseInfo['http_code'] ) == 200 )
271 | return $response;
272 | else
273 | return false;
274 | }
275 |
276 | /**
277 | * Function to prepare data for return to client
278 | * @access private
279 | * @param string $data
280 | */
281 | function objectify($data) {
282 | if( $this->type == 'json' )
283 | return (object) json_decode($data);
284 |
285 | else if( $this->type == 'xml' ) {
286 | if( function_exists('simplexml_load_string') ) {
287 | $obj = simplexml_load_string( $data );
288 |
289 | $statuses = array();
290 | foreach( $obj->status as $status ) {
291 | $statuses[] = $status;
292 | }
293 | return (object) $statuses;
294 | }
295 | else {
296 | return $out;
297 | }
298 | }
299 | else
300 | return false;
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/TweetMunger.php:
--------------------------------------------------------------------------------
1 | $value) {
163 | if (property_exists($this, $option)) {
164 | $this->$option = $value;
165 | }
166 | }
167 |
168 | // debug
169 | if ($this->debugMode) {
170 | $this->debug('(Debug mode on, not posting to twitter)
');
171 | }
172 |
173 | // get the latest tweet from the munged account
174 | $this->twitterSearch = new TwitterSearch();
175 | $this->twitterSearch->user_agent = 'phptwittersearch:'.$this->userAgentAccount;
176 | $latestMungedTweetId = $this->getLatestMungedTweetId();
177 |
178 | // check if there have been any new tweets since this
179 | $tweets = $this->getLatestTweets($latestMungedTweetId);
180 | $tweets = array_reverse($tweets);
181 |
182 | // loop through all new tweets
183 | foreach ($tweets as $key => $tweet) {
184 |
185 | // mung text
186 | $text = $this->mungText($tweet->text, $tweet->id_str);
187 |
188 | // condition : if a translation is found, post to twitter
189 | // also save latest translated id, to stop re-munging
190 | if (!empty($text)) {
191 | $this->tweet($text);
192 | }
193 | }
194 | }
195 |
196 |
197 | /**
198 | * Get the Twitter ID of the latest translated tweet
199 | *
200 | * @return string
201 | */
202 | private function getLatestMungedTweetId() {
203 | // try to get the latest translated tweet id from local file
204 | $latestMungedTweetId = @file_get_contents($this->latestTweetIdFile);
205 |
206 | // if there is no local file, try to get the latest translated tweet id from the twitter account
207 | if (empty($latestMungedTweetId)) {
208 | $this->twitterSearch->from($this->mungedTwitterAccount);
209 | $lastMungedTweet = $this->twitterSearch->rpp(1)->results();
210 | $latestMungedTweetId = @$lastMungedTweet[0]->id_str;
211 | $this->debug('$latestMungedTweetId:'.$latestMungedTweetId.' (retrieved from twitter)
');
212 | $this->saveTranslatedId($latestMungedTweetId);
213 | } else {
214 | $this->debug('$latestMungedTweetId:'.$latestMungedTweetId.' (retrieved from text file)
');
215 | }
216 | return $latestMungedTweetId;
217 | }
218 |
219 |
220 | /**
221 | * Save the id of the latest translated tweet
222 | *
223 | * @return void
224 | */
225 | private function saveTranslatedId($id) {
226 | $file = fopen($this->latestTweetIdFile, 'w') or exit("Can't open $this->latestTweetIdFile!");
227 | fwrite($file, $id);
228 | fclose($file);
229 | $this->debug('Latest translated id saved: '.$id.'
');
230 | }
231 |
232 |
233 | /**
234 | * Get all new tweets since the last munged tweet
235 | *
236 | * @var string
237 | * @return array
238 | */
239 | private function getLatestTweets($latestMungedTweetId) {
240 | $this->twitterSearch->from($this->originalTwitterAccount);
241 | $this->twitterSearch->since($latestMungedTweetId);
242 | $results = $this->twitterSearch->rpp($this->newTweetCount)->results();
243 | $this->debug('New Tweet count: '.count($results).'
');
244 | $this->debug('
');
245 | return $results;
246 | }
247 |
248 |
249 | /**
250 | * Translate a tweet
251 | *
252 | * @var string $text
253 | * @var int $id
254 | * @return string
255 | */
256 | private function mungText($text, $id) {
257 |
258 | $this->debug('Original Tweet (ID - '.$id.'): ' . $text . '
');
259 |
260 | // condition : ignore retweet?
261 | if ($this->ignoreRetweets && strpos($text, "RT") === 0) {
262 | $this->debug("Retweet found, ignoring...
");
263 | $this->debug('
');
264 | return false;
265 | }
266 |
267 | // remove content twitter automatically turns into hashtags and user ids - so as not to annoy people!
268 | $text = strip_tags(trim($text));
269 | $text = str_replace('@', '_', $text);
270 | $text = str_replace('#', '_', $text);
271 |
272 | $languageCount = count($this->translatableLanguages);
273 |
274 | // first translation - original language into first translatable language
275 | $text = $this->translate($text, $this->originalLanguage, $this->translatableLanguages[0]);
276 | $this->debug('Translation from '.$this->originalLanguage.' into '.$this->translatableLanguages[0].': ' . $text . '
');
277 |
278 | // translate through each subsequent language
279 | for ($counter = 1; $counter < $languageCount; $counter++) {
280 | $text = $this->translate($text, $this->translatableLanguages[$counter-1], $this->translatableLanguages[$counter]);
281 | $this->debug('Translation from '.$this->translatableLanguages[$counter-1].' into '.$this->translatableLanguages[$counter].': ' . $text . '
');
282 | }
283 |
284 | // translate back into the original language
285 | $text = $this->translate($text, $this->translatableLanguages[$languageCount-1], $this->originalLanguage);
286 | $this->debug('Translation from '.$this->translatableLanguages[$languageCount-1].' into '.$this->originalLanguage.': ' . $text . '
');
287 |
288 | // ensure new text length is <= 140 characters
289 | if (strlen($text) > 140) {
290 | $text = substr($text, 0, 137) . "...";
291 | $this->debug('Text is too long, truncating...
');
292 | }
293 |
294 | // return the newly translated text
295 | return $text;
296 | }
297 |
298 |
299 | /**
300 | * Work out whether we're translating through Google or Bing API
301 | *
302 | * @var string $text
303 | * @var string $sourceLang
304 | * @var string $targetLang
305 | * @return string
306 | */
307 | private function translate($text, $sourceLang, $targetLang) {
308 | $translationService = "translate".ucFirst($this->translationService);
309 | return $this->$translationService($text, $sourceLang, $targetLang);
310 | }
311 |
312 |
313 | /**
314 | * one-step google translate
315 | * http://code.google.com/apis/language/translate/v2/getting_started.html
316 | *
317 | * @var string $text
318 | * @var string $sourceLang
319 | * @var string $targetLang
320 | * @return string
321 | */
322 | private function translateGoogle($text, $sourceLang, $targetLang) {
323 | $url = "https://www.googleapis.com/language/translate/v2?key=";
324 | $url .= $this->googleTranslateApiKey;
325 | $url .= "&q=";
326 | $url .= urlencode($text);
327 | $url .= "&source=".$sourceLang;
328 | $url .= "&target=".$targetLang;
329 |
330 | $ch = curl_init();
331 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
332 | curl_setopt($ch, CURLOPT_URL, $url);
333 | $responseJson = curl_exec($ch);
334 | curl_close($ch);
335 |
336 | $response = json_decode($responseJson);
337 | $translation = $response->data->translations[0]->translatedText;
338 |
339 | return $translation;
340 | }
341 |
342 |
343 |
344 | /**
345 | * one-step bing translate
346 | * http://msdn.microsoft.com/en-us/library/ff512421.aspx
347 | *
348 | * @var string $text
349 | * @var string $sourceLang
350 | * @var string $targetLang
351 | * @return string
352 | */
353 | private function translateBing($text, $sourceLang, $targetLang) {
354 | $url = "http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=";
355 | $url .= $this->bingAppId;
356 | $url .= "&text=";
357 | $url .= urlencode($text);
358 | $url .= "&from=".$sourceLang;
359 | $url .= "&to=".$targetLang;
360 |
361 | $ch = curl_init();
362 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
363 | curl_setopt($ch, CURLOPT_URL, $url);
364 | $responseXml = curl_exec($ch);
365 | curl_close($ch);
366 |
367 | $response = strip_tags($responseXml);
368 |
369 | return $response;
370 | }
371 |
372 |
373 |
374 | /**
375 | * Tweet the new text (if not in debug mode)
376 | *
377 | * @var text
378 | * @return void
379 | */
380 | private function tweet($text) {
381 | $tweet = new TwitterOAuth($this->twitterConsumerKey, $this->twitterConsumerSecret, $this->twitterConsumerOauthToken, $this->twitterConsumerOauthSecret);
382 | if (!$this->debugMode) {
383 | $post = $tweet->post('statuses/update', array('status' => $text));
384 | $this->saveTranslatedId($post->id_str);
385 | }
386 | $this->debug('tweeting: ' . $text . '
');
387 | if (!$this->debugMode) {
388 | $this->debug("");
389 | $this->debug(print_r($post));
390 | $this->debug("");
391 | }
392 | $this->debug('
');
393 | }
394 |
395 |
396 | /**
397 | * If we're in debug mode, output the debug text to the browser
398 | */
399 | private function debug($text) {
400 | // if ($this->debugMode) {
401 | echo $text;
402 | // }
403 | }
404 | }
405 |
--------------------------------------------------------------------------------
/OAuth.php:
--------------------------------------------------------------------------------
1 | key = $key;
16 | $this->secret = $secret;
17 | $this->callback_url = $callback_url;
18 | }
19 |
20 | function __toString() {
21 | return "OAuthConsumer[key=$this->key,secret=$this->secret]";
22 | }
23 | }
24 |
25 | class OAuthToken {
26 | // access tokens and request tokens
27 | public $key;
28 | public $secret;
29 |
30 | /**
31 | * key = the token
32 | * secret = the token secret
33 | */
34 | function __construct($key, $secret) {
35 | $this->key = $key;
36 | $this->secret = $secret;
37 | }
38 |
39 | /**
40 | * generates the basic string serialization of a token that a server
41 | * would respond to request_token and access_token calls with
42 | */
43 | function to_string() {
44 | return "oauth_token=" .
45 | OAuthUtil::urlencode_rfc3986($this->key) .
46 | "&oauth_token_secret=" .
47 | OAuthUtil::urlencode_rfc3986($this->secret);
48 | }
49 |
50 | function __toString() {
51 | return $this->to_string();
52 | }
53 | }
54 |
55 | /**
56 | * A class for implementing a Signature Method
57 | * See section 9 ("Signing Requests") in the spec
58 | */
59 | abstract class OAuthSignatureMethod {
60 | /**
61 | * Needs to return the name of the Signature Method (ie HMAC-SHA1)
62 | * @return string
63 | */
64 | abstract public function get_name();
65 |
66 | /**
67 | * Build up the signature
68 | * NOTE: The output of this function MUST NOT be urlencoded.
69 | * the encoding is handled in OAuthRequest when the final
70 | * request is serialized
71 | * @param OAuthRequest $request
72 | * @param OAuthConsumer $consumer
73 | * @param OAuthToken $token
74 | * @return string
75 | */
76 | abstract public function build_signature($request, $consumer, $token);
77 |
78 | /**
79 | * Verifies that a given signature is correct
80 | * @param OAuthRequest $request
81 | * @param OAuthConsumer $consumer
82 | * @param OAuthToken $token
83 | * @param string $signature
84 | * @return bool
85 | */
86 | public function check_signature($request, $consumer, $token, $signature) {
87 | $built = $this->build_signature($request, $consumer, $token);
88 | return $built == $signature;
89 | }
90 | }
91 |
92 | /**
93 | * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104]
94 | * where the Signature Base String is the text and the key is the concatenated values (each first
95 | * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&'
96 | * character (ASCII code 38) even if empty.
97 | * - Chapter 9.2 ("HMAC-SHA1")
98 | */
99 | class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
100 | function get_name() {
101 | return "HMAC-SHA1";
102 | }
103 |
104 | public function build_signature($request, $consumer, $token) {
105 | $base_string = $request->get_signature_base_string();
106 | $request->base_string = $base_string;
107 |
108 | $key_parts = array(
109 | $consumer->secret,
110 | ($token) ? $token->secret : ""
111 | );
112 |
113 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
114 | $key = implode('&', $key_parts);
115 |
116 | return base64_encode(hash_hmac('sha1', $base_string, $key, true));
117 | }
118 | }
119 |
120 | /**
121 | * The PLAINTEXT method does not provide any security protection and SHOULD only be used
122 | * over a secure channel such as HTTPS. It does not use the Signature Base String.
123 | * - Chapter 9.4 ("PLAINTEXT")
124 | */
125 | class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
126 | public function get_name() {
127 | return "PLAINTEXT";
128 | }
129 |
130 | /**
131 | * oauth_signature is set to the concatenated encoded values of the Consumer Secret and
132 | * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is
133 | * empty. The result MUST be encoded again.
134 | * - Chapter 9.4.1 ("Generating Signatures")
135 | *
136 | * Please note that the second encoding MUST NOT happen in the SignatureMethod, as
137 | * OAuthRequest handles this!
138 | */
139 | public function build_signature($request, $consumer, $token) {
140 | $key_parts = array(
141 | $consumer->secret,
142 | ($token) ? $token->secret : ""
143 | );
144 |
145 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
146 | $key = implode('&', $key_parts);
147 | $request->base_string = $key;
148 |
149 | return $key;
150 | }
151 | }
152 |
153 | /**
154 | * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in
155 | * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for
156 | * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a
157 | * verified way to the Service Provider, in a manner which is beyond the scope of this
158 | * specification.
159 | * - Chapter 9.3 ("RSA-SHA1")
160 | */
161 | abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
162 | public function get_name() {
163 | return "RSA-SHA1";
164 | }
165 |
166 | // Up to the SP to implement this lookup of keys. Possible ideas are:
167 | // (1) do a lookup in a table of trusted certs keyed off of consumer
168 | // (2) fetch via http using a url provided by the requester
169 | // (3) some sort of specific discovery code based on request
170 | //
171 | // Either way should return a string representation of the certificate
172 | protected abstract function fetch_public_cert(&$request);
173 |
174 | // Up to the SP to implement this lookup of keys. Possible ideas are:
175 | // (1) do a lookup in a table of trusted certs keyed off of consumer
176 | //
177 | // Either way should return a string representation of the certificate
178 | protected abstract function fetch_private_cert(&$request);
179 |
180 | public function build_signature($request, $consumer, $token) {
181 | $base_string = $request->get_signature_base_string();
182 | $request->base_string = $base_string;
183 |
184 | // Fetch the private key cert based on the request
185 | $cert = $this->fetch_private_cert($request);
186 |
187 | // Pull the private key ID from the certificate
188 | $privatekeyid = openssl_get_privatekey($cert);
189 |
190 | // Sign using the key
191 | $ok = openssl_sign($base_string, $signature, $privatekeyid);
192 |
193 | // Release the key resource
194 | openssl_free_key($privatekeyid);
195 |
196 | return base64_encode($signature);
197 | }
198 |
199 | public function check_signature($request, $consumer, $token, $signature) {
200 | $decoded_sig = base64_decode($signature);
201 |
202 | $base_string = $request->get_signature_base_string();
203 |
204 | // Fetch the public key cert based on the request
205 | $cert = $this->fetch_public_cert($request);
206 |
207 | // Pull the public key ID from the certificate
208 | $publickeyid = openssl_get_publickey($cert);
209 |
210 | // Check the computed signature against the one passed in the query
211 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
212 |
213 | // Release the key resource
214 | openssl_free_key($publickeyid);
215 |
216 | return $ok == 1;
217 | }
218 | }
219 |
220 | class OAuthRequest {
221 | private $parameters;
222 | private $http_method;
223 | private $http_url;
224 | // for debug purposes
225 | public $base_string;
226 | public static $version = '1.0';
227 | public static $POST_INPUT = 'php://input';
228 |
229 | function __construct($http_method, $http_url, $parameters=NULL) {
230 | @$parameters or $parameters = array();
231 | $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
232 | $this->parameters = $parameters;
233 | $this->http_method = $http_method;
234 | $this->http_url = $http_url;
235 | }
236 |
237 |
238 | /**
239 | * attempt to build up a request from what was passed to the server
240 | */
241 | public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
242 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
243 | ? 'http'
244 | : 'https';
245 | @$http_url or $http_url = $scheme .
246 | '://' . $_SERVER['HTTP_HOST'] .
247 | ':' .
248 | $_SERVER['SERVER_PORT'] .
249 | $_SERVER['REQUEST_URI'];
250 | @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
251 |
252 | // We weren't handed any parameters, so let's find the ones relevant to
253 | // this request.
254 | // If you run XML-RPC or similar you should use this to provide your own
255 | // parsed parameter-list
256 | if (!$parameters) {
257 | // Find request headers
258 | $request_headers = OAuthUtil::get_headers();
259 |
260 | // Parse the query-string to find GET parameters
261 | $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
262 |
263 | // It's a POST request of the proper content-type, so parse POST
264 | // parameters and add those overriding any duplicates from GET
265 | if ($http_method == "POST"
266 | && @strstr($request_headers["Content-Type"],
267 | "application/x-www-form-urlencoded")
268 | ) {
269 | $post_data = OAuthUtil::parse_parameters(
270 | file_get_contents(self::$POST_INPUT)
271 | );
272 | $parameters = array_merge($parameters, $post_data);
273 | }
274 |
275 | // We have a Authorization-header with OAuth data. Parse the header
276 | // and add those overriding any duplicates from GET or POST
277 | if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
278 | $header_parameters = OAuthUtil::split_header(
279 | $request_headers['Authorization']
280 | );
281 | $parameters = array_merge($parameters, $header_parameters);
282 | }
283 |
284 | }
285 |
286 | return new OAuthRequest($http_method, $http_url, $parameters);
287 | }
288 |
289 | /**
290 | * pretty much a helper function to set up the request
291 | */
292 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
293 | @$parameters or $parameters = array();
294 | $defaults = array("oauth_version" => OAuthRequest::$version,
295 | "oauth_nonce" => OAuthRequest::generate_nonce(),
296 | "oauth_timestamp" => OAuthRequest::generate_timestamp(),
297 | "oauth_consumer_key" => $consumer->key);
298 | if ($token)
299 | $defaults['oauth_token'] = $token->key;
300 |
301 | $parameters = array_merge($defaults, $parameters);
302 |
303 | return new OAuthRequest($http_method, $http_url, $parameters);
304 | }
305 |
306 | public function set_parameter($name, $value, $allow_duplicates = true) {
307 | if ($allow_duplicates && isset($this->parameters[$name])) {
308 | // We have already added parameter(s) with this name, so add to the list
309 | if (is_scalar($this->parameters[$name])) {
310 | // This is the first duplicate, so transform scalar (string)
311 | // into an array so we can add the duplicates
312 | $this->parameters[$name] = array($this->parameters[$name]);
313 | }
314 |
315 | $this->parameters[$name][] = $value;
316 | } else {
317 | $this->parameters[$name] = $value;
318 | }
319 | }
320 |
321 | public function get_parameter($name) {
322 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
323 | }
324 |
325 | public function get_parameters() {
326 | return $this->parameters;
327 | }
328 |
329 | public function unset_parameter($name) {
330 | unset($this->parameters[$name]);
331 | }
332 |
333 | /**
334 | * The request parameters, sorted and concatenated into a normalized string.
335 | * @return string
336 | */
337 | public function get_signable_parameters() {
338 | // Grab all parameters
339 | $params = $this->parameters;
340 |
341 | // Remove oauth_signature if present
342 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
343 | if (isset($params['oauth_signature'])) {
344 | unset($params['oauth_signature']);
345 | }
346 |
347 | return OAuthUtil::build_http_query($params);
348 | }
349 |
350 | /**
351 | * Returns the base string of this request
352 | *
353 | * The base string defined as the method, the url
354 | * and the parameters (normalized), each urlencoded
355 | * and the concated with &.
356 | */
357 | public function get_signature_base_string() {
358 | $parts = array(
359 | $this->get_normalized_http_method(),
360 | $this->get_normalized_http_url(),
361 | $this->get_signable_parameters()
362 | );
363 |
364 | $parts = OAuthUtil::urlencode_rfc3986($parts);
365 |
366 | return implode('&', $parts);
367 | }
368 |
369 | /**
370 | * just uppercases the http method
371 | */
372 | public function get_normalized_http_method() {
373 | return strtoupper($this->http_method);
374 | }
375 |
376 | /**
377 | * parses the url and rebuilds it to be
378 | * scheme://host/path
379 | */
380 | public function get_normalized_http_url() {
381 | $parts = parse_url($this->http_url);
382 |
383 | $port = @$parts['port'];
384 | $scheme = $parts['scheme'];
385 | $host = $parts['host'];
386 | $path = @$parts['path'];
387 |
388 | $port or $port = ($scheme == 'https') ? '443' : '80';
389 |
390 | if (($scheme == 'https' && $port != '443')
391 | || ($scheme == 'http' && $port != '80')) {
392 | $host = "$host:$port";
393 | }
394 | return "$scheme://$host$path";
395 | }
396 |
397 | /**
398 | * builds a url usable for a GET request
399 | */
400 | public function to_url() {
401 | $post_data = $this->to_postdata();
402 | $out = $this->get_normalized_http_url();
403 | if ($post_data) {
404 | $out .= '?'.$post_data;
405 | }
406 | return $out;
407 | }
408 |
409 | /**
410 | * builds the data one would send in a POST request
411 | */
412 | public function to_postdata() {
413 | return OAuthUtil::build_http_query($this->parameters);
414 | }
415 |
416 | /**
417 | * builds the Authorization: header
418 | */
419 | public function to_header($realm=null) {
420 | $first = true;
421 | if($realm) {
422 | $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"';
423 | $first = false;
424 | } else
425 | $out = 'Authorization: OAuth';
426 |
427 | $total = array();
428 | foreach ($this->parameters as $k => $v) {
429 | if (substr($k, 0, 5) != "oauth") continue;
430 | if (is_array($v)) {
431 | throw new OAuthException('Arrays not supported in headers');
432 | }
433 | $out .= ($first) ? ' ' : ',';
434 | $out .= OAuthUtil::urlencode_rfc3986($k) .
435 | '="' .
436 | OAuthUtil::urlencode_rfc3986($v) .
437 | '"';
438 | $first = false;
439 | }
440 | return $out;
441 | }
442 |
443 | public function __toString() {
444 | return $this->to_url();
445 | }
446 |
447 |
448 | public function sign_request($signature_method, $consumer, $token) {
449 | $this->set_parameter(
450 | "oauth_signature_method",
451 | $signature_method->get_name(),
452 | false
453 | );
454 | $signature = $this->build_signature($signature_method, $consumer, $token);
455 | $this->set_parameter("oauth_signature", $signature, false);
456 | }
457 |
458 | public function build_signature($signature_method, $consumer, $token) {
459 | $signature = $signature_method->build_signature($this, $consumer, $token);
460 | return $signature;
461 | }
462 |
463 | /**
464 | * util function: current timestamp
465 | */
466 | private static function generate_timestamp() {
467 | return time();
468 | }
469 |
470 | /**
471 | * util function: current nonce
472 | */
473 | private static function generate_nonce() {
474 | $mt = microtime();
475 | $rand = mt_rand();
476 |
477 | return md5($mt . $rand); // md5s look nicer than numbers
478 | }
479 | }
480 |
481 | class OAuthServer {
482 | protected $timestamp_threshold = 300; // in seconds, five minutes
483 | protected $version = '1.0'; // hi blaine
484 | protected $signature_methods = array();
485 |
486 | protected $data_store;
487 |
488 | function __construct($data_store) {
489 | $this->data_store = $data_store;
490 | }
491 |
492 | public function add_signature_method($signature_method) {
493 | $this->signature_methods[$signature_method->get_name()] =
494 | $signature_method;
495 | }
496 |
497 | // high level functions
498 |
499 | /**
500 | * process a request_token request
501 | * returns the request token on success
502 | */
503 | public function fetch_request_token(&$request) {
504 | $this->get_version($request);
505 |
506 | $consumer = $this->get_consumer($request);
507 |
508 | // no token required for the initial token request
509 | $token = NULL;
510 |
511 | $this->check_signature($request, $consumer, $token);
512 |
513 | // Rev A change
514 | $callback = $request->get_parameter('oauth_callback');
515 | $new_token = $this->data_store->new_request_token($consumer, $callback);
516 |
517 | return $new_token;
518 | }
519 |
520 | /**
521 | * process an access_token request
522 | * returns the access token on success
523 | */
524 | public function fetch_access_token(&$request) {
525 | $this->get_version($request);
526 |
527 | $consumer = $this->get_consumer($request);
528 |
529 | // requires authorized request token
530 | $token = $this->get_token($request, $consumer, "request");
531 |
532 | $this->check_signature($request, $consumer, $token);
533 |
534 | // Rev A change
535 | $verifier = $request->get_parameter('oauth_verifier');
536 | $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
537 |
538 | return $new_token;
539 | }
540 |
541 | /**
542 | * verify an api call, checks all the parameters
543 | */
544 | public function verify_request(&$request) {
545 | $this->get_version($request);
546 | $consumer = $this->get_consumer($request);
547 | $token = $this->get_token($request, $consumer, "access");
548 | $this->check_signature($request, $consumer, $token);
549 | return array($consumer, $token);
550 | }
551 |
552 | // Internals from here
553 | /**
554 | * version 1
555 | */
556 | private function get_version(&$request) {
557 | $version = $request->get_parameter("oauth_version");
558 | if (!$version) {
559 | // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present.
560 | // Chapter 7.0 ("Accessing Protected Ressources")
561 | $version = '1.0';
562 | }
563 | if ($version !== $this->version) {
564 | throw new OAuthException("OAuth version '$version' not supported");
565 | }
566 | return $version;
567 | }
568 |
569 | /**
570 | * figure out the signature with some defaults
571 | */
572 | private function get_signature_method(&$request) {
573 | $signature_method =
574 | @$request->get_parameter("oauth_signature_method");
575 |
576 | if (!$signature_method) {
577 | // According to chapter 7 ("Accessing Protected Ressources") the signature-method
578 | // parameter is required, and we can't just fallback to PLAINTEXT
579 | throw new OAuthException('No signature method parameter. This parameter is required');
580 | }
581 |
582 | if (!in_array($signature_method,
583 | array_keys($this->signature_methods))) {
584 | throw new OAuthException(
585 | "Signature method '$signature_method' not supported " .
586 | "try one of the following: " .
587 | implode(", ", array_keys($this->signature_methods))
588 | );
589 | }
590 | return $this->signature_methods[$signature_method];
591 | }
592 |
593 | /**
594 | * try to find the consumer for the provided request's consumer key
595 | */
596 | private function get_consumer(&$request) {
597 | $consumer_key = @$request->get_parameter("oauth_consumer_key");
598 | if (!$consumer_key) {
599 | throw new OAuthException("Invalid consumer key");
600 | }
601 |
602 | $consumer = $this->data_store->lookup_consumer($consumer_key);
603 | if (!$consumer) {
604 | throw new OAuthException("Invalid consumer");
605 | }
606 |
607 | return $consumer;
608 | }
609 |
610 | /**
611 | * try to find the token for the provided request's token key
612 | */
613 | private function get_token(&$request, $consumer, $token_type="access") {
614 | $token_field = @$request->get_parameter('oauth_token');
615 | $token = $this->data_store->lookup_token(
616 | $consumer, $token_type, $token_field
617 | );
618 | if (!$token) {
619 | throw new OAuthException("Invalid $token_type token: $token_field");
620 | }
621 | return $token;
622 | }
623 |
624 | /**
625 | * all-in-one function to check the signature on a request
626 | * should guess the signature method appropriately
627 | */
628 | private function check_signature(&$request, $consumer, $token) {
629 | // this should probably be in a different method
630 | $timestamp = @$request->get_parameter('oauth_timestamp');
631 | $nonce = @$request->get_parameter('oauth_nonce');
632 |
633 | $this->check_timestamp($timestamp);
634 | $this->check_nonce($consumer, $token, $nonce, $timestamp);
635 |
636 | $signature_method = $this->get_signature_method($request);
637 |
638 | $signature = $request->get_parameter('oauth_signature');
639 | $valid_sig = $signature_method->check_signature(
640 | $request,
641 | $consumer,
642 | $token,
643 | $signature
644 | );
645 |
646 | if (!$valid_sig) {
647 | throw new OAuthException("Invalid signature");
648 | }
649 | }
650 |
651 | /**
652 | * check that the timestamp is new enough
653 | */
654 | private function check_timestamp($timestamp) {
655 | if( ! $timestamp )
656 | throw new OAuthException(
657 | 'Missing timestamp parameter. The parameter is required'
658 | );
659 |
660 | // verify that timestamp is recentish
661 | $now = time();
662 | if (abs($now - $timestamp) > $this->timestamp_threshold) {
663 | throw new OAuthException(
664 | "Expired timestamp, yours $timestamp, ours $now"
665 | );
666 | }
667 | }
668 |
669 | /**
670 | * check that the nonce is not repeated
671 | */
672 | private function check_nonce($consumer, $token, $nonce, $timestamp) {
673 | if( ! $nonce )
674 | throw new OAuthException(
675 | 'Missing nonce parameter. The parameter is required'
676 | );
677 |
678 | // verify that the nonce is uniqueish
679 | $found = $this->data_store->lookup_nonce(
680 | $consumer,
681 | $token,
682 | $nonce,
683 | $timestamp
684 | );
685 | if ($found) {
686 | throw new OAuthException("Nonce already used: $nonce");
687 | }
688 | }
689 |
690 | }
691 |
692 | class OAuthDataStore {
693 | function lookup_consumer($consumer_key) {
694 | // implement me
695 | }
696 |
697 | function lookup_token($consumer, $token_type, $token) {
698 | // implement me
699 | }
700 |
701 | function lookup_nonce($consumer, $token, $nonce, $timestamp) {
702 | // implement me
703 | }
704 |
705 | function new_request_token($consumer, $callback = null) {
706 | // return a new token attached to this consumer
707 | }
708 |
709 | function new_access_token($token, $consumer, $verifier = null) {
710 | // return a new access token attached to this consumer
711 | // for the user associated with this token if the request token
712 | // is authorized
713 | // should also invalidate the request token
714 | }
715 |
716 | }
717 |
718 | class OAuthUtil {
719 | public static function urlencode_rfc3986($input) {
720 | if (is_array($input)) {
721 | return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
722 | } else if (is_scalar($input)) {
723 | return str_replace(
724 | '+',
725 | ' ',
726 | str_replace('%7E', '~', rawurlencode($input))
727 | );
728 | } else {
729 | return '';
730 | }
731 | }
732 |
733 |
734 | // This decode function isn't taking into consideration the above
735 | // modifications to the encoding process. However, this method doesn't
736 | // seem to be used anywhere so leaving it as is.
737 | public static function urldecode_rfc3986($string) {
738 | return urldecode($string);
739 | }
740 |
741 | // Utility function for turning the Authorization: header into
742 | // parameters, has to do some unescaping
743 | // Can filter out any non-oauth parameters if needed (default behaviour)
744 | public static function split_header($header, $only_allow_oauth_parameters = true) {
745 | $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
746 | $offset = 0;
747 | $params = array();
748 | while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
749 | $match = $matches[0];
750 | $header_name = $matches[2][0];
751 | $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
752 | if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
753 | $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content);
754 | }
755 | $offset = $match[1] + strlen($match[0]);
756 | }
757 |
758 | if (isset($params['realm'])) {
759 | unset($params['realm']);
760 | }
761 |
762 | return $params;
763 | }
764 |
765 | // helper to try to sort out headers for people who aren't running apache
766 | public static function get_headers() {
767 | if (function_exists('apache_request_headers')) {
768 | // we need this to get the actual Authorization: header
769 | // because apache tends to tell us it doesn't exist
770 | $headers = apache_request_headers();
771 |
772 | // sanitize the output of apache_request_headers because
773 | // we always want the keys to be Cased-Like-This and arh()
774 | // returns the headers in the same case as they are in the
775 | // request
776 | $out = array();
777 | foreach( $headers AS $key => $value ) {
778 | $key = str_replace(
779 | " ",
780 | "-",
781 | ucwords(strtolower(str_replace("-", " ", $key)))
782 | );
783 | $out[$key] = $value;
784 | }
785 | } else {
786 | // otherwise we don't have apache and are just going to have to hope
787 | // that $_SERVER actually contains what we need
788 | $out = array();
789 | if( isset($_SERVER['CONTENT_TYPE']) )
790 | $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
791 | if( isset($_ENV['CONTENT_TYPE']) )
792 | $out['Content-Type'] = $_ENV['CONTENT_TYPE'];
793 |
794 | foreach ($_SERVER as $key => $value) {
795 | if (substr($key, 0, 5) == "HTTP_") {
796 | // this is chaos, basically it is just there to capitalize the first
797 | // letter of every word that is not an initial HTTP and strip HTTP
798 | // code from przemek
799 | $key = str_replace(
800 | " ",
801 | "-",
802 | ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
803 | );
804 | $out[$key] = $value;
805 | }
806 | }
807 | }
808 | return $out;
809 | }
810 |
811 | // This function takes a input like a=b&a=c&d=e and returns the parsed
812 | // parameters like this
813 | // array('a' => array('b','c'), 'd' => 'e')
814 | public static function parse_parameters( $input ) {
815 | if (!isset($input) || !$input) return array();
816 |
817 | $pairs = explode('&', $input);
818 |
819 | $parsed_parameters = array();
820 | foreach ($pairs as $pair) {
821 | $split = explode('=', $pair, 2);
822 | $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
823 | $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
824 |
825 | if (isset($parsed_parameters[$parameter])) {
826 | // We have already recieved parameter(s) with this name, so add to the list
827 | // of parameters with this name
828 |
829 | if (is_scalar($parsed_parameters[$parameter])) {
830 | // This is the first duplicate, so transform scalar (string) into an array
831 | // so we can add the duplicates
832 | $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
833 | }
834 |
835 | $parsed_parameters[$parameter][] = $value;
836 | } else {
837 | $parsed_parameters[$parameter] = $value;
838 | }
839 | }
840 | return $parsed_parameters;
841 | }
842 |
843 | public static function build_http_query($params) {
844 | if (!$params) return '';
845 |
846 | // Urlencode both keys and values
847 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
848 | $values = OAuthUtil::urlencode_rfc3986(array_values($params));
849 | $params = array_combine($keys, $values);
850 |
851 | // Parameters are sorted by name, using lexicographical byte value ordering.
852 | // Ref: Spec: 9.1.1 (1)
853 | uksort($params, 'strcmp');
854 |
855 | $pairs = array();
856 | foreach ($params as $parameter => $value) {
857 | if (is_array($value)) {
858 | // If two or more parameters share the same name, they are sorted by their value
859 | // Ref: Spec: 9.1.1 (1)
860 | natsort($value);
861 | foreach ($value as $duplicate_value) {
862 | $pairs[] = $parameter . '=' . $duplicate_value;
863 | }
864 | } else {
865 | $pairs[] = $parameter . '=' . $value;
866 | }
867 | }
868 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
869 | // Each name-value pair is separated by an '&' character (ASCII code 38)
870 | return implode('&', $pairs);
871 | }
872 | }
873 |
874 | ?>
875 |
--------------------------------------------------------------------------------