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