1<?php
2
3namespace ComboStrap;
4
5
6use ComboStrap\Meta\Field\PageTemplateName;
7use ComboStrap\Web\Url;
8
9/**
10 * Bundle page from the same namespace
11 * with {@link FetcherPageBundler::getBundledOutline() corrected outline}
12 *
13 * From a wiki app, just add: `?do=combo_pagebundler`
14 *
15 */
16class FetcherPageBundler extends IFetcherAbs implements IFetcherString
17{
18
19    use FetcherTraitWikiPath;
20
21    const CANONICAL = self::NAME;
22    const NAME = "pagebundler";
23    private ?Outline $bundledOutline = null;
24    /**
25     * @var int - the maximum number of pages to bundle
26     * Security to not get DDOS by a Search engine
27     */
28    private int $maxPages = 5;
29    /**
30     * @var int the number of pages processed (ie actually added to the outline)
31     */
32    private int $countPageProcessed = 0;
33
34    public static function createPageBundler(): FetcherPageBundler
35    {
36        return new FetcherPageBundler();
37    }
38
39    public function buildFromUrl(Url $url): FetcherPageBundler
40    {
41        /**
42         * Just to return the good type
43         */
44        parent::buildFromUrl($url);
45        return $this;
46    }
47
48    /**
49     * @throws ExceptionBadArgument
50     * @throws ExceptionBadSyntax
51     * @throws ExceptionNotExists
52     * @throws ExceptionNotFound
53     */
54    public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherPageBundler
55    {
56        parent::buildFromTagAttributes($tagAttributes);
57        $this->buildOriginalPathFromTagAttributes($tagAttributes);
58        return $this;
59    }
60
61
62    function getBuster(): string
63    {
64        return "";
65    }
66
67    /**
68     * @return Mime
69     */
70    public function getMime(): Mime
71    {
72        return Mime::getHtml();
73    }
74
75    public function getFetcherName(): string
76    {
77        return self::NAME;
78    }
79
80    public function getFetchString(): string
81    {
82
83        $outline = $this->getBundledOutline();
84        $instructionsCalls = $outline->toHtmlSectionOutlineCalls();
85        $mainContent = MarkupRenderer::createFromInstructions($instructionsCalls)
86            ->setRequestedExecutingPath($this->getStartPath())
87            ->setRequestedContextPath($this->getRequestedContextPath())
88            ->setRequestedMime($this->getMime())
89            ->getOutput();
90
91
92        $startMarkup = $this->getStartPath();
93        $title = PageTitle::createForMarkup($startMarkup)->getValueOrDefault();
94        $lang = Lang::createForMarkup($startMarkup);
95        try {
96            $startMarkupWikiPath = WikiPath::createFromPathObject($startMarkup->getPathObject());
97        } catch (ExceptionBadArgument $e) {
98            /**
99             * should not happen as this class accepts only wiki path as {@link FetcherPageBundler::setContextPath() context path}
100             */
101            throw new ExceptionRuntimeInternal("We were unable to get the start markup wiki path. Error:{$e->getMessage()}", self::CANONICAL);
102        }
103
104        $layoutName = PageTemplateName::BLANK_TEMPLATE_VALUE;
105        try {
106            $toc = Toc::createEmpty()
107                ->setValue($this->getBundledOutline()->toTocDokuwikiFormat());
108        } catch (ExceptionBadArgument $e) {
109            // this is an array
110            throw new ExceptionRuntimeInternal("The toc could not be created. Error:{$e->getMessage()}", self::CANONICAL, 1, $e);
111        }
112        try {
113            return TemplateForWebPage::create()
114                ->setRequestedTemplateName($layoutName)
115                ->setRequestedContextPath($startMarkupWikiPath)
116                ->setRequestedTitle($title)
117                ->setRequestedLang($lang)
118                ->setToc($toc)
119                ->setIsSocial(false)
120                ->setRequestedEnableTaskRunner(false)
121                ->setMainContent($mainContent)
122                ->render();
123        } catch (ExceptionBadSyntax|ExceptionNotFound|ExceptionBadArgument $e) {
124            // layout should be good
125            throw new ExceptionRuntimeInternal("The $layoutName template returns an error", self::CANONICAL, 1, $e);
126        }
127
128
129    }
130
131    public function getBundledOutline(): Outline
132    {
133
134        if (isset($this->bundledOutline)) {
135            return $this->bundledOutline;
136        }
137
138        if (!Identity::isAnonymous()) {
139            $this->maxPages = 99999;
140            set_time_limit(5 * 60);
141        }
142        $startPath = $this->getStartPath();
143        $actualLevel = 0;
144        $this->buildOutlineRecursive($startPath, $actualLevel);
145
146        return $this->bundledOutline;
147
148    }
149
150    /**
151     * The path from where the bundle should start
152     * If this is not an index markup, the index markup will be chosen {@link FetcherPageBundler::getStartPath()}
153     *
154     * @throws ExceptionBadArgument - if the path is not a {@link WikiPath web path}
155     */
156    public function setContextPath(Path $requestedPath): FetcherPageBundler
157    {
158        $this->setSourcePath(WikiPath::createFromPathObject($requestedPath));
159        return $this;
160    }
161
162    private function getRequestedContextPath(): WikiPath
163    {
164        return $this->getSourcePath();
165    }
166
167
168    /**
169     *
170     * @return MarkupPath The index path or the request path is none
171     *
172     */
173    private function getStartPath(): MarkupPath
174    {
175        $requestedPath = MarkupPath::createPageFromPathObject($this->getRequestedContextPath());
176        if ($requestedPath->isIndexPage()) {
177            return $requestedPath;
178        }
179        try {
180            /**
181             * Parent is an index path in the {@link MarkupFileSystem}
182             */
183            return $requestedPath->getParent();
184        } catch (ExceptionNotFound $e) {
185            // home markup case (should not happen - home page is a index page)
186            return $requestedPath;
187        }
188
189    }
190
191    /**
192     * If a page does not have any h1
193     * (Case of index page for instance)
194     *
195     * If this is the case, the outline is broken.
196     * @param Outline $outline
197     * @return Outline
198     */
199    private function addFirstSectionIfMissing(Outline $outline): Outline
200    {
201        $rootOutlineSection = $outline->getRootOutlineSection();
202        $addFirstSection = false;
203        try {
204            $firstChild = $rootOutlineSection->getFirstChild();
205            if ($firstChild->getLevel() >= 2) {
206                $addFirstSection = true;
207            }
208        } catch (ExceptionNotFound $e) {
209            $addFirstSection = true;
210        }
211        if ($addFirstSection) {
212            $enterHeading = Call::createComboCall(
213                HeadingTag::HEADING_TAG,
214                DOKU_LEXER_ENTER,
215                array(HeadingTag::LEVEL => 1),
216                HeadingTag::TYPE_OUTLINE,
217                null,
218                null,
219                null,
220                \syntax_plugin_combo_xmlblocktag::TAG
221            );
222            $title = PageTitle::createForMarkup($outline->getMarkupPath())->getValueOrDefault();
223            $unmatchedHeading = Call::createComboCall(
224                HeadingTag::HEADING_TAG,
225                DOKU_LEXER_UNMATCHED,
226                [],
227                null,
228                $title,
229                $title,
230                null,
231                \syntax_plugin_combo_xmlblocktag::TAG
232            );
233            $exitHeading = Call::createComboCall(
234                HeadingTag::HEADING_TAG,
235                DOKU_LEXER_EXIT,
236                array(HeadingTag::LEVEL => 1),
237                null,
238                null,
239                null,
240                null,
241                \syntax_plugin_combo_xmlblocktag::TAG
242            );
243            $h1Section = OutlineSection::createFromEnterHeadingCall($outline, $enterHeading)
244                ->addHeaderCall($unmatchedHeading)
245                ->addHeaderCall($exitHeading);
246            $children = $rootOutlineSection->getChildren();
247            foreach ($children as $child) {
248                $child->detachBeforeAppend();
249                try {
250                    $h1Section->appendChild($child);
251                } catch (ExceptionBadState $e) {
252                    LogUtility::error("An error occurs when trying to move the h2 children below the recreated heading title ($title)", self::CANONICAL);
253                }
254            }
255            /**
256             * Without h1
257             * The content is in the root heading
258             */
259            foreach ($rootOutlineSection->getContentCalls() as $rootHeadingCall) {
260                $h1Section->addContentCall($rootHeadingCall);
261            }
262            $rootOutlineSection->deleteContentCalls();
263            try {
264                $rootOutlineSection->appendChild($h1Section);
265            } catch (ExceptionBadState $e) {
266                LogUtility::error("An error occurs when trying to add the recreated title heading ($title) to the root", self::CANONICAL);
267            }
268        }
269        return $outline;
270    }
271
272    public function getLabel(): string
273    {
274        return self::CANONICAL;
275    }
276
277    private function buildOutlineRecursive(MarkupPath $indexPath, int $actualLevel)
278    {
279        /**
280         * Index Page
281         */
282        if (FileSystems::exists($indexPath)) {
283            $outline = FetcherMarkup::confRoot()
284                ->setRequestedExecutingPath($indexPath)
285                ->setRequestedContextPath($indexPath->toWikiPath())
286                ->setRequestedMimeToInstructions()
287                ->build()
288                ->getOutline();
289            $indexOutline = $this->addFirstSectionIfMissing($outline);
290            foreach ($indexOutline->getRootOutlineSection()->getChildren() as $childOuterSection) {
291                $childOuterSection->updatePageLinkToInternal($indexPath);
292            }
293        } else {
294            $title = PageTitle::createForMarkup($indexPath)->getValueOrDefault();
295            $content = <<<EOF
296====== $title ======
297EOF;
298            $indexOutline = Outline::createFromMarkup($content, $indexPath, $this->getRequestedContextPath());
299            $indexOutline = $this->addFirstSectionIfMissing($indexOutline);
300        }
301
302        /**
303         * Start of bundled outline or not
304         */
305        if ($this->bundledOutline === null) {
306            $this->bundledOutline = $indexOutline;
307        } else {
308            Outline::merge($this->bundledOutline, $indexOutline, $actualLevel);
309        }
310        $this->countPageProcessed = +1;
311        if ($this->countPageProcessed > $this->maxPages) {
312            return;
313        }
314
315        /**
316         * Children Pages (Same level)
317         */
318        $childrenPages = MarkupFileSystem::getOrCreate()->getChildren($indexPath, FileSystems::LEAF);
319        foreach ($childrenPages as $child) {
320            if ($child->isSlot()) {
321                continue;
322            }
323            try {
324                $outline = FetcherMarkup::confRoot()
325                    ->setRequestedExecutingPath($child)
326                    ->setRequestedContextPath($child->toWikiPath())
327                    ->setRequestedMimeToInstructions()
328                    ->build()
329                    ->getOutline();
330            } catch (ExceptionNotExists $e) {
331                // as it's in a file system loop, the page should exist
332                continue;
333            }
334            $outer = $this->addFirstSectionIfMissing($outline);
335            Outline::merge($this->bundledOutline, $outer, $actualLevel);
336            $this->countPageProcessed = +1;
337            if ($this->countPageProcessed > $this->maxPages) {
338                return;
339            }
340        }
341        $containerPages = MarkupFileSystem::getOrCreate()->getChildren($indexPath, FileSystems::CONTAINER);
342        $nextLevel = $actualLevel + 1;
343        foreach ($containerPages as $child) {
344            $this->buildOutlineRecursive($child, $nextLevel);
345        }
346
347    }
348}
349