1<?php 2 3// must be run within Dokuwiki 4if (!defined('DOKU_INC')) { 5 die(); 6} 7 8class helper_plugin_sitemapnavi extends DokuWiki_Plugin { 9 10 public function getSiteMap($baseNS) 11 { 12 global $conf, $INFO; 13 14 $subdir = trim(str_replace(':', '/', $baseNS),'/'); 15 $level = $this->getNumberOfSubnamespaces($baseNS) + 1; 16 17 $pages = array(); 18 $currentNS = utf8_encodeFN(str_replace(':', '/', $INFO['namespace'])); 19 search($pages, $conf['datadir'], 'search_index', array('ns' => $currentNS), $subdir, $level); 20 $media = array(); 21 search($media, $conf['mediadir'], [$this, 'searchMediaIndex'], array('ns' => $currentNS, 'depth' => 1, 'showmsg'=>false), str_replace(':', '/', $baseNS)); 22 $media = array_map(function($mediaFile) { 23 $cleanedNamespace = trim(getNS($mediaFile['id']), ':'); 24 if ($cleanedNamespace === '') { 25 $mediaFile['level'] = 1; 26 } else { 27 $mediaFile['level'] = count(explode(':', $cleanedNamespace)) + 1; 28 } 29 return $mediaFile; 30 }, $media); 31 $items = $this->mergePagesAndMedia($pages, $media); 32 $items = $this->sortMediaAfterPages($items); 33 34 $html = html_buildlist($items, 'idx', [$this, 'listItemCallback'], [$this, 'liCallback'], true); 35 return $html; 36 } 37 38 /** 39 * Calculate the number of subnamespaces, the given namespace is consisting of 40 * 41 * @param string $namespace 42 * @return int 43 */ 44 protected function getNumberOfSubnamespaces($namespace) { 45 $cleanedNamespace = trim($namespace, ':'); 46 if ($cleanedNamespace === '') { 47 return 0; 48 } 49 return substr_count($cleanedNamespace, ':') + 1; 50 } 51 52 /** 53 * A stable sort, that moves media entries after the pages in the same namespace 54 * 55 * @param array $items list of items to be sorted, consisting both of directories, pages and media 56 * @return array 57 */ 58 protected function sortMediaAfterPages(array $items) { 59 $numberOfItems = count($items); 60 61 if (empty($items)) { 62 return $items; 63 } 64 $count = 0; 65 $hasChanged = false; 66 $isUnsorted = true; 67 while($isUnsorted) { 68 $item1 = $items[$count]; 69 $item2 = $items[$count + 1]; 70 if ($this->compareMediaPages($item1, $item2) === 1) { 71 $temp = $item1; 72 $items[$count] = $item2; 73 $items[$count + 1] = $temp; 74 $hasChanged = true; 75 } 76 $count++; 77 if ($count === $numberOfItems) { 78 if ($hasChanged) { 79 $count = 0; 80 $hasChanged = false; 81 continue; 82 } 83 $isUnsorted = false; 84 } 85 } 86 87 return $items; 88 } 89 90 /** 91 * "compare" media items to pages and directories 92 * 93 * Considers media items to be "larger" than pages and directories if those are in the same namespace or a subnamespace 94 * Considers media items to be "larger" than other media items if those are in a subnamespace 95 * 96 * @param $item1 97 * @param $item2 98 * @return int 99 */ 100 protected function compareMediaPages($item1, $item2) { 101 $item1IsMedia = !isset($item1['type']); 102 $item2IsMedia = !isset($item2['type']); 103 if ($item1IsMedia) { 104 $nameSpaceDifference = $this->namespaceDifference($item1['id'], $item2['id']); 105 if ($nameSpaceDifference > 0) { 106 return 1; 107 } 108 if ($nameSpaceDifference === 0 && !$item2IsMedia) { 109 return 1; 110 } 111 } 112 return -1; 113 } 114 115 /** 116 * Calculate how far $id2 is in the namespace of $id1 117 * 118 * If $id2 is not in the same namespace or a subnamespace of $id1 return false 119 * If they are in the same namespace return 0 120 * If $id2 is in a subnamespace to the namespace of $id1, return the relative number of subnamespaces 121 * 122 * @param $id1 123 * @param $id2 124 * @return bool|int 125 */ 126 protected function namespaceDifference($id1, $id2) { 127 $nslist1 = explode(':', getNS($id1)); 128 $nslist2 = explode(':', getNS($id2)); 129 if (empty($nslist1)) { 130 return count($nslist2); 131 } 132 $NS1depth = count($nslist1); 133 for ($i = 0; $i < $NS1depth; $i += 1) { 134 if (empty($nslist2[$i]) || $nslist1[$i] !== $nslist2[$i]) { 135 // not in our namespace 136 return false; 137 } 138 } 139 return (count($nslist2) - count($nslist1)); 140 } 141 142 /** 143 * Merge media items into an flat ordered list of index items, after their respecitve directories 144 * 145 * @param array $pages 146 * @param array $mediaFiles 147 * @return array 148 */ 149 protected function mergePagesAndMedia(array $pages, array $mediaFiles) { 150 $items = []; 151 $unhandledMediaFiles = $mediaFiles; 152 foreach ($pages as $page) { 153 if ($page['type'] === 'f') { 154 $items[] = $page; 155 continue; 156 } 157 $items[] = $page; 158 $currentMediaFiles = $unhandledMediaFiles; 159 $unhandledMediaFiles = []; 160 foreach ($currentMediaFiles as $mediaFile) { 161 $mediafileNamespace = getNs($mediaFile['id']); 162 if ($page['id'] === $mediafileNamespace) { 163 $items[] = $mediaFile; 164 continue; 165 } 166 $unhandledMediaFiles[] = $mediaFile; 167 } 168 } 169 $items = array_merge($items, $unhandledMediaFiles); 170 return $items; 171 } 172 173 /** 174 * Wrapper for search_media, that descends only towards the current directory 175 * 176 * @see search_media 177 * 178 * @param $data 179 * @param $base 180 * @param $file 181 * @param $type 182 * @param $lvl 183 * @param $opts 184 * @return bool 185 */ 186 public function searchMediaIndex(&$data,$base,$file,$type,$lvl,$opts) { 187 if($type === 'd') { 188 if (strpos($opts['ns'] . '/', trim($file,'/') . '/') === 0) { 189 return true; 190 } 191 } 192 return search_media($data,$base,$file,$type,$lvl,$opts); 193 } 194 195 196 public function listItemCallback($item) 197 { 198 $fullId = cleanID($item['id']); 199 200 $ret = ''; 201 $fullId = ':' . $fullId; 202 $base = substr($fullId, strrpos($fullId, ':') + 1); 203 204 if ($item['type'] === 'd') { 205 // FS#2766, no need for search bots to follow namespace links in the index 206 $ret .= '<button title="' . $fullId . '" class="plugin__sitemapnavi__dir" ><strong>'; 207 $ret .= $base; 208 $ret .= '</strong></button>'; 209 } elseif ($item['type'] === 'f') { 210 // default is noNSorNS($id), but we want noNS($id) when useheading is off FS#2605 211 $ret .= html_wikilink($fullId, useHeading('navigation') ? null : noNS($fullId)); 212 } else { 213 list($ext) = mimetype($item['file'],false); 214 $class = "mf_$ext media mediafile"; 215 $ret .= '<a class="'.$class.'" href="'.ml($item['id']).'" target="_blank">' . $item['file'] . '</a>'; 216 } 217 return $ret; 218 } 219 220 public function liCallback($item) 221 { 222 global $INFO; 223 $currentClass = ''; 224 $adjustedItemID = str_replace('::', ':', ':' . $item['id']); 225 if (strpos(':' . $INFO['id'] . ':', $adjustedItemID . ':') === 0) { 226 $currentClass = 'current'; 227 } 228 229 if (!isset($item['type'])) { 230 return '<li class="level' . $item['level'] . ' media">'; 231 } 232 if ($item['type'] === 'f') { 233 return '<li class="level' . $item['level'] . ' ' . $currentClass . '">'; 234 } 235 if ($item['open']) { 236 return '<li class="open ' . $currentClass . '">'; 237 } 238 239 return '<li class="closed ' . $currentClass . '" data-ns="'.$adjustedItemID.'">'; 240 241 } 242} 243