]> git.wh0rd.org - tt-rss.git/blame - lib/tmhoauth/tmhOAuth.php
add auth_base check_password()
[tt-rss.git] / lib / tmhoauth / tmhOAuth.php
CommitLineData
54a3dd8d
AD
1<?php
2/**
3 * tmhOAuth
4 *
5 * An OAuth 1.0A library written in PHP.
6 * The library supports file uploading using multipart/form as well as general
7 * REST requests. OAuth authentication is sent using the an Authorization Header.
8 *
9 * @author themattharris
10 * @version 0.4
11 *
12 * 03 March 2011
13 */
14class tmhOAuth {
15 /**
16 * Creates a new tmhOAuth object
17 *
18 * @param string $config, the configuration to use for this request
19 */
20 function __construct($config) {
21 $this->params = array();
22 $this->auto_fixed_time = false;
23
24 // default configuration options
25 $this->config = array_merge(
26 array(
27 'consumer_key' => '',
28 'consumer_secret' => '',
29 'user_token' => '',
30 'user_secret' => '',
31 'use_ssl' => true,
32 'host' => 'api.twitter.com',
33 'debug' => false,
34 'force_nonce' => false,
35 'nonce' => false, // used for checking signatures. leave as false for auto
36 'force_timestamp' => false,
37 'timestamp' => false, // used for checking signatures. leave as false for auto
38 'oauth_version' => '1.0',
39
40 // you probably don't want to change any of these curl values
41 'curl_connecttimeout' => 30,
42 'curl_timeout' => 10,
43 // for security you may want to set this to TRUE. If you do you need
44 // to install the servers certificate in your local certificate store.
45 'curl_ssl_verifypeer' => false,
46 'curl_followlocation' => false, // whether to follow redirects or not
47 // support for proxy servers
48 'curl_proxy' => false, // really you don't want to use this if you are using streaming
49 'curl_proxyuserpwd' => false, // format username:password for proxy, if required
50
51 // streaming API
52 'is_streaming' => false,
53 'streaming_eol' => "\r\n",
54 'streaming_metrics_interval' => 60,
55 ),
56 $config
57 );
58 }
59
60 /**
61 * Generates a random OAuth nonce.
62 * If 'force_nonce' is true a nonce is not generated and the value in the configuration will be retained.
63 *
64 * @param string $length how many characters the nonce should be before MD5 hashing. default 12
65 * @param string $include_time whether to include time at the beginning of the nonce. default true
66 * @return void
67 */
68 private function create_nonce($length=12, $include_time=true) {
69 if ($this->config['force_nonce'] == false) {
70 $sequence = array_merge(range(0,9), range('A','Z'), range('a','z'));
71 $length = $length > count($sequence) ? count($sequence) : $length;
72 shuffle($sequence);
73 $this->config['nonce'] = md5(substr(microtime() . implode($sequence), 0, $length));
74 }
75 }
76
77 /**
78 * Generates a timestamp.
79 * If 'force_timestamp' is true a nonce is not generated and the value in the configuration will be retained.
80 *
81 * @return void
82 */
83 private function create_timestamp() {
84 $this->config['timestamp'] = ($this->config['force_timestamp'] == false ? time() : $this->config['timestamp']);
85 }
86
87 /**
88 * Encodes the string or array passed in a way compatible with OAuth.
89 * If an array is passed each array value will will be encoded.
90 *
91 * @param mixed $data the scalar or array to encode
92 * @return $data encoded in a way compatible with OAuth
93 */
94 private function safe_encode($data) {
95 if (is_array($data)) {
96 return array_map(array($this, 'safe_encode'), $data);
97 } else if (is_scalar($data)) {
98 return str_ireplace(
99 array('+', '%7E'),
100 array(' ', '~'),
101 rawurlencode($data)
102 );
103 } else {
104 return '';
105 }
106 }
107
108 /**
109 * Decodes the string or array from it's URL encoded form
110 * If an array is passed each array value will will be decoded.
111 *
112 * @param mixed $data the scalar or array to decode
113 * @return $data decoded from the URL encoded form
114 */
115 private function safe_decode($data) {
116 if (is_array($data)) {
117 return array_map(array($this, 'safe_decode'), $data);
118 } else if (is_scalar($data)) {
119 return rawurldecode($data);
120 } else {
121 return '';
122 }
123 }
124
125 /**
126 * Returns an array of the standard OAuth parameters.
127 *
128 * @return array all required OAuth parameters, safely encoded
129 */
130 private function get_defaults() {
131 $defaults = array(
132 'oauth_version' => $this->config['oauth_version'],
133 'oauth_nonce' => $this->config['nonce'],
134 'oauth_timestamp' => $this->config['timestamp'],
135 'oauth_consumer_key' => $this->config['consumer_key'],
136 'oauth_signature_method' => 'HMAC-SHA1',
137 );
138
139 // include the user token if it exists
140 if ( $this->config['user_token'] )
141 $defaults['oauth_token'] = $this->config['user_token'];
142
143 // safely encode
144 foreach ($defaults as $k => $v) {
145 $_defaults[$this->safe_encode($k)] = $this->safe_encode($v);
146 }
147
148 return $_defaults;
149 }
150
151 /**
152 * Extracts and decodes OAuth parameters from the passed string
153 *
154 * @param string $body the response body from an OAuth flow method
155 * @return array the response body safely decoded to an array of key => values
156 */
157 function extract_params($body) {
158 $kvs = explode('&', $body);
159 $decoded = array();
160 foreach ($kvs as $kv) {
161 $kv = explode('=', $kv, 2);
162 $kv[0] = $this->safe_decode($kv[0]);
163 $kv[1] = $this->safe_decode($kv[1]);
164 $decoded[$kv[0]] = $kv[1];
165 }
166 return $decoded;
167 }
168
169 /**
170 * Prepares the HTTP method for use in the base string by converting it to
171 * uppercase.
172 *
173 * @param string $method an HTTP method such as GET or POST
174 * @return void value is stored to a class variable
175 * @author themattharris
176 */
177 private function prepare_method($method) {
178 $this->method = strtoupper($method);
179 }
180
181 /**
182 * Prepares the URL for use in the base string by ripping it apart and
183 * reconstructing it.
184 *
185 * @param string $url the request URL
186 * @return void value is stored to a class variable
187 * @author themattharris
188 */
189 private function prepare_url($url) {
190 $parts = parse_url($url);
191
192 $port = @$parts['port'];
193 $scheme = $parts['scheme'];
194 $host = $parts['host'];
195 $path = @$parts['path'];
196
197 $port or $port = ($scheme == 'https') ? '443' : '80';
198
199 if (($scheme == 'https' && $port != '443')
200 || ($scheme == 'http' && $port != '80')) {
201 $host = "$host:$port";
202 }
203 $this->url = "$scheme://$host$path";
204 }
205
206 /**
207 * Prepares all parameters for the base string and request.
208 * Multipart parameters are ignored as they are not defined in the specification,
209 * all other types of parameter are encoded for compatibility with OAuth.
210 *
211 * @param array $params the parameters for the request
212 * @return void prepared values are stored in class variables
213 */
214 private function prepare_params($params) {
215 // do not encode multipart parameters, leave them alone
216 if ($this->config['multipart']) {
217 $this->request_params = $params;
218 $params = array();
219 }
220
221 // signing parameters are request parameters + OAuth default parameters
222 $this->signing_params = array_merge($this->get_defaults(), (array)$params);
223
224 // Remove oauth_signature if present
225 // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
226 if (isset($this->signing_params['oauth_signature'])) {
227 unset($this->signing_params['oauth_signature']);
228 }
229
230 // Parameters are sorted by name, using lexicographical byte value ordering.
231 // Ref: Spec: 9.1.1 (1)
232 uksort($this->signing_params, 'strcmp');
233
234 // encode. Also sort the signed parameters from the POST parameters
235 foreach ($this->signing_params as $k => $v) {
236 $k = $this->safe_encode($k);
237 $v = $this->safe_encode($v);
238 $_signing_params[$k] = $v;
239 $kv[] = "{$k}={$v}";
240 }
241
242 // auth params = the default oauth params which are present in our collection of signing params
243 $this->auth_params = array_intersect_key($this->get_defaults(), $_signing_params);
244 if (isset($_signing_params['oauth_callback'])) {
245 $this->auth_params['oauth_callback'] = $_signing_params['oauth_callback'];
246 unset($_signing_params['oauth_callback']);
247 }
248
249 // request_params is already set if we're doing multipart, if not we need to set them now
250 if ( ! $this->config['multipart'])
251 $this->request_params = array_diff_key($_signing_params, $this->get_defaults());
252
253 // create the parameter part of the base string
254 $this->signing_params = implode('&', $kv);
255 }
256
257 /**
258 * Prepares the OAuth signing key
259 *
260 * @return void prepared signing key is stored in a class variables
261 */
262 private function prepare_signing_key() {
263 $this->signing_key = $this->safe_encode($this->config['consumer_secret']) . '&' . $this->safe_encode($this->config['user_secret']);
264 }
265
266 /**
267 * Prepare the base string.
268 * Ref: Spec: 9.1.3 ("Concatenate Request Elements")
269 *
270 * @return void prepared base string is stored in a class variables
271 */
272 private function prepare_base_string() {
273 $base = array(
274 $this->method,
275 $this->url,
276 $this->signing_params
277 );
278 $this->base_string = implode('&', $this->safe_encode($base));
279 }
280
281 /**
282 * Prepares the Authorization header
283 *
284 * @return void prepared authorization header is stored in a class variables
285 */
286 private function prepare_auth_header() {
287 $this->headers = array();
288 uksort($this->auth_params, 'strcmp');
289 foreach ($this->auth_params as $k => $v) {
290 $kv[] = "{$k}=\"{$v}\"";
291 }
292 $this->auth_header = 'OAuth ' . implode(', ', $kv);
293 $this->headers[] = 'Authorization: ' . $this->auth_header;
294 }
295
296 /**
297 * Signs the request and adds the OAuth signature. This runs all the request
298 * parameter preparation methods.
299 *
300 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
301 * @param string $url the request URL without query string parameters
302 * @param array $params the request parameters as an array of key=value pairs
303 * @param string $useauth whether to use authentication when making the request.
304 */
305 private function sign($method, $url, $params, $useauth) {
306 $this->prepare_method($method);
307 $this->prepare_url($url);
308 $this->prepare_params($params);
309
310 // we don't sign anything is we're not using auth
311 if ($useauth) {
312 $this->prepare_base_string();
313 $this->prepare_signing_key();
314
315 $this->auth_params['oauth_signature'] = $this->safe_encode(
316 base64_encode(
317 hash_hmac(
318 'sha1', $this->base_string, $this->signing_key, true
319 )));
320
321 $this->prepare_auth_header();
322 }
323 }
324
325 /**
326 * Make an HTTP request using this library. This method doesn't return anything.
327 * Instead the response should be inspected directly.
328 *
329 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
330 * @param string $url the request URL without query string parameters
331 * @param array $params the request parameters as an array of key=value pairs
332 * @param string $useauth whether to use authentication when making the request. Default true.
333 * @param string $multipart whether this request contains multipart data. Default false
334 */
335 function request($method, $url, $params=array(), $useauth=true, $multipart=false) {
336 $this->config['multipart'] = $multipart;
337
338 $this->create_nonce();
339 $this->create_timestamp();
340
341 $this->sign($method, $url, $params, $useauth);
342 return $this->curlit($multipart);
343 }
344
345 /**
346 * Make an HTTP request using this library. This method is different to 'request'
347 * because on a 401 error it will retry the request.
348 *
349 * When a 401 error is returned it is possible the timestamp of the client is
350 * too different to that of the API server. In this situation it is recommended
351 * the request is retried with the OAuth timestamp set to the same as the API
352 * server. This method will automatically try that technique.
353 *
354 * This method doesn't return anything. Instead the response should be
355 * inspected directly.
356 *
357 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
358 * @param string $url the request URL without query string parameters
359 * @param array $params the request parameters as an array of key=value pairs
360 * @param string $useauth whether to use authentication when making the request. Default true.
361 * @param string $multipart whether this request contains multipart data. Default false
362 */
363 function auto_fix_time_request($method, $url, $params=array(), $useauth=true, $multipart=false) {
364 $this->request($method, $url, $params, $useauth, $multipart);
365
366 // if we're not doing auth the timestamp isn't important
367 if ( ! $useauth)
368 return;
369
370 // some error that isn't a 401
371 if ($this->response['code'] != 401)
372 return;
373
374 // some error that is a 401 but isn't because the OAuth token and signature are incorrect
375 // TODO: this check is horrid but helps avoid requesting twice when the username and password are wrong
376 if (stripos($this->response['response'], 'password') !== false)
377 return;
378
379 // force the timestamp to be the same as the Twitter servers, and re-request
380 $this->auto_fixed_time = true;
381 $this->config['force_timestamp'] = true;
382 $this->config['timestamp'] = strtotime($this->response['headers']['date']);
383 $this->request($method, $url, $params, $useauth, $multipart);
384 }
385
386 /**
387 * Make a long poll HTTP request using this library. This method is
388 * different to the other request methods as it isn't supposed to disconnect
389 *
390 * Using this method expects a callback which will receive the streaming
391 * responses.
392 *
393 * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc
394 * @param string $url the request URL without query string parameters
395 * @param array $params the request parameters as an array of key=value pairs
396 * @param string $callback the callback function to stream the buffer to.
397 */
398 function streaming_request($method, $url, $params=array(), $callback='') {
399 if ( ! empty($callback) ) {
400 if ( ! function_exists($callback) ) {
401 return false;
402 }
403 $this->config['streaming_callback'] = $callback;
404 }
405 $this->metrics['start'] = time();
406 $this->metrics['interval_start'] = $this->metrics['start'];
407 $this->metrics['tweets'] = 0;
408 $this->metrics['last_tweets'] = 0;
409 $this->metrics['bytes'] = 0;
410 $this->metrics['last_bytes'] = 0;
411 $this->config['is_streaming'] = true;
412 $this->request($method, $url, $params);
413 }
414
415 /**
416 * Handles the updating of the current Streaming API metrics.
417 */
418 function update_metrics() {
419 $now = time();
420 if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now)
421 return false;
422
423 $this->metrics['tps'] = round( ($this->metrics['tweets'] - $this->metrics['last_tweets']) / $this->config['streaming_metrics_interval'], 2);
424 $this->metrics['bps'] = round( ($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2);
425
426 $this->metrics['last_bytes'] = $this->metrics['bytes'];
427 $this->metrics['last_tweets'] = $this->metrics['tweets'];
428 $this->metrics['interval_start'] = $now;
429 return $this->metrics;
430 }
431
432 /**
433 * Utility function to create the request URL in the requested format
434 *
435 * @param string $request the API method without extension
436 * @param string $format the format of the response. Default json. Set to an empty string to exclude the format
437 * @return string the concatenation of the host, API version, API method and format
438 */
439 function url($request, $format='json') {
440 $format = strlen($format) > 0 ? ".$format" : '';
441 $proto = $this->config['use_ssl'] ? 'https:/' : 'http:/';
442
443 // backwards compatibility with v0.1
444 if (isset($this->config['v']))
445 $this->config['host'] = $this->config['host'] . '/' . $this->config['v'];
446
447 return implode('/', array(
448 $proto,
449 $this->config['host'],
450 $request . $format
451 ));
452 }
453
454 /**
455 * Utility function to parse the returned curl headers and store them in the
456 * class array variable.
457 *
458 * @param object $ch curl handle
459 * @param string $header the response headers
460 * @return the string length of the header
461 */
462 private function curlHeader($ch, $header) {
463 $i = strpos($header, ':');
464 if ( ! empty($i) ) {
465 $key = str_replace('-', '_', strtolower(substr($header, 0, $i)));
466 $value = trim(substr($header, $i + 2));
467 $this->response['headers'][$key] = $value;
468 }
469 return strlen($header);
470 }
471
472 /**
473 * Utility function to parse the returned curl buffer and store them until
474 * an EOL is found. The buffer for curl is an undefined size so we need
475 * to collect the content until an EOL is found.
476 *
477 * This function calls the previously defined streaming callback method.
478 *
479 * @param object $ch curl handle
480 * @param string $data the current curl buffer
481 */
482 private function curlWrite($ch, $data) {
483 $l = strlen($data);
484 if (strpos($data, $this->config['streaming_eol']) === false) {
485 $this->buffer .= $data;
486 return $l;
487 }
488
489 $buffered = explode($this->config['streaming_eol'], $data);
490 $content = $this->buffer . $buffered[0];
491
492 $this->metrics['tweets']++;
493 $this->metrics['bytes'] += strlen($content);
494
495 if ( ! function_exists($this->config['streaming_callback']))
496 return 0;
497
498 $metrics = $this->update_metrics();
499 $stop = call_user_func(
500 $this->config['streaming_callback'],
501 $content,
502 strlen($content),
503 $metrics
504 );
505 $this->buffer = $buffered[1];
506 if ($stop)
507 return 0;
508
509 return $l;
510 }
511
512 /**
513 * Makes a curl request. Takes no parameters as all should have been prepared
514 * by the request method
515 *
516 * @return void response data is stored in the class variable 'response'
517 */
518 private function curlit() {
519 // method handling
520 switch ($this->method) {
521 case 'POST':
522 break;
523 default:
524 // GET, DELETE request so convert the parameters to a querystring
525 if ( ! empty($this->request_params)) {
526 foreach ($this->request_params as $k => $v) {
527 // Multipart params haven't been encoded yet.
528 // Not sure why you would do a multipart GET but anyway, here's the support for it
529 if ($this->config['multipart']) {
530 $params[] = $this->safe_encode($k) . '=' . $this->safe_encode($v);
531 } else {
532 $params[] = $k . '=' . $v;
533 }
534 }
535 $qs = implode('&', $params);
536 $this->url = strlen($qs) > 0 ? $this->url . '?' . $qs : $this->url;
537 $this->request_params = array();
538 }
539 break;
540 }
541
542 if (@$this->config['prevent_request'])
543 return;
544
545 // configure curl
546 $c = curl_init();
547 curl_setopt($c, CURLOPT_USERAGENT, "themattharris' HTTP Client");
548 curl_setopt($c, CURLOPT_CONNECTTIMEOUT, $this->config['curl_connecttimeout']);
549 curl_setopt($c, CURLOPT_TIMEOUT, $this->config['curl_timeout']);
550 curl_setopt($c, CURLOPT_RETURNTRANSFER, TRUE);
551 curl_setopt($c, CURLOPT_SSL_VERIFYPEER, $this->config['curl_ssl_verifypeer']);
552 curl_setopt($c, CURLOPT_FOLLOWLOCATION, $this->config['curl_followlocation']);
553 curl_setopt($c, CURLOPT_PROXY, $this->config['curl_proxy']);
554 curl_setopt($c, CURLOPT_URL, $this->url);
555 // process the headers
556 curl_setopt($c, CURLOPT_HEADERFUNCTION, array($this, 'curlHeader'));
557 curl_setopt($c, CURLOPT_HEADER, FALSE);
558 curl_setopt($c, CURLINFO_HEADER_OUT, true);
559
560 if ($this->config['curl_proxyuserpwd'] !== false)
561 curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']);
562
563 if ($this->config['is_streaming']) {
564 // process the body
565 $this->response['content-length'] = 0;
566 curl_setopt($c, CURLOPT_TIMEOUT, 0);
567 curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite'));
568 }
569
570 switch ($this->method) {
571 case 'GET':
572 break;
573 case 'POST':
574 curl_setopt($c, CURLOPT_POST, TRUE);
575 break;
576 default:
577 curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->method);
578 }
579
580 if ( ! empty($this->request_params) ) {
581 // if not doing multipart we need to implode the parameters
582 if ( ! $this->config['multipart'] ) {
583 foreach ($this->request_params as $k => $v) {
584 $ps[] = "{$k}={$v}";
585 }
586 $this->request_params = implode('&', $ps);
587 }
588 curl_setopt($c, CURLOPT_POSTFIELDS, $this->request_params);
589 } else {
590 // CURL will set length to -1 when there is no data, which breaks Twitter
591 $this->headers[] = 'Content-Type:';
592 $this->headers[] = 'Content-Length:';
593 }
594
595 // CURL defaults to setting this to Expect: 100-Continue which Twitter rejects
596 $this->headers[] = 'Expect:';
597
598 if ( ! empty($this->headers))
599 curl_setopt($c, CURLOPT_HTTPHEADER, $this->headers);
600
601 // do it!
602 $response = curl_exec($c);
603 $code = curl_getinfo($c, CURLINFO_HTTP_CODE);
604 $info = curl_getinfo($c);
605 curl_close($c);
606
607 // store the response
608 $this->response['code'] = $code;
609 $this->response['response'] = $response;
610 $this->response['info'] = $info;
611 return $code;
612 }
613
614 /**
615 * Debug function for printing the content of an object
616 *
617 * @param mixes $obj
618 */
619 function pr($obj) {
620 $cli = (PHP_SAPI == 'cli' && empty($_SERVER['REMOTE_ADDR']));
621 if (!$cli)
622 echo '<pre style="word-wrap: break-word">';
623 if ( is_object($obj) )
624 print_r($obj);
625 elseif ( is_array($obj) )
626 print_r($obj);
627 else
628 echo $obj;
629 if (!$cli)
630 echo '</pre>';
631 }
632
633 /**
634 * Returns the current URL. This is instead of PHP_SELF which is unsafe
635 *
636 * @param bool $dropqs whether to drop the querystring or not. Default true
637 * @return string the current URL
638 */
639 function php_self($dropqs=true) {
640 $url = sprintf('%s://%s%s',
641 empty($_SERVER['HTTPS']) ? 'http' : 'https',
642 $_SERVER['SERVER_NAME'],
643 $_SERVER['REQUEST_URI']
644 );
645
646 $parts = parse_url($url);
647
648 $port = $_SERVER['SERVER_PORT'];
649 $scheme = $parts['scheme'];
650 $host = $parts['host'];
651 $path = @$parts['path'];
652 $qs = @$parts['query'];
653
654 $port or $port = ($scheme == 'https') ? '443' : '80';
655
656 if (($scheme == 'https' && $port != '443')
657 || ($scheme == 'http' && $port != '80')) {
658 $host = "$host:$port";
659 }
660 $url = "$scheme://$host$path";
661 if ( ! $dropqs)
662 return "{$url}?{$qs}";
663 else
664 return $url;
665 }
666
667 /**
668 * Entifies the tweet using the given entities element
669 *
670 * @param array $tweet the json converted to normalised array
671 * @return the tweet text with entities replaced with hyperlinks
672 */
673 function entify($tweet) {
674 $keys = array();
675 $replacements = array();
676 $is_retweet = false;
677
678 if (isset($tweet['retweeted_status'])) {
679 $tweet = $tweet['retweeted_status'];
680 $is_retweet = true;
681 }
682
683 if (!isset($tweet['entities'])) {
684 return $tweet['text'];
685 }
686
687 // prepare the entities
688 foreach ($tweet['entities'] as $type => $things) {
689 foreach ($things as $entity => $value) {
690 $tweet_link = "<a href=\"http://twitter.com/{$value['screen_name']}/statuses/{$tweet['id']}\">{$tweet['created_at']}</a>";
691
692 switch ($type) {
693 case 'hashtags':
694 $href = "<a href=\"http://search.twitter.com/search?q=%23{$value['text']}\">#{$value['text']}</a>";
695 break;
696 case 'user_mentions':
697 $href = "@<a href=\"http://twitter.com/{$value['screen_name']}\" title=\"{$value['name']}\">{$value['screen_name']}</a>";
698 break;
699 case 'urls':
700 $url = empty($value['expanded_url']) ? $value['url'] : $value['expanded_url'];
701 $display = isset($value['display_url']) ? $value['display_url'] : str_replace('http://', '', $url);
702 // Not all pages are served in UTF-8 so you may need to do this ...
703 $display = urldecode(str_replace('%E2%80%A6', '&hellip;', urlencode($display)));
704 $href = "<a href=\"{$value['url']}\">{$display}</a>";
705 break;
706 }
707 $keys[$value['indices']['0']] = substr(
708 $tweet['text'],
709 $value['indices']['0'],
710 $value['indices']['1'] - $value['indices']['0']
711 );
712 $replacements[$value['indices']['0']] = $href;
713 }
714 }
715
716 ksort($replacements);
717 $replacements = array_reverse($replacements, true);
718 $entified_tweet = $tweet['text'];
719 foreach ($replacements as $k => $v) {
720 $entified_tweet = substr_replace($entified_tweet, $v, $k, strlen($keys[$k]));
721 }
722 return $entified_tweet;
723 }
724}
725
726?>