├── .gitignore ├── LICENSE ├── README.md ├── TweetPHP.php └── lib ├── tmhOAuth ├── LICENSE ├── README.md ├── cacert.pem ├── composer.json └── tmhOAuth.php └── twitter-text-php ├── .gitmodules ├── LICENSE ├── README ├── README.markdown ├── TODO ├── lib └── Twitter │ ├── Autolink.php │ ├── Extractor.php │ ├── HitHighlighter.php │ └── Regex.php ├── phpunit.xml └── tests ├── Twitter ├── AutolinkTest.php ├── ExtractorTest.php └── HitHighlighterTest.php ├── bootstrap.php ├── example.php └── runtests.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jonathan Nicol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TweetPHP 2 | 3 | A PHP class for querying the Twitter API and rendering tweets as an HTML list. 4 | 5 | ## Features 6 | 7 | - Works with Twitter API v1.1 8 | - Tweets are cached to avoid exceeding Twitter’s API request rate limits 9 | - A fallback is provided in case the API request fails 10 | - A configuration parameter allows you to specify how many tweets are displayed 11 | - Dates can optionally be displayed in “Twitter style”, e.g. “12 minutes ago” 12 | - You can customize the HTML that wraps your tweets, tweet status and meta information 13 | 14 | ## Authentication 15 | 16 | To interact with Twitter's API you will need to create a Twitter application at: https://dev.twitter.com/apps 17 | 18 | After creating your app you will need to take note of following API values: "Consumer key", "Consumer secret", "Access token", "Access token secret" 19 | 20 | ## Usage 21 | 22 | Your API credentials can be passed as options to the class constructor, along with the any other configuration options: 23 | 24 | $TweetPHP = new TweetPHP(array( 25 | 'consumer_key' => 'xxxxxxxxxxxxxxxxxxxxx', 26 | 'consumer_secret' => 'xxxxxxxxxxxxxxxxxxxxx', 27 | 'access_token' => 'xxxxxxxxxxxxxxxxxxxxx', 28 | 'access_token_secret' => 'xxxxxxxxxxxxxxxxxxxxx', 29 | 'api_params' => array('screen_name' => 'twitteruser') 30 | )); 31 | 32 | Then you can display the results like so: 33 | 34 | echo $TweetPHP->get_tweet_list(); 35 | 36 | You can also retreive the raw data received from Twitter: 37 | 38 | $tweet_array = $TweetPHP->get_tweet_array(); 39 | 40 | ## Options 41 | 42 | Options can be overridden by passing an array of key/value pairs to the class constructor. At a minimum you must set the `consumer_key`, `consumer_secret`, `access_token`, `access_token_secret` options, as shown above. 43 | 44 | You should also set an `api_endpoint` and `api_params`, an array of parameters to include with the call to the Twitter API. 45 | 46 | Here is a full list of options, and their default values: 47 | 48 | 'consumer_key' => '', 49 | 'consumer_secret' => '', 50 | 'access_token' => '', 51 | 'access_token_secret' => '', 52 | 'api_endpoint' => 'statuses/user_timeline', 53 | 'api_params' => array(), 54 | 'enable_cache' => true, 55 | 'cache_dir' => dirname(__FILE__) . '/cache/', // Where on the server to save cached tweets 56 | 'cachetime' => 60 * 60, // Seconds to cache feed (1 hour). 57 | 'tweets_to_retrieve' => 25, // Specifies the number of tweets to try and fetch, up to a maximum of 200 58 | 'tweets_to_display' => 10, // Number of tweets to display 59 | 'twitter_style_dates' => false, // Use twitter style dates e.g. 2 hours ago 60 | 'twitter_date_text' => array('seconds', 'minutes', 'about', 'hour', 'ago'), 61 | 'date_format' => '%I:%M %p %b %e%O', // The defult date format e.g. 12:08 PM Jun 12th. See: http://php.net/manual/en/function.strftime.php 62 | 'date_lang' => null, // Language for date e.g. 'fr_FR'. See: http://php.net/manual/en/function.setlocale.php 63 | 'twitter_template' => '

Latest tweets

', 64 | 'tweet_template' => '
  • {tweet}{date}
  • ', 65 | 'error_template' => '
  • Our twitter feed is unavailable right now. Follow us on Twitter
  • ', 66 | 'nofollow_links' => false, // Add rel="nofollow" attribute to links 67 | 'debug' => false 68 | 69 | ### Deprecated options 70 | 71 | The following options have been deprecated. You should use `api_params` to set API parameters instead. 72 | 73 | 'twitter_screen_name' => '' 74 | 'ignore_replies' => true 75 | 'ignore_retweets' => true 76 | 77 | ## API endpoints 78 | 79 | Since TweetPHP uses Twitter's [Application-only](https://developer.twitter.com/en/docs/basics/authentication/overview/application-only) API authentication model, it can only access certain GET endpoints. 80 | 81 | It has been tested with the statuses/user_timeline endpoint (its default) and the search/tweets endpoint. 82 | 83 | ## Examples 84 | 85 | ### Fetch a user's timeline 86 | 87 | 'xxxxxxxxxxxxxxxxxxxxx', 92 | 'consumer_secret' => 'xxxxxxxxxxxxxxxxxxxxx', 93 | 'access_token' => 'xxxxxxxxxxxxxxxxxxxxx', 94 | 'access_token_secret' => 'xxxxxxxxxxxxxxxxxxxxx', 95 | 'api_endpoint' => 'statuses/user_timeline', 96 | 'api_params' => array('screen_name' => 'twitteruser') 97 | )); 98 | 99 | echo $TweetPHP->get_tweet_list(); 100 | ?> 101 | 102 | Note that the `api_endpoint` option could be omitted in this case, since 'statuses/user_timeline' is its default value. 103 | 104 | ### Search for a hashtag 105 | 106 | 'xxxxxxxxxxxxxxxxxxxxx', 111 | 'consumer_secret' => 'xxxxxxxxxxxxxxxxxxxxx', 112 | 'access_token' => 'xxxxxxxxxxxxxxxxxxxxx', 113 | 'access_token_secret' => 'xxxxxxxxxxxxxxxxxxxxx', 114 | 'api_endpoint' => 'search/tweets', 115 | 'api_params' => array('q' => '#php', 'result_type'=>'latest') 116 | )); 117 | 118 | echo $TweetPHP->get_tweet_list(); 119 | ?> 120 | 121 | ## Caching 122 | 123 | Caching is employed because Twitter rate limits how many times their feeds can be accessed per hour. 124 | 125 | When the user timeline is first loaded, the resultant HTML list is saved as a text file on your web server. The default location for this file is: `cache/twitter.txt` 126 | 127 | The raw Twitter response is saved as a serialized array in: `cache/twitter-array.txt` 128 | 129 | You can change these file paths using the `cache_dir` option. For example, to set a path from your root public directory try: 130 | 131 | $_SERVER['DOCUMENT_ROOT'] . '/path/to/my/cache/dir/' 132 | 133 | ## Debugging 134 | 135 | If you are experiencing problems using the script please set the `debug` option to `true`. This will set PHP's error reporting level to `E_ALL`, and will also display a debugging report. 136 | 137 | You can also fetch the debugging report as an array or HTML list, even when the `debug` option is set to `false`: 138 | 139 | echo $TweetPHP->get_debug_list(); 140 | $debug_array = $TweetPHP->get_debug_array(); 141 | 142 | ## Helper methods 143 | 144 | ### autolink 145 | 146 | Pass raw tweet text to `autolink()` and it will convert all usernames, hashtags and URLs to HTML links. 147 | 148 | $autolinked_tweet = $TweetPHP->autolink($tweet); 149 | 150 | This might be handy if you want to process tweets yourself, using the array returned by `get_tweet_array()`. 151 | 152 | ## Credits 153 | 154 | - Feed parsing uses Matt Harris' [tmhOAuth](https://github.com/themattharris/tmhOAuth) 155 | - Hashtag/username parsing uses Mike Cochrane's [twitter-text-php](https://github.com/mikenz/twitter-text-php) 156 | - Other contributors: [Matt Pugh](https://github.com/mattpugh), [Dario Bauer](https://github.com/dariobauer), [Lee Collings](https://github.com/leecollings), [Dom Abbott](https://github.com/domabbott92) 157 | -------------------------------------------------------------------------------- /TweetPHP.php: -------------------------------------------------------------------------------- 1 | options = array_merge( 45 | array( 46 | 'consumer_key' => '', 47 | 'consumer_secret' => '', 48 | 'access_token' => '', 49 | 'access_token_secret' => '', 50 | 'api_endpoint' => 'statuses/user_timeline', 51 | 'api_params' => array(), 52 | 'enable_cache' => true, 53 | 'cache_dir' => dirname(__FILE__) . '/cache/', // Where on the server to save cached tweets 54 | 'cachetime' => 60 * 60, // Seconds to cache feed (1 hour). 55 | 'tweets_to_retrieve' => 25, // Specifies the number of tweets to try and fetch, up to a maximum of 200 56 | 'tweets_to_display' => 10, // Number of tweets to display 57 | 'twitter_style_dates' => false, // Use twitter style dates e.g. 2 hours ago 58 | 'twitter_date_text' => array('seconds', 'minutes', 'about', 'hour', 'ago'), 59 | 'date_format' => '%I:%M %p %b %e%O', // The defult date format e.g. 12:08 PM Jun 12th. See: http://php.net/manual/en/function.strftime.php 60 | 'date_lang' => null, // Language for date e.g. 'fr_FR'. See: http://php.net/manual/en/function.setlocale.php 61 | 'twitter_template' => '

    Latest tweets

    ', 62 | 'tweet_template' => '
  • {tweet} {date}
  • ', 63 | 'error_template' => '
  • Our twitter feed is unavailable right now. Follow us on Twitter
  • ', 64 | 'nofollow_links' => false, // Add rel="nofollow" attribute to links 65 | 'debug' => false, 66 | 'twitter_screen_name' => '', // Deprecated. Use api_params. 67 | 'ignore_replies' => true, // Deprecated. Use api_params. 68 | 'ignore_retweets' => true // Deprecated. Use api_params. 69 | ), 70 | $options 71 | ); 72 | 73 | if ($this->options['debug']) { 74 | error_reporting(E_ALL); 75 | } 76 | 77 | // Check for Windows to find and replace the %e modifier with %#d, since Windows' %e implementation is broken 78 | if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { 79 | $this->options['date_format'] = str_replace('%e', '%#d', $this->options['date_format']); 80 | } 81 | 82 | if ($this->options['date_lang']) { 83 | setlocale(LC_ALL, $this->options['date_lang']); 84 | } 85 | 86 | if ($this->options['enable_cache']) { 87 | if (!file_exists($this->options['cache_dir'])) { 88 | mkdir($this->options['cache_dir'], 0755, true); 89 | } 90 | $this->cache_file = $this->options['cache_dir'] . 'twitter.txt'; 91 | $this->cache_file_raw = $this->options['cache_dir'] . 'twitter-array.txt'; 92 | $cache_file_timestamp = ((file_exists($this->cache_file))) ? filemtime($this->cache_file) : 0; 93 | $this->add_debug_item('Cache expiration timestamp: ' . (time() - $this->options['cachetime'])); 94 | $this->add_debug_item('Cache file timestamp: ' . $cache_file_timestamp); 95 | 96 | // Show file from cache if still valid. 97 | if (time() - $this->options['cachetime'] < $cache_file_timestamp) { 98 | $this->tweet_found = true; 99 | $this->add_debug_item('Cache file is newer than cachetime.'); 100 | $this->tweet_list = file_get_contents($this->cache_file); 101 | $this->tweet_array = unserialize(file_get_contents($this->cache_file_raw)); 102 | } else { 103 | $this->add_debug_item("Cache file doesn't exist or is older than cachetime."); 104 | $this->fetch_tweets(); 105 | } 106 | } else { 107 | $this->add_debug_item('Caching is disabled.'); 108 | $this->fetch_tweets(); 109 | } 110 | 111 | // In case the feed did not parse or load correctly, show a link to the Twitter account. 112 | if (!$this->tweet_found) { 113 | $this->add_debug_item('No tweets were found. error_message will be displayed.'); 114 | $html = str_replace('{link}', 'http://twitter.com/' . $this->options['twitter_screen_name'], $this->options['error_template']); 115 | $this->tweet_list = str_replace('{tweets}', $html, $this->options['twitter_template']); 116 | $this->tweet_array = array('Error fetching or loading tweets'); 117 | } 118 | } 119 | 120 | /** 121 | * Fetch tweets using Twitter API 122 | */ 123 | private function fetch_tweets () { 124 | $this->add_debug_item('Fetching fresh tweets using Twitter API.'); 125 | 126 | require_once(dirname(__FILE__) . '/lib/tmhOAuth/tmhOAuth.php'); 127 | 128 | // Creates a tmhOAuth object. 129 | $this->tmhOAuth = new tmhOAuth(array( 130 | 'consumer_key' => $this->options['consumer_key'], 131 | 'consumer_secret' => $this->options['consumer_secret'], 132 | 'token' => $this->options['access_token'], 133 | 'secret' => $this->options['access_token_secret'] 134 | )); 135 | 136 | // Set Twitter API parameters 137 | $params = $this->options['api_params']; 138 | $params['count'] = $this->options['tweets_to_retrieve']; 139 | // Legacy param options for backwards compatibility. 140 | if (!empty($this->options['twitter_screen_name'])) { 141 | $params['screen_name'] = $this->options['twitter_screen_name']; 142 | } 143 | $params['include_rts'] = $this->options['ignore_retweets'] ? 'false' : 'true'; 144 | $params['exclude_replies'] = $this->options['ignore_replies'] ? 'true' : 'false'; 145 | 146 | // Request Twitter timeline. 147 | $response_code = $this->tmhOAuth->request('GET', $this->tmhOAuth->url('1.1/' . $this->options['api_endpoint'] . '.json'), $params); 148 | 149 | $this->add_debug_item('tmhOAuth response code: ' . $response_code); 150 | 151 | if ($response_code == 200) { 152 | $response = json_decode($this->tmhOAuth->response['response'], true); 153 | 154 | // Some twitter endpoints (e.g. search/tweets) store tweets in a `statuses` array. 155 | $data = array_key_exists('statuses', $response) ? $response['statuses'] : $response; 156 | 157 | $tweets_html = ''; 158 | 159 | // Iterate over tweets. 160 | foreach($data as $tweet) { 161 | $tweets_html .= $this->parse_tweet($tweet); 162 | // If we have processed enough tweets, stop. 163 | if ($this->tweet_count >= $this->options['tweets_to_display']){ 164 | break; 165 | } 166 | } 167 | 168 | // Close the twitter wrapping element. 169 | $html = str_replace('{tweets}', $tweets_html, $this->options['twitter_template']); 170 | 171 | if ($this->options['enable_cache']) { 172 | // Save the formatted tweet list to a file. 173 | $file = fopen($this->cache_file, 'w'); 174 | fwrite($file, $html); 175 | fclose($file); 176 | 177 | // Save the raw data array to a file. 178 | $file = fopen($this->cache_file_raw, 'w'); 179 | fwrite($file, serialize($data)); 180 | fclose($file); 181 | } 182 | 183 | $this->tweet_list = $html; 184 | $this->tweet_array = $data; 185 | } else { 186 | $this->add_debug_item('Bad tmhOAuth response code.'); 187 | } 188 | } 189 | 190 | /** 191 | * Parse an individual tweet 192 | */ 193 | private function parse_tweet ($tweet) { 194 | $this->tweet_found = true; 195 | $this->tweet_count++; 196 | 197 | // Format tweet text 198 | $tweet_text_raw = $tweet['text']; 199 | $tweet_text = $this->autolink($tweet_text_raw); 200 | 201 | // Tweet date is in GMT. Convert to UNIX timestamp in the local time of the tweeter. 202 | $utc_offset = $tweet['user']['utc_offset']; 203 | $tweet_time = strtotime($tweet['created_at']) + $utc_offset; 204 | 205 | if ($this->options['twitter_style_dates']){ 206 | // Convert tweet timestamp into Twitter style date ("About 2 hours ago") 207 | $current_time = time(); 208 | $time_diff = abs($current_time - $tweet_time); 209 | switch ($time_diff) { 210 | case ($time_diff < 60): 211 | $display_time = $time_diff . ' ' . $this->options['twitter_date_text'][0] . ' ' . $this->options['twitter_date_text'][4]; 212 | break; 213 | case ($time_diff >= 60 && $time_diff < 3600): 214 | $min = floor($time_diff/60); 215 | $display_time = $min . ' ' . $this->options['twitter_date_text'][1] . ' ' . $this->options['twitter_date_text'][4]; 216 | break; 217 | case ($time_diff >= 3600 && $time_diff < 86400): 218 | $hour = floor($time_diff/3600); 219 | $display_time = $this->options['twitter_date_text'][2] . ' ' . $hour . ' ' . $this->options['twitter_date_text'][3]; 220 | if ($hour > 1){ $display_time .= 's'; } 221 | $display_time .= ' ' . $this->options['twitter_date_text'][4]; 222 | break; 223 | default: 224 | $format = str_replace('%O', date('S', $tweet_time), $this->options['date_format']); 225 | $display_time = strftime($format, $tweet_time); 226 | break; 227 | } 228 | } else { 229 | $format = str_replace('%O', date('S', $tweet_time), $this->options['date_format']); 230 | $display_time = strftime($format, $tweet_time); 231 | } 232 | 233 | $href = 'http://twitter.com/' . $tweet['user']['screen_name'] . '/status/' . $tweet['id_str']; 234 | $output = str_replace('{tweet}', $tweet_text, $this->options['tweet_template']); 235 | $output = str_replace('{link}', $href, $output); 236 | $output = str_replace('{date}', $display_time, $output); 237 | 238 | return $output; 239 | } 240 | 241 | /** 242 | * Add a debugging item. 243 | */ 244 | private function add_debug_item ($msg) { 245 | array_push($this->debug_report, $msg); 246 | } 247 | 248 | /** 249 | * Get debugging information as an HTML list. 250 | */ 251 | public function get_debug_list () { 252 | $debug_list = ''; 257 | return $debug_list; 258 | } 259 | 260 | /** 261 | * Get debugging information as an array. 262 | */ 263 | public function get_debug_array () { 264 | return $this->debug_report; 265 | } 266 | 267 | /** 268 | * Helper function to convert usernames, hashtags and URLs 269 | * in a tweet to HTML links. 270 | */ 271 | public function autolink ($tweet) { 272 | require_once(dirname(__FILE__) . '/lib/twitter-text-php/lib/Twitter/Autolink.php'); 273 | 274 | $autolinked_tweet = Twitter_Autolink::create($tweet, false) 275 | ->setNoFollow($this->options['nofollow_links']) 276 | ->setExternal(false) 277 | ->setTarget('') 278 | ->setUsernameClass('') 279 | ->setHashtagClass('') 280 | ->setURLClass('') 281 | ->addLinks(); 282 | 283 | return $autolinked_tweet; 284 | } 285 | 286 | /** 287 | * Get tweets as HTML list 288 | */ 289 | public function get_tweet_list () { 290 | if ($this->options['debug']) { 291 | return $this->get_debug_list() . $this->tweet_list; 292 | } else { 293 | return $this->tweet_list; 294 | } 295 | } 296 | 297 | /** 298 | * Get tweets as an array 299 | */ 300 | public function get_tweet_array () { 301 | return $this->tweet_array; 302 | } 303 | 304 | } 305 | -------------------------------------------------------------------------------- /lib/tmhOAuth/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/tmhOAuth/README.md: -------------------------------------------------------------------------------- 1 | # tmhOAuth 2 | 3 | An OAuth library written in PHP by @themattharris. 4 | 5 | **Disclaimer**: This project is a work in progress. Please use the issue tracker 6 | to report any enhancements or issues you encounter. 7 | 8 | ## Goals 9 | 10 | - Support OAuth 1.0A 11 | - Use Authorisation headers instead of query string or POST parameters 12 | - Allow uploading of images 13 | - Provide enough information to assist with debugging 14 | 15 | ## Dependencies 16 | 17 | The library has been tested with PHP 5.3+ and relies on CURL and hash_hmac. The 18 | vast majority of hosting providers include these libraries and run with PHP 5.1+. 19 | 20 | The code makes use of hash_hmac, which was introduced in PHP 5.1.2. If your version 21 | of PHP is lower than this you should ask your hosting provider for an update. 22 | 23 | ## A note about security and SSL 24 | 25 | Version 0.60 hardened the security of the library and defaulted `curl_ssl_verifypeer` to `true`. 26 | As some hosting providers do not provide the most current certificate root file 27 | it is now included in this repository. If the version is out of date OR you prefer 28 | to download the certificate roots yourself, you can get them 29 | from: http://curl.haxx.se/ca/cacert.pem 30 | 31 | If you are getting http code 0 responses inspect `$tmhOAuth->response['error']` to see what the 32 | problem is. usually code 0 means cacert.pem cannot be found, and it can be fixed by putting cacert.pem 33 | in the location tmhOAuth is looking for it (indicated in the `$tmhOAuth->response['error']` message), or 34 | by setting `$tmhOAuth->config['curl_cainfo']` and `$tmhOAuth->config['curl_capath']` values. setting 35 | `$tmhOAuth->config['use_ssl']` to false *IS NOT* the way to solve this problem. 36 | 37 | ## Usage 38 | 39 | This will be built out later but for the moment review the examples repository 40 | for ways the library can be 41 | used. Each example contains instructions on how to use it. 42 | 43 | For guidance on how to use [composer](http://getcomposer.org) to install tmhOAuth see the 44 | [tmhOAuthExamples](https://github.com/themattharris/tmhOAuthExamples) project. 45 | 46 | ## Notes for users of previous versions 47 | 48 | As of version 0.8.0 tmhUtilities is no longer included. If you found them useful open an issue against me 49 | and i'll create a new repository for them. version 0.8.0 also ignores `$tmhOAuth->config['v']`. if you used 50 | this before you should instead specify the API version in the path you pass to `$tmhOAuth->url` 51 | 52 | Versions prior to 0.7.3 collapsed headers with the same value into one 53 | `$tmhOAuth->response['headers']` key. Since 0.7.3 headers with the same key will use an array 54 | to store their values. 55 | 56 | If you previously used version 0.4 be aware the utility functions 57 | have now been broken into their own file. Before you use version 0.5+ in your app 58 | test locally to ensure your code doesn't need tmhUtilities included. 59 | 60 | If you used custom HTTP request headers when they were defined as `'key: value'` strings 61 | you should now define them as `'key' => 'value'` pairs. 62 | 63 | ## Change History 64 | 65 | This is now published on the tmhOAuth wiki 66 | 67 | ## Community 68 | 69 | License: Apache 2 (see [included LICENSE file](https://github.com/themattharris/tmhOAuth/blob/master/LICENSE)) 70 | 71 | Follow [@tmhOAuth](https://twitter.com/intent/follow?screen_name=tmhOAuth) to receive updates on releases, or ask for support 72 | Follow me on Twitter: [@themattharris](https://twitter.com/intent/follow?screen_name=themattharris) 73 | Check out the Twitter Developer Resources: -------------------------------------------------------------------------------- /lib/tmhOAuth/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "themattharris/tmhoauth", 3 | "description": "An OAuth library written in PHP by @themattharris", 4 | "license": "Apache-2.0", 5 | "authors": [ 6 | { 7 | "name": "themattharris", 8 | "email": "matt@themattharris.com", 9 | "role": "Developer" 10 | } 11 | ], 12 | "keywords": [ 13 | "twitter", 14 | "oauth" 15 | ], 16 | "support": { 17 | "issues": "https://github.com/themattharris/tmhOAuth/issues" 18 | }, 19 | "require": { 20 | "php": ">=5.3.0", 21 | "ext-curl": "*" 22 | }, 23 | "autoload": { 24 | "psr-0": { "tmhOAuth": "" } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/tmhOAuth/tmhOAuth.php: -------------------------------------------------------------------------------- 1 | buffer = null; 27 | $this->reconfigure($config); 28 | $this->reset_request_settings(); 29 | $this->set_user_agent(); 30 | } 31 | 32 | public function reconfigure($config=array()) { 33 | // default configuration options 34 | $this->config = array_merge( 35 | array( 36 | // leave 'user_agent' blank for default, otherwise set this to 37 | // something that clearly identifies your app 38 | 'user_agent' => '', 39 | 'host' => 'api.twitter.com', 40 | 41 | 'consumer_key' => '', 42 | 'consumer_secret' => '', 43 | 'token' => '', 44 | 'secret' => '', 45 | 46 | // OAuth2 bearer token. This should already be URL encoded 47 | 'bearer' => '', 48 | 49 | // oauth signing variables that are not dynamic 50 | 'oauth_version' => '1.0', 51 | 'oauth_signature_method' => 'HMAC-SHA1', 52 | 53 | // you probably don't want to change any of these curl values 54 | 'curl_connecttimeout' => 30, 55 | 'curl_timeout' => 10, 56 | 57 | // for security this should always be set to 2. 58 | 'curl_ssl_verifyhost' => 2, 59 | // for security this should always be set to true. 60 | 'curl_ssl_verifypeer' => true, 61 | // for security this should always be set to true. 62 | 'use_ssl' => true, 63 | 64 | // you can get the latest cacert.pem from here http://curl.haxx.se/ca/cacert.pem 65 | // if you're getting HTTP 0 responses, check cacert.pem exists and is readable 66 | // without it curl won't be able to create an SSL connection 67 | 'curl_cainfo' => __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem', 68 | 'curl_capath' => __DIR__, 69 | 70 | 'curl_followlocation' => false, // whether to follow redirects or not 71 | 72 | // support for proxy servers 73 | 'curl_proxy' => false, // really you don't want to use this if you are using streaming 74 | 'curl_proxyuserpwd' => false, // format username:password for proxy, if required 75 | 'curl_encoding' => '', // leave blank for all supported formats, else use gzip, deflate, identity etc 76 | 77 | // streaming API configuration 78 | 'is_streaming' => false, 79 | 'streaming_eol' => "\r\n", 80 | 'streaming_metrics_interval' => 10, 81 | 82 | // header or querystring. You should always use header! 83 | // this is just to help me debug other developers implementations 84 | 'as_header' => true, 85 | 'force_nonce' => false, // used for checking signatures. leave as false for auto 86 | 'force_timestamp' => false, // used for checking signatures. leave as false for auto 87 | ), 88 | $config 89 | ); 90 | } 91 | 92 | private function reset_request_settings($options=array()) { 93 | $this->request_settings = array( 94 | 'params' => array(), 95 | 'headers' => array(), 96 | 'with_user' => true, 97 | 'multipart' => false, 98 | ); 99 | 100 | if (!empty($options)) 101 | $this->request_settings = array_merge($this->request_settings, $options); 102 | } 103 | 104 | /** 105 | * Sets the useragent for PHP to use 106 | * If '$this->config['user_agent']' already has a value it is used instead of one 107 | * being generated. 108 | * 109 | * @return void value is stored to the config array class variable 110 | */ 111 | private function set_user_agent() { 112 | if (!empty($this->config['user_agent'])) 113 | return; 114 | 115 | $ssl = ($this->config['curl_ssl_verifyhost'] && $this->config['curl_ssl_verifypeer'] && $this->config['use_ssl']) ? '+' : '-'; 116 | $ua = 'tmhOAuth ' . self::VERSION . $ssl . 'SSL - //github.com/themattharris/tmhOAuth'; 117 | $this->config['user_agent'] = $ua; 118 | } 119 | 120 | /** 121 | * Generates a random OAuth nonce. 122 | * If 'force_nonce' is false a nonce will be generated, otherwise the value of '$this->config['force_nonce']' will be used. 123 | * 124 | * @param string $length how many characters the nonce should be before MD5 hashing. default 12 125 | * @param string $include_time whether to include time at the beginning of the nonce. default true 126 | * @return $nonce as a string 127 | */ 128 | private function nonce($length=12, $include_time=true) { 129 | if ($this->config['force_nonce'] === false) { 130 | $prefix = $include_time ? microtime() : ''; 131 | return md5(substr($prefix . uniqid(), 0, $length)); 132 | } else { 133 | return $this->config['force_nonce']; 134 | } 135 | } 136 | 137 | /** 138 | * Generates a timestamp. 139 | * If 'force_timestamp' is false a timestamp will be generated, otherwise the value of '$this->config['force_timestamp']' will be used. 140 | * 141 | * @return $time as a string 142 | */ 143 | private function timestamp() { 144 | if ($this->config['force_timestamp'] === false) { 145 | $time = time(); 146 | } else { 147 | $time = $this->config['force_timestamp']; 148 | } 149 | return (string) $time; 150 | } 151 | 152 | /** 153 | * Encodes the string or array passed in a way compatible with OAuth. 154 | * If an array is passed each array value will will be encoded. 155 | * 156 | * @param mixed $data the scalar or array to encode 157 | * @return $data encoded in a way compatible with OAuth 158 | */ 159 | private function safe_encode($data) { 160 | if (is_array($data)) { 161 | return array_map(array($this, 'safe_encode'), $data); 162 | } else if (is_scalar($data)) { 163 | return str_ireplace( 164 | array('+', '%7E'), 165 | array(' ', '~'), 166 | rawurlencode($data) 167 | ); 168 | } else { 169 | return ''; 170 | } 171 | } 172 | 173 | /** 174 | * Decodes the string or array from it's URL encoded form 175 | * If an array is passed each array value will will be decoded. 176 | * 177 | * @param mixed $data the scalar or array to decode 178 | * @return string $data decoded from the URL encoded form 179 | */ 180 | private function safe_decode($data) { 181 | if (is_array($data)) { 182 | return array_map(array($this, 'safe_decode'), $data); 183 | } else if (is_scalar($data)) { 184 | return rawurldecode($data); 185 | } else { 186 | return ''; 187 | } 188 | } 189 | 190 | /** 191 | * Prepares OAuth1 signing parameters. 192 | * 193 | * @return void all required OAuth parameters, safely encoded, are stored to the class variable '$this->request_settings['oauth1_params']' 194 | */ 195 | private function prepare_oauth1_params() { 196 | $defaults = array( 197 | 'oauth_nonce' => $this->nonce(), 198 | 'oauth_timestamp' => $this->timestamp(), 199 | 'oauth_version' => $this->config['oauth_version'], 200 | 'oauth_consumer_key' => $this->config['consumer_key'], 201 | 'oauth_signature_method' => $this->config['oauth_signature_method'], 202 | ); 203 | 204 | // include the user token if it exists 205 | if ( $oauth_token = $this->token() ) 206 | $defaults['oauth_token'] = $oauth_token; 207 | 208 | $this->request_settings['oauth1_params'] = array(); 209 | 210 | // safely encode 211 | foreach ($defaults as $k => $v) { 212 | $this->request_settings['oauth1_params'][$this->safe_encode($k)] = $this->safe_encode($v); 213 | } 214 | } 215 | 216 | private function token() { 217 | if ( $this->request_settings['with_user'] ) { 218 | if (isset($this->config['token']) && !empty($this->config['token'])) return $this->config['token']; 219 | elseif (isset($this->config['user_token'])) return $this->config['user_token']; 220 | } 221 | return ''; 222 | } 223 | 224 | private function secret() { 225 | if ( $this->request_settings['with_user'] ) { 226 | if (isset($this->config['secret']) && !empty($this->config['secret'])) return $this->config['secret']; 227 | elseif (isset($this->config['user_secret'])) return $this->config['user_secret']; 228 | } 229 | return ''; 230 | } 231 | 232 | /** 233 | * Extracts and decodes OAuth parameters from the passed string 234 | * 235 | * @param string $body the response body from an OAuth flow method 236 | * @return array the response body safely decoded to an array of key => values 237 | */ 238 | public function extract_params($body) { 239 | $kvs = explode('&', $body); 240 | $decoded = array(); 241 | foreach ($kvs as $kv) { 242 | $kv = explode('=', $kv, 2); 243 | $kv[0] = $this->safe_decode($kv[0]); 244 | $kv[1] = $this->safe_decode($kv[1]); 245 | $decoded[$kv[0]] = $kv[1]; 246 | } 247 | return $decoded; 248 | } 249 | 250 | /** 251 | * Prepares the HTTP method for use in the base string by converting it to 252 | * uppercase. 253 | * 254 | * @return void value is stored to the class variable '$this->request_settings['method']' 255 | */ 256 | private function prepare_method() { 257 | $this->request_settings['method'] = strtoupper($this->request_settings['method']); 258 | } 259 | 260 | /** 261 | * Prepares the URL for use in the base string by ripping it apart and 262 | * reconstructing it. 263 | * 264 | * Ref: 3.4.1.2 265 | * 266 | * @return void value is stored to the class array variable '$this->request_settings['url']' 267 | */ 268 | private function prepare_url() { 269 | $parts = parse_url($this->request_settings['url']); 270 | 271 | $port = isset($parts['port']) ? $parts['port'] : false; 272 | $scheme = $parts['scheme']; 273 | $host = $parts['host']; 274 | $path = isset($parts['path']) ? $parts['path'] : false; 275 | 276 | $port or $port = ($scheme == 'https') ? '443' : '80'; 277 | 278 | if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { 279 | $host = "$host:$port"; 280 | } 281 | 282 | // the scheme and host MUST be lowercase 283 | $this->request_settings['url'] = strtolower("$scheme://$host"); 284 | // but not the path 285 | $this->request_settings['url'] .= $path; 286 | } 287 | 288 | /** 289 | * Prepares all parameters for the base string and request. 290 | * Multipart parameters are ignored as they are not defined in the specification, 291 | * all other types of parameter are encoded for compatibility with OAuth. 292 | * 293 | * @param array $params the parameters for the request 294 | * @return void prepared values are stored in the class array variable '$this->request_settings' 295 | */ 296 | private function prepare_params() { 297 | $doing_oauth1 = false; 298 | $this->request_settings['prepared_params'] = array(); 299 | $prepared = &$this->request_settings['prepared_params']; 300 | $prepared_pairs = array(); 301 | $prepared_pairs_with_oauth = array(); 302 | 303 | if (isset($this->request_settings['oauth1_params'])) { 304 | $oauth1 = &$this->request_settings['oauth1_params']; 305 | $doing_oauth1 = true; 306 | $params = array_merge($oauth1, $this->request_settings['params']); 307 | 308 | // Remove oauth_signature if present 309 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 310 | unset($params['oauth_signature']); 311 | 312 | // empty the oauth1 array. we reset these values later in this method 313 | $oauth1 = array(); 314 | } else { 315 | $params = $this->request_settings['params']; 316 | } 317 | 318 | // Parameters are sorted by name, using lexicographical byte value ordering. 319 | // Ref: Spec: 9.1.1 (1) 320 | uksort($params, 'strcmp'); 321 | 322 | // encode params unless we're doing multipart 323 | foreach ($params as $k => $v) { 324 | $k = $this->request_settings['multipart'] ? $k : $this->safe_encode($k); 325 | 326 | if (is_array($v)) 327 | $v = implode(',', $v); 328 | 329 | $v = $this->request_settings['multipart'] ? $v : $this->safe_encode($v); 330 | 331 | // split parameters for the basestring and authorization header, and recreate the oauth1 array 332 | if ($doing_oauth1) { 333 | // if we're doing multipart, only store the oauth_* params, ignore the users request params 334 | if ((strpos($k, 'oauth') === 0) || !$this->request_settings['multipart']) 335 | $prepared_pairs_with_oauth[] = "{$k}={$v}"; 336 | 337 | if (strpos($k, 'oauth') === 0) { 338 | $oauth1[$k] = $v; 339 | continue; 340 | } 341 | } 342 | $prepared[$k] = $v; 343 | $prepared_pairs[] = "{$k}={$v}"; 344 | } 345 | 346 | if ($doing_oauth1) { 347 | $this->request_settings['basestring_params'] = implode('&', $prepared_pairs_with_oauth); 348 | } 349 | 350 | // setup params for GET/POST method handling 351 | if (!empty($prepared_pairs)) { 352 | $content = implode('&', $prepared_pairs); 353 | 354 | switch ($this->request_settings['method']) { 355 | case 'POST': 356 | $this->request_settings['postfields'] = $this->request_settings['multipart'] ? $prepared : $content; 357 | break; 358 | default: 359 | $this->request_settings['querystring'] = $content; 360 | break; 361 | } 362 | } 363 | } 364 | 365 | /** 366 | * Prepares the OAuth signing key 367 | * 368 | * @return void prepared signing key is stored in the class variable 'signing_key' 369 | */ 370 | private function prepare_signing_key() { 371 | $left = $this->safe_encode($this->config['consumer_secret']); 372 | $right = $this->safe_encode($this->secret()); 373 | $this->request_settings['signing_key'] = $left . '&' . $right; 374 | } 375 | 376 | /** 377 | * Prepare the base string. 378 | * Ref: Spec: 9.1.3 ("Concatenate Request Elements") 379 | * 380 | * @return void prepared base string is stored in the class variable 'base_string' 381 | */ 382 | private function prepare_base_string() { 383 | $url = $this->request_settings['url']; 384 | 385 | # if the host header is set we need to rewrite the basestring to use 386 | # that, instead of the request host. otherwise the signature won't match 387 | # on the server side 388 | if (!empty($this->request_settings['headers']['Host'])) { 389 | $url = str_ireplace( 390 | $this->config['host'], 391 | $this->request_settings['headers']['Host'], 392 | $url 393 | ); 394 | } 395 | 396 | $base = array( 397 | $this->request_settings['method'], 398 | $url, 399 | $this->request_settings['basestring_params'] 400 | ); 401 | $this->request_settings['basestring'] = implode('&', $this->safe_encode($base)); 402 | } 403 | 404 | /** 405 | * Signs the OAuth 1 request 406 | * 407 | * @return void oauth_signature is added to the parameters in the class array variable '$this->request_settings' 408 | */ 409 | private function prepare_oauth_signature() { 410 | $this->request_settings['oauth1_params']['oauth_signature'] = $this->safe_encode( 411 | base64_encode( 412 | hash_hmac( 413 | 'sha1', $this->request_settings['basestring'], $this->request_settings['signing_key'], true 414 | ))); 415 | } 416 | 417 | /** 418 | * Prepares the Authorization header 419 | * 420 | * @return void prepared authorization header is stored in the class variable headers['Authorization'] 421 | */ 422 | private function prepare_auth_header() { 423 | if (!$this->config['as_header']) 424 | return; 425 | 426 | // oauth1 427 | if (isset($this->request_settings['oauth1_params'])) { 428 | // sort again as oauth_signature was added post param preparation 429 | uksort($this->request_settings['oauth1_params'], 'strcmp'); 430 | $encoded_quoted_pairs = array(); 431 | foreach ($this->request_settings['oauth1_params'] as $k => $v) { 432 | $encoded_quoted_pairs[] = "{$k}=\"{$v}\""; 433 | } 434 | $header = 'OAuth ' . implode(', ', $encoded_quoted_pairs); 435 | } elseif (!empty($this->config['bearer'])) { 436 | $header = 'Bearer ' . $this->config['bearer']; 437 | } 438 | 439 | if (isset($header)) 440 | $this->request_settings['headers']['Authorization'] = $header; 441 | } 442 | 443 | /** 444 | * Create the bearer token for OAuth2 requests from the consumer_key and consumer_secret. 445 | * 446 | * @return string the bearer token 447 | */ 448 | public function bearer_token_credentials() { 449 | $credentials = implode(':', array( 450 | $this->safe_encode($this->config['consumer_key']), 451 | $this->safe_encode($this->config['consumer_secret']) 452 | )); 453 | return base64_encode($credentials); 454 | } 455 | 456 | /** 457 | * Make an HTTP request using this library. This method doesn't return anything. 458 | * Instead the response should be inspected directly. 459 | * 460 | * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc 461 | * @param string $url the request URL without query string parameters 462 | * @param array $params the request parameters as an array of key=value pairs. Default empty array 463 | * @param string $useauth whether to use authentication when making the request. Default true 464 | * @param string $multipart whether this request contains multipart data. Default false 465 | * @param array $headers any custom headers to send with the request. Default empty array 466 | * @return int the http response code for the request. 0 is returned if a connection could not be made 467 | */ 468 | public function request($method, $url, $params=array(), $useauth=true, $multipart=false, $headers=array()) { 469 | $options = array( 470 | 'method' => $method, 471 | 'url' => $url, 472 | 'params' => $params, 473 | 'with_user' => true, 474 | 'multipart' => $multipart, 475 | 'headers' => $headers 476 | ); 477 | $options = array_merge($this->default_options(), $options); 478 | 479 | if ($useauth) { 480 | return $this->user_request($options); 481 | } else { 482 | return $this->unauthenticated_request($options); 483 | } 484 | } 485 | 486 | public function apponly_request($options=array()) { 487 | $options = array_merge($this->default_options(), $options, array( 488 | 'with_user' => false, 489 | )); 490 | $this->reset_request_settings($options); 491 | if ($options['without_bearer']) { 492 | return $this->oauth1_request(); 493 | } else { 494 | $this->prepare_method(); 495 | $this->prepare_url(); 496 | $this->prepare_params(); 497 | $this->prepare_auth_header(); 498 | return $this->curlit(); 499 | } 500 | } 501 | 502 | public function user_request($options=array()) { 503 | $options = array_merge($this->default_options(), $options, array( 504 | 'with_user' => true, 505 | )); 506 | $this->reset_request_settings($options); 507 | return $this->oauth1_request(); 508 | } 509 | 510 | public function unauthenticated_request($options=array()) { 511 | $options = array_merge($this->default_options(), $options, array( 512 | 'with_user' => false, 513 | )); 514 | $this->reset_request_settings($options); 515 | $this->prepare_method(); 516 | $this->prepare_url(); 517 | $this->prepare_params(); 518 | return $this->curlit(); 519 | } 520 | 521 | /** 522 | * Signs the request and adds the OAuth signature. This runs all the request 523 | * parameter preparation methods. 524 | * 525 | * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc 526 | * @param string $url the request URL without query string parameters 527 | * @param array $params the request parameters as an array of key=value pairs 528 | * @param boolean $with_user whether to include the user credentials when making the request. 529 | * @return void 530 | */ 531 | private function oauth1_request() { 532 | $this->prepare_oauth1_params(); 533 | $this->prepare_method(); 534 | $this->prepare_url(); 535 | $this->prepare_params(); 536 | $this->prepare_base_string(); 537 | $this->prepare_signing_key(); 538 | $this->prepare_oauth_signature(); 539 | $this->prepare_auth_header(); 540 | return $this->curlit(); 541 | } 542 | 543 | private function default_options() { 544 | return array( 545 | 'method' => 'GET', 546 | 'params' => array(), 547 | 'with_user' => true, 548 | 'multipart' => false, 549 | 'headers' => array(), 550 | 'without_bearer' => false, 551 | ); 552 | } 553 | 554 | /** 555 | * Make a long poll HTTP request using this library. This method is 556 | * different to the other request methods as it isn't supposed to disconnect 557 | * 558 | * Using this method expects a callback which will receive the streaming 559 | * responses. 560 | * 561 | * @param string $method the HTTP method being used. e.g. POST, GET, HEAD etc 562 | * @param string $url the request URL without query string parameters 563 | * @param array $params the request parameters as an array of key=value pairs 564 | * @param string $callback the callback function to stream the buffer to. 565 | * @return void 566 | */ 567 | public function streaming_request($method, $url, $params=array(), $callback='') { 568 | if ( ! empty($callback) ) { 569 | if ( ! is_callable($callback) ) { 570 | return false; 571 | } 572 | $this->config['streaming_callback'] = $callback; 573 | } 574 | $this->metrics['start'] = time(); 575 | $this->metrics['interval_start'] = $this->metrics['start']; 576 | $this->metrics['messages'] = 0; 577 | $this->metrics['last_messages'] = 0; 578 | $this->metrics['bytes'] = 0; 579 | $this->metrics['last_bytes'] = 0; 580 | $this->config['is_streaming'] = true; 581 | $this->request($method, $url, $params); 582 | } 583 | 584 | /** 585 | * Handles the updating of the current Streaming API metrics. 586 | * 587 | * @return array the metrics for the streaming api connection 588 | */ 589 | private function update_metrics() { 590 | $now = time(); 591 | if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now) 592 | return null; 593 | 594 | $this->metrics['mps'] = round( ($this->metrics['messages'] - $this->metrics['last_messages']) / $this->config['streaming_metrics_interval'], 2); 595 | $this->metrics['bps'] = round( ($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2); 596 | 597 | $this->metrics['last_bytes'] = $this->metrics['bytes']; 598 | $this->metrics['last_messages'] = $this->metrics['messages']; 599 | $this->metrics['interval_start'] = $now; 600 | return $this->metrics; 601 | } 602 | 603 | /** 604 | * Utility function to create the request URL in the requested format. 605 | * If a fully-qualified URI is provided, it will be returned. 606 | * Any multi-slashes (except for the protocol) will be replaced with a single slash. 607 | * 608 | * 609 | * @param string $request the API method without extension 610 | * @param string $extension the format of the response. Default json. Set to an empty string to exclude the format 611 | * @return string the concatenation of the host, API version, API method and format, or $request if it begins with http 612 | */ 613 | public function url($request, $extension='json') { 614 | // remove multi-slashes 615 | $request = preg_replace('$([^:])//+$', '$1/', $request); 616 | 617 | if (stripos($request, 'http') === 0 || stripos($request, '//') === 0) { 618 | return $request; 619 | } 620 | 621 | $extension = strlen($extension) > 0 ? ".$extension" : ''; 622 | $proto = $this->config['use_ssl'] ? 'https:/' : 'http:/'; 623 | 624 | // trim trailing slash 625 | $request = ltrim($request, '/'); 626 | 627 | $pos = strlen($request) - strlen($extension); 628 | if (substr($request, $pos) === $extension) 629 | $request = substr_replace($request, '', $pos); 630 | 631 | return implode('/', array( 632 | $proto, 633 | $this->config['host'], 634 | $request . $extension 635 | )); 636 | } 637 | 638 | /** 639 | * Public access to the private safe decode/encode methods 640 | * 641 | * @param string $text the text to transform 642 | * @param string $mode the transformation mode. either encode or decode 643 | * @return string $text transformed by the given $mode 644 | */ 645 | public function transformText($text, $mode='encode') { 646 | return $this->{"safe_$mode"}($text); 647 | } 648 | 649 | /** 650 | * Utility function to parse the returned curl headers and store them in the 651 | * class array variable. 652 | * 653 | * @param object $ch curl handle 654 | * @param string $header the response headers 655 | * @return string the length of the header 656 | */ 657 | private function curlHeader($ch, $header) { 658 | $this->response['raw'] .= $header; 659 | 660 | list($key, $value) = array_pad(explode(':', $header, 2), 2, null); 661 | 662 | $key = trim($key); 663 | $value = trim($value); 664 | 665 | if ( ! isset($this->response['headers'][$key])) { 666 | $this->response['headers'][$key] = $value; 667 | } else { 668 | if (!is_array($this->response['headers'][$key])) { 669 | $this->response['headers'][$key] = array($this->response['headers'][$key]); 670 | } 671 | $this->response['headers'][$key][] = $value; 672 | } 673 | 674 | return strlen($header); 675 | } 676 | 677 | /** 678 | * Utility function to parse the returned curl buffer and store them until 679 | * an EOL is found. The buffer for curl is an undefined size so we need 680 | * to collect the content until an EOL is found. 681 | * 682 | * This function calls the previously defined streaming callback method. 683 | * 684 | * @param object $ch curl handle 685 | * @param string $data the current curl buffer 686 | * @return int the length of the data string processed in this function 687 | */ 688 | private function curlWrite($ch, $data) { 689 | $l = strlen($data); 690 | if (strpos($data, $this->config['streaming_eol']) === false) { 691 | $this->buffer .= $data; 692 | return $l; 693 | } 694 | 695 | $buffered = explode($this->config['streaming_eol'], $data); 696 | $content = $this->buffer . $buffered[0]; 697 | 698 | $this->metrics['messages']++; 699 | $this->metrics['bytes'] += strlen($content); 700 | 701 | if ( ! is_callable($this->config['streaming_callback'])) 702 | return 0; 703 | 704 | $metrics = $this->update_metrics(); 705 | $stop = call_user_func( 706 | $this->config['streaming_callback'], 707 | $content, 708 | strlen($content), 709 | $metrics 710 | ); 711 | $this->buffer = $buffered[1]; 712 | if ($stop) 713 | return 0; 714 | 715 | return $l; 716 | } 717 | 718 | /** 719 | * Makes a curl request. Takes no parameters as all should have been prepared 720 | * by the request method 721 | * 722 | * the response data is stored in the class variable 'response' 723 | * 724 | * @return int the http response code for the request. 0 is returned if a connection could not be made 725 | */ 726 | private function curlit() { 727 | $this->response = array( 728 | 'raw' => '' 729 | ); 730 | 731 | // configure curl 732 | $c = curl_init(); 733 | switch ($this->request_settings['method']) { 734 | case 'GET': 735 | if (isset($this->request_settings['querystring'])) 736 | $this->request_settings['url'] = $this->request_settings['url'] . '?' . $this->request_settings['querystring']; 737 | break; 738 | case 'POST': 739 | curl_setopt($c, CURLOPT_POST, true); 740 | if (isset($this->request_settings['postfields'])) 741 | $postfields = $this->request_settings['postfields']; 742 | else 743 | $postfields = array(); 744 | 745 | curl_setopt($c, CURLOPT_POSTFIELDS, $postfields); 746 | break; 747 | default: 748 | if (isset($this->request_settings['postfields'])) 749 | curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->request_settings['postfields']); 750 | } 751 | 752 | curl_setopt_array($c, array( 753 | CURLOPT_USERAGENT => $this->config['user_agent'], 754 | CURLOPT_CONNECTTIMEOUT => $this->config['curl_connecttimeout'], 755 | CURLOPT_TIMEOUT => $this->config['curl_timeout'], 756 | CURLOPT_RETURNTRANSFER => true, 757 | CURLOPT_SSL_VERIFYPEER => $this->config['curl_ssl_verifypeer'], 758 | CURLOPT_SSL_VERIFYHOST => $this->config['curl_ssl_verifyhost'], 759 | 760 | CURLOPT_FOLLOWLOCATION => $this->config['curl_followlocation'], 761 | CURLOPT_PROXY => $this->config['curl_proxy'], 762 | CURLOPT_ENCODING => $this->config['curl_encoding'], 763 | CURLOPT_URL => $this->request_settings['url'], 764 | // process the headers 765 | CURLOPT_HEADERFUNCTION => array($this, 'curlHeader'), 766 | CURLOPT_HEADER => false, 767 | CURLINFO_HEADER_OUT => true, 768 | )); 769 | 770 | if ($this->config['curl_cainfo'] !== false) 771 | curl_setopt($c, CURLOPT_CAINFO, $this->config['curl_cainfo']); 772 | 773 | if ($this->config['curl_capath'] !== false) 774 | curl_setopt($c, CURLOPT_CAPATH, $this->config['curl_capath']); 775 | 776 | if ($this->config['curl_proxyuserpwd'] !== false) 777 | curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']); 778 | 779 | if ($this->config['is_streaming']) { 780 | // process the body 781 | $this->response['content-length'] = 0; 782 | curl_setopt($c, CURLOPT_TIMEOUT, 0); 783 | curl_setopt($c, CURLOPT_WRITEFUNCTION, array($this, 'curlWrite')); 784 | } 785 | 786 | if ( ! empty($this->request_settings['headers'])) { 787 | foreach ($this->request_settings['headers'] as $k => $v) { 788 | $headers[] = trim($k . ': ' . $v); 789 | } 790 | curl_setopt($c, CURLOPT_HTTPHEADER, $headers); 791 | } 792 | 793 | if (isset($this->config['block']) && (true === $this->config['block'])) 794 | return 0; 795 | 796 | // do it! 797 | $response = curl_exec($c); 798 | $code = curl_getinfo($c, CURLINFO_HTTP_CODE); 799 | $info = curl_getinfo($c); 800 | $error = curl_error($c); 801 | $errno = curl_errno($c); 802 | curl_close($c); 803 | 804 | // store the response 805 | $this->response['code'] = $code; 806 | $this->response['response'] = $response; 807 | $this->response['info'] = $info; 808 | $this->response['error'] = $error; 809 | $this->response['errno'] = $errno; 810 | 811 | if (!isset($this->response['raw'])) { 812 | $this->response['raw'] = ''; 813 | } 814 | $this->response['raw'] .= $response; 815 | 816 | return $code; 817 | } 818 | } -------------------------------------------------------------------------------- /lib/twitter-text-php/.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/data/twitter-text-conformance"] 2 | path = tests/data/twitter-text-conformance 3 | url = git://github.com/mzsanford/twitter-text-conformance.git 4 | -------------------------------------------------------------------------------- /lib/twitter-text-php/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Mike Cochrane 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | use this file except in compliance with the License. You may obtain a copy of 5 | the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | License for the specific language governing permissions and limitations under 13 | the License. 14 | -------------------------------------------------------------------------------- /lib/twitter-text-php/README: -------------------------------------------------------------------------------- 1 | # Twitter Text (PHP Edition) # 2 | 3 | A library of PHP classes that provide auto-linking and extraction of usernames, 4 | lists, hashtags and URLs from tweets. Originally created from twitter-text-rb 5 | and twitter-text-java projects by Matt Sanford and ported to PHP by Mike 6 | Cochrane, this library has been improved and made more complete by Nick Pope. 7 | 8 | ## Features ## 9 | 10 | ### Autolink ## 11 | 12 | - Add links to all matching Twitter usernames (no account verification). 13 | - Add links to all user lists (of the form @username/list-name). 14 | - Add links to all valid hashtags. 15 | - Add links to all URLs. 16 | - Support for international character sets. 17 | 18 | ### Extractor ### 19 | 20 | - Extract mentioned Twitter usernames (from anywhere in the tweet). 21 | - Extract replied to Twitter usernames (from start of the tweet). 22 | - Extract all user lists (of the form @username/list-name). 23 | - Extract all valid hashtags. 24 | - Extract all URLs. 25 | - Support for international character sets. 26 | 27 | ### Hit Highlighter ### 28 | 29 | - Highlight text specifed by a range by surrounding with a tag. 30 | - Support for highlighting when tweet has already been autolinked. 31 | - Support for international character sets. 32 | 33 | ## Examples ## 34 | 35 | For examples, please see `tests/example.php` which you can view in a browser or 36 | run from the command line. 37 | 38 | ## Conformance ## 39 | 40 | You'll need the test data which is in YAML format from the following 41 | repository: 42 | 43 | http://github.com/mzsanford/twitter-text-conformance 44 | 45 | It has already been added as a git submodule so you should just need to run: 46 | 47 | git submodule init 48 | git submodule update 49 | 50 | As PHP has no native support for YAML you'll need to checkout spyc from svn 51 | into `tests/spyc`: 52 | 53 | svn checkout http://spyc.googlecode.com/svn/trunk/ tests/spyc 54 | 55 | There are a couple of options for testing conformance: 56 | 57 | 1. Run `phpunit` in from the root folder of the project. 58 | 2. Run `tests/runtests.php` from the command line. 59 | 3. Make `tests/runtests.php` accessible on a web server and view it in your 60 | browser. 61 | 62 | ## Thanks & Contributions ## 63 | 64 | The bulk of this library is from the heroic efforts of: 65 | 66 | - Matt Sanford (https://github.com/mzsanford): For the orignal Ruby and Java implementions. 67 | - Mike Cochrane (https://github.com/mikenz): For the initial PHP code. 68 | - Nick Pope (https://github.com/ngnpope): For the bulk of the maintenance work to date. 69 | -------------------------------------------------------------------------------- /lib/twitter-text-php/README.markdown: -------------------------------------------------------------------------------- 1 | # Twitter Text (PHP Edition) # 2 | 3 | A library of PHP classes that provide auto-linking and extraction of usernames, 4 | lists, hashtags and URLs from tweets. Originally created from twitter-text-rb 5 | and twitter-text-java projects by Matt Sanford and ported to PHP by Mike 6 | Cochrane, this library has been improved and made more complete by Nick Pope. 7 | 8 | ## Features ## 9 | 10 | ### Autolink ## 11 | 12 | - Add links to all matching Twitter usernames (no account verification). 13 | - Add links to all user lists (of the form @username/list-name). 14 | - Add links to all valid hashtags. 15 | - Add links to all URLs. 16 | - Support for international character sets. 17 | 18 | ### Extractor ### 19 | 20 | - Extract mentioned Twitter usernames (from anywhere in the tweet). 21 | - Extract replied to Twitter usernames (from start of the tweet). 22 | - Extract all user lists (of the form @username/list-name). 23 | - Extract all valid hashtags. 24 | - Extract all URLs. 25 | - Support for international character sets. 26 | 27 | ### Hit Highlighter ### 28 | 29 | - Highlight text specifed by a range by surrounding with a tag. 30 | - Support for highlighting when tweet has already been autolinked. 31 | - Support for international character sets. 32 | 33 | ## Examples ## 34 | 35 | For examples, please see `tests/example.php` which you can view in a browser or 36 | run from the command line. 37 | 38 | ## Conformance ## 39 | 40 | You'll need the test data which is in YAML format from the following 41 | repository: 42 | 43 | http://github.com/mzsanford/twitter-text-conformance 44 | 45 | It has already been added as a git submodule so you should just need to run: 46 | 47 | git submodule init 48 | git submodule update 49 | 50 | As PHP has no native support for YAML you'll need to checkout spyc from svn 51 | into `tests/spyc`: 52 | 53 | svn checkout http://spyc.googlecode.com/svn/trunk/ tests/spyc 54 | 55 | There are a couple of options for testing conformance: 56 | 57 | 1. Run `phpunit` in from the root folder of the project. 58 | 2. Run `tests/runtests.php` from the command line. 59 | 3. Make `tests/runtests.php` accessible on a web server and view it in your 60 | browser. 61 | 62 | ## Thanks & Contributions ## 63 | 64 | The bulk of this library is from the heroic efforts of: 65 | 66 | - Matt Sanford (https://github.com/mzsanford): For the orignal Ruby and Java implementions. 67 | - Mike Cochrane (https://github.com/mikenz): For the initial PHP code. 68 | - Nick Pope (https://github.com/ngnpope): For the bulk of the maintenance work to date. 69 | -------------------------------------------------------------------------------- /lib/twitter-text-php/TODO: -------------------------------------------------------------------------------- 1 | - Generate PHP Documentation 2 | - Add a phing build.xml file for generating documentation and running tests. 3 | - Tidy up regular expressions: 4 | - Modularise repeated sections. 5 | - Remove unnecessary capturing. 6 | - Tidy up tests/runtests.php to refactor repeated sections. 7 | - Check unicode/mbstring handling... 8 | -------------------------------------------------------------------------------- /lib/twitter-text-php/lib/Twitter/Autolink.php: -------------------------------------------------------------------------------- 1 | 4 | * @author Nick Pope 5 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 6 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 7 | * @package Twitter 8 | */ 9 | 10 | require_once 'Regex.php'; 11 | 12 | /** 13 | * Twitter Autolink Class 14 | * 15 | * Parses tweets and generates HTML anchor tags around URLs, usernames, 16 | * username/list pairs and hashtags. 17 | * 18 | * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this 19 | * is based on code by {@link http://github.com/mzsanford Matt Sanford} and 20 | * heavily modified by {@link http://github.com/ngnpope Nick Pope}. 21 | * 22 | * @author Mike Cochrane 23 | * @author Nick Pope 24 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 25 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 26 | * @package Twitter 27 | */ 28 | class Twitter_Autolink extends Twitter_Regex { 29 | 30 | /** 31 | * CSS class for auto-linked URLs. 32 | * 33 | * @var string 34 | */ 35 | protected $class_url = 'url'; 36 | 37 | /** 38 | * CSS class for auto-linked username URLs. 39 | * 40 | * @var string 41 | */ 42 | protected $class_user = 'username'; 43 | 44 | /** 45 | * CSS class for auto-linked list URLs. 46 | * 47 | * @var string 48 | */ 49 | protected $class_list = 'list'; 50 | 51 | /** 52 | * CSS class for auto-linked hashtag URLs. 53 | * 54 | * @var string 55 | */ 56 | protected $class_hash = 'hashtag'; 57 | 58 | /** 59 | * URL base for username links (the username without the @ will be appended). 60 | * 61 | * @var string 62 | */ 63 | protected $url_base_user = 'http://twitter.com/'; 64 | 65 | /** 66 | * URL base for list links (the username/list without the @ will be appended). 67 | * 68 | * @var string 69 | */ 70 | protected $url_base_list = 'http://twitter.com/'; 71 | 72 | /** 73 | * URL base for hashtag links (the hashtag without the # will be appended). 74 | * 75 | * @var string 76 | */ 77 | protected $url_base_hash = 'http://twitter.com/search?q=%23'; 78 | 79 | /** 80 | * Whether to include the value 'nofollow' in the 'rel' attribute. 81 | * 82 | * @var bool 83 | */ 84 | protected $nofollow = true; 85 | 86 | /** 87 | * Whether to include the value 'external' in the 'rel' attribute. 88 | * 89 | * Often this is used to be matched on in JavaScript for dynamically adding 90 | * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has 91 | * been undeprecated and thus the 'target' attribute can be used. If this is 92 | * set to false then the 'target' attribute will be output. 93 | * 94 | * @var bool 95 | */ 96 | protected $external = true; 97 | 98 | /** 99 | * The scope to open the link in. 100 | * 101 | * Support for the 'target' attribute was deprecated in HTML 4.01 but has 102 | * since been reinstated in HTML 5. To output the 'target' attribute you 103 | * must disable the adding of the string 'external' to the 'rel' attribute. 104 | * 105 | * @var string 106 | */ 107 | protected $target = '_blank'; 108 | 109 | /** 110 | * Provides fluent method chaining. 111 | * 112 | * @param string $tweet The tweet to be converted. 113 | * @param bool $full_encode Whether to encode all special characters. 114 | * 115 | * @see __construct() 116 | * 117 | * @return Twitter_Autolink 118 | */ 119 | public static function create($tweet, $full_encode = false) { 120 | return new self($tweet, $full_encode); 121 | } 122 | 123 | /** 124 | * Reads in a tweet to be parsed and converted to contain links. 125 | * 126 | * As the intent is to produce links and output the modified tweet to the 127 | * user, we take this opportunity to ensure that we escape user input. 128 | * 129 | * @see htmlspecialchars() 130 | * 131 | * @param string $tweet The tweet to be converted. 132 | * @param bool $escape Whether to escape the tweet (default: true). 133 | * @param bool $full_encode Whether to encode all special characters. 134 | */ 135 | public function __construct($tweet, $escape = true, $full_encode = false) { 136 | if ($escape) { 137 | if ($full_encode) { 138 | parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false)); 139 | } else { 140 | parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false)); 141 | } 142 | } else { 143 | parent::__construct($tweet); 144 | } 145 | } 146 | 147 | /** 148 | * CSS class for auto-linked URLs. 149 | * 150 | * @return string CSS class for URL links. 151 | */ 152 | public function getURLClass() { 153 | return $this->class_url; 154 | } 155 | 156 | /** 157 | * CSS class for auto-linked URLs. 158 | * 159 | * @param string $v CSS class for URL links. 160 | * 161 | * @return Twitter_Autolink Fluid method chaining. 162 | */ 163 | public function setURLClass($v) { 164 | $this->class_url = trim($v); 165 | return $this; 166 | } 167 | 168 | /** 169 | * CSS class for auto-linked username URLs. 170 | * 171 | * @return string CSS class for username links. 172 | */ 173 | public function getUsernameClass() { 174 | return $this->class_user; 175 | } 176 | 177 | /** 178 | * CSS class for auto-linked username URLs. 179 | * 180 | * @param string $v CSS class for username links. 181 | * 182 | * @return Twitter_Autolink Fluid method chaining. 183 | */ 184 | public function setUsernameClass($v) { 185 | $this->class_user = trim($v); 186 | return $this; 187 | } 188 | 189 | /** 190 | * CSS class for auto-linked username/list URLs. 191 | * 192 | * @return string CSS class for username/list links. 193 | */ 194 | public function getListClass() { 195 | return $this->class_list; 196 | } 197 | 198 | /** 199 | * CSS class for auto-linked username/list URLs. 200 | * 201 | * @param string $v CSS class for username/list links. 202 | * 203 | * @return Twitter_Autolink Fluid method chaining. 204 | */ 205 | public function setListClass($v) { 206 | $this->class_list = trim($v); 207 | return $this; 208 | } 209 | 210 | /** 211 | * CSS class for auto-linked hashtag URLs. 212 | * 213 | * @return string CSS class for hashtag links. 214 | */ 215 | public function getHashtagClass() { 216 | return $this->class_hash; 217 | } 218 | 219 | /** 220 | * CSS class for auto-linked hashtag URLs. 221 | * 222 | * @param string $v CSS class for hashtag links. 223 | * 224 | * @return Twitter_Autolink Fluid method chaining. 225 | */ 226 | public function setHashtagClass($v) { 227 | $this->class_hash = trim($v); 228 | return $this; 229 | } 230 | 231 | /** 232 | * Whether to include the value 'nofollow' in the 'rel' attribute. 233 | * 234 | * @return bool Whether to add 'nofollow' to the 'rel' attribute. 235 | */ 236 | public function getNoFollow() { 237 | return $this->nofollow; 238 | } 239 | 240 | /** 241 | * Whether to include the value 'nofollow' in the 'rel' attribute. 242 | * 243 | * @param bool $v The value to add to the 'target' attribute. 244 | * 245 | * @return Twitter_Autolink Fluid method chaining. 246 | */ 247 | public function setNoFollow($v) { 248 | $this->nofollow = $v; 249 | return $this; 250 | } 251 | 252 | /** 253 | * Whether to include the value 'external' in the 'rel' attribute. 254 | * 255 | * Often this is used to be matched on in JavaScript for dynamically adding 256 | * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has 257 | * been undeprecated and thus the 'target' attribute can be used. If this is 258 | * set to false then the 'target' attribute will be output. 259 | * 260 | * @return bool Whether to add 'external' to the 'rel' attribute. 261 | */ 262 | public function getExternal() { 263 | return $this->external; 264 | } 265 | 266 | /** 267 | * Whether to include the value 'external' in the 'rel' attribute. 268 | * 269 | * Often this is used to be matched on in JavaScript for dynamically adding 270 | * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has 271 | * been undeprecated and thus the 'target' attribute can be used. If this is 272 | * set to false then the 'target' attribute will be output. 273 | * 274 | * @param bool $v The value to add to the 'target' attribute. 275 | * 276 | * @return Twitter_Autolink Fluid method chaining. 277 | */ 278 | public function setExternal($v) { 279 | $this->external = $v; 280 | return $this; 281 | } 282 | 283 | /** 284 | * The scope to open the link in. 285 | * 286 | * Support for the 'target' attribute was deprecated in HTML 4.01 but has 287 | * since been reinstated in HTML 5. To output the 'target' attribute you 288 | * must disable the adding of the string 'external' to the 'rel' attribute. 289 | * 290 | * @return string The value to add to the 'target' attribute. 291 | */ 292 | public function getTarget() { 293 | return $this->target; 294 | } 295 | 296 | /** 297 | * The scope to open the link in. 298 | * 299 | * Support for the 'target' attribute was deprecated in HTML 4.01 but has 300 | * since been reinstated in HTML 5. To output the 'target' attribute you 301 | * must disable the adding of the string 'external' to the 'rel' attribute. 302 | * 303 | * @param string $v The value to add to the 'target' attribute. 304 | * 305 | * @return Twitter_Autolink Fluid method chaining. 306 | */ 307 | public function setTarget($v) { 308 | $this->target = trim($v); 309 | return $this; 310 | } 311 | 312 | /** 313 | * Adds links to all elements in the tweet. 314 | * 315 | * @return string The modified tweet. 316 | */ 317 | public function addLinks() { 318 | $original = $this->tweet; 319 | $this->tweet = $this->addLinksToURLs(); 320 | $this->tweet = $this->addLinksToHashtags(); 321 | $this->tweet = $this->addLinksToUsernamesAndLists(); 322 | $modified = $this->tweet; 323 | $this->tweet = $original; 324 | return $modified; 325 | } 326 | 327 | /** 328 | * Adds links to hashtag elements in the tweet. 329 | * 330 | * @return string The modified tweet. 331 | */ 332 | public function addLinksToHashtags() { 333 | return preg_replace_callback( 334 | self::REGEX_HASHTAG, 335 | array($this, '_addLinksToHashtags'), 336 | $this->tweet); 337 | } 338 | 339 | /** 340 | * Adds links to URL elements in the tweet. 341 | * 342 | * @return string The modified tweet. 343 | */ 344 | public function addLinksToURLs() { 345 | return preg_replace_callback( 346 | self::$REGEX_VALID_URL, 347 | array($this, '_addLinksToURLs'), 348 | $this->tweet); 349 | } 350 | 351 | /** 352 | * Adds links to username/list elements in the tweet. 353 | * 354 | * @return string The modified tweet. 355 | */ 356 | public function addLinksToUsernamesAndLists() { 357 | return preg_replace_callback( 358 | self::REGEX_USERNAME_LIST, 359 | array($this, '_addLinksToUsernamesAndLists'), 360 | $this->tweet); 361 | } 362 | 363 | /** 364 | * Wraps a tweet element in an HTML anchor tag using the provided URL. 365 | * 366 | * This is a helper function to perform the generation of the link. 367 | * 368 | * @param string $url The URL to use as the href. 369 | * @param string $class The CSS class(es) to apply (space separated). 370 | * @param string $element The tweet element to wrap. 371 | * 372 | * @return string The tweet element with a link applied. 373 | */ 374 | protected function wrap($url, $class, $element) { 375 | $link = 'external) $rel[] = 'external'; 380 | if ($this->nofollow) $rel[] = 'nofollow'; 381 | if (!empty($rel)) $link .= ' rel="'.implode(' ', $rel).'"'; 382 | if ($this->target) $link .= ' target="'.$this->target.'"'; 383 | $link .= '>'.$element.''; 384 | return $link; 385 | } 386 | 387 | /** 388 | * Callback used by the method that adds links to hashtags. 389 | * 390 | * @see addLinksToHashtags() 391 | * 392 | * @param array $matches The regular expression matches. 393 | * 394 | * @return string The link-wrapped hashtag. 395 | */ 396 | protected function _addLinksToHashtags($matches) { 397 | $replacement = $matches[1]; 398 | $element = $matches[2] . $matches[3]; 399 | $url = $this->url_base_hash . $matches[3]; 400 | $replacement .= $this->wrap($url, $this->class_hash, $element); 401 | return $replacement; 402 | } 403 | 404 | /** 405 | * Callback used by the method that adds links to URLs. 406 | * 407 | * @see addLinksToURLs() 408 | * 409 | * @param array $matches The regular expression matches. 410 | * 411 | * @return string The link-wrapped URL. 412 | */ 413 | protected function _addLinksToURLs($matches) { 414 | list($all, $before, $url, $protocol, $domain, $path, $query) = array_pad($matches, 7, ''); 415 | $url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false); 416 | if (!$protocol) return $all; 417 | return $before . $this->wrap($url, $this->class_url, $url); 418 | } 419 | 420 | /** 421 | * Callback used by the method that adds links to username/list pairs. 422 | * 423 | * @see addLinksToUsernamesAndLists() 424 | * 425 | * @param array $matches The regular expression matches. 426 | * 427 | * @return string The link-wrapped username/list pair. 428 | */ 429 | protected function _addLinksToUsernamesAndLists($matches) { 430 | list($all, $before, $at, $username, $slash_listname, $after) = array_pad($matches, 6, ''); 431 | # If $after is not empty, there is an invalid character. 432 | if (!empty($after)) return $all; 433 | if (!empty($slash_listname)) { 434 | # Replace the list and username 435 | $element = $username . substr($slash_listname, 0, 26); 436 | $class = $this->class_list; 437 | $url = $this->url_base_list . $element; 438 | $postfix = substr($slash_listname, 26); 439 | } else { 440 | # Replace the username 441 | $element = $username; 442 | $class = $this->class_user; 443 | $url = $this->url_base_user . $element; 444 | $postfix = ''; 445 | } 446 | return $before . $this->wrap($url, $class, $at . $element) . $postfix . $after; 447 | } 448 | 449 | } 450 | -------------------------------------------------------------------------------- /lib/twitter-text-php/lib/Twitter/Extractor.php: -------------------------------------------------------------------------------- 1 | 4 | * @author Nick Pope 5 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 6 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 7 | * @package Twitter 8 | */ 9 | 10 | require_once 'Regex.php'; 11 | 12 | /** 13 | * Twitter Extractor Class 14 | * 15 | * Parses tweets and extracts URLs, usernames, username/list pairs and 16 | * hashtags. 17 | * 18 | * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this 19 | * is based on code by {@link http://github.com/mzsanford Matt Sanford} and 20 | * heavily modified by {@link http://github.com/ngnpope Nick Pope}. 21 | * 22 | * @author Mike Cochrane 23 | * @author Nick Pope 24 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 25 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 26 | * @package Twitter 27 | */ 28 | class Twitter_Extractor extends Twitter_Regex { 29 | 30 | /** 31 | * Provides fluent method chaining. 32 | * 33 | * @param string $tweet The tweet to be converted. 34 | * 35 | * @see __construct() 36 | * 37 | * @return Twitter_Extractor 38 | */ 39 | public static function create($tweet) { 40 | return new self($tweet); 41 | } 42 | 43 | /** 44 | * Reads in a tweet to be parsed and extracts elements from it. 45 | * 46 | * Extracts various parts of a tweet including URLs, usernames, hashtags... 47 | * 48 | * @param string $tweet The tweet to extract. 49 | */ 50 | public function __construct($tweet) { 51 | parent::__construct($tweet); 52 | } 53 | 54 | /** 55 | * Extracts all parts of a tweet and returns an associative array containing 56 | * the extracted elements. 57 | * 58 | * @return array The elements in the tweet. 59 | */ 60 | public function extract() { 61 | return array( 62 | 'hashtags' => $this->extractHashtags(), 63 | 'urls' => $this->extractURLs(), 64 | 'mentions' => $this->extractMentionedUsernames(), 65 | 'replyto' => $this->extractRepliedUsernames(), 66 | 'hashtags_with_indices' => $this->extractHashtagsWithIndices(), 67 | 'urls_with_indices' => $this->extractURLsWithIndices(), 68 | 'mentions_with_indices' => $this->extractMentionedUsernamesWithIndices(), 69 | ); 70 | } 71 | 72 | /** 73 | * Extracts all the hashtags from the tweet. 74 | * 75 | * @return array The hashtag elements in the tweet. 76 | */ 77 | public function extractHashtags() { 78 | preg_match_all(self::REGEX_HASHTAG, $this->tweet, $matches); 79 | return $matches[3]; 80 | } 81 | 82 | /** 83 | * Extracts all the URLs from the tweet. 84 | * 85 | * @return array The URL elements in the tweet. 86 | */ 87 | public function extractURLs() { 88 | preg_match_all(self::$REGEX_VALID_URL, $this->tweet, $matches); 89 | list($all, $before, $url, $protocol, $domain, $path, $query) = array_pad($matches, 7, ''); 90 | return $url; 91 | } 92 | 93 | /** 94 | * Extract all the usernames from the tweet. 95 | * 96 | * A mention is an occurrence of a username anywhere in a tweet. 97 | * 98 | * @return array The usernames elements in the tweet. 99 | */ 100 | public function extractMentionedUsernames() { 101 | preg_match_all(self::REGEX_USERNAME_MENTION, $this->tweet, $matches); 102 | list($all, $before, $username, $after) = array_pad($matches, 4, ''); 103 | $usernames = array(); 104 | for ($i = 0; $i < count($username); $i ++) { 105 | # If $after is not empty, there is an invalid character. 106 | if (!empty($after[$i])) continue; 107 | array_push($usernames, $username[$i]); 108 | } 109 | return $usernames; 110 | } 111 | 112 | /** 113 | * Extract all the usernames replied to from the tweet. 114 | * 115 | * A reply is an occurrence of a username at the beginning of a tweet. 116 | * 117 | * @return array The usernames replied to in a tweet. 118 | */ 119 | public function extractRepliedUsernames() { 120 | preg_match(self::$REGEX_REPLY_USERNAME, $this->tweet, $matches); 121 | return isset($matches[2]) ? $matches[2] : ''; 122 | } 123 | 124 | /** 125 | * Extracts all the hashtags and the indices they occur at from the tweet. 126 | * 127 | * @return array The hashtag elements in the tweet. 128 | */ 129 | public function extractHashtagsWithIndices() { 130 | preg_match_all(self::REGEX_HASHTAG, $this->tweet, $matches, PREG_OFFSET_CAPTURE); 131 | $m = &$matches[3]; 132 | for ($i = 0; $i < count($m); $i++) { 133 | $m[$i] = array_combine(array('hashtag', 'indices'), $m[$i]); 134 | # XXX: Fix for PREG_OFFSET_CAPTURE returning byte offsets... 135 | $start = mb_strlen(substr($this->tweet, 0, $matches[1][$i][1])); 136 | $start += mb_strlen($matches[1][$i][0]); 137 | $length = mb_strlen($m[$i]['hashtag']); 138 | $m[$i]['indices'] = array($start, $start + $length + 1); 139 | } 140 | return $m; 141 | } 142 | 143 | /** 144 | * Extracts all the URLs and the indices they occur at from the tweet. 145 | * 146 | * @return array The URLs elements in the tweet. 147 | */ 148 | public function extractURLsWithIndices() { 149 | preg_match_all(self::$REGEX_VALID_URL, $this->tweet, $matches, PREG_OFFSET_CAPTURE); 150 | $m = &$matches[2]; 151 | for ($i = 0; $i < count($m); $i++) { 152 | $m[$i] = array_combine(array('url', 'indices'), $m[$i]); 153 | # XXX: Fix for PREG_OFFSET_CAPTURE returning byte offsets... 154 | $start = mb_strlen(substr($this->tweet, 0, $matches[1][$i][1])); 155 | $start += mb_strlen($matches[1][$i][0]); 156 | $length = mb_strlen($m[$i]['url']); 157 | $m[$i]['indices'] = array($start, $start + $length); 158 | } 159 | return $m; 160 | } 161 | 162 | /** 163 | * Extracts all the usernames and the indices they occur at from the tweet. 164 | * 165 | * @return array The username elements in the tweet. 166 | */ 167 | public function extractMentionedUsernamesWithIndices() { 168 | preg_match_all(self::REGEX_USERNAME_MENTION, $this->tweet, $matches, PREG_OFFSET_CAPTURE); 169 | $m = &$matches[2]; 170 | for ($i = 0; $i < count($m); $i++) { 171 | $m[$i] = array_combine(array('screen_name', 'indices'), $m[$i]); 172 | # XXX: Fix for PREG_OFFSET_CAPTURE returning byte offsets... 173 | $start = mb_strlen(substr($this->tweet, 0, $matches[1][$i][1])); 174 | $start += mb_strlen($matches[1][$i][0]); 175 | $length = mb_strlen($m[$i]['screen_name']); 176 | $m[$i]['indices'] = array($start, $start + $length + 1); 177 | } 178 | return $m; 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /lib/twitter-text-php/lib/Twitter/HitHighlighter.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright © 2010, Nick Pope 5 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 6 | * @package Twitter 7 | */ 8 | 9 | require_once 'Regex.php'; 10 | 11 | /** 12 | * Twitter HitHighlighter Class 13 | * 14 | * Performs "hit highlighting" on tweets that have been auto-linked already. 15 | * Useful with the results returned from the search API. 16 | * 17 | * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this 18 | * is based on code by {@link http://github.com/mzsanford Matt Sanford} and 19 | * heavily modified by {@link http://github.com/ngnpope Nick Pope}. 20 | * 21 | * @author Nick Pope 22 | * @copyright Copyright © 2010, Nick Pope 23 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 24 | * @package Twitter 25 | */ 26 | class Twitter_HitHighlighter extends Twitter_Regex { 27 | 28 | /** 29 | * The tag to surround hits with. 30 | * 31 | * @var string 32 | */ 33 | protected $tag = 'em'; 34 | 35 | /** 36 | * Provides fluent method chaining. 37 | * 38 | * @param string $tweet The tweet to be hit highlighted. 39 | * @param bool $full_encode Whether to encode all special characters. 40 | * 41 | * @see __construct() 42 | * 43 | * @return Twitter_HitHighlighter 44 | */ 45 | public static function create($tweet, $full_encode = false) { 46 | return new self($tweet, $full_encode); 47 | } 48 | 49 | /** 50 | * Reads in a tweet to be parsed and hit highlighted. 51 | * 52 | * We take this opportunity to ensure that we escape user input. 53 | * 54 | * @see htmlspecialchars() 55 | * 56 | * @param string $tweet The tweet to be hit highlighted. 57 | * @param bool $escape Whether to escape the tweet (default: true). 58 | * @param bool $full_encode Whether to encode all special characters. 59 | */ 60 | public function __construct($tweet, $escape = true, $full_encode = false) { 61 | if ($escape) { 62 | if ($full_encode) { 63 | parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false)); 64 | } else { 65 | parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false)); 66 | } 67 | } else { 68 | parent::__construct($tweet); 69 | } 70 | } 71 | 72 | /** 73 | * Set the highlighting tag to surround hits with. The default tag is 'em'. 74 | * 75 | * @return string The tag name. 76 | */ 77 | public function getTag() { 78 | return $this->tag; 79 | } 80 | 81 | /** 82 | * Set the highlighting tag to surround hits with. The default tag is 'em'. 83 | * 84 | * @param string $v The tag name. 85 | * 86 | * @return Twitter_HitHighlighter Fluid method chaining. 87 | */ 88 | public function setTag($v) { 89 | $this->tag = $v; 90 | return $this; 91 | } 92 | 93 | /** 94 | * Hit highlights the tweet. 95 | * 96 | * @param array $hits An array containing the start and end index pairs 97 | * for the highlighting. 98 | * 99 | * @return string The hit highlighted tweet. 100 | */ 101 | public function addHitHighlighting(array $hits) { 102 | if (empty($hits)) return $this->tweet; 103 | $tweet = ''; 104 | $tags = array('<'.$this->tag.'>', 'tag.'>'); 105 | # Check whether we can simply replace or whether we need to chunk... 106 | if (strpos($this->tweet, '<') === false) { 107 | $ti = 0; // tag increment (for added tags) 108 | $tweet = $this->tweet; 109 | foreach ($hits as $hit) { 110 | $tweet = self::mb_substr_replace($tweet, $tags[0], $hit[0] + $ti, 0); 111 | $ti += mb_strlen($tags[0]); 112 | $tweet = self::mb_substr_replace($tweet, $tags[1], $hit[1] + $ti, 0); 113 | $ti += mb_strlen($tags[1]); 114 | } 115 | } else { 116 | $chunks = preg_split('/[<>]/iu', $this->tweet); 117 | $chunk = $chunks[0]; 118 | $chunk_index = 0; 119 | $chunk_cursor = 0; 120 | $offset = 0; 121 | $start_in_chunk = false; 122 | # Flatten the multidimensional hits array: 123 | $hits_flat = array(); 124 | foreach ($hits as $hit) $hits_flat = array_merge($hits_flat, $hit); 125 | # Loop over the hit indices: 126 | for ($index = 0; $index < count($hits_flat); $index++) { 127 | $hit = $hits_flat[$index]; 128 | $tag = $tags[$index % 2]; 129 | $placed = false; 130 | while ($chunk !== null && $hit >= ($i = $offset + mb_strlen($chunk))) { 131 | $tweet .= mb_substr($chunk, $chunk_cursor); 132 | if ($start_in_chunk && $hit === $i) { 133 | $tweet .= $tag; 134 | $placed = true; 135 | } 136 | if (isset($chunks[$chunk_index+1])) $tweet .= '<' . $chunks[$chunk_index+1] . '>'; 137 | $offset += mb_strlen($chunk); 138 | $chunk_cursor = 0; 139 | $chunk_index += 2; 140 | $chunk = (isset($chunks[$chunk_index]) ? $chunks[$chunk_index] : null); 141 | $start_in_chunk = false; 142 | } 143 | if (!$placed && $chunk !== null) { 144 | $hit_spot = $hit - $offset; 145 | $tweet .= mb_substr($chunk, $chunk_cursor, $hit_spot - $chunk_cursor) . $tag; 146 | $chunk_cursor = $hit_spot; 147 | $start_in_chunk = ($index % 2 === 0); 148 | $placed = true; 149 | } 150 | # Ultimate fallback - hits that run off the end get a closing tag: 151 | if (!$placed) $tweet .= $tag; 152 | } 153 | if ($chunk !== null) { 154 | if ($chunk_cursor < mb_strlen($chunk)) { 155 | $tweet .= mb_substr($chunk, $chunk_cursor); 156 | } 157 | for ($index = $chunk_index + 1; $index < count($chunks); $index++) { 158 | $tweet .= ($index % 2 === 0 ? $chunks[$index] : '<' . $chunks[$index] . '>'); 159 | } 160 | } 161 | } 162 | return $tweet; 163 | } 164 | 165 | /** 166 | * A multibyte-aware substring replacement function. 167 | * 168 | * @param string $string The string to modify. 169 | * @param string $replacement The replacement string. 170 | * @param int $start The start of the replacement. 171 | * @param int $length The number of characters to replace. 172 | * @param string $encoding The encoding of the string. 173 | * 174 | * @return string The modified string. 175 | * 176 | * @see http://www.php.net/manual/en/function.substr-replace.php#90146 177 | */ 178 | protected static function mb_substr_replace($string, $replacement, $start, $length = null, $encoding = null) { 179 | if (extension_loaded('mbstring') === true) { 180 | $string_length = (is_null($encoding) === true) ? mb_strlen($string) : mb_strlen($string, $encoding); 181 | if ($start < 0) { 182 | $start = max(0, $string_length + $start); 183 | } else if ($start > $string_length) { 184 | $start = $string_length; 185 | } 186 | if ($length < 0) { 187 | $length = max(0, $string_length - $start + $length); 188 | } else if ((is_null($length) === true) || ($length > $string_length)) { 189 | $length = $string_length; 190 | } 191 | if (($start + $length) > $string_length) { 192 | $length = $string_length - $start; 193 | } 194 | if (is_null($encoding) === true) { 195 | return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length); 196 | } 197 | return mb_substr($string, 0, $start, $encoding) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length, $encoding); 198 | } 199 | return (is_null($length) === true) ? substr_replace($string, $replacement, $start) : substr_replace($string, $replacement, $start, $length); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /lib/twitter-text-php/lib/Twitter/Regex.php: -------------------------------------------------------------------------------- 1 | 4 | * @author Nick Pope 5 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 6 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 7 | * @package Twitter 8 | */ 9 | 10 | /** 11 | * Twitter Regex Abstract Class 12 | * 13 | * Used by subclasses that need to parse tweets. 14 | * 15 | * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this 16 | * is based on code by {@link http://github.com/mzsanford Matt Sanford} and 17 | * heavily modified by {@link http://github.com/ngnpope Nick Pope}. 18 | * 19 | * @author Mike Cochrane 20 | * @author Nick Pope 21 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 22 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 23 | * @package Twitter 24 | */ 25 | abstract class Twitter_Regex { 26 | 27 | /** 28 | * Expression to at sign characters 29 | * 30 | * @var string 31 | */ 32 | const REGEX_AT_SIGNS = '[@@]'; 33 | 34 | /** 35 | * Expression to match characters that may come before a URL. 36 | * 37 | * @var string 38 | */ 39 | const REGEX_URL_CHARS_BEFORE = '(?:[^-\\/"\':!=a-z0-9_@@]|^|\\:)'; 40 | 41 | /** 42 | * Expression to match the domain portion of a URL. 43 | * 44 | * @var string 45 | */ 46 | const REGEX_URL_DOMAIN = '(?:[^\\p{P}\\p{Lo}\\s][\\.-](?=[^\\p{P}\\p{Lo}\\s])|[^\\p{P}\\p{Lo}\\s])+\\.[a-z]{2,}(?::[0-9]+)?'; 47 | 48 | /** 49 | * Expression to match characters that may come in the URL path. 50 | * 51 | * @var string 52 | */ 53 | const REGEX_URL_CHARS_PATH = '(?:(?:\\([a-z0-9!\\*\';:=\\+\\$\\/%#\\[\\]\\-_,~]+\\))|@[a-z0-9!\\*\';:=\\+\\$\\/%#\\[\\]\\-_,~]+\\/|[\\.\\,]?(?:[a-z0-9!\\*\';:=\\+\\$\\/%#\\[\\]\\-_~]|,(?!\s)))'; 54 | 55 | /** 56 | * Expression to match characters that may come at the end of the URL path. 57 | * 58 | * @var string 59 | */ 60 | const REGEX_URL_CHARS_PATH_END = '[a-z0-9=#\\/]'; 61 | 62 | /** 63 | * Expression to match characters that may come in the URL query string. 64 | * 65 | * @var string 66 | */ 67 | const REGEX_URL_CHARS_QUERY = '[a-z0-9!\\*\'\\(\\);:&=\\+\\$\\/%#\\[\\]\\-_\\.,~]'; 68 | 69 | /** 70 | * Expression to match characters that may come at the end of the URL query 71 | * string. 72 | * 73 | * @var string 74 | */ 75 | const REGEX_URL_CHARS_QUERY_END = '[a-z0-9_&=#\\/]'; 76 | 77 | /** 78 | * Expression to match a username followed by a list. 79 | * 80 | * @var string 81 | */ 82 | const REGEX_USERNAME_LIST = '/([^a-z0-9_\/]|^|RT:?)([@@]+)([a-z0-9_]{1,20})(\/[a-z][-_a-z0-9\x80-\xFF]{0,24})?([@@\xC0-\xD6\xD8-\xF6\xF8-\xFF]?)/iu'; 83 | 84 | /** 85 | * Expression to match a username mentioned anywhere in a tweet. 86 | * 87 | * @var string 88 | */ 89 | const REGEX_USERNAME_MENTION = '/(^|[^a-z0-9_])[@@]([a-z0-9_]{1,20})([@@\xC0-\xD6\xD8-\xF6\xF8-\xFF]?)/iu'; 90 | 91 | /** 92 | * Expression to match a hashtag. 93 | * 94 | * @var string 95 | */ 96 | const REGEX_HASHTAG = '/(^|[^0-9A-Z&\/\?]+)([##]+)([0-9A-Z_]*[A-Z_]+[a-z0-9_üÀ-ÖØ-öø-ÿ]*)/iu'; 97 | 98 | /** 99 | * Expression to match whitespace. 100 | * 101 | * Single byte whitespace characters 102 | * 0x0009-0x000D White_Space # Cc # .. 103 | * 0x0020 White_Space # Zs # SPACE 104 | * 0x0085 White_Space # Cc # 105 | * 0x00A0 White_Space # Zs # NO-BREAK SPACE 106 | * Multi byte whitespace characters 107 | * 0x1680 White_Space # Zs # OGHAM SPACE MARK 108 | * 0x180E White_Space # Zs # MONGOLIAN VOWEL SEPARATOR 109 | * 0x2000-0x200A White_Space # Zs # EN QUAD..HAIR SPACE 110 | * 0x2028 White_Space # Zl # LINE SEPARATOR 111 | * 0x2029 White_Space # Zp # PARAGRAPH SEPARATOR 112 | * 0x202F White_Space # Zs # NARROW NO-BREAK SPACE 113 | * 0x205F White_Space # Zs # MEDIUM MATHEMATICAL SPACE 114 | * 0x3000 White_Space # Zs # IDEOGRAPHIC SPACE 115 | * 116 | * @var string 117 | */ 118 | const REGEX_WHITESPACE = '[\x09-\x0D\x20\x85\xA0]|\xe1\x9a\x80|\xe1\xa0\x8e|\xe2\x80[\x80-\x8a,\xa8,\xa9,\xaf\xdf]|\xe3\x80\x80'; 119 | 120 | /** 121 | * Contains the complete valid URL pattern string. 122 | * 123 | * This should be generated the first time the constructor is called. 124 | * 125 | * @var string The regex pattern for a valid URL. 126 | */ 127 | protected static $REGEX_VALID_URL = null; 128 | 129 | /** 130 | * Contains the reply username pattern string. 131 | * 132 | * This should be generated the first time the constructor is called. 133 | * 134 | * @var string The regex pattern for a reply username. 135 | */ 136 | protected static $REGEX_REPLY_USERNAME = null; 137 | 138 | /** 139 | * The tweet to be used in parsing. This should be populated by the 140 | * constructor of all subclasses. 141 | * 142 | * @var string 143 | */ 144 | protected $tweet = ''; 145 | 146 | /** 147 | * This constructor is used to populate some variables. 148 | * 149 | * @param string $tweet The tweet to parse. 150 | */ 151 | protected function __construct($tweet) { 152 | if (is_null(self::$REGEX_VALID_URL)) { 153 | self::$REGEX_VALID_URL = '/(?:' # $1 Complete match (preg_match already matches everything.) 154 | . '('.self::REGEX_URL_CHARS_BEFORE.')' # $2 Preceding character 155 | . '(' # $3 Complete URL 156 | . '(https?:\\/\\/)' # $4 Protocol (or www) 157 | . '('.self::REGEX_URL_DOMAIN.')' # $5 Domain(s) (and port) 158 | . '(\\/'.self::REGEX_URL_CHARS_PATH.'*' # $6 URL Path 159 | . self::REGEX_URL_CHARS_PATH_END.'?)?' 160 | . '(\\?'.self::REGEX_URL_CHARS_QUERY.'*' # $7 Query String 161 | . self::REGEX_URL_CHARS_QUERY_END.')?' 162 | . ')' 163 | . ')/iux'; 164 | } 165 | if (is_null(self::$REGEX_REPLY_USERNAME)) { 166 | self::$REGEX_REPLY_USERNAME = '/^('.self::REGEX_WHITESPACE.')*[@@]([a-zA-Z0-9_]{1,20})/'; 167 | } 168 | $this->tweet = $tweet; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /lib/twitter-text-php/phpunit.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | tests/Twitter 12 | 13 | 14 | 15 | 16 | 17 | lib/Twitter 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/twitter-text-php/tests/Twitter/AutolinkTest.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 5 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 6 | * @package Twitter 7 | */ 8 | 9 | /** 10 | * Twitter Autolink Class Unit Tests 11 | * 12 | * @author Nick Pope 13 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 14 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 15 | * @package Twitter 16 | */ 17 | class Twitter_AutolinkTest extends PHPUnit_Framework_TestCase { 18 | 19 | /** 20 | * A helper function for providers. 21 | * 22 | * @param string $test The test to fetch data for. 23 | * 24 | * @return array The test data to provide. 25 | */ 26 | protected function providerHelper($test) { 27 | $data = Spyc::YAMLLoad(DATA.'/autolink.yml'); 28 | return isset($data['tests'][$test]) ? $data['tests'][$test] : array(); 29 | } 30 | 31 | /** 32 | * @dataProvider addLinksToUsernamesProvider 33 | */ 34 | public function testAddLinksToUsernames($description, $text, $expected) { 35 | $linked = Twitter_Autolink::create($text, false) 36 | ->setNoFollow(false)->setExternal(false)->setTarget('') 37 | ->setUsernameClass('tweet-url username') 38 | ->setListClass('tweet-url list-slug') 39 | ->setHashtagClass('tweet-url hashtag') 40 | ->setURLClass('') 41 | ->addLinksToUsernamesAndLists(); 42 | $this->assertSame($expected, $linked, $description); 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | public function addLinksToUsernamesProvider() { 49 | return $this->providerHelper('usernames'); 50 | } 51 | 52 | /** 53 | * @dataProvider addLinksToListsProvider 54 | */ 55 | public function testAddLinksToLists($description, $text, $expected) { 56 | $linked = Twitter_Autolink::create($text, false) 57 | ->setNoFollow(false)->setExternal(false)->setTarget('') 58 | ->setUsernameClass('tweet-url username') 59 | ->setListClass('tweet-url list-slug') 60 | ->setHashtagClass('tweet-url hashtag') 61 | ->setURLClass('') 62 | ->addLinksToUsernamesAndLists(); 63 | $this->assertSame($expected, $linked, $description); 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | public function addLinksToListsProvider() { 70 | return $this->providerHelper('lists'); 71 | } 72 | 73 | /** 74 | * @dataProvider addLinksToHashtagsProvider 75 | */ 76 | public function testAddLinksToHashtags($description, $text, $expected) { 77 | $linked = Twitter_Autolink::create($text, false) 78 | ->setNoFollow(false)->setExternal(false)->setTarget('') 79 | ->setUsernameClass('tweet-url username') 80 | ->setListClass('tweet-url list-slug') 81 | ->setHashtagClass('tweet-url hashtag') 82 | ->setURLClass('') 83 | ->addLinksToHashtags(); 84 | # XXX: Need to re-order for hashtag as it is written out differently... 85 | # We use the same wrapping function for adding links for all methods. 86 | $linked = preg_replace(array( 87 | '!([^<]*)!', 88 | '!title="#([^"]+)"!' 89 | ), array( 90 | '$3', 91 | 'title="#$1"' 92 | ), $linked); 93 | $this->assertSame($expected, $linked, $description); 94 | } 95 | 96 | /** 97 | * 98 | */ 99 | public function addLinksToHashtagsProvider() { 100 | return $this->providerHelper('hashtags'); 101 | } 102 | 103 | /** 104 | * @dataProvider addLinksToURLsProvider 105 | */ 106 | public function testAddLinksToURLs($description, $text, $expected) { 107 | $linked = Twitter_Autolink::create($text, false) 108 | ->setNoFollow(false)->setExternal(false)->setTarget('') 109 | ->setUsernameClass('tweet-url username') 110 | ->setListClass('tweet-url list-slug') 111 | ->setHashtagClass('tweet-url hashtag') 112 | ->setURLClass('') 113 | ->addLinksToURLs(); 114 | $this->assertSame($expected, $linked, $description); 115 | } 116 | 117 | /** 118 | * 119 | */ 120 | public function addLinksToURLsProvider() { 121 | return $this->providerHelper('urls'); 122 | } 123 | 124 | /** 125 | * @dataProvider addLinksProvider 126 | */ 127 | public function testAddLinks($description, $text, $expected) { 128 | $linked = Twitter_Autolink::create($text, false) 129 | ->setNoFollow(false)->setExternal(false)->setTarget('') 130 | ->setUsernameClass('tweet-url username') 131 | ->setListClass('tweet-url list-slug') 132 | ->setHashtagClass('tweet-url hashtag') 133 | ->setURLClass('') 134 | ->addLinks(); 135 | $this->assertSame($expected, $linked, $description); 136 | } 137 | 138 | /** 139 | * 140 | */ 141 | public function addLinksProvider() { 142 | return $this->providerHelper('all'); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /lib/twitter-text-php/tests/Twitter/ExtractorTest.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 5 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 6 | * @package Twitter 7 | */ 8 | 9 | /** 10 | * Twitter Extractor Class Unit Tests 11 | * 12 | * @author Nick Pope 13 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 14 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 15 | * @package Twitter 16 | */ 17 | class Twitter_ExtractorTest extends PHPUnit_Framework_TestCase { 18 | 19 | /** 20 | * A helper function for providers. 21 | * 22 | * @param string $test The test to fetch data for. 23 | * 24 | * @return array The test data to provide. 25 | */ 26 | protected function providerHelper($test) { 27 | $data = Spyc::YAMLLoad(DATA.'/extract.yml'); 28 | return isset($data['tests'][$test]) ? $data['tests'][$test] : array(); 29 | } 30 | 31 | /** 32 | * @dataProvider extractMentionedUsernamesProvider 33 | */ 34 | public function testExtractMentionedUsernames($description, $text, $expected) { 35 | $extracted = Twitter_Extractor::create($text)->extractMentionedUsernames(); 36 | $this->assertSame($expected, $extracted, $description); 37 | } 38 | 39 | /** 40 | * 41 | */ 42 | public function extractMentionedUsernamesProvider() { 43 | return $this->providerHelper('mentions'); 44 | } 45 | 46 | /** 47 | * @dataProvider extractRepliedUsernamesProvider 48 | */ 49 | public function testExtractRepliedUsernames($description, $text, $expected) { 50 | $extracted = Twitter_Extractor::create($text)->extractRepliedUsernames(); 51 | $this->assertSame($expected, $extracted, $description); 52 | } 53 | 54 | /** 55 | * 56 | */ 57 | public function extractRepliedUsernamesProvider() { 58 | return $this->providerHelper('replies'); 59 | } 60 | 61 | /** 62 | * @dataProvider extractURLsProvider 63 | */ 64 | public function testExtractURLs($description, $text, $expected) { 65 | $extracted = Twitter_Extractor::create($text)->extractURLs(); 66 | $this->assertSame($expected, $extracted, $description); 67 | } 68 | 69 | /** 70 | * 71 | */ 72 | public function extractURLsProvider() { 73 | return $this->providerHelper('urls'); 74 | } 75 | 76 | /** 77 | * @dataProvider extractHashtagsProvider 78 | */ 79 | public function testExtractHashtags($description, $text, $expected) { 80 | $extracted = Twitter_Extractor::create($text)->extractHashtags(); 81 | $this->assertSame($expected, $extracted, $description); 82 | } 83 | 84 | /** 85 | * 86 | */ 87 | public function extractHashtagsProvider() { 88 | return $this->providerHelper('hashtags'); 89 | } 90 | 91 | /** 92 | * @dataProvider extractHashtagsWithIndicesProvider 93 | */ 94 | public function testExtractHashtagsWithIndices($description, $text, $expected) { 95 | $extracted = Twitter_Extractor::create($text)->extractHashtagsWithIndices(); 96 | $this->assertSame($expected, $extracted, $description); 97 | } 98 | 99 | /** 100 | * 101 | */ 102 | public function extractHashtagsWithIndicesProvider() { 103 | return $this->providerHelper('hashtags_with_indices'); 104 | } 105 | 106 | /** 107 | * @dataProvider extractURLsWithIndicesProvider 108 | */ 109 | public function testExtractURLsWithIndices($description, $text, $expected) { 110 | $extracted = Twitter_Extractor::create($text)->extractURLsWithIndices(); 111 | $this->assertSame($expected, $extracted, $description); 112 | } 113 | 114 | /** 115 | * 116 | */ 117 | public function extractURLsWithIndicesProvider() { 118 | return $this->providerHelper('urls_with_indices'); 119 | } 120 | 121 | /** 122 | * @dataProvider extractMentionedUsernamesWithIndicesProvider 123 | */ 124 | public function testExtractMentionedUsernamesWithIndices($description, $text, $expected) { 125 | $extracted = Twitter_Extractor::create($text)->extractMentionedUsernamesWithIndices(); 126 | $this->assertSame($expected, $extracted, $description); 127 | } 128 | 129 | /** 130 | * 131 | */ 132 | public function extractMentionedUsernamesWithIndicesProvider() { 133 | return $this->providerHelper('mentions_with_indices'); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /lib/twitter-text-php/tests/Twitter/HitHighlighterTest.php: -------------------------------------------------------------------------------- 1 | 4 | * @copyright Copyright © 2010, Nick Pope 5 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 6 | * @package Twitter 7 | */ 8 | 9 | /** 10 | * Twitter HitHighlighter Class Unit Tests 11 | * 12 | * @author Nick Pope 13 | * @copyright Copyright © 2010, Nick Pope 14 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 15 | * @package Twitter 16 | */ 17 | class Twitter_HitHighlighterTest extends PHPUnit_Framework_TestCase { 18 | 19 | /** 20 | * A helper function for providers. 21 | * 22 | * @param string $test The test to fetch data for. 23 | * 24 | * @return array The test data to provide. 25 | */ 26 | protected function providerHelper($test) { 27 | $data = Spyc::YAMLLoad(DATA.'/hit_highlighting.yml'); 28 | return isset($data['tests'][$test]) ? $data['tests'][$test] : array(); 29 | } 30 | 31 | /** 32 | * @dataProvider addHitHighlightingProvider 33 | */ 34 | public function testAddHitHighlighting($description, $text, $hits, $expected) { 35 | $extracted = Twitter_HitHighlighter::create($text)->addHitHighlighting($hits); 36 | $this->assertSame($expected, $extracted, $description); 37 | } 38 | 39 | /** 40 | * 41 | */ 42 | public function addHitHighlightingProvider() { 43 | return array_merge($this->providerHelper('plain_text'), $this->providerHelper('with_links')); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /lib/twitter-text-php/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Nick Pope 9 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 10 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 11 | */ 12 | 13 | if (!defined('E_DEPRECATED')) define('E_DEPRECATED', 8192); 14 | error_reporting(E_ALL | E_STRICT | E_DEPRECATED); 15 | 16 | $ROOT = dirname(dirname(__FILE__)); 17 | 18 | require_once $ROOT.'/lib/Twitter/Autolink.php'; 19 | require_once $ROOT.'/lib/Twitter/Extractor.php'; 20 | require_once $ROOT.'/lib/Twitter/HitHighlighter.php'; 21 | 22 | $browser = (PHP_SAPI != 'cli'); 23 | 24 | function print_array(array $a) { 25 | $p = print_r($a, true); 26 | $p = str_replace(' ', ' ', $p); 27 | echo preg_replace(array( 28 | '!^Array\s+\(\s+!', 29 | '!=> Array\s+\(!', 30 | '! (\[\d|\))!', 31 | '!\s+\)\s*$!', 32 | ), array( 33 | ' ', '=> (', '\1', '', 34 | ), $p); 35 | } 36 | 37 | $tweet = 'Tweet mentioning @mikenz and referring to his list @mikeNZ/sports and website http://mikenz.geek.nz #awesome'; 38 | 39 | if ($browser) echo << 41 | 42 | 43 | 44 | Twitter Text (PHP Edition) Library » Examples 45 | 77 | 78 | 79 | EOHTML; 80 | 81 | if ($browser) echo '

    '; 82 | echo 'Twitter Text (PHP Edition) Library » Examples'; 83 | if ($browser) echo '

    '; 84 | else echo PHP_EOL, '============================================', PHP_EOL; 85 | echo PHP_EOL; 86 | 87 | if ($browser) echo '

    '; 88 | echo 'Extraction Examples'; 89 | if ($browser) echo '

    '; 90 | else echo PHP_EOL, '-------------------', PHP_EOL; 91 | echo PHP_EOL; 92 | 93 | $code = <<extract(); 98 | print_r(\$data); 99 | EOPHP; 100 | if ($browser) { 101 | echo '

    Source

    ', PHP_EOL; 102 | echo '
    ';
    103 |   highlight_string($code);
    104 |   echo '
    ', PHP_EOL; 105 | } else { 106 | echo 'Source:', PHP_EOL, PHP_EOL; 107 | echo $code; 108 | echo PHP_EOL, PHP_EOL; 109 | } 110 | 111 | $data = Twitter_Extractor::create($tweet) 112 | ->extract(); 113 | 114 | if ($browser) { 115 | echo '

    Output

    ', PHP_EOL; 116 | echo '
    ';
    117 |   print_array($data);
    118 |   echo '
    ', PHP_EOL; 119 | } else { 120 | echo 'Output:', PHP_EOL, PHP_EOL; 121 | print_array($data); 122 | echo PHP_EOL, PHP_EOL; 123 | } 124 | 125 | if ($browser) echo '

    '; 126 | echo 'Autolink Examples'; 127 | if ($browser) echo '

    '; 128 | else echo PHP_EOL, '-----------------', PHP_EOL; 129 | echo PHP_EOL; 130 | 131 | $code = <<setNoFollow(false) 136 | ->addLinks(); 137 | echo \$html; 138 | EOPHP; 139 | if ($browser) { 140 | echo '

    Source

    ', PHP_EOL; 141 | echo '
    ';
    142 |   highlight_string($code);
    143 |   echo '
    ', PHP_EOL; 144 | } else { 145 | echo 'Source:', PHP_EOL, PHP_EOL; 146 | echo $code; 147 | echo PHP_EOL, PHP_EOL; 148 | } 149 | 150 | $html = Twitter_Autolink::create($tweet) 151 | ->setNoFollow(false) 152 | ->addLinks(); 153 | 154 | if ($browser) { 155 | echo '

    Markup

    ', PHP_EOL; 156 | echo '
    ';
    157 |   echo htmlspecialchars($html, ENT_QUOTES, 'UTF-8', false);
    158 |   echo '
    ', PHP_EOL; 159 | } else { 160 | echo 'Markup:', PHP_EOL, PHP_EOL; 161 | echo wordwrap(htmlspecialchars($html, ENT_QUOTES, 'UTF-8', false)); 162 | echo PHP_EOL, PHP_EOL; 163 | } 164 | 165 | if ($browser) { 166 | echo '

    Output

    ', PHP_EOL; 167 | echo '
    '; 168 | echo $html; 169 | echo '
    ', PHP_EOL; 170 | } else { 171 | echo 'Output:', PHP_EOL, PHP_EOL; 172 | echo wordwrap($html); 173 | echo PHP_EOL, PHP_EOL; 174 | } 175 | 176 | if ($browser) echo '

    '; 177 | echo 'Hit Highlighter Examples'; 178 | if ($browser) echo '

    '; 179 | else echo PHP_EOL, '------------------------', PHP_EOL; 180 | echo PHP_EOL; 181 | 182 | $code = <<addHitHighlighting(\$hits); 188 | echo \$html; 189 | EOPHP; 190 | if ($browser) { 191 | echo '

    Source

    ', PHP_EOL; 192 | echo '
    ';
    193 |   highlight_string($code);
    194 |   echo '
    ', PHP_EOL; 195 | } else { 196 | echo 'Source:', PHP_EOL, PHP_EOL; 197 | echo $code; 198 | echo PHP_EOL, PHP_EOL; 199 | } 200 | 201 | $html = Twitter_HitHighlighter::create($tweet) 202 | ->addHitHighlighting(array(array(70, 77), array(101, 108))); 203 | 204 | if ($browser) { 205 | echo '

    Markup

    ', PHP_EOL; 206 | echo '
    ';
    207 |   echo htmlspecialchars($html, ENT_QUOTES, 'UTF-8', false);
    208 |   echo '
    ', PHP_EOL; 209 | } else { 210 | echo 'Markup:', PHP_EOL, PHP_EOL; 211 | echo wordwrap(htmlspecialchars($html, ENT_QUOTES, 'UTF-8', false)); 212 | echo PHP_EOL, PHP_EOL; 213 | } 214 | 215 | if ($browser) { 216 | echo '

    Output

    ', PHP_EOL; 217 | echo '
    '; 218 | echo $html; 219 | echo '
    ', PHP_EOL; 220 | } else { 221 | echo 'Output:', PHP_EOL, PHP_EOL; 222 | echo wordwrap($html); 223 | echo PHP_EOL, PHP_EOL; 224 | } 225 | 226 | if ($browser) echo << 228 | 229 | EOHTML; 230 | -------------------------------------------------------------------------------- /lib/twitter-text-php/tests/runtests.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Nick Pope 9 | * @copyright Copyright © 2010, Mike Cochrane, Nick Pope 10 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 11 | */ 12 | 13 | require_once dirname(__FILE__).'/bootstrap.php'; 14 | 15 | $browser = (PHP_SAPI != 'cli'); 16 | 17 | function pretty_format($a) { 18 | return preg_replace(array( 19 | "/\n/", '/ +\[/', '/ +\)/', '/Array +\(/', '/(? 33 | 34 | 35 | 36 | Twitter Text (PHP Edition) Library » Conformance 37 | 47 | 48 | 49 | EOHTML; 50 | 51 | echo ($browser ? '

    ' : "\033[1m"); 52 | echo 'Twitter Text (PHP Edition) Library » Conformance'; 53 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL.'==============================================='.PHP_EOL); 54 | echo PHP_EOL; 55 | 56 | echo ($browser ? '

    ' : "\033[1m"); 57 | echo 'Extraction Conformance'; 58 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL.'----------------------'.PHP_EOL); 59 | echo PHP_EOL; 60 | 61 | # Load the test data. 62 | $data = Spyc::YAMLLoad($DATA.'/extract.yml'); 63 | 64 | # Define the functions to be tested. 65 | $functions = array( 66 | 'hashtags' => 'extractHashtags', 67 | 'urls' => 'extractURLs', 68 | 'mentions' => 'extractMentionedUsernames', 69 | 'replies' => 'extractRepliedUsernames', 70 | 'hashtags_with_indices' => 'extractHashtagsWithIndices', 71 | 'urls_with_indices' => 'extractURLsWithIndices', 72 | 'mentions_with_indices' => 'extractMentionedUsernamesWithIndices', 73 | ); 74 | 75 | # Perform testing. 76 | foreach ($data['tests'] as $group => $tests) { 77 | 78 | echo ($browser ? '

    ' : "\033[1m"); 79 | echo 'Test Group - '.ucfirst(str_replace('_', ' ', $group)); 80 | echo ($browser ? '

    ' : ":\033[0m".PHP_EOL); 81 | echo PHP_EOL; 82 | 83 | if (!array_key_exists($group, $functions)) { 84 | echo ($browser ? '

    ' : " \033[1;35m"); 85 | echo 'Skipping Test...'; 86 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 87 | echo PHP_EOL; 88 | continue; 89 | } 90 | $function = $functions[$group]; 91 | $pass_group = 0; 92 | $fail_group = 0; 93 | if ($browser) echo '
      ', PHP_EOL; 94 | foreach ($tests as $test) { 95 | echo ($browser ? '
    • ' : ' - '); 96 | echo $test['description'], ' ... '; 97 | $extracted = Twitter_Extractor::create($test['text'])->$function(); 98 | if ($test['expected'] == $extracted) { 99 | $pass_group++; 100 | echo ($browser ? 'PASS' : "\033[1;32mPASS\033[0m"); 101 | } else { 102 | $fail_group++; 103 | echo ($browser ? 'FAIL' : "\033[1;31mFAIL\033[0m"); 104 | if ($browser) { 105 | echo '
      ';
      106 |         echo 'Original: '.htmlspecialchars($test['text'], ENT_QUOTES, 'UTF-8', false), PHP_EOL;
      107 |         echo 'Expected: '.pretty_format($test['expected']), PHP_EOL;
      108 |         echo 'Actual:   '.pretty_format($extracted);
      109 |         echo '
      '; 110 | } else { 111 | echo PHP_EOL, PHP_EOL; 112 | echo ' Original: '.$test['text'], PHP_EOL; 113 | echo ' Expected: '.pretty_format($test['expected']), PHP_EOL; 114 | echo ' Actual: '.pretty_format($extracted), PHP_EOL; 115 | } 116 | } 117 | if ($browser) echo '
    • '; 118 | echo PHP_EOL; 119 | } 120 | if ($browser) echo '
    '; 121 | echo PHP_EOL; 122 | $pass_total += $pass_group; 123 | $fail_total += $fail_group; 124 | echo ($browser ? '

    ' : " \033[1;33m"); 125 | printf('Group Results: %d passes, %d failures', $pass_group, $fail_group); 126 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 127 | echo PHP_EOL; 128 | } 129 | 130 | echo ($browser ? '

    ' : "\033[1m"); 131 | echo 'Autolink Conformance'; 132 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL.'--------------------'.PHP_EOL); 133 | echo PHP_EOL; 134 | 135 | # Load the test data. 136 | $data = Spyc::YAMLLoad($DATA.'/autolink.yml'); 137 | 138 | # Define the functions to be tested. 139 | $functions = array( 140 | 'usernames' => 'addLinksToUsernamesAndLists', 141 | 'lists' => 'addLinksToUsernamesAndLists', 142 | 'hashtags' => 'addLinksToHashtags', 143 | 'urls' => 'addLinksToURLs', 144 | 'all' => 'addLinks', 145 | ); 146 | 147 | # Perform testing. 148 | foreach ($data['tests'] as $group => $tests) { 149 | 150 | echo ($browser ? '

    ' : "\033[1m"); 151 | echo 'Test Group - '.ucfirst(str_replace('_', ' ', $group)); 152 | echo ($browser ? '

    ' : ":\033[0m".PHP_EOL); 153 | echo PHP_EOL; 154 | 155 | if (!array_key_exists($group, $functions)) { 156 | echo ($browser ? '

    ' : " \033[1;35m"); 157 | echo 'Skipping Test...'; 158 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 159 | echo PHP_EOL; 160 | continue; 161 | } 162 | $function = $functions[$group]; 163 | $pass_group = 0; 164 | $fail_group = 0; 165 | if ($browser) echo '
      ', PHP_EOL; 166 | foreach ($tests as $test) { 167 | echo ($browser ? '
    • ' : ' - '); 168 | echo $test['description'], ' ... '; 169 | $linked = Twitter_Autolink::create($test['text'], false) 170 | ->setNoFollow(false)->setExternal(false)->setTarget('') 171 | ->setUsernameClass('tweet-url username') 172 | ->setListClass('tweet-url list-slug') 173 | ->setHashtagClass('tweet-url hashtag') 174 | ->setURLClass('') 175 | ->$function(); 176 | # XXX: Need to re-order for hashtag as it is written out differently... 177 | # We use the same wrapping function for adding links for all methods. 178 | if ($group == 'hashtags') { 179 | $linked = preg_replace(array( 180 | '!([^<]*)!', 181 | '!title="#([^"]+)"!' 182 | ), array( 183 | '$3', 184 | 'title="#$1"' 185 | ), $linked); 186 | } 187 | if ($test['expected'] == $linked) { 188 | $pass_group++; 189 | echo ($browser ? 'PASS' : "\033[1;32mPASS\033[0m"); 190 | } else { 191 | $fail_group++; 192 | echo ($browser ? 'FAIL' : "\033[1;31mFAIL\033[0m"); 193 | if ($browser) { 194 | echo '
      ';
      195 |         echo 'Original: '.htmlspecialchars($test['text'], ENT_QUOTES, 'UTF-8', false), PHP_EOL;
      196 |         echo 'Expected: '.pretty_format($test['expected']), PHP_EOL;
      197 |         echo 'Actual:   '.pretty_format($linked);
      198 |         echo '
      '; 199 | } else { 200 | echo PHP_EOL, PHP_EOL; 201 | echo ' Original: '.$test['text'], PHP_EOL; 202 | echo ' Expected: '.pretty_format($test['expected']), PHP_EOL; 203 | echo ' Actual: '.pretty_format($linked), PHP_EOL; 204 | } 205 | } 206 | if ($browser) echo '
    • '; 207 | echo PHP_EOL; 208 | } 209 | if ($browser) echo '
    '; 210 | echo PHP_EOL; 211 | $pass_total += $pass_group; 212 | $fail_total += $fail_group; 213 | echo ($browser ? '

    ' : " \033[1;33m"); 214 | printf('Group Results: %d passes, %d failures', $pass_group, $fail_group); 215 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 216 | echo PHP_EOL; 217 | } 218 | 219 | echo ($browser ? '

    ' : "\033[1m"); 220 | echo 'Hit Highlighter Conformance'; 221 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL.'---------------------------'.PHP_EOL); 222 | echo PHP_EOL; 223 | 224 | # Load the test data. 225 | $data = Spyc::YAMLLoad($DATA.'/hit_highlighting.yml'); 226 | 227 | # Define the functions to be tested. 228 | $functions = array( 229 | 'plain_text' => 'addHitHighlighting', 230 | 'with_links' => 'addHitHighlighting', 231 | ); 232 | 233 | # Perform testing. 234 | foreach ($data['tests'] as $group => $tests) { 235 | 236 | echo ($browser ? '

    ' : "\033[1m"); 237 | echo 'Test Group - '.ucfirst(str_replace('_', ' ', $group)); 238 | echo ($browser ? '

    ' : ":\033[0m".PHP_EOL); 239 | echo PHP_EOL; 240 | 241 | if (!array_key_exists($group, $functions)) { 242 | echo ($browser ? '

    ' : " \033[1;35m"); 243 | echo 'Skipping Test...'; 244 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 245 | echo PHP_EOL; 246 | continue; 247 | } 248 | $function = $functions[$group]; 249 | $pass_group = 0; 250 | $fail_group = 0; 251 | if ($browser) echo '
      ', PHP_EOL; 252 | foreach ($tests as $test) { 253 | echo ($browser ? '
    • ' : ' - '); 254 | echo $test['description'], ' ... '; 255 | $highlighted = Twitter_HitHighlighter::create($test['text'])->$function($test['hits']); 256 | if ($test['expected'] == $highlighted) { 257 | $pass_group++; 258 | echo ($browser ? 'PASS' : "\033[1;32mPASS\033[0m"); 259 | } else { 260 | $fail_group++; 261 | echo ($browser ? 'FAIL' : "\033[1;31mFAIL\033[0m"); 262 | if ($browser) { 263 | echo '
      ';
      264 |         echo 'Original: '.htmlspecialchars($test['text'], ENT_QUOTES, 'UTF-8', false), PHP_EOL;
      265 |         echo 'Expected: '.pretty_format($test['expected']), PHP_EOL;
      266 |         echo 'Actual:   '.pretty_format($highlighted);
      267 |         echo '
      '; 268 | } else { 269 | echo PHP_EOL, PHP_EOL; 270 | echo ' Original: '.$test['text'], PHP_EOL; 271 | echo ' Expected: '.pretty_format($test['expected']), PHP_EOL; 272 | echo ' Actual: '.pretty_format($highlighted), PHP_EOL; 273 | } 274 | } 275 | if ($browser) echo '
    • '; 276 | echo PHP_EOL; 277 | } 278 | if ($browser) echo '
    '; 279 | echo PHP_EOL; 280 | $pass_total += $pass_group; 281 | $fail_total += $fail_group; 282 | echo ($browser ? '

    ' : " \033[1;33m"); 283 | printf('Group Results: %d passes, %d failures', $pass_group, $fail_group); 284 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 285 | echo PHP_EOL; 286 | } 287 | 288 | echo ($browser ? '

    ' : " \033[1;36m"); 289 | printf('Total Results: %d passes, %d failures', $pass_total, $fail_total); 290 | echo ($browser ? '

    ' : "\033[0m".PHP_EOL); 291 | echo PHP_EOL; 292 | 293 | if ($browser) echo << 295 | 296 | EOHTML; 297 | --------------------------------------------------------------------------------