1<?php
2/**
3 * bliki Plugin: Adds a simple blogging engine to your wiki
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Beau Lebens <beau@dentedreality.com.au>
7 * @author		Anthony Caetano <Anthony.Caetano@Sanlam.co.za>
8 * 2011-10-31  modified by Taggic to get is work with current dokuwiki (Rincewind)
9 */
10
11if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
13require_once(DOKU_PLUGIN.'syntax.php');
14
15/**
16 * All DokuWiki plugins to extend the parser/rendering mechanism
17 * need to inherit from this class
18 */
19class syntax_plugin_bliki extends DokuWiki_Syntax_Plugin {
20
21    /**
22     * return some info
23     */
24    function getInfo(){
25        return array(
26            'author' => 'Beau Lebens',
27            'email'  => 'beau@dentedreality.com.au',
28            'date'   => '2011-10-31',
29            'name'   => 'Bliki: The Wiki Blog',
30            'desc'   => 'Adds basic blogging functionality to any page of your wiki.',
31            'url'    => 'http://www.dokuwiki.org/plugin:bliki',
32        );
33    }
34
35    /**
36     * What kind of syntax are we?
37     */
38    function getType(){
39        return 'substition';
40    }
41
42    /**
43     * What kind of syntax do we allow (optional)
44     */
45    function getAllowedTypes() {
46        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
47    }
48
49    /**
50     * What about paragraphs? (optional)
51     */
52    function getPType(){
53        return 'block';
54    }
55
56    /**
57     * Where to sort in?
58     */
59    function getSort(){
60        return 400;
61    }
62
63
64    /**
65     * Connect pattern to lexer
66     */
67    function connectTo($mode) {
68      $this->Lexer->addSpecialPattern('~~BLIKI~~', $mode, 'plugin_bliki');
69    }
70
71    /**
72     * Handle the match
73     */
74    function handle($match, $state, $pos, &$handler){
75		return array();
76    }
77
78    /**
79    * @return Array
80    * @param String $ID
81    * @param Int $num
82    * @param Int $offset
83    * @desc Finds the full pathnames of the most recent $num posts, starting at the optional within the $ID blog/namespace.
84    */
85    function getPosts($ID, $num, $offset = 0) {
86		global $conf;
87		$recents = array();
88		$counter = 0;
89
90		// fully-qaulify the ID that we're working with (to dig into the namespace)
91		$fp = wikiFN($ID);
92		$ID = substr($fp, 0, strrpos($fp, '.'));
93
94		// Only do it if the namespace exists
95		if (is_dir($ID . '/')) {
96			if ($this->getConf('structure') == 'flat') {
97				$posts = $this->read_dir_to_array($ID . '/', 'file', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.txt$/');
98				sort($posts);
99
100				while (sizeof($recents) < $num && sizeof($posts)) {
101					$post = array_pop($posts);
102					$counter++;
103					if ($counter > $offset) {
104						$recents[] = $ID . '/' . $post;
105					}
106				}
107
108				return $recents;
109			}
110			else {   // $this->getConf('structure') == 'deep'
111				$years = $this->read_dir_to_array($ID . '/', 'dir');
112				sort($years);
113
114				// Now start working backwards through it all and get the most recent posts
115				while (sizeof($recents) < $num && sizeof($years)) {
116					$year = array_pop($years);
117					$months = $this->read_dir_to_array($ID . '/' . $year . '/', 'dir');
118					sort($months);
119
120					while (sizeof($recents) < $num && sizeof($months)) {
121						$month = array_pop($months);
122						$days = $this->read_dir_to_array($ID . '/' . $year . '/' . $month . '/', 'dir');
123						sort($days);
124
125						while (sizeof($recents) < $num && sizeof($days)) {
126							$day = array_pop($days);
127							$posts = $this->read_dir_to_array($ID . '/' . $year . '/' . $month . '/' . $day . '/',
128							                           'file',
129							                           '/^[0-9]{6}\.txt$/');
130							sort($posts);
131
132							while (sizeof($recents) < $num && sizeof($posts)) {
133								$post = array_pop($posts);
134								$counter++;
135								if ($counter > $offset) {
136									$recents[] = $ID . '/' . $year . '/' . $month . '/' . $day . '/' . $post;
137								}
138							}
139						}
140					}
141				}
142				return $recents;
143			}
144		}
145    }
146
147    /**
148    * @return String
149    * @param Array $list
150    * @desc Compiles the contents of all the files listed (as fully-qualified paths)
151    *       in $list into a single string. Compiles in the order listed. Adds date headers
152    *       where required and a footer after each post.
153    */
154    function compilePosts($list) {
155    	global $ID, $conf;
156
157    	if (sizeof($list)) {
158    		$last_date = false;
159    		$str = '';
160
161    		foreach ($list as $file) {
162    			// Decide if we need to add a date divider
163    			$file = str_replace('\\', '/', $file);
164            $ts = $this->getTimestampFromFile($file);
165    			$date = date('Y-m-d', $ts);
166    			if ($date != $last_date) {
167    				$str .= $this->getDateHeader($ts);
168    				$last_date = $date;
169    			}
170
171    			// Add this file's contents to the output
172    			$str .= file_get_contents($file);
173
174    			// And add a wiki-formatted footer of meta data as well, accounting for rewrites
175    			$post_url = $this->getUrlPartFromTimestamp($ID, $ts);
176   		   $edit_url = $this->getRewriteUrl($post_url, 'do=edit', false);
177
178    			$timestamp = date($this->getConf('datefooter'), $ts);
179    			$str .= str_replace(array('{timestamp}', '{permalink}', '{edit}'), array($timestamp, $post_url, "this>$edit_url"), $this->getConf('footer'));
180    		}
181    		return $str;
182    	}
183    	else {
184    		return '';
185    	}
186    }
187
188    /**
189    * @return String
190    * @param timestamp $ts
191    * @desc Returns a wiki-formatted date header for between posts.
192    */
193    function getDateHeader($ts) {
194    	global $conf;
195
196    	$date = date($this->getConf('dateheader'), $ts);
197    	return $date . "\n";
198    }
199
200    /**
201    * @return timestamp
202    * @param String $filename
203    * @desc Returns a timestamp based on the filename/namespace structure
204    */
205    function getTimestampFromFile($file) {
206		global $conf;
207
208		if ($this->getConf('structure') == 'flat') {
209			$parts = explode('-', basename($file));
210			$ts = mktime(substr($parts[3], 0, 2), substr($parts[3], 2, 2), substr($parts[3], 4, 2), $parts[1], $parts[2], $parts[0]);
211		}
212		else {  // $this->getConf('structure') == 'deep'
213			$parts = explode('/', dirname($file));
214			$s = sizeof($parts);
215			$date = $parts[$s-3] . '-' . $parts[$s-2] . '-' . $parts[$s-1];
216			$filename = basename($file);
217			$ts = mktime(substr($filename, 0, 2), substr($filename, 2, 2), substr($filename, 4, 2), $parts[$s-2], $parts[$s-1], $parts[$s-3]);
218		}
219    	return $ts;
220    }
221
222    /**
223    * @return String
224    * @param String $ID
225    * @param timestamp $ts
226    * @desc Returns a post url for a post based on the post's timestamp and the base ID
227    */
228    function getUrlPartFromTimestamp($ID, $ts=0) {
229		global $conf;
230
231      if ($ts == 0) {
232          $ts = time();
233      }
234
235/*      if ($conf['userewrite'] > 0) {
236 		    $sep = ($conf['useslash'] == true ? '/' : ':');
237      }
238      else {      */
239          $sep = ':';
240//      }
241
242      if ($this->getConf('structure') == 'flat') {
243          return $ID . $sep . date('Y-m-d-His', $ts);
244      }
245      else { // $this->getConf('structure') == 'deep'
246  			return $ID . $sep . date('Y' . $sep . 'm' . $sep . 'd' . $sep . 'His', $ts);
247      }
248    }
249
250    /**
251    * @return String
252    * @param String $url
253    * @param String $query
254    * @desc Returns a url properly prefixed according to the $conf['rewrite'] option
255    *       A $page is an appropriately constructed (namespace inclusive and considering $conf['useslash']) page reference
256    *       A $query contains a query string parameters to append
257    */
258    function getRewriteUrl($page, $query, $base = true) {
259    	global $conf;
260
261    	if ($conf['userewrite'] == 0) {
262			if ($base) {
263				$str = DOKU_BASE;
264			}
265			$str .= DOKU_SCRIPT . '?id=' . $page;
266			if ($query != '') {
267				$str .=  '&' . $query;
268			}
269		}
270      else if ($conf['userewrite'] == 1) {
271			if ($base) {
272				$str = DOKU_BASE;
273			}
274			$str .= idfilter($page, false);
275			if ($query != '') {
276				$str .=  '?' . $query;
277			}
278		}
279      else {
280			if ($base) {
281				$str = DOKU_BASE;
282			}
283			$str .= DOKU_SCRIPT . '/' . idfilter($page, false);
284			if ($query != '') {
285				$str .=  '?' . $query;
286			}
287      }
288      return $str;
289    }
290
291    /**
292    * @return String
293    * @param String $label
294    * @desc Creates the HTML required for the "New Post" link, using the lable provided.
295    */
296    function newPostLink($label) {
297    	global $conf, $ID;
298
299//    	$sep = ($conf['useslash'] == true ? '/' : ':');
300      $sep = ':';
301   	                                                   //+ (isset($this->getConf('offset')) ? ($this->getConf('offset') * 3600) : 0)
302   	  $page = $this->getUrlPartFromTimestamp($ID, time());
303    	$html = '<div id="blognew">';
304    	$hilf = $this->getRewriteUrl($page, 'do=edit');
305      $output = '<a href="'.$hilf.'">' . $label . '</a>';
306		  $html .= $output.'</div>';
307
308		return $html;
309    }
310
311    /**
312    * @return String
313    * @param Int $page
314    * @param String $label
315    * @desc Creates the HTML required for a link to an older/newer page of posts.
316    */
317    function pagingLink($page, $label) {
318    	global $conf, $ID;
319
320    	$html = '<a href="';
321   	$html .= $this->getRewriteUrl($ID, "page=$page");
322		$html .= '">' . $label . '</a>';
323
324		return $html;
325    }
326
327    /**
328	 * @return Array
329	 * @param String $dir
330	 * @param String $select
331	 * @param String $match
332	 * @desc Reads all entries in a directory into an array, sorted alpha, dirs then files.
333	 *       $select is used to selects either only dir(ectories), file(s) or both
334	 *       If $match is supplied, it should be a / delimited regex to match the filename against
335	 */
336	function read_dir_to_array($dir, $select = 'both', $match = false) {
337		$files = array();
338		$dirs  = array();
339
340		// Read all the entries in the directory specified
341		$handle = @opendir($dir);
342		if (!$handle) {
343			return false;
344		}
345		while ($file = @readdir($handle)) {
346			// Ignore self and parent references
347			if ($file != '.' && $file != '..') {
348				if (($select == 'both' || $select == 'dir') && is_dir($dir . $file)) {
349					$dirs[] = $file;
350				}
351				else if (($select == 'both' || $select == 'file') && !is_dir($dir . $file)) {
352					if (is_string($match)) {
353						if (!preg_match($match, $file)) {
354							continue;
355						}
356					}
357					$files[] = $file;
358				}
359			}
360		}
361		@closedir($handle);
362
363		// Sort anything found alphabetically and combine the results (dirs->files)
364		if (sizeof($dirs) > 0) {
365			sort($dirs, SORT_STRING);
366		}
367		if (sizeof($files) > 0) {
368			sort($files, SORT_STRING);
369		}
370
371		// Put the directories and files back together and return them
372		return array_merge($dirs, $files);
373	}
374
375    /**
376     * Create output
377     */
378    function render($mode, &$renderer, $data) {
379    	global $ID, $conf;
380
381    	// Set the page number (determines which posts we display)
382    	if (isset($_REQUEST['page'])) {
383    		$page = $_REQUEST['page'];
384    	}
385    	else {
386    		$page = 0;
387    	}
388
389		if ($mode == 'xhtml') {
390			// Addlink for creating a new post
391			$renderer->doc .= $this->newPostLink($this->getConf('newlabel'));
392
393			// Go and get the required blog posts and compile them into one wikitext string
394			// FIXME $config var for how many? or inline directive?
395			$recents = $this->getPosts($ID, $this->getConf('numposts'), ($page * $this->getConf('numposts')));
396			$compiled = $this->compilePosts($recents);
397
398			// Disable section editing to avoid weird links
399			$conf['maxseclevel'] = 0;
400
401			// Disbale caching because we need to get new files always
402			$renderer->info['cache'] = false;
403
404			// Add the compiled blog posts after rendering them.
405			$renderer->doc .= p_render('xhtml', p_get_instructions($compiled), $info);
406
407			// Add a link to older entries if we filled the number per page (assuming there's more)
408			if (sizeof($recents) == $this->getConf('numposts')) {
409				$renderer->doc .= '<div id="blogolder">' . $this->pagingLink($page+1, $this->getConf('olderlabel')) . '</div>';
410			}
411
412			// And also a link to newer posts if we're not on page 0
413			if ($page != 0) {
414				$renderer->doc .= '<div id="blognewer">' . $this->pagingLink($page-1, $this->getConf('newerlabel')) . '</div>';
415			}
416
417			return true;
418		}
419		return false;
420		}
421}
422?>