1<?php 2 3use dokuwiki\Extension\SyntaxPlugin; 4use dokuwiki\File\PageResolver; 5use dokuwiki\TreeBuilder\Node\AbstractNode; 6use dokuwiki\TreeBuilder\Node\WikiNamespace; 7use dokuwiki\TreeBuilder\Node\WikiStartpage; 8use dokuwiki\TreeBuilder\PageTreeBuilder; 9use dokuwiki\TreeBuilder\TreeSort; 10 11/** 12 * DokuWiki Plugin simplenavi (Syntax Component) 13 * 14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 15 * @author Andreas Gohr <gohr@cosmocode.de> 16 */ 17class syntax_plugin_simplenavi extends SyntaxPlugin 18{ 19 protected string $ns; 20 protected string $currentID; 21 protected bool $usetitle; 22 protected string $sort; 23 protected bool $home; 24 protected int $peek=0; 25 26 /** @inheritdoc */ 27 public function getType() 28 { 29 return 'substition'; 30 } 31 32 /** @inheritdoc */ 33 public function getPType() 34 { 35 return 'block'; 36 } 37 38 /** @inheritdoc */ 39 public function getSort() 40 { 41 return 155; 42 } 43 44 /** @inheritdoc */ 45 public function connectTo($mode) 46 { 47 $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 48 } 49 50 /** @inheritdoc */ 51 public function handle($match, $state, $pos, Doku_Handler $handler) 52 { 53 return explode(' ', substr($match, 13, -2)); 54 } 55 56 /** @inheritdoc */ 57 public function render($format, Doku_Renderer $renderer, $data) 58 { 59 if ($format != 'xhtml') return false; 60 $renderer->nocache(); 61 62 global $INFO; 63 64 // first data is namespace, rest is options 65 $ns = array_shift($data); 66 if ($ns && $ns[0] === '.') { 67 // resolve relative to current page 68 $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 69 } else { 70 $ns = cleanID($ns); 71 } 72 73 $this->initState( 74 $ns, 75 $INFO['id'], 76 (bool)$this->getConf('usetitle'), 77 $this->getConf('sort'), 78 in_array('home', $data), 79 $this->getConf('peek', 0), 80 ); 81 82 $tree = $this->getTree(); 83 $this->renderTree($renderer, $tree->getTop()); 84 85 return true; 86 } 87 88 /** 89 * Initialize the configuration state of the plugin 90 * 91 * Also used in testing 92 * 93 * @param string $ns 94 * @param string $currentID 95 * @param bool $usetitle 96 */ 97 public function initState( 98 string $ns, 99 string $currentID, 100 bool $usetitle, 101 string $sort, 102 bool $home, 103 int $peek = 0 104 ) 105 { 106 $this->ns = $ns; 107 $this->currentID = $currentID; 108 $this->usetitle = $usetitle; 109 $this->sort = $sort; 110 $this->home = $home; 111 $this->peek = $peek; 112 } 113 114 /** 115 * Create the tree 116 * 117 * @return PageTreeBuilder 118 */ 119 protected function getTree(): PageTreeBuilder 120 { 121 $tree = new PageTreeBuilder($this->ns); 122 $tree->addFlag(PageTreeBuilder::FLAG_NS_AS_STARTPAGE); 123 if ($this->home) $tree->addFlag(PageTreeBuilder::FLAG_SELF_TOP); 124 $tree->setRecursionDecision(\Closure::fromCallable([$this, 'treeRecursionDecision'])); 125 $tree->setNodeProcessor(\Closure::fromCallable([$this, 'treeNodeProcessor'])); 126 $tree->generate(); 127 128 switch ($this->sort) { 129 case 'id': 130 $tree->sort(TreeSort::SORT_BY_ID); 131 break; 132 case 'title': 133 $tree->sort(TreeSort::SORT_BY_TITLE); 134 break; 135 case 'ns_id': 136 $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_ID); 137 break; 138 default: 139 $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_TITLE); 140 break; 141 } 142 143 return $tree; 144 } 145 146 147 /** 148 * Callback for the PageTreeBuilder to decide if we want to recurse into a node 149 * 150 * @param AbstractNode $node 151 * @param int $depth 152 * @return bool 153 */ 154 protected function treeRecursionDecision(AbstractNode $node, int $depth): bool 155 { 156 if ($node instanceof WikiStartpage) { 157 $id = $node->getNs(); // use the namespace for startpages 158 } else { 159 $id = $node->getId(); 160 } 161 162 $is_current = $this->isParent($this->currentID, $id); 163 $node->setProperty('is_current', $is_current); 164 165 // always recurse into the current page path 166 if ($is_current) return true; 167 168 // should we peek deeper to see if there's something readable? 169 if($depth < $this->peek && auth_quickaclcheck($node->getId()) < AUTH_READ ) { 170 return true; 171 } 172 173 return false; 174 } 175 176 /** 177 * Callback for the PageTreeBuilder to process a node 178 * 179 * @param AbstractNode $node 180 * @return AbstractNode|null 181 */ 182 protected function treeNodeProcessor(AbstractNode $node): ?AbstractNode 183 { 184 $perm = auth_quickaclcheck($node->getId()); 185 $node->setProperty('permission', $perm); 186 $node->setTitle($this->getTitle($node->getId())); 187 188 189 if ($node->hasChildren()) { 190 // this node has children, we add it to the tree regardless of the permission 191 // permissions are checked again when rendering 192 return $node; 193 } 194 195 if ($perm < AUTH_READ) { 196 // no children, no permission. No need to add it to the tree 197 return null; 198 } 199 200 return $node; 201 } 202 203 204 /** 205 * Example on how to render a TreeBuilder tree 206 * 207 * @param Doku_Renderer $R The current renderer 208 * @param AbstractNode $top The top node of the tree (use getTop() to get it) 209 * @param int $level current nesting level, starting at 1 210 * @return void 211 */ 212 protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1) 213 { 214 $R->listu_open(); 215 foreach ($top->getChildren() as $node) { 216 $isfolder = $node instanceof WikiNamespace; 217 $incurrent = $node->getProperty('is_current', false); 218 219 $R->listitem_open(1, $isfolder); 220 $R->listcontent_open(); 221 if ($incurrent) $R->strong_open(); 222 223 if (((int)$node->getProperty('permission', 0)) < AUTH_READ) { 224 $R->cdata($node->getTitle()); 225 } else { 226 $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation'); 227 } 228 229 if ($incurrent) $R->strong_close(); 230 $R->listcontent_close(); 231 if ($node->hasChildren()) { 232 $this->renderTree($R, $node, $level + 1); 233 } 234 $R->listitem_close(); 235 } 236 $R->listu_close(); 237 } 238 239 /** 240 * Check if the given parent ID is a parent of the child ID 241 * 242 * @param string $child 243 * @param string $parent 244 * @return bool 245 */ 246 protected function isParent(string $child, string $parent) 247 { 248 // Empty parent is considered a parent of all pages 249 if ($parent === '') { 250 return true; 251 } 252 253 $child = explode(':', $child); 254 $parent = explode(':', $parent); 255 return array_slice($child, 0, count($parent)) === $parent; 256 } 257 258 259 /** 260 * Get the title for the given page ID 261 * 262 * @param string $id 263 * @return string 264 */ 265 protected function getTitle($id) 266 { 267 global $conf; 268 269 if ($this->usetitle) { 270 $p = p_get_first_heading($id); 271 if (!empty($p)) return $p; 272 } 273 274 $p = noNS($id); 275 if ($p == $conf['start'] || !$p) { 276 $p = noNS(getNS($id)); 277 if (!$p) { 278 return $conf['start']; 279 } 280 } 281 return $p; 282 } 283} 284