1<?php 2 3namespace dokuwiki\TreeBuilder; 4 5use dokuwiki\File\PageResolver; 6use dokuwiki\TreeBuilder\Node\AbstractNode; 7use dokuwiki\TreeBuilder\Node\Top; 8use dokuwiki\TreeBuilder\Node\WikiNamespace; 9use dokuwiki\TreeBuilder\Node\WikiPage; 10use dokuwiki\TreeBuilder\Node\WikiStartpage; 11use dokuwiki\Utf8\PhpString; 12 13/** 14 * A tree builder for wiki pages and namespaces 15 * 16 * This replace the classic search_* functions approach and provides a way to create a traversable tree 17 * of wiki pages and namespaces. 18 * 19 * The created hierarchy can either use WikiNamespace nodes or represent namespaces as WikiPage nodes 20 * associated with the namespace's start page. 21 */ 22class PageTreeBuilder extends AbstractBuilder 23{ 24 /** @var array Used to remember already seen start pages */ 25 protected array $startpages = []; 26 27 /** @var int Return WikiPage(startpage) instead of WikiNamespace(id) for namespaces */ 28 public const FLAG_NS_AS_STARTPAGE = 1; 29 30 /** @var int Do not return Namespaces, will also disable recursion */ 31 public const FLAG_NO_NS = 2; 32 33 /** @var int Do not return pages */ 34 public const FLAG_NO_PAGES = 4; 35 36 /** @var int Do not filter out hidden pages */ 37 public const FLAG_KEEP_HIDDEN = 8; 38 39 /** @var int The given namespace should be added as top element */ 40 public const FLAG_SELF_TOP = 16; 41 42 /** @var string The top level namespace to iterate over */ 43 protected string $namespace; 44 45 /** @var int The maximum depth to iterate into, -1 for infinite */ 46 protected int $maxdepth; 47 48 49 /** 50 * Constructor 51 * 52 * @param string $namespace The namespace to start from 53 * @param int $maxdepth The maximum depth to iterate into, -1 for infinite 54 */ 55 public function __construct(string $namespace, int $maxdepth = -1) 56 { 57 $this->namespace = $namespace; 58 $this->maxdepth = $maxdepth; 59 } 60 61 /** @inheritdoc */ 62 public function generate(): void 63 { 64 $this->generated = true; 65 66 $this->top = new Top(); 67 68 // add directly to top or add the namespace under the top element? 69 if ($this->hasFlag(self::FLAG_SELF_TOP)) { 70 $parent = $this->createNamespaceNode($this->namespace, noNS($this->namespace)); 71 $parent->setParent($this->top); 72 } else { 73 if ($this->hasFlag(self::FLAG_NS_AS_STARTPAGE)) { 74 // do not add the namespace's own startpage in this mode 75 $this->startpages[$this->getStartpage($this->namespace)] = 1; 76 } 77 78 $parent = $this->top; 79 } 80 81 // if FLAG_SELF_TOP, we need to run a recursion decision on the parent 82 if ($parent instanceof Top || $this->applyRecursionDecision($parent, 0)) { 83 $dir = $this->namespacePath($this->namespace); 84 $this->createHierarchy($parent, $dir, $this->maxdepth); 85 } 86 87 // if FLAG_SELF_TOP, we need to add the parent to the top 88 if (!$parent instanceof Top) { 89 $this->addNodeToHierarchy($this->top, $parent); 90 } 91 } 92 93 /** 94 * Recursive function to create the page hierarchy 95 * 96 * @param AbstractNode $parent results are added as children to this element 97 * @param string $dir The directory relative to the page directory 98 * @param int $depth Current depth, recursion stops at 0 99 * @return void 100 */ 101 protected function createHierarchy(AbstractNode $parent, string $dir, int $depth) 102 { 103 // Process namespaces (subdirectories) 104 if ($this->hasNotFlag(self::FLAG_NO_NS)) { 105 $this->processNamespaces($parent, $dir, $depth); 106 } 107 108 // Process pages (files) 109 if ($this->hasNotFlag(self::FLAG_NO_PAGES)) { 110 $this->processPages($parent, $dir); 111 } 112 } 113 114 /** 115 * Process namespaces (subdirectories) and add them to the hierarchy 116 * 117 * @param AbstractNode $parent Parent node to add children to 118 * @param string $dir Current directory path 119 * @param int $depth Current depth level 120 * @return void 121 */ 122 protected function processNamespaces(AbstractNode $parent, string $dir, int $depth) 123 { 124 global $conf; 125 $base = $conf['datadir'] . '/'; 126 127 $dirs = glob($base . $dir . '/*', GLOB_ONLYDIR); 128 foreach ($dirs as $subdir) { 129 $subdir = basename($subdir); 130 $id = pathID($dir . '/' . $subdir); 131 132 $node = $this->createNamespaceNode($id, $subdir); 133 134 // Recurse into subdirectory if depth and filter allows 135 if ($depth !== 0 && $this->applyRecursionDecision($node, $this->maxdepth - $depth)) { 136 $this->createHierarchy($node, $dir . '/' . $subdir, $depth - 1); 137 } 138 139 // Add to hierarchy 140 $this->addNodeToHierarchy($parent, $node); 141 } 142 } 143 144 /** 145 * Create a namespace node based on the flags 146 * 147 * @param string $id 148 * @param string $title 149 * @return AbstractNode 150 */ 151 protected function createNamespaceNode(string $id, string $title): AbstractNode 152 { 153 if ($this->hasFlag(self::FLAG_NS_AS_STARTPAGE)) { 154 $ns = $id; 155 $id = $this->getStartpage($id); // use the start page for the namespace 156 $this->startpages[$id] = 1; // mark as seen 157 $node = new WikiStartpage($id, $title, $ns); 158 } else { 159 $node = new WikiNamespace($id, $title); 160 } 161 return $node; 162 } 163 164 /** 165 * Process pages (files) and add them to the hierarchy 166 * 167 * @param AbstractNode $parent Parent node to add children to 168 * @param string $dir Current directory path 169 * @return void 170 */ 171 protected function processPages(AbstractNode $parent, string $dir) 172 { 173 global $conf; 174 $base = $conf['datadir'] . '/'; 175 176 $files = glob($base . $dir . '/*.txt'); 177 foreach ($files as $file) { 178 $file = basename($file); 179 $id = pathID($dir . '/' . $file); 180 181 // Skip already shown start pages 182 if (isset($this->startpages[$id])) { 183 continue; 184 } 185 186 $page = new WikiPage($id, $file); 187 188 // Add to hierarchy 189 $this->addNodeToHierarchy($parent, $page); 190 } 191 } 192 193 /** 194 * Run custom node processor and add it to the hierarchy 195 * 196 * @param AbstractNode $parent Parent node 197 * @param AbstractNode $node Node to add 198 * @return void 199 */ 200 protected function addNodeToHierarchy(AbstractNode $parent, AbstractNode $node): void 201 { 202 $node->setParent($parent); // set the parent even when not added, yet 203 $node = $this->applyNodeProcessor($node); 204 if ($node instanceof AbstractNode) { 205 $parent->addChild($node); 206 } 207 } 208 209 /** 210 * Get the start page for the given namespace 211 * 212 * @param string $ns The namespace to get the start page for 213 * @return string The start page id 214 */ 215 protected function getStartpage(string $ns): string 216 { 217 $id = $ns . ':'; 218 return (new PageResolver(''))->resolveId($id); 219 } 220 221 /** 222 * Get the file path for the given namespace relative to the page directory 223 * 224 * @param string $namespace 225 * @return string 226 */ 227 protected function namespacePath(string $namespace): string 228 { 229 global $conf; 230 231 $base = $conf['datadir'] . '/'; 232 $dir = wikiFN($namespace . ':xxx'); 233 $dir = substr($dir, strlen($base)); 234 $dir = dirname($dir); // remove the 'xxx' part 235 if ($dir === '.') $dir = ''; // dirname returns '.' for root namespace 236 return $dir; 237 } 238 239 /** @inheritdoc */ 240 protected function applyRecursionDecision(AbstractNode $node, int $depth): bool 241 { 242 // automatically skip hidden elements unless disabled by flag 243 if (!$this->hasNotFlag(self::FLAG_KEEP_HIDDEN) && isHiddenPage($node->getId())) { 244 return false; 245 } 246 return parent::applyRecursionDecision($node, $depth); 247 } 248 249 /** @inheritdoc */ 250 protected function applyNodeProcessor(AbstractNode $node): ?AbstractNode 251 { 252 // automatically skip hidden elements unless disabled by flag 253 if (!$this->hasNotFlag(self::FLAG_KEEP_HIDDEN) && isHiddenPage($node->getId())) { 254 return null; 255 } 256 return parent::applyNodeProcessor($node); 257 } 258} 259