11169a1acSAndreas Gohr<?php 2d8ce5486SAndreas Gohr 3d418c031SAndreas Gohruse dokuwiki\Extension\SyntaxPlugin; 4d8ce5486SAndreas Gohruse dokuwiki\File\PageResolver; 5ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\Node\AbstractNode; 6ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiNamespace; 7ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiStartpage; 8ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\PageTreeBuilder; 9ba73b2c8SAndreas Gohruse dokuwiki\TreeBuilder\TreeSort; 10d8ce5486SAndreas Gohr 111169a1acSAndreas Gohr/** 121169a1acSAndreas Gohr * DokuWiki Plugin simplenavi (Syntax Component) 131169a1acSAndreas Gohr * 141169a1acSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 151169a1acSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 161169a1acSAndreas Gohr */ 17d418c031SAndreas Gohrclass syntax_plugin_simplenavi extends SyntaxPlugin 18d8ce5486SAndreas Gohr{ 194c5e7fe5SAndreas Gohr protected string $ns; 204c5e7fe5SAndreas Gohr protected string $currentID; 214c5e7fe5SAndreas Gohr protected bool $usetitle; 224c5e7fe5SAndreas Gohr protected string $sort; 234c5e7fe5SAndreas Gohr protected bool $home; 248a3822efSAndreas Gohr protected int $peek = 0; 25*10f2bde6SAndreas Gohr protected bool $filter = false; 261169a1acSAndreas Gohr 27d8ce5486SAndreas Gohr /** @inheritdoc */ 28d8ce5486SAndreas Gohr public function getType() 29d8ce5486SAndreas Gohr { 301169a1acSAndreas Gohr return 'substition'; 311169a1acSAndreas Gohr } 321169a1acSAndreas Gohr 33d8ce5486SAndreas Gohr /** @inheritdoc */ 34d8ce5486SAndreas Gohr public function getPType() 35d8ce5486SAndreas Gohr { 361169a1acSAndreas Gohr return 'block'; 371169a1acSAndreas Gohr } 381169a1acSAndreas Gohr 39d8ce5486SAndreas Gohr /** @inheritdoc */ 40d8ce5486SAndreas Gohr public function getSort() 41d8ce5486SAndreas Gohr { 421169a1acSAndreas Gohr return 155; 431169a1acSAndreas Gohr } 441169a1acSAndreas Gohr 45d8ce5486SAndreas Gohr /** @inheritdoc */ 46d8ce5486SAndreas Gohr public function connectTo($mode) 47d8ce5486SAndreas Gohr { 481169a1acSAndreas Gohr $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 491169a1acSAndreas Gohr } 501169a1acSAndreas Gohr 51d8ce5486SAndreas Gohr /** @inheritdoc */ 52d8ce5486SAndreas Gohr public function handle($match, $state, $pos, Doku_Handler $handler) 53d8ce5486SAndreas Gohr { 545655937aSAndreas Gohr return explode(' ', substr($match, 13, -2)); 551169a1acSAndreas Gohr } 561169a1acSAndreas Gohr 57d8ce5486SAndreas Gohr /** @inheritdoc */ 58d8ce5486SAndreas Gohr public function render($format, Doku_Renderer $renderer, $data) 59d8ce5486SAndreas Gohr { 60d8ce5486SAndreas Gohr if ($format != 'xhtml') return false; 614c5e7fe5SAndreas Gohr $renderer->nocache(); 621169a1acSAndreas Gohr 631169a1acSAndreas Gohr global $INFO; 641169a1acSAndreas Gohr 65b3e02951SAndreas Gohr // first data is namespace, rest is options 665655937aSAndreas Gohr $ns = array_shift($data); 675655937aSAndreas Gohr if ($ns && $ns[0] === '.') { 685655937aSAndreas Gohr // resolve relative to current page 695655937aSAndreas Gohr $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 705655937aSAndreas Gohr } else { 715655937aSAndreas Gohr $ns = cleanID($ns); 725655937aSAndreas Gohr } 73b3e02951SAndreas Gohr 744c5e7fe5SAndreas Gohr $this->initState( 754c5e7fe5SAndreas Gohr $ns, 764c5e7fe5SAndreas Gohr $INFO['id'], 774c5e7fe5SAndreas Gohr (bool)$this->getConf('usetitle'), 784c5e7fe5SAndreas Gohr $this->getConf('sort'), 798a3822efSAndreas Gohr in_array('home', $data), 808a3822efSAndreas Gohr $this->getConf('peek', 0), 81*10f2bde6SAndreas Gohr in_array('filter', $data) 824c5e7fe5SAndreas Gohr ); 834c5e7fe5SAndreas Gohr 848a3822efSAndreas Gohr $tree = $this->getTree(); 85*10f2bde6SAndreas Gohr 86*10f2bde6SAndreas Gohr $class = 'plugin__simplenavi'; 87*10f2bde6SAndreas Gohr if ($this->filter) { 88*10f2bde6SAndreas Gohr $class .= ' plugin__simplenavi_filter'; 89*10f2bde6SAndreas Gohr } 90*10f2bde6SAndreas Gohr 91*10f2bde6SAndreas Gohr $renderer->doc .= '<div class="' . $class . '">'; 92ba73b2c8SAndreas Gohr $this->renderTree($renderer, $tree->getTop()); 93*10f2bde6SAndreas Gohr $renderer->doc .= '</div>'; 941169a1acSAndreas Gohr 951169a1acSAndreas Gohr return true; 961169a1acSAndreas Gohr } 971169a1acSAndreas Gohr 98d8ce5486SAndreas Gohr /** 994c5e7fe5SAndreas Gohr * Initialize the configuration state of the plugin 1004c5e7fe5SAndreas Gohr * 1014c5e7fe5SAndreas Gohr * Also used in testing 1024c5e7fe5SAndreas Gohr */ 1034c5e7fe5SAndreas Gohr public function initState( 1044c5e7fe5SAndreas Gohr string $ns, 1054c5e7fe5SAndreas Gohr string $currentID, 1064c5e7fe5SAndreas Gohr bool $usetitle, 1074c5e7fe5SAndreas Gohr string $sort, 1088a3822efSAndreas Gohr bool $home, 109*10f2bde6SAndreas Gohr int $peek = 0, 110*10f2bde6SAndreas Gohr bool $filter = false 1114c5e7fe5SAndreas Gohr ) 1124c5e7fe5SAndreas Gohr { 1134c5e7fe5SAndreas Gohr $this->ns = $ns; 1144c5e7fe5SAndreas Gohr $this->currentID = $currentID; 1154c5e7fe5SAndreas Gohr $this->usetitle = $usetitle; 1164c5e7fe5SAndreas Gohr $this->sort = $sort; 1174c5e7fe5SAndreas Gohr $this->home = $home; 1188a3822efSAndreas Gohr $this->peek = $peek; 119*10f2bde6SAndreas Gohr $this->filter = $filter; 1204c5e7fe5SAndreas Gohr } 1214c5e7fe5SAndreas Gohr 1224c5e7fe5SAndreas Gohr /** 1234c5e7fe5SAndreas Gohr * Create the tree 1244c5e7fe5SAndreas Gohr * 125ba73b2c8SAndreas Gohr * @return PageTreeBuilder 126e75a33bfSAndreas Gohr */ 1274c5e7fe5SAndreas Gohr protected function getTree(): PageTreeBuilder 128e75a33bfSAndreas Gohr { 1294c5e7fe5SAndreas Gohr $tree = new PageTreeBuilder($this->ns); 130ba73b2c8SAndreas Gohr $tree->addFlag(PageTreeBuilder::FLAG_NS_AS_STARTPAGE); 1314c5e7fe5SAndreas Gohr if ($this->home) $tree->addFlag(PageTreeBuilder::FLAG_SELF_TOP); 132ba73b2c8SAndreas Gohr $tree->setRecursionDecision(\Closure::fromCallable([$this, 'treeRecursionDecision'])); 133ba73b2c8SAndreas Gohr $tree->setNodeProcessor(\Closure::fromCallable([$this, 'treeNodeProcessor'])); 134ba73b2c8SAndreas Gohr $tree->generate(); 1354c5e7fe5SAndreas Gohr 1364c5e7fe5SAndreas Gohr switch ($this->sort) { 1374c5e7fe5SAndreas Gohr case 'id': 1384c5e7fe5SAndreas Gohr $tree->sort(TreeSort::SORT_BY_ID); 1394c5e7fe5SAndreas Gohr break; 1404c5e7fe5SAndreas Gohr case 'title': 1414c5e7fe5SAndreas Gohr $tree->sort(TreeSort::SORT_BY_TITLE); 1424c5e7fe5SAndreas Gohr break; 1434c5e7fe5SAndreas Gohr case 'ns_id': 1444c5e7fe5SAndreas Gohr $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_ID); 1454c5e7fe5SAndreas Gohr break; 1464c5e7fe5SAndreas Gohr default: 147ba73b2c8SAndreas Gohr $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_TITLE); 1484c5e7fe5SAndreas Gohr break; 1494c5e7fe5SAndreas Gohr } 1504c5e7fe5SAndreas Gohr 151ba73b2c8SAndreas Gohr return $tree; 152e75a33bfSAndreas Gohr } 153e75a33bfSAndreas Gohr 154e75a33bfSAndreas Gohr 155e75a33bfSAndreas Gohr /** 156ba73b2c8SAndreas Gohr * Callback for the PageTreeBuilder to decide if we want to recurse into a node 157d8ce5486SAndreas Gohr * 158ba73b2c8SAndreas Gohr * @param AbstractNode $node 159ba73b2c8SAndreas Gohr * @param int $depth 160ba73b2c8SAndreas Gohr * @return bool 161d8ce5486SAndreas Gohr */ 162ba73b2c8SAndreas Gohr protected function treeRecursionDecision(AbstractNode $node, int $depth): bool 163d8ce5486SAndreas Gohr { 164ba73b2c8SAndreas Gohr if ($node instanceof WikiStartpage) { 165ba73b2c8SAndreas Gohr $id = $node->getNs(); // use the namespace for startpages 166492ddc4eSAndreas Gohr } else { 167ba73b2c8SAndreas Gohr $id = $node->getId(); 168492ddc4eSAndreas Gohr } 169ba73b2c8SAndreas Gohr 1704c5e7fe5SAndreas Gohr $is_current = $this->isParent($this->currentID, $id); 171ba73b2c8SAndreas Gohr $node->setProperty('is_current', $is_current); 172ba73b2c8SAndreas Gohr 173ba73b2c8SAndreas Gohr // always recurse into the current page path 174ba73b2c8SAndreas Gohr if ($is_current) return true; 175ba73b2c8SAndreas Gohr 1768a3822efSAndreas Gohr // should we peek deeper to see if there's something readable? 1778a3822efSAndreas Gohr if ($depth < $this->peek && auth_quickaclcheck($node->getId()) < AUTH_READ) { 1788a3822efSAndreas Gohr return true; 1798a3822efSAndreas Gohr } 180ba73b2c8SAndreas Gohr 181ba73b2c8SAndreas Gohr return false; 1821169a1acSAndreas Gohr } 1831169a1acSAndreas Gohr 184d8ce5486SAndreas Gohr /** 185ba73b2c8SAndreas Gohr * Callback for the PageTreeBuilder to process a node 186d8ce5486SAndreas Gohr * 187ba73b2c8SAndreas Gohr * @param AbstractNode $node 188ba73b2c8SAndreas Gohr * @return AbstractNode|null 189d8ce5486SAndreas Gohr */ 190ba73b2c8SAndreas Gohr protected function treeNodeProcessor(AbstractNode $node): ?AbstractNode 191d8ce5486SAndreas Gohr { 192ba73b2c8SAndreas Gohr $perm = auth_quickaclcheck($node->getId()); 193ba73b2c8SAndreas Gohr $node->setProperty('permission', $perm); 1944c5e7fe5SAndreas Gohr $node->setTitle($this->getTitle($node->getId())); 195ba73b2c8SAndreas Gohr 196ba73b2c8SAndreas Gohr 197ba73b2c8SAndreas Gohr if ($node->hasChildren()) { 198ba73b2c8SAndreas Gohr // this node has children, we add it to the tree regardless of the permission 199ba73b2c8SAndreas Gohr // permissions are checked again when rendering 200ba73b2c8SAndreas Gohr return $node; 2011169a1acSAndreas Gohr } 202ba73b2c8SAndreas Gohr 203ba73b2c8SAndreas Gohr if ($perm < AUTH_READ) { 204ba73b2c8SAndreas Gohr // no children, no permission. No need to add it to the tree 205ba73b2c8SAndreas Gohr return null; 206ba73b2c8SAndreas Gohr } 207ba73b2c8SAndreas Gohr 208ba73b2c8SAndreas Gohr return $node; 209ba73b2c8SAndreas Gohr } 210ba73b2c8SAndreas Gohr 211ba73b2c8SAndreas Gohr 212ba73b2c8SAndreas Gohr /** 213*10f2bde6SAndreas Gohr * Render the tree 214ba73b2c8SAndreas Gohr * 215ba73b2c8SAndreas Gohr * @param Doku_Renderer $R The current renderer 216ba73b2c8SAndreas Gohr * @param AbstractNode $top The top node of the tree (use getTop() to get it) 217ba73b2c8SAndreas Gohr * @param int $level current nesting level, starting at 1 218ba73b2c8SAndreas Gohr * @return void 219ba73b2c8SAndreas Gohr */ 220ba73b2c8SAndreas Gohr protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1) 221ba73b2c8SAndreas Gohr { 222ba73b2c8SAndreas Gohr $R->listu_open(); 223ba73b2c8SAndreas Gohr foreach ($top->getChildren() as $node) { 224ba73b2c8SAndreas Gohr $isfolder = $node instanceof WikiNamespace; 225ba73b2c8SAndreas Gohr $incurrent = $node->getProperty('is_current', false); 226ba73b2c8SAndreas Gohr 227ba73b2c8SAndreas Gohr $R->listitem_open(1, $isfolder); 228ba73b2c8SAndreas Gohr $R->listcontent_open(); 229ba73b2c8SAndreas Gohr if ($incurrent) $R->strong_open(); 230ba73b2c8SAndreas Gohr 231ba73b2c8SAndreas Gohr if (((int)$node->getProperty('permission', 0)) < AUTH_READ) { 232ba73b2c8SAndreas Gohr $R->cdata($node->getTitle()); 233ba73b2c8SAndreas Gohr } else { 234ba73b2c8SAndreas Gohr $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation'); 235ba73b2c8SAndreas Gohr } 236ba73b2c8SAndreas Gohr 237ba73b2c8SAndreas Gohr if ($incurrent) $R->strong_close(); 238ba73b2c8SAndreas Gohr $R->listcontent_close(); 239ba73b2c8SAndreas Gohr if ($node->hasChildren()) { 240ba73b2c8SAndreas Gohr $this->renderTree($R, $node, $level + 1); 241ba73b2c8SAndreas Gohr } 242ba73b2c8SAndreas Gohr $R->listitem_close(); 243ba73b2c8SAndreas Gohr } 244ba73b2c8SAndreas Gohr $R->listu_close(); 2451169a1acSAndreas Gohr } 2461169a1acSAndreas Gohr 247d8ce5486SAndreas Gohr /** 248ba73b2c8SAndreas Gohr * Check if the given parent ID is a parent of the child ID 249d8ce5486SAndreas Gohr * 250ba73b2c8SAndreas Gohr * @param string $child 251ba73b2c8SAndreas Gohr * @param string $parent 252d8ce5486SAndreas Gohr * @return bool 253d8ce5486SAndreas Gohr */ 254ba73b2c8SAndreas Gohr protected function isParent(string $child, string $parent) 255d8ce5486SAndreas Gohr { 256ea3588fbSAndreas Gohr (aider) // Empty parent is considered a parent of all pages 257ea3588fbSAndreas Gohr (aider) if ($parent === '') { 258ea3588fbSAndreas Gohr (aider) return true; 259ea3588fbSAndreas Gohr (aider) } 260ea3588fbSAndreas Gohr (aider) 261ba73b2c8SAndreas Gohr $child = explode(':', $child); 262ba73b2c8SAndreas Gohr $parent = explode(':', $parent); 263ba73b2c8SAndreas Gohr return array_slice($child, 0, count($parent)) === $parent; 2641169a1acSAndreas Gohr } 2651169a1acSAndreas Gohr 266d418c031SAndreas Gohr 267d418c031SAndreas Gohr /** 268d8ce5486SAndreas Gohr * Get the title for the given page ID 269d8ce5486SAndreas Gohr * 270d8ce5486SAndreas Gohr * @param string $id 271d8ce5486SAndreas Gohr * @return string 272d8ce5486SAndreas Gohr */ 2734c5e7fe5SAndreas Gohr protected function getTitle($id) 274d8ce5486SAndreas Gohr { 275e306992cSAndreas Gohr global $conf; 276e306992cSAndreas Gohr 2774c5e7fe5SAndreas Gohr if ($this->usetitle) { 278e306992cSAndreas Gohr $p = p_get_first_heading($id); 279303e1405SMichael Große if (!empty($p)) return $p; 280e75a33bfSAndreas Gohr } 281e306992cSAndreas Gohr 282e306992cSAndreas Gohr $p = noNS($id); 283d8ce5486SAndreas Gohr if ($p == $conf['start'] || !$p) { 284e306992cSAndreas Gohr $p = noNS(getNS($id)); 285d8ce5486SAndreas Gohr if (!$p) { 286e306992cSAndreas Gohr return $conf['start']; 287e306992cSAndreas Gohr } 288e306992cSAndreas Gohr } 289e306992cSAndreas Gohr return $p; 290e306992cSAndreas Gohr } 2911169a1acSAndreas Gohr} 292