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