1<?php
2
3namespace ComboStrap;
4
5
6use ComboStrap\Meta\Field\PageTemplateName;
7use ComboStrap\Web\Url;
8use ComboStrap\Web\UrlEndpoint;
9use ComboStrap\Web\UrlRewrite;
10use ComboStrap\Xml\XmlDocument;
11
12class FetcherPage extends IFetcherAbs implements IFetcherSource, IFetcherString
13{
14
15    use FetcherTraitWikiPath;
16
17    const NAME = "page";
18    const CANONICAL = "page";
19    const PURGE = "purge";
20
21    private string $requestedLayout;
22    private bool $build = false;
23    private bool $closed = false;
24
25
26    private MarkupPath $requestedMarkupPath;
27    private string $requestedLayoutName;
28    private TemplateForWebPage $pageTemplate;
29    private FetcherCache $fetcherCache;
30
31
32    public static function createPageFetcherFromPath(Path $path): FetcherPage
33    {
34        $fetcherPage = new FetcherPage();
35        $fetcherPage->setRequestedPath($path);
36        return $fetcherPage;
37    }
38
39    public static function createPageFetcherFromId(string $wikiId): FetcherPage
40    {
41        $wikiPath = WikiPath::createMarkupPathFromId($wikiId);
42        return self::createPageFetcherFromPath($wikiPath);
43    }
44
45    /**
46     * @throws ExceptionBadArgument
47     */
48    public static function createPageFragmentFetcherFromUrl(Url $fetchUrl): FetcherPage
49    {
50        $pageFragment = new FetcherPage();
51        $rewrite = Site::getUrlRewrite();
52        if ($rewrite === UrlRewrite::WEB_SERVER_REWRITE) {
53            try {
54                $path = $fetchUrl->getPath();
55                $id = substr(str_replace("/", ":", $path), 1);
56                $fetchUrl->setPath(UrlEndpoint::DOKU_PHP);
57                $fetchUrl->setQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
58            } catch (ExceptionNotFound $e) {
59                // Paht should be there
60            }
61        }
62        $pageFragment->buildFromUrl($fetchUrl);
63        return $pageFragment;
64    }
65
66    /**
67     * @param Url|null $url
68     * @return Url
69     *
70     * Note: The fetch url is the {@link FetcherCache keyCache}
71     */
72    function getFetchUrl(Url $url = null): Url
73    {
74        /**
75         * Overwrite default fetcher endpoint
76         * that is {@link UrlEndpoint::createFetchUrl()}
77         */
78        $url = UrlEndpoint::createDokuUrl();
79        $url = parent::getFetchUrl($url);
80        try {
81            $template = $this->getRequestedTemplate();
82            $url->addQueryParameter(PageTemplateName::PROPERTY_NAME, $template);
83        } catch (ExceptionNotFound $e) {
84            // ok
85        }
86
87        $this->addLocalPathParametersToFetchUrl($url, DokuwikiId::DOKUWIKI_ID_ATTRIBUTE);
88
89        // the drive is not needed
90        $url->deleteQueryParameter(WikiPath::DRIVE_ATTRIBUTE);
91
92        // this is the default fetcher, no need to add it as parameter
93        $url->deleteQueryParameter(IFetcher::FETCHER_KEY);
94
95        return $url;
96    }
97
98    /**
99     * @throws ExceptionBadArgument
100     * @throws ExceptionBadSyntax
101     * @throws ExceptionNotExists
102     * @throws ExceptionNotFound
103     */
104    public function buildFromTagAttributes(TagAttributes $tagAttributes): IFetcher
105    {
106        parent::buildFromTagAttributes($tagAttributes);
107        $this->buildOriginalPathFromTagAttributes($tagAttributes);
108        $layout = $tagAttributes->getValueAndRemoveIfPresent(PageTemplateName::PROPERTY_NAME);
109        if ($layout !== null) {
110            $this->setRequestedLayout($layout);
111        }
112        /**
113         * Purge the cache
114         */
115        $tagAttributes->getValueAndRemoveIfPresent(self::PURGE);
116        return $this;
117    }
118
119
120    public static function createPageFetcherFromMarkupPath(MarkupPath $markupPath): FetcherPage
121    {
122        return self::createPageFetcherFromPath($markupPath->getPathObject());
123    }
124
125
126    /**
127     * @return string
128     */
129    public function getFetchString(): string
130    {
131
132        $this->buildObjectIfNeeded();
133
134        $cache = $this->fetcherCache;
135
136        /**
137         * Public static cache
138         * (Do we create the page or return the cache)
139         */
140        $isPublicStaticPage = $this->isPublicStaticPage();
141        $isCacheUsable = $cache->isCacheUsable();
142        if ($isCacheUsable && $isPublicStaticPage) {
143            try {
144                return FileSystems::getContent($cache->getFile());
145            } catch (ExceptionNotFound $e) {
146                // the cache file should exists
147                LogUtility::internalError("The cache HTML fragment file was not found", self::NAME);
148            }
149        }
150
151        /**
152         * Generate the whole html page via the layout
153         */
154        $htmlDocumentString = $this->pageTemplate->render();
155
156
157        /**
158         * We store only the static public pages
159         * without messages (they are dynamically insert)
160         */
161        $hasMessages = $this->pageTemplate->hasMessages();
162        if ($isPublicStaticPage && !$hasMessages) {
163            $cache->storeCache($htmlDocumentString);
164        }
165
166        return $htmlDocumentString;
167
168    }
169
170
171    /**
172     *
173     */
174    function getBuster(): string
175    {
176        return "";
177    }
178
179    public function getMime(): Mime
180    {
181        return Mime::create(Mime::HTML);
182    }
183
184    public function getFetcherName(): string
185    {
186        return self::NAME;
187    }
188
189    /**
190     * @throws ExceptionBadSyntax
191     * @throws ExceptionNotFound
192     * @throws ExceptionBadArgument
193     */
194    public function getFetchAsHtmlDom(): XmlDocument
195    {
196        $content = $this->getFetchString();
197        return XmlDocument::createHtmlDocFromMarkup($content);
198    }
199
200
201    /**
202     *
203     */
204    private function buildObjectIfNeeded(): void
205    {
206
207        if ($this->build) {
208            if ($this->closed) {
209                throw new ExceptionRuntimeInternal("This fetcher page object has already been close and cannot be reused", self::NAME);
210            }
211            return;
212        }
213
214        $this->build = true;
215
216        $this->requestedMarkupPath = MarkupPath::createPageFromPathObject($this->getRequestedPath());
217
218        $pageLang = Lang::createForMarkup($this->getRequestedPage());
219        $title = PageTitle::createForMarkup($this->getRequestedPage())->getValueOrDefault();
220
221        $layoutName = $this->getRequestedTemplateOrDefault();
222        $this->pageTemplate = TemplateForWebPage::create()
223            ->setRequestedTemplateName($layoutName)
224            ->setRequestedContextPath($this->getRequestedPath())
225            ->setRequestedLang($pageLang)
226            ->setRequestedTitle($title);
227
228        /**
229         * Build the cache
230         * The template is mandatory as cache key
231         * If the user change it, the cache should be disabled
232         */
233        $this->fetcherCache = FetcherCache::createFrom($this, [$layoutName]);
234        // the requested page
235        $this->fetcherCache->addFileDependency($this->getRequestedPath());
236        if (PluginUtility::isDevOrTest()) {
237            /**
238             * The hbs template dependency
239             * (only on test/dev)
240             */
241            try {
242                $this->fetcherCache->addFileDependency($this->pageTemplate->getCssPath());
243            } catch (ExceptionNotFound $e) {
244                // no css file
245            }
246            try {
247                $this->fetcherCache->addFileDependency($this->pageTemplate->getJsPath());
248            } catch (ExceptionNotFound $e) {
249                // no js
250            }
251            // mandatory, should not throw
252            try {
253                $this->fetcherCache->addFileDependency($this->pageTemplate->getHtmlTemplatePath());
254            } catch (ExceptionNotFound $e) {
255                LogUtility::internalError("The html template should be found", self::CANONICAL, $e);
256            }
257        }
258        /**
259         * The Slots of the requested template
260         */
261        foreach ($this->pageTemplate->getSlots() as $templateSlot) {
262            try {
263                $this->fetcherCache->addFileDependency($templateSlot->getMarkupFetcher()->getSourcePath());
264            } catch (ExceptionNotFound $e) {
265                // no slot page found
266            }
267        }
268
269    }
270
271    public function getRequestedPath(): WikiPath
272    {
273        return $this->getSourcePath();
274    }
275
276    public function setRequestedLayout(string $layoutValue): FetcherPage
277    {
278        $this->requestedLayout = $layoutValue;
279        return $this;
280    }
281
282    /**
283     * @throws ExceptionNotFound
284     */
285    private function getRequestedTemplate(): string
286    {
287        if (!isset($this->requestedLayout)) {
288            throw new ExceptionNotFound("No requested layout");
289        }
290        return $this->requestedLayout;
291    }
292
293
294    public function setRequestedPath(Path $requestedPath): FetcherPage
295    {
296        try {
297            $requestedPath = WikiPath::createFromPathObject($requestedPath);
298        } catch (ExceptionBadArgument $e) {
299            throw new ExceptionRuntimeInternal("Not a local wiki path", self::NAME, 1, $e);
300        }
301        $this->setSourcePath($requestedPath);
302        return $this;
303    }
304
305
306    /**
307     *
308     */
309    public function close(): FetcherPage
310    {
311        // nothing to do
312        return $this;
313    }
314
315
316    private function getRequestedPage(): MarkupPath
317    {
318        $this->buildObjectIfNeeded();
319        return $this->requestedMarkupPath;
320    }
321
322
323    /**
324     * The cache stores only public pages.
325     *
326     * ie when the user is unknown
327     * and there is no railbar
328     * (railbar is dynamically created even for the public
329     * and the javascript for the menu item expects to run after a window load event)
330     *
331     * @return bool
332     */
333    private function isPublicStaticPage(): bool
334    {
335        $privateRailbar = SiteConfig::getConfValue(FetcherRailBar::CONF_PRIVATE_RAIL_BAR, FetcherRailBar::CONF_PRIVATE_RAIL_BAR_DEFAULT);
336        $isLoggedIn = Identity::isLoggedIn();
337        return $privateRailbar === 1 && !$isLoggedIn;
338    }
339
340    private function getRequestedTemplateOrDefault(): string
341    {
342        try {
343            return $this->getRequestedTemplate();
344        } catch (ExceptionNotFound $e) {
345            return PageTemplateName::createFromPage($this->getRequestedPage())->getValueOrDefault();
346        }
347    }
348
349    public function getContentCachePath(): LocalPath
350    {
351        $this->buildObjectIfNeeded();
352        return $this->fetcherCache->getFile();
353    }
354
355    public function getLabel(): string
356    {
357        $sourcePath = $this->getSourcePath();
358        return ResourceName::getFromPath($sourcePath);
359    }
360
361
362}
363