1<?php 2/** 3 * Changes Plugin: List the most recent changes of the wiki 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 * @author Mykola Ostrovskyy <spambox03@mail.ru> 8 */ 9 10/** 11 * Class syntax_plugin_changes 12 */ 13class syntax_plugin_changes extends DokuWiki_Syntax_Plugin 14{ 15 /** 16 * What kind of syntax are we? 17 */ 18 public function getType() 19 { 20 return 'substition'; 21 } 22 23 /** 24 * What type of XHTML do we create? 25 */ 26 public function getPType() 27 { 28 return 'block'; 29 } 30 31 /** 32 * Where to sort in? 33 */ 34 public function getSort() 35 { 36 return 105; 37 } 38 39 /** 40 * Connect pattern to lexer 41 * @param string $mode 42 */ 43 public function connectTo($mode) 44 { 45 $this->Lexer->addSpecialPattern('\{\{changes>[^}]*\}\}', $mode, 'plugin_changes'); 46 } 47 48 /** 49 * Handler to prepare matched data for the rendering process 50 * 51 * @param string $match The text matched by the patterns 52 * @param int $state The lexer state for the match 53 * @param int $pos The character position of the matched text 54 * @param Doku_Handler $handler The Doku_Handler object 55 * @return array Return an array with all data you want to use in render 56 */ 57 public function handle($match, $state, $pos, Doku_Handler $handler) 58 { 59 $match = substr($match, 10, -2); 60 61 $data = [ 62 'ns' => [], 63 'excludedpages' => [], 64 'count' => 10, 65 'type' => [], 66 'render' => 'list', 67 'render-flags' => [], 68 'maxage' => null, 69 'reverse' => false, 70 'user' => [], 71 'excludedusers' => [], 72 ]; 73 74 $match = explode('&', $match); 75 foreach ($match as $m) { 76 if (is_numeric($m)) { 77 $data['count'] = (int) $m; 78 } else { 79 if (preg_match('/(\w+)\s*=(.+)/', $m, $temp) == 1) { 80 $this->handleNamedParameter($temp[1], trim($temp[2]), $data); 81 } else { 82 $this->addNamespace($data, trim($m)); 83 } 84 } 85 } 86 87 return $data; 88 } 89 90 /** 91 * Handle parameters that are specified using <name>=<value> syntax 92 * @param string $name 93 * @param $value 94 * @param array $data 95 */ 96 protected function handleNamedParameter($name, $value, &$data) 97 { 98 global $ID; 99 100 static $types = array('edit' => 'E', 'create' => 'C', 'delete' => 'D', 'minor' => 'e'); 101 static $renderers = array('list', 'pagelist'); 102 103 switch ($name) { 104 case 'count': 105 case 'maxage': 106 $data[$name] = intval($value); 107 break; 108 case 'ns': 109 foreach (preg_split('/\s*,\s*/', $value) as $value) { 110 $this->addNamespace($data, $value); 111 } 112 break; 113 case 'type': 114 foreach (preg_split('/\s*,\s*/', $value) as $value) { 115 if (array_key_exists($value, $types)) { 116 $data[$name][] = $types[$value]; 117 } 118 } 119 break; 120 case 'render': 121 // parse "name(flag1, flag2)" syntax 122 if (preg_match('/(\w+)(?:\((.*)\))?/', $value, $match) == 1) { 123 if (in_array($match[1], $renderers)) { 124 $data[$name] = $match[1]; 125 $flags = trim($match[2]); 126 if ($flags != '') { 127 $data['render-flags'] = preg_split('/\s*,\s*/', $flags); 128 } 129 } 130 } 131 break; 132 case 'user': 133 case 'excludedusers': 134 foreach (preg_split('/\s*,\s*/', $value) as $value) { 135 $data[$name][] = $value; 136 } 137 break; 138 case 'excludedpages': 139 foreach (preg_split('/\s*,\s*/', $value) as $page) { 140 if (!empty($page)) { 141 resolve_pageid(getNS($ID), $page, $exists); 142 $data[$name][] = $page; 143 } 144 } 145 break; 146 case 'reverse': 147 $data[$name] = (bool)$value; 148 break; 149 } 150 } 151 152 /** 153 * Clean-up the namespace name and add it (if valid) into the $data array 154 * @param array $data 155 * @param string $namespace 156 */ 157 protected function addNamespace(&$data, $namespace) 158 { 159 if (empty($namespace)) return; 160 $action = ($namespace[0] == '-') ? 'exclude' : 'include'; 161 $namespace = cleanID(preg_replace('/^[+-]/', '', $namespace)); 162 if (!empty($namespace)) { 163 $data['ns'][$action][] = $namespace; 164 } 165 } 166 167 /** 168 * Handles the actual output creation. 169 * 170 * @param string $mode output format being rendered 171 * @param Doku_Renderer $R the current renderer object 172 * @param array $data data created by handler() 173 * @return boolean rendered correctly? 174 */ 175 public function render($mode, Doku_Renderer $R, $data) 176 { 177 if ($mode === 'xhtml') { 178 /* @var Doku_Renderer_xhtml $R */ 179 $R->info['cache'] = false; 180 $changes = $this->getChanges( 181 $data['count'], 182 $data['ns'], 183 $data['excludedpages'], 184 $data['type'], 185 $data['user'], 186 $data['maxage'], 187 $data['excludedusers'], 188 $data['reverse'] 189 ); 190 if (!count($changes)) return true; 191 192 switch ($data['render']) { 193 case 'list': 194 $this->renderSimpleList($changes, $R, $data['render-flags']); 195 break; 196 case 'pagelist': 197 $this->renderPageList($changes, $R, $data['render-flags']); 198 break; 199 } 200 return true; 201 } elseif ($mode === 'metadata') { 202 /* @var Doku_Renderer_metadata $R */ 203 global $conf; 204 $R->meta['relation']['depends']['rendering'][$conf['changelog']] = true; 205 return true; 206 } 207 return false; 208 } 209 210 /** 211 * Based on getRecents() from inc/changelog.php 212 * 213 * @param int $num 214 * @param array $ns 215 * @param array $excludedpages 216 * @param array $type 217 * @param array $user 218 * @param int $maxage 219 * @return array 220 */ 221 protected function getChanges($num, $ns, $excludedpages, $type, $user, $maxage, $excludedusers, $reverse) 222 { 223 global $conf; 224 $changes = array(); 225 $seen = array(); 226 $count = 0; 227 $lines = array(); 228 229 // Get global changelog 230 if (file_exists($conf['changelog']) && is_readable($conf['changelog'])) { 231 $lines = @file($conf['changelog']); 232 } 233 234 // Merge media changelog 235 if ($this->getConf('listmedia')) { 236 if (file_exists($conf['media_changelog']) && is_readable($conf['media_changelog'])) { 237 $linesMedia = @file($conf['media_changelog']); 238 // Add a tag to identiy the media lines 239 foreach ($linesMedia as $key => $value) { 240 $value = parseChangelogLine($value); 241 $value['extra'] = 'media'; 242 $linesMedia[$key] = implode("\t", $value) . "\n"; 243 } 244 $lines = array_merge($lines, $linesMedia); 245 } 246 } 247 248 if (is_null($maxage)) { 249 $maxage = (int) $conf['recent_days'] * 60 * 60 * 24; 250 } 251 252 for ($i = count($lines) - 1; $i >= 0; $i--) { 253 $change = $this->handleChangelogLine( 254 $lines[$i], 255 $ns, 256 $excludedpages, 257 $type, 258 $user, 259 $maxage, 260 $seen, 261 $excludedusers 262 ); 263 if ($change !== false) { 264 $changes[] = $change; 265 // break when we have enough entries 266 if (++$count >= $num) break; 267 } 268 } 269 270 // Date sort merged page and media changes 271 if ($this->getConf('listmedia') || $reverse) { 272 $dates = array(); 273 foreach ($changes as $change) { 274 $dates[] = $change['date']; 275 } 276 array_multisort($dates, ($reverse ? SORT_ASC : SORT_DESC), $changes); 277 } 278 279 return $changes; 280 } 281 282 /** 283 * Based on _handleRecent() from inc/changelog.php 284 * 285 * @param string $line 286 * @param array $ns 287 * @param array $excludedpages 288 * @param array $type 289 * @param array $user 290 * @param int $maxage 291 * @param array $seen 292 * @return array|bool 293 */ 294 protected function handleChangelogLine($line, $ns, $excludedpages, $type, $user, $maxage, &$seen, $excludedusers) 295 { 296 // split the line into parts 297 $change = parseChangelogLine($line); 298 if ($change === false) return false; 299 300 // skip seen ones 301 if (isset($seen[$change['id']])) return false; 302 303 // filter type 304 if (!empty($type) && !in_array($change['type'], $type)) return false; 305 306 // filter user 307 if (!empty($user) && (empty($change['user']) || !in_array($change['user'], $user))) return false; 308 309 // remember in seen to skip additional sights 310 $seen[$change['id']] = 1; 311 312 // show only not existing pages for delete 313 if ($change['extra'] != 'media' && $change['type'] != 'D' && !page_exists($change['id'])) return false; 314 315 // filter maxage 316 if ($maxage && $change['date'] < (time() - $maxage)) { 317 return false; 318 } 319 320 // check if it's a hidden page 321 if (isHiddenPage($change['id'])) return false; 322 323 // filter included namespaces 324 if (isset($ns['include'])) { 325 if (!$this->isInNamespace($ns['include'], $change['id'])) return false; 326 } 327 328 // filter excluded namespaces 329 if (isset($ns['exclude'])) { 330 if ($this->isInNamespace($ns['exclude'], $change['id'])) return false; 331 } 332 // exclude pages 333 if (!empty($excludedpages)) { 334 foreach ($excludedpages as $page) { 335 if ($change['id'] == $page) return false; 336 } 337 } 338 339 // exclude users 340 if (!empty($excludedusers)) { 341 foreach ($excludedusers as $user) { 342 if ($change['user'] == $user) return false; 343 } 344 } 345 346 // check ACL 347 $change['perms'] = auth_quickaclcheck($change['id']); 348 if ($change['perms'] < AUTH_READ) return false; 349 350 return $change; 351 } 352 353 /** 354 * Check if page belongs to one of namespaces in the list 355 * 356 * @param array $namespaces 357 * @param string $id page id 358 * @return bool 359 */ 360 protected function isInNamespace($namespaces, $id) 361 { 362 foreach ($namespaces as $ns) { 363 if ((strpos($id, $ns . ':') === 0)) return true; 364 } 365 return false; 366 } 367 368 /** 369 * Render via the Pagelist plugin 370 * 371 * @param $changes 372 * @param Doku_Renderer_xhtml $R 373 * @param $flags 374 */ 375 protected function renderPageList($changes, &$R, $flags) 376 { 377 /** @var helper_plugin_pagelist $pagelist */ 378 $pagelist = @plugin_load('helper', 'pagelist'); 379 if ($pagelist) { 380 $pagelist->setFlags($flags); 381 $pagelist->startList(); 382 foreach ($changes as $change) { 383 if ($change['extra'] == 'media') continue; 384 $page['id'] = $change['id']; 385 $page['date'] = $change['date']; 386 $page['user'] = $this->getUserName($change); 387 $page['desc'] = $change['sum']; 388 $pagelist->addPage($page); 389 } 390 $R->doc .= $pagelist->finishList(); 391 } else { 392 // Fallback to the simple list renderer 393 $this->renderSimpleList($changes, $R); 394 } 395 } 396 397 /** 398 * Render the day header 399 * 400 * @param Doku_Renderer $R 401 * @param int $date 402 */ 403 protected function dayheader(&$R, $date) 404 { 405 if ($R->getFormat() == 'xhtml') { 406 /* @var Doku_Renderer_xhtml $R */ 407 $R->doc .= '<h3 class="changes">'; 408 $R->cdata(dformat($date, $this->getConf('dayheaderfmt'))); 409 $R->doc .= '</h3>'; 410 } else { 411 $R->header(dformat($date, $this->getConf('dayheaderfmt')), 3, 0); 412 } 413 } 414 415 /** 416 * Render with a simple list render 417 * 418 * @param array $changes 419 * @param Doku_Renderer_xhtml $R 420 * @param null $flags 421 */ 422 protected function renderSimpleList($changes, &$R, $flags = null) 423 { 424 global $conf; 425 $flags = $this->parseSimpleListFlags($flags); 426 427 $dayheaders_date = ''; 428 if ($flags['dayheaders']) { 429 $dayheaders_date = date('Ymd', $changes[0]['date']); 430 $this->dayheader($R, $changes[0]['date']); 431 } 432 433 $R->listu_open(); 434 foreach ($changes as $change) { 435 if ($flags['dayheaders']) { 436 $tdate = date('Ymd', $change['date']); 437 if ($tdate != $dayheaders_date) { 438 $R->listu_close(); // break list to insert new header 439 $this->dayheader($R, $change['date']); 440 $R->listu_open(); 441 $dayheaders_date = $tdate; 442 } 443 } 444 445 $R->listitem_open(1); 446 $R->listcontent_open(); 447 if (trim($change['extra']) == 'media') { 448 $R->internalmedia(':' . $change['id'], null, null, null, null, null, 'linkonly'); 449 } else { 450 $R->internallink(':' . $change['id'], null, null, false, 'navigation'); 451 } 452 if ($flags['summary']) { 453 $R->cdata(' ' . $change['sum']); 454 } 455 if ($flags['signature']) { 456 $user = $this->getUserName($change); 457 $date = strftime($conf['dformat'], $change['date']); 458 $R->cdata(' '); 459 $R->entity('---'); 460 $R->cdata(' '); 461 $R->emphasis_open(); 462 $R->cdata($user . ' ' . $date); 463 $R->emphasis_close(); 464 } 465 $R->listcontent_close(); 466 $R->listitem_close(); 467 } 468 $R->listu_close(); 469 } 470 471 /** 472 * Parse flags for the simple list render 473 * 474 * @param array $flags 475 * @return array 476 */ 477 protected function parseSimpleListFlags($flags) 478 { 479 $outFlags = array('summary' => true, 'signature' => false, 'dayheaders' => false); 480 if (!empty($flags)) { 481 foreach ($flags as $flag) { 482 if (array_key_exists($flag, $outFlags)) { 483 $outFlags[$flag] = true; 484 } elseif (substr($flag, 0, 2) == 'no') { 485 $flag = substr($flag, 2); 486 if (array_key_exists($flag, $outFlags)) { 487 $outFlags[$flag] = false; 488 } 489 } 490 } 491 } 492 return $outFlags; 493 } 494 495 /** 496 * Get username or fallback to ip 497 * 498 * @param array $change 499 * @return mixed 500 */ 501 protected function getUserName($change) 502 { 503 /* @var DokuWiki_Auth_Plugin $auth */ 504 global $auth; 505 if (!empty($change['user'])) { 506 $user = $auth->getUserData($change['user']); 507 if (empty($user)) { 508 return $change['user']; 509 } else { 510 return $user['name']; 511 } 512 } else { 513 return $change['ip']; 514 } 515 } 516} 517