* @author Mykola Ostrovskyy */ /** * Class syntax_plugin_changes */ class syntax_plugin_changes extends SyntaxPlugin { /** * What kind of syntax are we? */ public function getType() { return 'substition'; } /** * What type of XHTML do we create? */ public function getPType() { return 'block'; } /** * Where to sort in? */ public function getSort() { return 105; } /** * Connect pattern to lexer * @param string $mode */ public function connectTo($mode) { $this->Lexer->addSpecialPattern('\{\{changes>[^}]*\}\}', $mode, 'plugin_changes'); } /** * Handler to prepare matched data for the rendering process * * @param string $match The text matched by the patterns * @param int $state The lexer state for the match * @param int $pos The character position of the matched text * @param Doku_Handler $handler The Doku_Handler object * @return array Return an array with all data you want to use in render */ public function handle($match, $state, $pos, Doku_Handler $handler) { $match = substr($match, 10, -2); $data = [ 'ns' => [], 'excludedpages' => [], 'count' => 10, 'type' => [], 'render' => 'list', 'render-flags' => [], 'maxage' => null, 'reverse' => false, 'user' => [], 'excludedusers' => [], ]; $match = explode('&', $match); foreach ($match as $m) { if (is_numeric($m)) { $data['count'] = (int) $m; } elseif (preg_match('/(\w+)\s*=(.+)/', $m, $temp) == 1) { $this->handleNamedParameter($temp[1], trim($temp[2]), $data); } else { $this->addNamespace($data, trim($m)); } } return $data; } /** * Handle parameters that are specified using = syntax * @param string $name * @param $value * @param array $data */ protected function handleNamedParameter($name, $value, &$data) { global $ID; static $types = ['edit' => 'E', 'create' => 'C', 'delete' => 'D', 'minor' => 'e']; static $renderers = ['list', 'pagelist']; switch ($name) { case 'count': case 'maxage': $data[$name] = (int) $value; break; case 'ns': foreach (preg_split('/\s*,\s*/', $value) as $value) { $this->addNamespace($data, $value); } break; case 'type': foreach (preg_split('/\s*,\s*/', $value) as $value) { if (array_key_exists($value, $types)) { $data[$name][] = $types[$value]; } } break; case 'render': // parse "name(flag1, flag2)" syntax if (preg_match('/(\w+)(?:\((.*)\))?/', $value, $match) == 1) { if (in_array($match[1], $renderers)) { $data[$name] = $match[1]; if (count($match) > 2) { $flags = trim($match[2]); if ($flags != '') { $data['render-flags'] = preg_split('/\s*,\s*/', $flags); } } } } break; case 'user': case 'excludedusers': foreach (preg_split('/\s*,\s*/', $value) as $value) { $data[$name][] = $value; } break; case 'excludedpages': foreach (preg_split('/\s*,\s*/', $value) as $page) { if (!empty($page)) { $data[$name][] = (new PageResolver($ID))->resolveId($page); } } break; case 'reverse': $data[$name] = (bool)$value; break; } } /** * Clean-up the namespace name and add it (if valid) into the $data array * @param array $data * @param string $namespace */ protected function addNamespace(&$data, $namespace) { if (empty($namespace)) return; $action = ($namespace[0] == '-') ? 'exclude' : 'include'; $namespace = cleanID(preg_replace('/^[+-]/', '', $namespace)); if (!empty($namespace)) { $data['ns'][$action][] = $namespace; } } /** * Handles the actual output creation. * * @param string $format output format being rendered * @param Doku_Renderer $renderer the current renderer object * @param array $data data created by handler() * @return boolean rendered correctly? */ public function render($format, Doku_Renderer $renderer, $data) { global $conf; if ($format === 'xhtml') { /* @var Doku_Renderer_xhtml $renderer */ $changes = $this->getChanges( $data['count'], $data['ns'], $data['excludedpages'], $data['type'], $data['user'], $data['maxage'], $data['excludedusers'], $data['reverse'] ); if (!count($changes)) return true; switch ($data['render']) { case 'list': $this->renderSimpleList($changes, $renderer, $data['render-flags']); break; case 'pagelist': $this->renderPageList($changes, $renderer, $data['render-flags']); break; } return true; } elseif ($format === 'metadata') { /* @var Doku_Renderer_metadata $renderer */ $renderer->meta['relation']['depends']['rendering'][$conf['changelog']] = true; $renderer->meta['relation']['depends']['rendering'][$conf['media_changelog']] = true; return true; } return false; } /** * Based on getRecents() from inc/changelog.php * * @param int $num * @param array $ns * @param array $excludedpages * @param array $type * @param array $user * @param int $maxage * @return array */ protected function getChanges($num, $ns, $excludedpages, $type, $user, $maxage, $excludedusers, $reverse) { global $conf; $changes = []; $seen = []; $count = 0; $lines = []; // Get global changelog if (file_exists($conf['changelog']) && is_readable($conf['changelog'])) { $lines = @file($conf['changelog']); } // Merge media changelog if ($this->getConf('listmedia')) { if (file_exists($conf['media_changelog']) && is_readable($conf['media_changelog'])) { $linesMedia = @file($conf['media_changelog']); // Add a tag to identiy the media lines foreach ($linesMedia as $key => $value) { $value = ChangeLog::parseLogLine($value); $value['extra'] = 'media'; $linesMedia[$key] = implode("\t", $value) . "\n"; } $lines = array_merge($lines, $linesMedia); } } if (is_null($maxage)) { $maxage = (int) $conf['recent_days'] * 60 * 60 * 24; } for ($i = count($lines) - 1; $i >= 0; $i--) { $change = $this->handleChangelogLine( $lines[$i], $ns, $excludedpages, $type, $user, $maxage, $seen, $excludedusers ); if ($change !== false) { $changes[] = $change; // break when we have enough entries if (++$count >= $num) break; } } // Date sort merged page and media changes if ($this->getConf('listmedia') || $reverse) { $dates = []; foreach ($changes as $change) { $dates[] = $change['date']; } array_multisort($dates, ($reverse ? SORT_ASC : SORT_DESC), $changes); } return $changes; } /** * Based on _handleRecent() from inc/changelog.php * * @param string $line * @param array $ns * @param array $excludedpages * @param array $type * @param array $user * @param int $maxage * @param array $seen * @return array|bool */ protected function handleChangelogLine($line, $ns, $excludedpages, $type, $user, $maxage, &$seen, $excludedusers) { // split the line into parts $change = ChangeLog::parseLogLine($line); if ($change === false) return false; // skip seen ones if (isset($seen[$change['id']])) return false; // filter type if (!empty($type) && !in_array($change['type'], $type)) return false; // filter user if (!empty($user) && (empty($change['user']) || !in_array($change['user'], $user))) return false; // remember in seen to skip additional sights $seen[$change['id']] = 1; // show only not existing pages for delete if ($change['extra'] != 'media' && $change['type'] != 'D' && !page_exists($change['id'])) return false; // filter maxage if ($maxage && $change['date'] < (time() - $maxage)) { return false; } // check if it's a hidden page if (isHiddenPage($change['id'])) return false; // filter included namespaces if (isset($ns['include'])) { if (!$this->isInNamespace($ns['include'], $change['id'])) return false; } // filter excluded namespaces if (isset($ns['exclude'])) { if ($this->isInNamespace($ns['exclude'], $change['id'])) return false; } // exclude pages if (!empty($excludedpages)) { if (in_array($change['id'], $excludedpages)) { return false; } } // exclude users if (!empty($excludedusers)) { foreach ($excludedusers as $user) { if ($change['user'] == $user) return false; } } // check ACL $change['perms'] = auth_quickaclcheck($change['id']); if ($change['perms'] < AUTH_READ) return false; return $change; } /** * Check if page belongs to one of namespaces in the list * * @param array $namespaces * @param string $id page id * @return bool */ protected function isInNamespace($namespaces, $id) { foreach ($namespaces as $ns) { if ((strpos($id, $ns . ':') === 0)) return true; } return false; } /** * Render via the Pagelist plugin * * @param $changes * @param Doku_Renderer_xhtml $renderer * @param $flags */ protected function renderPageList($changes, $renderer, $flags) { /** @var helper_plugin_pagelist $pagelist */ $pagelist = @plugin_load('helper', 'pagelist'); if ($pagelist) { $pagelist->setFlags($flags); $pagelist->startList(); foreach ($changes as $change) { if ($change['extra'] == 'media') continue; $page['id'] = $change['id']; $page['date'] = $change['date']; $page['user'] = $this->getUserName($change); $page['desc'] = $change['sum']; $pagelist->addPage($page); } $renderer->doc .= $pagelist->finishList(); } else { // Fallback to the simple list renderer $this->renderSimpleList($changes, $renderer); } } /** * Render the day header * * @param Doku_Renderer $renderer * @param int $date */ protected function dayheader($renderer, $date) { if ($renderer->getFormat() == 'xhtml') { /* @var Doku_Renderer_xhtml $renderer */ $renderer->doc .= '

'; $renderer->cdata(dformat($date, $this->getConf('dayheaderfmt'))); $renderer->doc .= '

'; } else { $renderer->header(dformat($date, $this->getConf('dayheaderfmt')), 3, 0); } } /** * Render with a simple list render * * @param array $changes * @param Doku_Renderer_xhtml $renderer * @param array $flags */ protected function renderSimpleList($changes, $renderer, $flags = []) { global $conf; $flags = $this->parseSimpleListFlags($flags); $dayheaders_date = ''; if ($flags['dayheaders']) { $dayheaders_date = date('Ymd', $changes[0]['date']); $this->dayheader($renderer, $changes[0]['date']); } $renderer->listu_open(); foreach ($changes as $change) { if ($flags['dayheaders']) { $tdate = date('Ymd', $change['date']); if ($tdate !== $dayheaders_date) { $renderer->listu_close(); // break list to insert new header $this->dayheader($renderer, $change['date']); $renderer->listu_open(); $dayheaders_date = $tdate; } } $renderer->listitem_open(1); $renderer->listcontent_open(); if (trim($change['extra']) == 'media') { $renderer->internalmedia(':' . $change['id'], null, null, null, null, null, 'linkonly'); } else { $renderer->internallink(':' . $change['id'], null, null, false, 'navigation'); } if ($flags['summary']) { $renderer->cdata(' ' . $change['sum']); } if ($flags['signature']) { $user = $this->getUserName($change); $date = strftime($conf['dformat'], $change['date']); $renderer->cdata(' '); $renderer->entity('---'); $renderer->cdata(' '); $renderer->emphasis_open(); $renderer->cdata($user . ' ' . $date); $renderer->emphasis_close(); } $renderer->listcontent_close(); $renderer->listitem_close(); } $renderer->listu_close(); } /** * Parse flags for the simple list render * * @param array $flags * @return array */ protected function parseSimpleListFlags($flags) { $outFlags = ['summary' => true, 'signature' => false, 'dayheaders' => false]; if (!empty($flags)) { foreach ($flags as $flag) { if (array_key_exists($flag, $outFlags)) { $outFlags[$flag] = true; } elseif (substr($flag, 0, 2) == 'no') { $flag = substr($flag, 2); if (array_key_exists($flag, $outFlags)) { $outFlags[$flag] = false; } } } } return $outFlags; } /** * Get username or fallback to ip * * @param array $change * @return mixed */ protected function getUserName($change) { /* @var DokuWiki_Auth_Plugin $auth */ global $auth; if (!empty($change['user'])) { $user = $auth->getUserData($change['user']); if (empty($user)) { return $change['user']; } else { return $user['name']; } } else { return $change['ip']; } } }