178a26510SAndreas Gohr<?php 278a26510SAndreas Gohr 378a26510SAndreas Gohrnamespace dokuwiki\TreeBuilder; 478a26510SAndreas Gohr 578a26510SAndreas Gohruse dokuwiki\File\PageResolver; 678a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\AbstractNode; 778a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\Top; 878a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiNamespace; 978a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiPage; 1078a26510SAndreas Gohruse dokuwiki\TreeBuilder\Node\WikiStartpage; 1178a26510SAndreas Gohruse dokuwiki\Utf8\PhpString; 1278a26510SAndreas Gohr 1378a26510SAndreas Gohr/** 1478a26510SAndreas Gohr * A tree builder for wiki pages and namespaces 1578a26510SAndreas Gohr * 1678a26510SAndreas Gohr * This replace the classic search_* functions approach and provides a way to create a traversable tree 1778a26510SAndreas Gohr * of wiki pages and namespaces. 1878a26510SAndreas Gohr * 1978a26510SAndreas Gohr * The created hierarchy can either use WikiNamespace nodes or represent namespaces as WikiPage nodes 2078a26510SAndreas Gohr * associated with the namespace's start page. 2178a26510SAndreas Gohr */ 2278a26510SAndreas Gohrclass PageTreeBuilder extends AbstractBuilder 2378a26510SAndreas Gohr{ 2478a26510SAndreas Gohr /** @var array Used to remember already seen start pages */ 2578a26510SAndreas Gohr protected array $startpages = []; 2678a26510SAndreas Gohr 2778a26510SAndreas Gohr /** @var int Return WikiPage(startpage) instead of WikiNamespace(id) for namespaces */ 2878a26510SAndreas Gohr public const FLAG_NS_AS_STARTPAGE = 1; 2978a26510SAndreas Gohr 3078a26510SAndreas Gohr /** @var int Do not return Namespaces, will also disable recursion */ 3178a26510SAndreas Gohr public const FLAG_NO_NS = 2; 3278a26510SAndreas Gohr 3378a26510SAndreas Gohr /** @var int Do not return pages */ 3478a26510SAndreas Gohr public const FLAG_NO_PAGES = 4; 3578a26510SAndreas Gohr 3678a26510SAndreas Gohr /** @var int Do not filter out hidden pages */ 3778a26510SAndreas Gohr public const FLAG_KEEP_HIDDEN = 8; 3878a26510SAndreas Gohr 3978a26510SAndreas Gohr /** @var int The given namespace should be added as top element */ 4078a26510SAndreas Gohr public const FLAG_SELF_TOP = 16; 4178a26510SAndreas Gohr 42*31003314SAndreas Gohr /** @var int Do not filter out pages/namespaces with invalid IDs */ 43*31003314SAndreas Gohr public const FLAG_KEEP_INVALID = 32; 44*31003314SAndreas Gohr 4578a26510SAndreas Gohr /** @var string The top level namespace to iterate over */ 4678a26510SAndreas Gohr protected string $namespace; 4778a26510SAndreas Gohr 4878a26510SAndreas Gohr /** @var int The maximum depth to iterate into, -1 for infinite */ 4978a26510SAndreas Gohr protected int $maxdepth; 5078a26510SAndreas Gohr 5178a26510SAndreas Gohr 5278a26510SAndreas Gohr /** 5378a26510SAndreas Gohr * Constructor 5478a26510SAndreas Gohr * 5578a26510SAndreas Gohr * @param string $namespace The namespace to start from 5678a26510SAndreas Gohr * @param int $maxdepth The maximum depth to iterate into, -1 for infinite 5778a26510SAndreas Gohr */ 5878a26510SAndreas Gohr public function __construct(string $namespace, int $maxdepth = -1) 5978a26510SAndreas Gohr { 6078a26510SAndreas Gohr $this->namespace = $namespace; 6178a26510SAndreas Gohr $this->maxdepth = $maxdepth; 6278a26510SAndreas Gohr } 6378a26510SAndreas Gohr 6478a26510SAndreas Gohr /** @inheritdoc */ 6578a26510SAndreas Gohr public function generate(): void 6678a26510SAndreas Gohr { 6778a26510SAndreas Gohr $this->generated = true; 6878a26510SAndreas Gohr 6978a26510SAndreas Gohr $this->top = new Top(); 7078a26510SAndreas Gohr 7178a26510SAndreas Gohr // add directly to top or add the namespace under the top element? 7278a26510SAndreas Gohr if ($this->hasFlag(self::FLAG_SELF_TOP)) { 7378a26510SAndreas Gohr $parent = $this->createNamespaceNode($this->namespace, noNS($this->namespace)); 7478a26510SAndreas Gohr $parent->setParent($this->top); 7578a26510SAndreas Gohr } else { 7678a26510SAndreas Gohr if ($this->hasFlag(self::FLAG_NS_AS_STARTPAGE)) { 7778a26510SAndreas Gohr // do not add the namespace's own startpage in this mode 7878a26510SAndreas Gohr $this->startpages[$this->getStartpage($this->namespace)] = 1; 7978a26510SAndreas Gohr } 8078a26510SAndreas Gohr 8178a26510SAndreas Gohr $parent = $this->top; 8278a26510SAndreas Gohr } 8378a26510SAndreas Gohr 8478a26510SAndreas Gohr // if FLAG_SELF_TOP, we need to run a recursion decision on the parent 8578a26510SAndreas Gohr if ($parent instanceof Top || $this->applyRecursionDecision($parent, 0)) { 8678a26510SAndreas Gohr $dir = $this->namespacePath($this->namespace); 8778a26510SAndreas Gohr $this->createHierarchy($parent, $dir, $this->maxdepth); 8878a26510SAndreas Gohr } 8978a26510SAndreas Gohr 9078a26510SAndreas Gohr // if FLAG_SELF_TOP, we need to add the parent to the top 9178a26510SAndreas Gohr if (!$parent instanceof Top) { 9278a26510SAndreas Gohr $this->addNodeToHierarchy($this->top, $parent); 9378a26510SAndreas Gohr } 9478a26510SAndreas Gohr } 9578a26510SAndreas Gohr 9678a26510SAndreas Gohr /** 9778a26510SAndreas Gohr * Recursive function to create the page hierarchy 9878a26510SAndreas Gohr * 9978a26510SAndreas Gohr * @param AbstractNode $parent results are added as children to this element 10078a26510SAndreas Gohr * @param string $dir The directory relative to the page directory 10178a26510SAndreas Gohr * @param int $depth Current depth, recursion stops at 0 10278a26510SAndreas Gohr * @return void 10378a26510SAndreas Gohr */ 10478a26510SAndreas Gohr protected function createHierarchy(AbstractNode $parent, string $dir, int $depth) 10578a26510SAndreas Gohr { 10678a26510SAndreas Gohr // Process namespaces (subdirectories) 10778a26510SAndreas Gohr if ($this->hasNotFlag(self::FLAG_NO_NS)) { 10878a26510SAndreas Gohr $this->processNamespaces($parent, $dir, $depth); 10978a26510SAndreas Gohr } 11078a26510SAndreas Gohr 11178a26510SAndreas Gohr // Process pages (files) 11278a26510SAndreas Gohr if ($this->hasNotFlag(self::FLAG_NO_PAGES)) { 11378a26510SAndreas Gohr $this->processPages($parent, $dir); 11478a26510SAndreas Gohr } 11578a26510SAndreas Gohr } 11678a26510SAndreas Gohr 11778a26510SAndreas Gohr /** 11878a26510SAndreas Gohr * Process namespaces (subdirectories) and add them to the hierarchy 11978a26510SAndreas Gohr * 12078a26510SAndreas Gohr * @param AbstractNode $parent Parent node to add children to 12178a26510SAndreas Gohr * @param string $dir Current directory path 12278a26510SAndreas Gohr * @param int $depth Current depth level 12378a26510SAndreas Gohr * @return void 12478a26510SAndreas Gohr */ 12578a26510SAndreas Gohr protected function processNamespaces(AbstractNode $parent, string $dir, int $depth) 12678a26510SAndreas Gohr { 12778a26510SAndreas Gohr global $conf; 12878a26510SAndreas Gohr $base = $conf['datadir'] . '/'; 12978a26510SAndreas Gohr 130*31003314SAndreas Gohr $dirs = glob($base . $dir . '/*', GLOB_ONLYDIR); // will always ignore hidden dirs 13178a26510SAndreas Gohr foreach ($dirs as $subdir) { 13278a26510SAndreas Gohr $subdir = basename($subdir); 13378a26510SAndreas Gohr $id = pathID($dir . '/' . $subdir); 13478a26510SAndreas Gohr 135*31003314SAndreas Gohr // Skip namespaces with invalid IDs (_private) 136*31003314SAndreas Gohr if ($this->hasNotFlag(self::FLAG_KEEP_INVALID) && cleanID($id) !== $id) { 137*31003314SAndreas Gohr continue; 138*31003314SAndreas Gohr } 139*31003314SAndreas Gohr 14078a26510SAndreas Gohr $node = $this->createNamespaceNode($id, $subdir); 14178a26510SAndreas Gohr 14278a26510SAndreas Gohr // Recurse into subdirectory if depth and filter allows 14378a26510SAndreas Gohr if ($depth !== 0 && $this->applyRecursionDecision($node, $this->maxdepth - $depth)) { 14478a26510SAndreas Gohr $this->createHierarchy($node, $dir . '/' . $subdir, $depth - 1); 14578a26510SAndreas Gohr } 14678a26510SAndreas Gohr 14778a26510SAndreas Gohr // Add to hierarchy 14878a26510SAndreas Gohr $this->addNodeToHierarchy($parent, $node); 14978a26510SAndreas Gohr } 15078a26510SAndreas Gohr } 15178a26510SAndreas Gohr 15278a26510SAndreas Gohr /** 15378a26510SAndreas Gohr * Create a namespace node based on the flags 15478a26510SAndreas Gohr * 15578a26510SAndreas Gohr * @param string $id 15678a26510SAndreas Gohr * @param string $title 15778a26510SAndreas Gohr * @return AbstractNode 15878a26510SAndreas Gohr */ 15978a26510SAndreas Gohr protected function createNamespaceNode(string $id, string $title): AbstractNode 16078a26510SAndreas Gohr { 16178a26510SAndreas Gohr if ($this->hasFlag(self::FLAG_NS_AS_STARTPAGE)) { 16278a26510SAndreas Gohr $ns = $id; 16378a26510SAndreas Gohr $id = $this->getStartpage($id); // use the start page for the namespace 16478a26510SAndreas Gohr $this->startpages[$id] = 1; // mark as seen 16578a26510SAndreas Gohr $node = new WikiStartpage($id, $title, $ns); 16678a26510SAndreas Gohr } else { 16778a26510SAndreas Gohr $node = new WikiNamespace($id, $title); 16878a26510SAndreas Gohr } 16978a26510SAndreas Gohr return $node; 17078a26510SAndreas Gohr } 17178a26510SAndreas Gohr 17278a26510SAndreas Gohr /** 17378a26510SAndreas Gohr * Process pages (files) and add them to the hierarchy 17478a26510SAndreas Gohr * 17578a26510SAndreas Gohr * @param AbstractNode $parent Parent node to add children to 17678a26510SAndreas Gohr * @param string $dir Current directory path 17778a26510SAndreas Gohr * @return void 17878a26510SAndreas Gohr */ 17978a26510SAndreas Gohr protected function processPages(AbstractNode $parent, string $dir) 18078a26510SAndreas Gohr { 18178a26510SAndreas Gohr global $conf; 18278a26510SAndreas Gohr $base = $conf['datadir'] . '/'; 18378a26510SAndreas Gohr 18478a26510SAndreas Gohr $files = glob($base . $dir . '/*.txt'); 18578a26510SAndreas Gohr foreach ($files as $file) { 18678a26510SAndreas Gohr $file = basename($file); 18778a26510SAndreas Gohr $id = pathID($dir . '/' . $file); 18878a26510SAndreas Gohr 18978a26510SAndreas Gohr // Skip already shown start pages 19078a26510SAndreas Gohr if (isset($this->startpages[$id])) { 19178a26510SAndreas Gohr continue; 19278a26510SAndreas Gohr } 19378a26510SAndreas Gohr 194*31003314SAndreas Gohr // Skip pages with invalid IDs (e.g., __template.txt) 195*31003314SAndreas Gohr if ($this->hasNotFlag(self::FLAG_KEEP_INVALID) && cleanID($id) !== $id) { 196*31003314SAndreas Gohr continue; 197*31003314SAndreas Gohr } 198*31003314SAndreas Gohr 19978a26510SAndreas Gohr $page = new WikiPage($id, $file); 20078a26510SAndreas Gohr 20178a26510SAndreas Gohr // Add to hierarchy 20278a26510SAndreas Gohr $this->addNodeToHierarchy($parent, $page); 20378a26510SAndreas Gohr } 20478a26510SAndreas Gohr } 20578a26510SAndreas Gohr 20678a26510SAndreas Gohr /** 20778a26510SAndreas Gohr * Run custom node processor and add it to the hierarchy 20878a26510SAndreas Gohr * 20978a26510SAndreas Gohr * @param AbstractNode $parent Parent node 21078a26510SAndreas Gohr * @param AbstractNode $node Node to add 21178a26510SAndreas Gohr * @return void 21278a26510SAndreas Gohr */ 21378a26510SAndreas Gohr protected function addNodeToHierarchy(AbstractNode $parent, AbstractNode $node): void 21478a26510SAndreas Gohr { 21578a26510SAndreas Gohr $node->setParent($parent); // set the parent even when not added, yet 21678a26510SAndreas Gohr $node = $this->applyNodeProcessor($node); 21778a26510SAndreas Gohr if ($node instanceof AbstractNode) { 21878a26510SAndreas Gohr $parent->addChild($node); 21978a26510SAndreas Gohr } 22078a26510SAndreas Gohr } 22178a26510SAndreas Gohr 22278a26510SAndreas Gohr /** 22378a26510SAndreas Gohr * Get the start page for the given namespace 22478a26510SAndreas Gohr * 22578a26510SAndreas Gohr * @param string $ns The namespace to get the start page for 22678a26510SAndreas Gohr * @return string The start page id 22778a26510SAndreas Gohr */ 22878a26510SAndreas Gohr protected function getStartpage(string $ns): string 22978a26510SAndreas Gohr { 23078a26510SAndreas Gohr $id = $ns . ':'; 23178a26510SAndreas Gohr return (new PageResolver(''))->resolveId($id); 23278a26510SAndreas Gohr } 23378a26510SAndreas Gohr 23478a26510SAndreas Gohr /** 23578a26510SAndreas Gohr * Get the file path for the given namespace relative to the page directory 23678a26510SAndreas Gohr * 23778a26510SAndreas Gohr * @param string $namespace 23878a26510SAndreas Gohr * @return string 23978a26510SAndreas Gohr */ 24078a26510SAndreas Gohr protected function namespacePath(string $namespace): string 24178a26510SAndreas Gohr { 24278a26510SAndreas Gohr global $conf; 24378a26510SAndreas Gohr 24478a26510SAndreas Gohr $base = $conf['datadir'] . '/'; 24578a26510SAndreas Gohr $dir = wikiFN($namespace . ':xxx'); 24678a26510SAndreas Gohr $dir = substr($dir, strlen($base)); 24778a26510SAndreas Gohr $dir = dirname($dir); // remove the 'xxx' part 24878a26510SAndreas Gohr if ($dir === '.') $dir = ''; // dirname returns '.' for root namespace 24978a26510SAndreas Gohr return $dir; 25078a26510SAndreas Gohr } 25178a26510SAndreas Gohr 25278a26510SAndreas Gohr /** @inheritdoc */ 25378a26510SAndreas Gohr protected function applyRecursionDecision(AbstractNode $node, int $depth): bool 25478a26510SAndreas Gohr { 25578a26510SAndreas Gohr // automatically skip hidden elements unless disabled by flag 25678a26510SAndreas Gohr if (!$this->hasNotFlag(self::FLAG_KEEP_HIDDEN) && isHiddenPage($node->getId())) { 25778a26510SAndreas Gohr return false; 25878a26510SAndreas Gohr } 25978a26510SAndreas Gohr return parent::applyRecursionDecision($node, $depth); 26078a26510SAndreas Gohr } 26178a26510SAndreas Gohr 26278a26510SAndreas Gohr /** @inheritdoc */ 26378a26510SAndreas Gohr protected function applyNodeProcessor(AbstractNode $node): ?AbstractNode 26478a26510SAndreas Gohr { 26578a26510SAndreas Gohr // automatically skip hidden elements unless disabled by flag 26678a26510SAndreas Gohr if (!$this->hasNotFlag(self::FLAG_KEEP_HIDDEN) && isHiddenPage($node->getId())) { 26778a26510SAndreas Gohr return null; 26878a26510SAndreas Gohr } 26978a26510SAndreas Gohr return parent::applyNodeProcessor($node); 27078a26510SAndreas Gohr } 27178a26510SAndreas Gohr} 272