1<?php
2/**
3 * @file syntax.php
4 * @brief DokuWiki syntax plugin : htmlabstract
5 * @author Vincent Feltz <psycho@feltzv.fr>
6 */
7
8if (!defined('DOKU_INC'))
9	define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
10if (!defined('DOKU_PLUGIN'))
11	define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
12require_once(DOKU_PLUGIN.'syntax.php');
13
14class syntax_plugin_htmlabstract extends DokuWiki_Syntax_Plugin
15{
16	/**
17	 * return informations for plugins managing page
18	 */
19    function getInfo()
20    {
21        return array(
22            'author' => 'Vincent Feltz',
23            'email'  => 'psycho@feltzv.fr',
24            'date'   => '2016-11-07', //first version 2008-11-14
25            'name'   => 'HtmlAbstract',
26            'desc'   => 'Allows integration of remote or local DW RSS feeds using html formatted abstracts instead of choosing between html OR abstract.',
27            'url'    => 'http://www.dokuwiki.org/plugin:htmlabstract',
28        );
29    }
30
31	/**
32	 * return the type of syntax defined by this plugin
33	 */
34    function getType() {return 'substition';} // This is not a mispelling! ;) (http://www.dokuwiki.org/devel:syntax_plugins#fn__5)
35
36	/**
37	 * return when to call this plugin
38	 */
39    function getSort() {return 310;} // 310 = Doku_Parser_Mode_rss (http://www.dokuwiki.org/devel:parser:getsort_list)
40
41	/**
42	 * connect the pattern to the lexer
43	 */
44    function connectTo($mode) {$this->Lexer->addSpecialPattern('{{htmlabs>.*?}}',$mode,'plugin_htmlabstract');}
45
46	/**
47	 * handle the pattern match
48	 */
49    function handle($match, $state, $pos, &$handler)
50    {
51    	$params = $this->splitAndSortParams($match);
52		$elements = $this->getFeedElements($params);
53		if (!is_array($elements) && false !== strpos($elements, 'ERROR'))
54			return array($elements);
55		$content = $this->formatElements($elements, $params);
56    	return array($content);
57    }
58
59	/**
60	 * handle rendering output
61	 */
62    function render($mode, &$renderer, $data)
63    {
64    	$renderer->doc .= $data[0];
65		$data[0] = "";
66    	return TRUE;
67    }
68
69	/**
70	 * determines params to be used from match and configuration
71	 */
72	function splitAndSortParams($match)
73	{
74		global $conf;
75
76		$match = trim(trim($match, '{}'));
77    	$match = substr($match, strlen("htmlabs>"));
78    	if (false !== strpos($match, ' '))
79			$params['feed_url'] = trim(substr($match, 0, strpos($match, ' ')));
80		else
81			$params['feed_url'] = trim($match);
82		$match = substr($match, strlen($params['feed_url']) + 1);
83		if (substr($params['feed_url'], 0, 7) != 'http://')
84			$params['feed_url'] = DOKU_URL.$params['feed_url'];
85		if (false !== ($pos = strpos($params['feed_url'], '?')))
86		{
87			$tmp = explode('?', $params['feed_url']);
88			$params['feed_url'] = $tmp[0];
89			$params['feed_params'] = $tmp[1];
90		}
91		else
92			$params['feed_params'] = '';
93		$params['feed_params'] .= '&content=html&type=rss2';
94		$opts = explode(' ', strtolower($match));
95		$params['author'] = !in_array('noauthor', $opts);
96		$params['title'] = !in_array('notitle', $opts);
97		$params['date'] = !in_array('nodate', $opts);
98		$params['textlink'] = $this->getConf('textlink') ? $this->getConf('textlink') : $this->getLang("textlink");
99		$params['maxlen'] = $this->getConf('maxlength');
100		if ($params['maxlen'] <= 0)
101			$params['maxlen'] = 750;
102		$params['trycleancut'] = $this->getConf('paragraph');
103		$params['bg_color'] = $this->getConf('bg_color');
104		$params['unknown_author'] = $this->getLang('extern_edit');
105		return $params;
106	}
107
108	/**
109	 * get and parse elements of targeted feed
110	 */
111	function getFeedElements($params)
112	{
113		if (!($xml = @file_get_contents($params['feed_url'].'?'.$params['feed_params'])))
114			return '<b>ERROR : </b>Cannot get content from <a href="'.$params['feed_url'].'">'.$params['feed_url'].' !</a> Please check the feed URL.<br/>';
115		$dom = new DOMDocument();
116		if (false === @$dom->loadXML($xml))
117			return '<b>ERROR : </b>XML error in feed, cannot parse.<br/>';
118		$elements = array();
119		$items = $dom->getElementsByTagName('item');
120		foreach ($items as $item)
121			{
122				$element = array();
123				$details = $item->getElementsByTagName('*');
124				foreach ($details as $detail)
125					switch ($detail->nodeName)
126					{
127						case 'title'  		:
128						case 'author' 		:
129						case 'pubDate'		:
130						case 'link'			:
131						case 'description'	:
132								$element[$detail->nodeName] = $detail->nodeValue;
133							break;
134					}
135				if (!isset($element['author']))
136					$element['author'] = $params['unknown_author'];
137				$elements[] = $element;
138			}
139		return $elements;
140	}
141
142	/**
143	 * format elements to put them in a list of coloured-background previews
144	 */
145	function formatElements($elements, $params)
146	{
147		$css = ' style="background-color:#'.$params['bg_color'].'; padding: 0px 5px 5px; overflow:auto; margin-bottom: 20px;"';
148		$content = '</p><ul class="rss">'."\n";  // need to close the <p> opened by DW before plugin handling for W3C compliance (<ul> mustn't be contained by <p>)
149		$content.= '<!-- preview produced with HtmlAbstract - http://dokuwiki.org/plugin:htmlabstract --> '."\n";
150		foreach ($elements as $element)
151		{
152			$item = '<li>'."\n";
153			$item .= '<div class="li"'.$css.'>'."\n";
154			if ($params['title'])
155				$item .= '<a href="'.$element['link'].'" class="wikilink1" title="'.$element['title'].'">'.$element['title'].'</a>';
156			if ($params['author'])
157				$item .= $this->getLang('author').$element['author'];
158			if ($params['date'])
159				$item .= ' '.$this->formatDate($element['pubDate']);
160			$item .= "\n";
161			$item .= '<div class="detail">'."\n".trim($this->formatDescription($element['description'], $params))."\n".'</div>'."\n";
162			$item .= '<div class="level1" style="clear:both;"><strong><a href="'.$element['link'].'">'.$params['textlink'].'</a></strong></div>'."\n";
163			$item .= '</div>';
164			$item .= '</li>'."\n\n";
165			$content .= $item;
166		}
167		$content .= '</ul><p>';
168		return $content;
169	}
170
171//TODO à refaire pour les windowsiens!
172	/**
173	 * format feed items'dates to adapt them to local wiki config (config:dformat)
174	 */
175	function formatDate($date)
176	{
177		global $conf;
178
179		if (false !== strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') ||
180			false !== strpos($_SERVER['SERVER_SOFTWARE'], 'Win64'))
181			return $date; //strptime() is not implemented on Windows platforms, sorry!
182
183		$decomposed_date = strptime($date, '%a, %d %b %Y %H:%M:%S %z');
184		extract($decomposed_date);
185		$date = strftime($conf['dformat'],
186				mktime($tm_hour, $tm_min, $tm_sec, $tm_mon + 1, $tm_mday, 1900 + $tm_year));
187		return $date;
188	}
189
190	/**
191	 * cut abstracts to desired length, searches the cleaner cut, and close broken tags
192 	 */
193	function formatDescription($text, $params)
194	{
195		$cut = $this->cutTextToLength($text, $params['maxlen']);
196		$text = preg_replace('/<([a-z]+[^>]*) id="([^>]+)"/', '<$1 id="${2}_htmlabstract_'.microtime(true).'"', $text);
197			//time() is used here for W3C compliance (avoid duplicated id's)
198		if ($cut && $params['trycleancut'])
199		{
200			$min = ($params['maxlen'] > 400) ? (200) : ($params['maxlen'] / 2);
201			if (FALSE !== ($pos = strrpos($text, "</p>")) && $pos >= $min)
202				$text = substr($text, 0, 4 + $pos);
203		}
204		if ($cut)
205			$text .= '... ';
206		$text = $this->closeBrokenTags($text);
207		return $text;
208	}
209
210	/**
211	 * brutally cut abstract to desired length without considering html tags
212	 */
213	function cutTextToLength(&$text, $maxlen)
214	{
215		$intag = false;
216		$i = -1;
217		$len = 0;
218		$textlen = strlen($text);
219		while (++$i < $textlen)
220			if ('<' == $text[$i])
221				$intag = true;
222			elseif ('>' == $text[$i])
223				$intag = false;
224			elseif (!$intag)
225				if ($maxlen == ++$len)
226				{
227					$text = substr($text, 0, $i);
228					return true;
229				}
230		return false;
231	}
232
233	/**
234	 * search tags broken by brutal cut and close them
235	 */
236	function closeBrokenTags($text)
237	{
238		$tags = array();
239		$i = -1;
240		$textlen = strlen($text);
241		while (++$i < $textlen)
242			if ($text[$i] == '<')
243			{
244				if ($text[$i + 1] != '/' && $text[$i + 1] != '!') //opening tag
245				{
246					$j = $i;
247					while ($text[++$j] != ' ' && $text[$j] != '>');
248					array_push($tags, substr($text, $i + 1, $j - $i - 1));
249				}
250				elseif ($text[$i + 1] == '/') //closing tag
251				{
252					$j = $i + 1;
253					while ($text[++$j] != ' ' && $text[$j] != '>');
254					$closed_tag = substr($text, $i + 2, $j - $i - 2);
255					while ($tags[count($tags) - 1] != $closed_tag)
256						array_pop($tags);
257					array_pop($tags);
258				}
259			}
260		while (count($tags))
261			$text .= '</'.array_pop($tags).'>';
262		return $text;
263	}
264
265}
266?>
267