1<?php 2 3use dokuwiki\File\PageResolver; 4 5/** 6 * DokuWiki Plugin simplenavi (Syntax Component) 7 * 8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9 * @author Andreas Gohr <gohr@cosmocode.de> 10 */ 11class syntax_plugin_simplenavi extends DokuWiki_Syntax_Plugin 12{ 13 private $startpages = []; 14 15 /** @inheritdoc */ 16 public function getType() 17 { 18 return 'substition'; 19 } 20 21 /** @inheritdoc */ 22 public function getPType() 23 { 24 return 'block'; 25 } 26 27 /** @inheritdoc */ 28 public function getSort() 29 { 30 return 155; 31 } 32 33 /** @inheritdoc */ 34 public function connectTo($mode) 35 { 36 $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 37 } 38 39 /** @inheritdoc */ 40 public function handle($match, $state, $pos, Doku_Handler $handler) 41 { 42 return explode(' ', substr($match, 13, -2)); 43 } 44 45 /** @inheritdoc */ 46 public function render($format, Doku_Renderer $renderer, $data) 47 { 48 if ($format != 'xhtml') return false; 49 50 global $conf; 51 global $INFO; 52 $renderer->nocache(); 53 54 // first data is namespace, rest is options 55 $ns = array_shift($data); 56 if ($ns && $ns[0] === '.') { 57 // resolve relative to current page 58 $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 59 } else { 60 $ns = cleanID($ns); 61 } 62 // convert to path 63 $ns = utf8_encodeFN(str_replace(':', '/', $ns)); 64 65 $items = []; 66 search($items, $conf['datadir'], [$this, 'cbSearch'], ['ns' => $INFO['id']], $ns, 1, 'natural'); 67 if ($this->getConf('sortByTitle')) { 68 $this->sortByTitle($items, "id"); 69 } else { 70 if ($this->getConf('sort') == 'ascii') { 71 uksort($items, [$this, 'pathCompare']); 72 } 73 } 74 75 $class = 'plugin__simplenavi'; 76 if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter'; 77 78 $renderer->doc .= '<div class="' . $class . '">'; 79 $renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']); 80 $renderer->doc .= '</div>'; 81 82 return true; 83 } 84 85 /** 86 * Create a list openening 87 * 88 * @param array $item 89 * @return string 90 * @see html_buildlist() 91 */ 92 public function cbList($item) 93 { 94 global $INFO; 95 96 if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) { 97 return '<strong>' . html_wikilink(':' . $item['id'], $this->getTitle($item['id'])) . '</strong>'; 98 } else { 99 return html_wikilink(':' . $item['id'], $this->getTitle($item['id'])); 100 } 101 102 } 103 104 /** 105 * Create a list item 106 * 107 * @param array $item 108 * @return string 109 * @see html_buildlist() 110 */ 111 public function cbListItem($item) 112 { 113 if ($item['type'] == "f") { 114 return '<li class="level' . $item['level'] . '">'; 115 } elseif ($item['open']) { 116 return '<li class="open">'; 117 } else { 118 return '<li class="closed">'; 119 } 120 } 121 122 /** 123 * Custom search callback 124 * 125 * @param $data 126 * @param $base 127 * @param $file 128 * @param $type 129 * @param $lvl 130 * @param $opts 131 * @return bool 132 */ 133 public function cbSearch(&$data, $base, $file, $type, $lvl, $opts) 134 { 135 global $conf; 136 $return = true; 137 138 $id = pathID($file); 139 140 if ($type == 'd' && !( 141 preg_match('#^' . $id . '(:|$)#', $opts['ns']) || 142 preg_match('#^' . $id . '(:|$)#', getNS($opts['ns'])) 143 144 )) { 145 //add but don't recurse 146 $return = false; 147 } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) { 148 //don't add 149 return false; 150 } 151 152 // for sneaky index, check access to the namespace's start page 153 if ($type == 'd' && $conf['sneaky_index']) { 154 $sp = (new PageResolver(''))->resolveId($id . ':'); 155 if (auth_quickaclcheck($sp) < AUTH_READ) { 156 return false; 157 } 158 } 159 160 if ($type == 'd') { 161 // link directories to their start pages 162 $id = "$id:"; 163 $id = (new PageResolver(''))->resolveId($id); 164 $this->startpages[$id] = 1; 165 } elseif (!empty($this->startpages[$id])) { 166 // skip already shown start pages 167 return false; 168 } elseif (noNS($id) == $conf['start']) { 169 // skip the main start page 170 return false; 171 } 172 173 //check hidden 174 if (isHiddenPage($id)) { 175 return false; 176 } 177 178 //check ACL 179 if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) { 180 return false; 181 } 182 183 $data[$id] = array( 184 'id' => $id, 185 'type' => $type, 186 'level' => $lvl, 187 'open' => $return, 188 ); 189 return $return; 190 } 191 192 /** 193 * Get the title for the given page ID 194 * 195 * @param string $id 196 * @return string 197 */ 198 protected function getTitle($id) 199 { 200 global $conf; 201 202 if (useHeading('navigation')) { 203 $p = p_get_first_heading($id); 204 } 205 if (!empty($p)) return $p; 206 207 $p = noNS($id); 208 if ($p == $conf['start'] || !$p) { 209 $p = noNS(getNS($id)); 210 if (!$p) { 211 return $conf['start']; 212 } 213 } 214 return $p; 215 } 216 217 /** 218 * Custom comparator to compare IDs 219 * 220 * @param string $a 221 * @param string $b 222 * @return int 223 */ 224 public function pathCompare($a, $b) 225 { 226 global $conf; 227 $a = preg_replace('/' . preg_quote($conf['start'], '/') . '$/', '', $a); 228 $b = preg_replace('/' . preg_quote($conf['start'], '/') . '$/', '', $b); 229 $a = str_replace(':', '/', $a); 230 $b = str_replace(':', '/', $b); 231 232 return strcmp($a, $b); 233 } 234 235 /** 236 * Sort items by title 237 * 238 * @param array[] $array a list of items 239 * @param string $key the key that contains the page ID in each item 240 * @return void 241 */ 242 protected function sortByTitle(&$array, $key) 243 { 244 $sorter = []; 245 $ret = []; 246 reset($array); 247 foreach ($array as $ii => $va) { 248 $sorter[$ii] = $this->getTitle($va[$key]); 249 } 250 if ($this->getConf('sort') == 'ascii') { 251 uksort($sorter, [$this, 'pathCompare']); 252 } else { 253 natcasesort($sorter); 254 } 255 foreach ($sorter as $ii => $va) { 256 $ret[$ii] = $array[$ii]; 257 } 258 $array = $ret; 259 } 260 261} 262