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?>