1<?php 2/* 3 * Twitter syntax plugin. 4 * 5 * @license GPL 2 (http://opensource.org/licenses/gpl-2.0.php) 6 * @author Christoph Lang <calbity@gmx.de> 7 * @author Mark C. Prins <mprins@users.sf.net> 8 */ 9 10/** 11 * Twitter Plugin Syntax plugin component. 12 */ 13class syntax_plugin_twitter extends DokuWiki_Syntax_Plugin { 14 15 private $_oauth_consumer_key; 16 private $_oauth_consumer_secret; 17 private $_oauth_token; 18 private $_oauth_token_secret; 19 20 private function replace($data) { 21 $sTitle = $data [1]; 22 $data = $data [0]; 23 24 $sResponse = '<div class="twtWrapper">'; 25 if (!isset($data)) { 26 return $sResponse . '<div class="error">Twitter error....</div></div>'; 27 } 28 // dbglog($data->errors,"error data"); 29 if (is_array($data->errors)) { 30 return $sResponse . '<div class="error">Twitter error...<br />' . $data->errors [0]->code . ': ' . $data->errors [0]->message . '</div></div>'; 31 } 32 33 $sResponse .= '<table class="twtEntries" >'; 34 $sResponse .= '<caption class="twtHeader">'; 35 $sResponse .= '<img class="twtLogo" src="' . DOKU_BASE . 'lib/plugins/' . $this->getPluginName() . '/bird_blue_32.png" alt=""/>'; 36 $sResponse .= $sTitle; 37 $sResponse .= '</caption>'; 38 39 foreach ($data as $entry) { 40 // dbglog($entry, "=================entry================="); 41 $text = $entry->text . " "; 42 $image = $entry->user->profile_image_url; 43 $time = $entry->created_at; 44 $time = strtotime($time); 45 $time = $this->Timesince($time); 46 $from = $entry->from_user; 47 $name = ""; 48 if (!empty($entry->user->name)) { 49 $name = $entry->user->name; 50 } 51 if (empty($from)) { 52 $from = $entry->user->screen_name; 53 } 54 $permalink = 'https://twitter.com/' . $from . '/status/' . $entry->id_str; 55 if (isset($entry->profile_image_url)) { 56 $image = $entry->profile_image_url; 57 } 58 // get links 59 $search = array( 60 '`((?:https?|ftp)://\S+[[:alnum:]]/?)`si', 61 '`((?<!//)(www\.\S+[[:alnum:]]/?))`si' 62 ); 63 $replace = array( 64 '<a href="$1" class="urlextern" target="_blank">$1</a> ', 65 '<a href="http://$1" class="urlextern" target="_blank">$1</a>' 66 ); 67 $text = preg_replace($search, $replace, $text); 68 69 // get hashtags 70 if (preg_match_all('/#(.*?)\s/', $text, $arMatches)) { 71 for ($i = 0; $i < count($arMatches [0]); $i ++) { 72 $text = str_replace($arMatches [0] [$i], '<a class="urlextern" target="_blank" href="https://twitter.com/search?q=' . $arMatches [1] [$i] . '">' . $arMatches [0] [$i] . "</a>", $text); 73 } 74 } 75 76 // get twitterer 77 if (preg_match_all('/(^| )@(.*?)\s/', $text, $arMatches)) { 78 for ($i = 0; $i < count($arMatches [0]); $i ++) { 79 $strTwitterer = preg_replace('/\W/', '', $arMatches [0] [$i]); 80 $text = str_replace($strTwitterer, '<a class="urlextern" target="_blank" href="https://twitter.com/' . $strTwitterer . '">' . $strTwitterer . "</a>", $text); 81 } 82 } 83 $sResponse .= '<tr class="twtRow">'; 84 $sResponse .= ' <td class="twtImage">' . p_render('xhtml', p_get_instructions('{{' . $image . '?48&nolink|' . $from . ' avatar}}'), $info) . '</td>'; 85 $sResponse .= ' <td class="twtMsg">' . $text . '<br/><a href="' . $permalink . '" class="urlextern twtUrlextern" target="_blank">' . sprintf($this->getLang('timestamp'), $time) . '</a> <a class="urlextern twtUrlextern" target="_blank" href="https://twitter.com/' . $from . '">' . $name . " (@" . $from . ")" . '</a></td>'; 86 $sResponse .= '</tr>'; 87 } 88 $sResponse .= '</table></div>'; 89 return $sResponse; 90 } 91 92 /** 93 * Works out the time since the entry post, takes a an argument in unix time (seconds). 94 * 95 * @param int $original unix time (seconds) 96 * @return string 97 */ 98 public function Timesince($original) { 99 $chunks = [ 100 [ 101 60 * 60 * 24 * 365, 102 $this->getLang('year'), 103 $this->getLang('years') 104 ], 105 [ 106 60 * 60 * 24 * 30, 107 $this->getLang('month'), 108 $this->getLang('months') 109 ], 110 [ 111 60 * 60 * 24 * 7, 112 $this->getLang('week'), 113 $this->getLang('weeks') 114 ], 115 [ 116 60 * 60 * 24, 117 $this->getLang('day'), 118 $this->getLang('days') 119 ], 120 [ 121 60 * 60, 122 $this->getLang('hour'), 123 $this->getLang('hours') 124 ], 125 [ 126 60, 127 $this->getLang('min'), 128 $this->getLang('mins') 129 ], 130 [ 131 1, 132 $this->getLang('sec'), 133 $this->getLang('secs') 134 ] 135 ]; 136 137 $today = time(); /* Current unix time */ 138 $since = $today - $original; 139 140 // $j saves performing the count function each time around the loop 141 for ($i = 0, $j = count($chunks); $i < $j; $i ++) { 142 $seconds = $chunks [$i] [0]; 143 $name = $chunks [$i] [1]; 144 $names = $chunks [$i] [2]; 145 // finding the biggest chunk (if the chunk fits, break) 146 if (($count = floor($since / $seconds)) != 0) { 147 break; 148 } 149 } 150 $print = ($count == 1) ? '1 ' . $name : "$count {$names}"; 151 if ($i + 1 < $j) { 152 // now getting the second item 153 $seconds2 = $chunks [$i + 1] [0]; 154 $name2 = $chunks [$i + 1] [1]; 155 $name2s = $chunks [$i + 1] [2]; 156 // add second item if its greater than 0 157 if (($count2 = floor(($since - ($seconds * $count)) / $seconds2)) != 0) { 158 $print .= ($count2 == 1) ? ', 1 ' . $name2 : ", $count2 {$name2s}"; 159 } 160 } 161 return $print; 162 } 163 164 /** 165 * Syntax patterns. 166 * (non-PHPdoc) 167 * 168 * @see Doku_Parser_Mode::connectTo() 169 */ 170 function connectTo($mode) { 171 $this->Lexer->addSpecialPattern('\[TWITTER\:USER\:.*?\]', $mode, 'plugin_twitter'); 172 $this->Lexer->addSpecialPattern('{{twitter>user\:.*?}}', $mode, 'plugin_twitter'); 173 174 $this->Lexer->addSpecialPattern('\[TWITTER\:SEARCH\:.*?\]', $mode, 'plugin_twitter'); 175 $this->Lexer->addSpecialPattern('{{twitter>search\:.*?}}', $mode, 'plugin_twitter'); 176 } 177 178 /** 179 * (non-PHPdoc) 180 * 181 * @see DokuWiki_Syntax_Plugin::getType() 182 */ 183 function getType() { 184 return 'substition'; 185 } 186 187 /** 188 * (non-PHPdoc) 189 * 190 * @see Doku_Parser_Mode::getSort() 191 */ 192 function getSort() { 193 return 314; 194 } 195 196 /** 197 * Paragraph Type. 198 * 199 * Defines how this syntax is handled regarding paragraphs. This is important 200 * for correct XHTML nesting. Should return one of the following: 201 * 202 * 'normal' - The plugin can be used inside paragraphs 203 * 'block' - Open paragraphs need to be closed before plugin output 204 * 'stack' - Special case. Plugin wraps other paragraphs. 205 * 206 * @see Doku_Handler_Block::getPType() 207 */ 208 function getPType() { 209 return 'block'; 210 } 211 212 /** 213 * Handler to prepare matched data for the rendering process. 214 * 215 * This function can only pass data to render() via its return value - render() 216 * may be not be run during the object's current life. 217 * 218 * Usually you should only need the $match param. 219 * 220 * @param string $match The text matched by the patterns 221 * @param int $state The lexer state for the match 222 * @param int $pos The character position of the matched text 223 * @param Doku_Handler $handler Reference to the Doku_Handler object 224 * @return array Return an array with all data you want to use in render 225 * 226 * @see DokuWiki_Syntax_Plugin::handle() 227 */ 228 function handle($match, $state, $pos, Doku_Handler $handler) { 229 $match = str_replace(array( 230 ">", 231 "{{", 232 "}}" 233 ), array( 234 ":", 235 "[", 236 "]" 237 ), $match); 238 $match = substr($match, 1, - 1); 239 $data = explode(":", $match); 240 241 $this->_oauth_consumer_key = $this->getConf('oauth_consumer_key'); 242 $this->_oauth_consumer_secret = $this->getConf('oauth_consumer_secret'); 243 $this->_oauth_token = $this->getConf('oauth_token'); 244 $this->_oauth_token_secret = $this->getConf('oauth_token_secret'); 245 if (empty($this->_oauth_consumer_key) || empty($this->_oauth_consumer_secret) || empty($this->_oauth_token) || empty($this->_oauth_token_secret)) { 246 msg($this->getLang('configerror'), - 1, '', '', MSG_ADMINS_ONLY); 247 dbglog($this->getLang('configerror'), "TWITTER PLUGIN"); 248 } 249 250 $number = $this->getConf('maxresults'); 251 if (isset($data [3])) { 252 $number = $data [3]; 253 } 254 $data [2] = str_replace(" ", "%20", $data [2]); 255 if (strtoupper($data [1]) == "SEARCH") { 256 $json = $this->getData("https://api.twitter.com/1.1/search/tweets.json", array( 257 'q' => $data [2], 258 'count' => $number, 259 'include_entities' => false 260 )); 261 } else { 262 $json = $this->getData("https://api.twitter.com/1.1/statuses/user_timeline.json", array( 263 'screen_name' => $data [2], 264 'count' => $number 265 )); 266 } 267 $decode = json_decode($json); 268 // dbglog($decode, "=======================decoded json from Twitter============================"); 269 if (isset($decode->search_metadata)) { 270 return array( 271 $decode->statuses, 272 $this->getLang('results') . ' <a class="urlextern" target="_blank" href="https://twitter.com/search?q=' . $data [2] . '">' . str_replace("%20", " and ", $data [2] . '</a>') 273 ); 274 } 275 return array( 276 $decode, 277 $this->getLang('header') . ' <a class="urlextern" target="_blank" href="https://twitter.com/' . $data [2] . '">@' . $data [2] . '</a>' 278 ); 279 } 280 281 /** 282 * get the data from twitter using either cURL or file_get_contents. 283 * 284 * @param String $url 285 * @return bool|string 286 */ 287 private function getData($url, $param) { 288 // dbglog($url, "Getting url from Twitter"); 289 if ($this->getConf('useCURL')) { 290 $ch = curl_init(); 291 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 292 curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; ' . PHP_OS . ')'); 293 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); 294 curl_setopt($ch, CURLOPT_URL, $this->signRequest($url, $param)); 295 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); 296 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); 297 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 298 $json = curl_exec($ch); 299 curl_close($ch); 300 } else { 301 global $conf; 302 $ctx = array( 303 'http' => array( 304 'proxy' => 'tcp:' . $conf ['proxy'] ['host'] . ':' . $conf ['proxy'] ['port'], 305 'request_fulluri' => true 306 ) 307 ); 308 $ctx = stream_context_create($ctx); 309 $json = file_get_contents($this->signRequest($url, $param), true, $ctx); 310 } 311 return $json; 312 } 313 314 /** 315 * (non-PHPdoc) 316 * 317 * @see DokuWiki_Syntax_Plugin::render() 318 */ 319 function render($mode, Doku_Renderer $renderer, $data) { 320 if ($mode == 'xhtml') { 321 // prevent caching to ensure content is always fresh 322 $renderer->info ['cache'] = false; 323 $renderer->doc .= $this->replace($data); 324 return true; 325 } elseif ($mode == 'metadata') { 326 // for metadata renderer 327 $renderer->meta ['relation'] ['haspart'] ['_plugin_twitter'] = true; 328 return true; 329 } 330 return false; 331 } 332 333 /** 334 * Generates the OAuth signed request url. 335 * 336 * @param string $endpointUrl 337 * The API endpoint to call 338 * @param array $params 339 * @return string The signed API endpoint call including the parameters 340 */ 341 private function signRequest($endpointUrl, $params = array()) { 342 $sign_params = array( 343 'oauth_consumer_key' => $this->_oauth_consumer_key, 344 'oauth_version' => '1.0', 345 'oauth_timestamp' => time(), 346 'oauth_nonce' => substr(md5(microtime(true)), 0, 16), 347 'oauth_signature_method' => 'HMAC-SHA1', 348 'oauth_token' => $this->_oauth_token 349 ); 350 351 $sign_base_params = array(); 352 foreach ($sign_params as $key => $value) { 353 $sign_base_params [$key] = $this->urlencode($value); 354 } 355 356 foreach ($params as $key => $value) { 357 $sign_base_params [$key] = $this->urlencode($value); 358 } 359 360 ksort($sign_base_params); 361 $sign_base_string = ''; 362 foreach ($sign_base_params as $key => $value) { 363 $sign_base_string .= $key . '=' . $value . '&'; 364 } 365 $sign_base_string = substr($sign_base_string, 0, - 1); 366 $signature = base64_encode(hash_hmac('sha1', ('GET&' . $this->urlencode($endpointUrl) . '&' . $this->urlencode($sign_base_string)), $this->_oauth_consumer_secret . '&' . ($this->_oauth_token_secret != null ? $this->_oauth_token_secret : ''), true)); 367 368 return $endpointUrl . '?' . $sign_base_string . '&oauth_signature=' . $this->urlencode($signature); 369 } 370 371 /** 372 * URL-encodes the data. 373 * 374 * @param mixed $data 375 * 376 * @return mixed The encoded data 377 */ 378 private function urlencode($data) { 379 if (is_array($data)) { 380 return array_map(array( 381 $this, 382 'urlencode' 383 ), $data); 384 } elseif (is_scalar($data)) { 385 return str_replace(array( 386 '+', 387 '!', 388 '*', 389 "'", 390 '(', 391 ')' 392 ), array( 393 ' ', 394 '%21', 395 '%2A', 396 '%27', 397 '%28', 398 '%29' 399 ), rawurlencode($data)); 400 } else { 401 return ''; 402 } 403 } 404 405} 406