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