xref: /dokuwiki/inc/TreeBuilder/PageTreeBuilder.php (revision 31003314c05cb43e9840ac995129d131e4e750b8)
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