1<?php
2
3namespace ComboStrap;
4
5
6use action_plugin_combo_qualitymessage;
7use DateTime;
8use Exception;
9use ModificationDate;
10use Slug;
11
12
13/**
14 * Page
15 */
16require_once(__DIR__ . '/PluginUtility.php');
17
18/**
19 *
20 * Class Page
21 * @package ComboStrap
22 *
23 * This is just a wrapper around a file with the mime Dokuwiki
24 * that has a doku path (ie with the `:` separator)
25 */
26class Page extends ResourceComboAbs
27{
28
29
30    // The page id abbreviation is used in the url
31    // to make them unique.
32    //
33    // A website is not git but an abbreviation of 7
34    // is enough for a website.
35    //
36    // 7 is also the initial length of the git has abbreviation
37    //
38    // It gives a probability of collision of 1 percent
39    // for 24 pages creation by day over a period of 100 year
40    // (You need to create 876k pages).
41    // with the 36 alphabet
42    // https://datacadamia.com/crypto/hash/collision
43
44
45    const TYPE = "page";
46
47
48    /**
49     * The id requested (ie the main page)
50     * The page may be a slot
51     * @var string
52     */
53    private $requestedId;
54    /**
55     * @var DatabasePageRow
56     */
57    private $databasePage;
58    /**
59     * @var Canonical
60     */
61    private $canonical;
62    /**
63     * @var PageH1
64     */
65    private $h1;
66    /**
67     * @var ResourceName
68     */
69    private $pageName;
70    /**
71     * @var PageType
72     */
73    private $type;
74    /**
75     * @var PageTitle $title
76     */
77    private $title;
78
79    /**
80     * @var LowQualityPageOverwrite
81     */
82    private $canBeOfLowQuality;
83    /**
84     * @var Region
85     */
86    private $region;
87    /**
88     * @var Lang
89     */
90    private $lang;
91    /**
92     * @var PageId
93     */
94    private $pageId;
95
96    /**
97     * @var LowQualityCalculatedIndicator
98     */
99    private $lowQualityIndicatorCalculated;
100
101    /**
102     * @var PageLayout
103     */
104    private $layout;
105    /**
106     * @var Aliases
107     */
108    private $aliases;
109    /**
110     * @var Slug a slug path
111     */
112    private $slug;
113
114
115    /**
116     * @var QualityDynamicMonitoringOverwrite
117     */
118    private $qualityMonitoringIndicator;
119
120    /**
121     * @var string the alias used to build this page
122     */
123    private $buildAliasPath;
124    /**
125     * @var PagePublicationDate
126     */
127    private $publishedDate;
128    /**
129     * @var StartDate
130     */
131    private $startDate;
132    /**
133     * @var EndDate
134     */
135    private $endDate;
136    /**
137     * @var PageImages
138     */
139    private $pageImages;
140    /**
141     * @var PageKeywords
142     */
143    private $keywords;
144    /**
145     * @var CacheExpirationFrequency
146     */
147    private $cacheExpirationFrequency;
148    /**
149     * @var CacheExpirationDate
150     */
151    private $cacheExpirationDate;
152    /**
153     *
154     * @var LdJson
155     */
156    private $ldJson;
157    /**
158     * @var HtmlDocument
159     */
160    private $htmlDocument;
161    /**
162     * @var InstructionsDocument
163     */
164    private $instructionsDocument;
165
166    private $dokuPath;
167    /**
168     * @var PageDescription $description
169     */
170    private $description;
171    /**
172     * @var PageCreationDate
173     */
174    private $creationTime;
175    /**
176     * @var Locale
177     */
178    private $locale;
179    /**
180     * @var ModificationDate
181     */
182    private $modifiedTime;
183    /**
184     * @var PageUrlPath
185     */
186    private $pageUrlPath;
187    /**
188     * @var MetadataStore|string
189     */
190    private $readStore;
191
192    /**
193     * Page constructor.
194     * @param $absolutePath - the qualified path (may be not relative)
195     *
196     */
197    public function __construct($absolutePath)
198    {
199
200        $this->dokuPath = DokuPath::createPagePathFromPath($absolutePath);
201
202        if ($this->isSecondarySlot()) {
203
204            /**
205             * Used when we want to remove the cache of slots for a requested page
206             * (ie {@link Cache::removeSideSlotCache()})
207             *
208             * The $absolutePath is the logical path and may not exists
209             *
210             * Find the first physical file
211             * Don't use ACL otherwise the ACL protection event 'AUTH_ACL_CHECK' will kick in
212             * and we got then a recursive problem
213             * with the {@link \action_plugin_combo_pageprotection}
214             */
215            $useAcl = false;
216            $id = page_findnearest($this->dokuPath->getLastNameWithoutExtension(), $useAcl);
217            if ($id !== false && $id !== $this->dokuPath->getDokuwikiId()) {
218                $absolutePath = DokuPath::PATH_SEPARATOR . $id;
219                $this->dokuPath = DokuPath::createPagePathFromPath($absolutePath);
220            }
221
222        }
223
224        global $ID;
225        $this->requestedId = $ID;
226
227        $this->buildPropertiesFromFileSystem();
228
229    }
230
231    public static function createPageFromGlobalDokuwikiId(): Page
232    {
233        global $ID;
234        if ($ID === null) {
235            LogUtility::msg("The global wiki ID is null, unable to instantiate a page");
236        }
237        return self::createPageFromId($ID);
238    }
239
240    public static function createPageFromId($id): Page
241    {
242        DokuPath::addRootSeparatorIfNotPresent($id);
243        return new Page($id);
244    }
245
246    public static function createPageFromNonQualifiedPath($pathOrId): Page
247    {
248        global $ID;
249        $qualifiedId = $pathOrId;
250        resolve_pageid(getNS($ID), $qualifiedId, $exists);
251        /**
252         * Root correction
253         * yeah no root functionality in the {@link resolve_pageid resolution}
254         * meaning that we get an empty string
255         * they correct it in the link creation {@link wl()}
256         */
257        if ($qualifiedId === '') {
258            global $conf;
259            $qualifiedId = $conf['start'];
260        }
261        return Page::createPageFromId($qualifiedId);
262
263    }
264
265    /**
266     * @return Page - the requested page
267     */
268    public static function createPageFromRequestedPage(): Page
269    {
270        $pageId = PluginUtility::getRequestedWikiId();
271        if ($pageId === null) {
272            $pageId = RenderUtility::DEFAULT_SLOT_ID_FOR_TEST;
273            if(!PluginUtility::isTest()) {
274                // should never happen, we don't throw an exception
275                LogUtility::msg("We were unable to determine the requested page from the variables environment, default non-existing page id used");
276            }
277        }
278        return Page::createPageFromId($pageId);
279    }
280
281
282    public static function getHomePageFromNamespace(string $namespacePath): Page
283    {
284        global $conf;
285
286        if ($namespacePath != ":") {
287            $namespacePath = $namespacePath . ":";
288        }
289
290        $startPageName = $conf['start'];
291        if (page_exists($namespacePath . $startPageName)) {
292            // start page inside namespace
293            return self::createPageFromId($namespacePath . $startPageName);
294        } elseif (page_exists($namespacePath . noNS(cleanID($namespacePath)))) {
295            // page named like the NS inside the NS
296            return self::createPageFromId($namespacePath . noNS(cleanID($namespacePath)));
297        } elseif (page_exists($namespacePath)) {
298            // page like namespace exists
299            return self::createPageFromId(substr($namespacePath, 0, -1));
300        } else {
301            // Does not exist but can be used by hierarchical function
302            return self::createPageFromId($namespacePath . $startPageName);
303        }
304    }
305
306
307    static function createPageFromQualifiedPath($qualifiedPath): Page
308    {
309        return new Page($qualifiedPath);
310    }
311
312
313    /**
314     *
315     * @throws ExceptionCombo
316     */
317    public
318    function setCanonical($canonical): Page
319    {
320        $this->canonical
321            ->setValue($canonical)
322            ->sendToWriteStore();
323        return $this;
324    }
325
326
327    /**
328     * @return bool true if this is not the main slot.
329     */
330    public function isSecondarySlot(): bool
331    {
332        $slotNames = Site::getSecondarySlotNames();
333        $name = $this->getPath()->getLastNameWithoutExtension();
334        if ($name === null) {
335            // root case
336            return false;
337        }
338        return in_array($name, $slotNames, true);
339    }
340
341    /**
342     * @return bool true if this is the main
343     */
344    public function isMainHeaderFooterSlot(): bool
345    {
346
347        try {
348            $slotNames = [Site::getMainHeaderSlotName(), Site::getMainFooterSlotName()];
349        } catch (ExceptionCombo $e) {
350            return false;
351        }
352        $name = $this->getPath()->getLastNameWithoutExtension();
353        if ($name === null) {
354            // root case
355            return false;
356        }
357        return in_array($name, $slotNames, true);
358    }
359
360
361    /**
362     *
363     * @return bool
364     * Used to delete the part path of a page for default name or canonical value
365     */
366    public
367    function isStartPage(): bool
368    {
369        $startPageName = Site::getHomePageName();
370        return $this->getPath()->getLastName() === $startPageName;
371    }
372
373    /**
374     * Return a canonical if set
375     * otherwise derive it from the id
376     * by taking the last two parts
377     *
378     * @return string
379     * @deprecated for {@link Canonical::getValueOrDefault()}
380     */
381    public
382    function getCanonicalOrDefault(): ?string
383    {
384        return $this->canonical->getValueFromStoreOrDefault();
385
386    }
387
388
389    /**
390     * Rebuild the page
391     * (refresh from disk, reset object to null)
392     * @return $this
393     */
394    public
395    function rebuild(): Page
396    {
397        $this->readStore = null;
398        $this->buildPropertiesFromFileSystem();
399        $this->databasePage = null;
400        $this->htmlDocument = null;
401        $this->instructionsDocument = null;
402        return $this;
403    }
404
405    /**
406     *
407     * @return Page[]|null the internal links or null
408     */
409    public
410    function getLinkReferences(): ?array
411    {
412        $store = $this->getReadStoreOrDefault();
413        if (!($store instanceof MetadataDokuWikiStore)) {
414            return null;
415        }
416        $metadata = $store->getCurrentFromName('relation');
417        if ($metadata === null) {
418            /**
419             * Happens when no rendering has been made
420             */
421            return null;
422        }
423        if (!key_exists('references', $metadata)) {
424            return null;
425        }
426
427        $pages = [];
428        foreach (array_keys($metadata['references']) as $referencePageId) {
429            $pages[$referencePageId] = Page::createPageFromId($referencePageId);
430        }
431        return $pages;
432
433    }
434
435
436    public
437    function getHtmlDocument(): HtmlDocument
438    {
439        if ($this->htmlDocument === null) {
440            $this->htmlDocument = new HtmlDocument($this);
441        }
442        return $this->htmlDocument;
443
444    }
445
446    /**
447     * Set the page quality
448     * @param boolean $value true if this is a low quality page rank false otherwise
449     * @throws ExceptionCombo
450     */
451    public
452    function setCanBeOfLowQuality(bool $value): Page
453    {
454        return $this->setQualityIndicatorAndDeleteCacheIfNeeded($this->canBeOfLowQuality, $value);
455    }
456
457    /**
458     * @return Page[] the backlinks
459     * Duplicate of related
460     *
461     * Same as {@link DokuPath::getReferencedBy()} ?
462     */
463    public
464    function getBacklinks(): array
465    {
466        $backlinks = array();
467        /**
468         * Same as
469         * idx_get_indexer()->lookupKey('relation_references', $ID);
470         */
471        $ft_backlinks = ft_backlinks($this->getDokuwikiId());
472        foreach ($ft_backlinks as $backlinkId) {
473            $backlinks[$backlinkId] = Page::createPageFromId($backlinkId);
474        }
475        return $backlinks;
476    }
477
478
479    /**
480     * Low page quality
481     * @return bool true if this is a low quality page
482     */
483    function isLowQualityPage(): bool
484    {
485
486        $canBeOfLowQuality = $this->getCanBeOfLowQuality();
487        if ($canBeOfLowQuality === false) {
488            return false;
489        }
490        if (!Site::isLowQualityProtectionEnable()) {
491            return false;
492        }
493        return $this->getLowQualityIndicatorCalculated();
494
495
496    }
497
498
499    public
500    function getCanBeOfLowQuality(): ?bool
501    {
502
503        return $this->canBeOfLowQuality->getValue();
504
505    }
506
507
508    public
509    function getH1(): ?string
510    {
511
512        return $this->h1->getValueFromStore();
513
514    }
515
516    /**
517     * Return the Title
518     * @deprecated for {@link PageTitle::getValue()}
519     */
520    public
521    function getTitle(): ?string
522    {
523        return $this->title->getValueFromStore();
524    }
525
526    /**
527     * If true, the page is quality monitored (a note is shown to the writer)
528     * @return null|bool
529     */
530    public
531    function getQualityMonitoringIndicator(): ?bool
532    {
533        return $this->qualityMonitoringIndicator->getValueFromStore();
534    }
535
536    /**
537     * @return string the title, or h1 if empty or the id if empty
538     * Shortcut to {@link PageTitle::getValueOrDefault()}
539     */
540    public
541    function getTitleOrDefault(): ?string
542    {
543        return $this->title->getValueFromStoreOrDefault();
544    }
545
546    /**
547     * @return mixed
548     * @deprecated for {@link PageH1::getValueOrDefault()}
549     */
550    public
551    function getH1OrDefault()
552    {
553
554        return $this->h1->getValueFromStoreOrDefault();
555
556
557    }
558
559    /**
560     * @return mixed
561     */
562    public
563    function getDescription(): ?string
564    {
565        return $this->description->getValueFromStore();
566    }
567
568
569    /**
570     * @return string - the description or the dokuwiki generated description
571     */
572    public
573    function getDescriptionOrElseDokuWiki(): ?string
574    {
575        return $this->description->getValueFromStoreOrDefault();
576    }
577
578
579    /**
580     * @return string
581     * A wrapper around {@link FileSystems::getContent()} with {@link DokuPath}
582     */
583    public
584    function getTextContent(): string
585    {
586        /**
587         *
588         * use {@link io_readWikiPage(wikiFN($id, $rev), $id, $rev)};
589         */
590        return rawWiki($this->getPath()->getDokuwikiId());
591    }
592
593
594    public
595    function isInIndex()
596    {
597        $Indexer = idx_get_indexer();
598        $pages = $Indexer->getPages();
599        $return = array_search($this->getPath()->getDokuwikiId(), $pages, true);
600        return $return !== false;
601    }
602
603
604    public
605    function upsertContent($content, $summary = "Default"): Page
606    {
607        saveWikiText($this->getPath()->getDokuwikiId(), $content, $summary);
608        return $this;
609    }
610
611    public
612    function addToIndex()
613    {
614        /**
615         * Add to index check the metadata cache
616         * Because we log the cache at the requested page level, we need to
617         * set the global ID
618         */
619        global $ID;
620        $keep = $ID;
621        try {
622            $ID = $this->getPath()->getDokuwikiId();
623            idx_addPage($ID);
624        } finally {
625            $ID = $keep;
626        }
627        return $this;
628
629    }
630
631    /**
632     * @return mixed
633     */
634    public
635    function getTypeOrDefault()
636    {
637        return $this->type->getValueFromStoreOrDefault();
638    }
639
640
641    public
642    function getFirstImage()
643    {
644        return $this->pageImages->getFirstImage();
645    }
646
647    /**
648     * Return the media stored during parsing
649     *
650     * They are saved via the function {@link \Doku_Renderer_metadata::_recordMediaUsage()}
651     * called by the {@link \Doku_Renderer_metadata::internalmedia()}
652     *
653     *
654     * {@link \Doku_Renderer_metadata::externalmedia()} does not save them
655     */
656    public
657    function getMediasMetadata(): ?array
658    {
659
660        $store = $this->getReadStoreOrDefault();
661        if (!($store instanceof MetadataDokuWikiStore)) {
662            return null;
663        }
664        $medias = [];
665
666        $relation = $store->getCurrentFromName('relation');
667        if (isset($relation['media'])) {
668            /**
669             * The relation is
670             * $this->meta['relation']['media'][$src] = $exists;
671             *
672             */
673            foreach ($relation['media'] as $src => $exists) {
674                if ($exists) {
675                    $medias[] = $src;
676                }
677            }
678        }
679        return $medias;
680    }
681
682    /**
683     * An array of local/internal images that represents the same image
684     * but in different dimension and ratio
685     * (may be empty)
686     * @return PageImage[]
687     */
688    public
689    function getPageImagesOrDefault(): array
690    {
691
692        return $this->pageImages->getValueAsPageImagesOrDefault();
693
694    }
695
696
697    /**
698     * @return Image
699     */
700    public
701    function getImage(): ?Image
702    {
703
704        $images = $this->getPageImagesOrDefault();
705        if (sizeof($images) >= 1) {
706            return $images[0]->getImage();
707        } else {
708            return null;
709        }
710
711    }
712
713    /**
714     * Get author name
715     *
716     * @return string
717     */
718    public
719    function getAuthor(): ?string
720    {
721        $store = $this->getReadStoreOrDefault();
722        if (!($store instanceof MetadataDokuWikiStore)) {
723            return null;
724        }
725
726        return $store->getFromPersistentName('creator');
727    }
728
729    /**
730     * Get author ID
731     *
732     * @return string
733     */
734    public
735    function getAuthorID(): ?string
736    {
737
738        $store = $this->getReadStoreOrDefault();
739        if (!($store instanceof MetadataDokuWikiStore)) {
740            return null;
741        }
742
743        return $store->getFromPersistentName('user');
744
745    }
746
747
748    /**
749     * Get the create date of page
750     *
751     * @return DateTime
752     */
753    public
754    function getCreatedTime(): ?DateTime
755    {
756        return $this->creationTime->getValueFromStore();
757    }
758
759
760    /**
761     *
762     * @return null|DateTime
763     */
764    public
765    function getModifiedTime(): ?\DateTime
766    {
767        return $this->modifiedTime->getValueFromStore();
768    }
769
770    public
771    function getModifiedTimeOrDefault(): ?\DateTime
772    {
773        return $this->modifiedTime->getValueFromStoreOrDefault();
774    }
775
776
777    /**
778     * Refresh the metadata (used only in test)
779     *
780     * Trigger a:
781     *  a {@link p_render_metadata() metadata render}
782     *  a {@link p_save_metadata() metadata save}
783     *
784     * Note that {@link p_get_metadata()} uses a strange recursion
785     * There is a metadata recursion logic to avoid rendering
786     * that is not easy to grasp
787     * and therefore you may get no metadata and no backlinks
788     */
789    public
790    function renderMetadataAndFlush(): Page
791    {
792
793        if (!$this->exists()) {
794            if (PluginUtility::isDevOrTest()) {
795                LogUtility::msg("You can't render the metadata of a page that does not exist");
796            }
797            return $this;
798        }
799
800        /**
801         * @var MetadataDokuWikiStore $metadataStore
802         */
803        $metadataStore = $this->getReadStoreOrDefault();
804        $metadataStore->renderAndPersist();
805
806        /**
807         * Return
808         */
809        return $this;
810
811    }
812
813    /**
814     * @return string|null
815     * @deprecated for {@link Region}
816     */
817    public
818    function getLocaleRegion(): ?string
819    {
820        return $this->region->getValueFromStore();
821    }
822
823    public
824    function getRegionOrDefault()
825    {
826
827        return $this->region->getValueFromStoreOrDefault();
828
829    }
830
831    public
832    function getLang(): ?string
833    {
834        return $this->lang->getValueFromStore();
835    }
836
837    public
838    function getLangOrDefault()
839    {
840
841        return $this->lang->getValueFromStoreOrDefault();
842    }
843
844    /**
845     * Adapted from {@link FsWikiUtility::getHomePagePath()}
846     * @return bool
847     */
848    public
849    function isHomePage(): bool
850    {
851        global $conf;
852        $startPageName = $conf['start'];
853        if ($this->getPath()->getLastName() == $startPageName) {
854            return true;
855        } else {
856            $namespace = $this->dokuPath->getParent();
857            if ($namespace->getLastName() === $this->getPath()->getLastName()) {
858                /**
859                 * page named like the NS inside the NS
860                 * ie ns:ns
861                 */
862                $startPage = Page::createPageFromId($namespace->getDokuwikiId() . DokuPath::PATH_SEPARATOR . $startPageName);
863                if (!$startPage->exists()) {
864                    return true;
865                }
866            }
867        }
868        return false;
869    }
870
871
872    public
873    function getPublishedTime(): ?DateTime
874    {
875        return $this->publishedDate->getValueFromStore();
876    }
877
878
879    /**
880     * @return DateTime
881     */
882    public
883    function getPublishedElseCreationTime(): ?DateTime
884    {
885        return $this->publishedDate->getValueFromStoreOrDefault();
886    }
887
888
889    public
890    function isLatePublication(): bool
891    {
892        $dateTime = $this->getPublishedElseCreationTime();
893        return $dateTime > new DateTime('now');
894    }
895
896    /**
897     * The unique page Url (also known as Canonical URL) used:
898     *   * in the link
899     *   * in the canonical ref
900     *   * in the site map
901     * @param array $urlParameters
902     * @param bool $absoluteUrlMandatory - by default, dokuwiki allows the canonical to be relative but it's mandatory to be absolute for the HTML meta
903     * @param string $separator - TODO: delete. HTML encoded or not ampersand (the default should always be good because the encoding is done just before printing (ie {@link TagAttributes::encodeToHtmlValue()})
904     * @return string|null
905     */
906    public
907    function getCanonicalUrl(array $urlParameters = [], bool $absoluteUrlMandatory = false, string $separator = DokuwikiUrl::AMPERSAND_CHARACTER): ?string
908    {
909
910        /**
911         * Conf
912         */
913        $urlType = PageUrlType::getOrCreateForPage($this)->getValue();
914        if ($urlType === PageUrlType::CONF_VALUE_PAGE_PATH && $absoluteUrlMandatory == false) {
915            $absoluteUrlMandatory = Site::shouldUrlBeAbsolute();
916        }
917
918        /**
919         * Dokuwiki Methodology Taken from {@link tpl_metaheaders()}
920         */
921        if ($absoluteUrlMandatory && $this->isRootHomePage()) {
922            return DOKU_URL;
923        }
924
925        return wl($this->getUrlId(), $urlParameters, $absoluteUrlMandatory, $separator);
926
927
928    }
929
930    public function getUrl($type = null): ?string
931    {
932        if ($type === null) {
933            return $this->getCanonicalUrl();
934        }
935        $pageUrlId = DokuPath::toDokuwikiId(PageUrlPath::createForPage($this)
936            ->getUrlPathFromType($type));
937        return wl($pageUrlId);
938    }
939
940
941    /**
942     *
943     * @return string|null - the locale facebook way
944     * @deprecated for {@link Locale}
945     */
946    public
947    function getLocale($default = null): ?string
948    {
949        $value = $this->locale->getValueFromStore();
950        if ($value === null) {
951            return $default;
952        }
953        return $value;
954    }
955
956
957    /**
958     *
959     */
960    public
961    function toXhtml(): ?string
962    {
963
964        return $this->getHtmlDocument()->getOrProcessContent();
965
966    }
967
968
969    public
970    function getHtmlAnchorLink($logicalTag = null): string
971    {
972        $id = $this->getPath()->getDokuwikiId();
973        try {
974            return MarkupRef::createFromPageId($id)
975                    ->toAttributes($logicalTag)
976                    ->toHtmlEnterTag("a")
977                . $this->getNameOrDefault()
978                . "</a>";
979        } catch (ExceptionCombo $e) {
980            LogUtility::msg("The markup ref returns an error for the creation of the page anchor html link ($this). Error: {$e->getMessage()}");
981            return "<a href=\"{$this->getCanonicalUrl()}\" data-wiki-id=\"$id\">{$this->getNameOrDefault()}</a>";
982        }
983    }
984
985
986    /**
987     * Without the `:` at the end
988     * @return string
989     * @deprecated / shortcut for {@link DokuPath::getParent()}
990     * Because a page has always a parent, the string is never null.
991     */
992    public
993    function getNamespacePath(): string
994    {
995
996        return $this->dokuPath->getParent()->toString();
997
998    }
999
1000
1001    /**
1002     * @return $this
1003     * @deprecated use {@link MetadataDokuWikiStore::deleteAndFlush()}
1004     */
1005    public
1006    function deleteMetadatasAndFlush(): Page
1007    {
1008        MetadataDokuWikiStore::getOrCreateFromResource($this)
1009            ->deleteAndFlush();
1010        return $this;
1011    }
1012
1013    public
1014    function getName(): ?string
1015    {
1016
1017        return $this->pageName->getValueFromStore();
1018
1019    }
1020
1021    public
1022    function getNameOrDefault(): string
1023    {
1024        return $this->pageName->getValueFromStoreOrDefault();
1025    }
1026
1027    /**
1028     * @param $property
1029     */
1030    public
1031    function unsetMetadata($property)
1032    {
1033        $meta = p_read_metadata($this->getPath()->getDokuwikiId());
1034        if (isset($meta['persistent'][$property])) {
1035            unset($meta['persistent'][$property]);
1036        }
1037        p_save_metadata($this->getPath()->getDokuwikiId(), $meta);
1038
1039    }
1040
1041    /**
1042     * @return array - return the standard / generated metadata
1043     * used in templating with the value or default
1044     * TODO: should move in the templating class
1045     */
1046    public
1047    function getMetadataForRendering(): array
1048    {
1049
1050        $metadataNames = [
1051            PageH1::PROPERTY_NAME,
1052            PageTitle::TITLE,
1053            PageId::PROPERTY_NAME,
1054            Canonical::PROPERTY_NAME,
1055            PagePath::PROPERTY_NAME,
1056            PageDescription::PROPERTY_NAME,
1057            ResourceName::PROPERTY_NAME,
1058            PageType::PROPERTY_NAME,
1059            Slug::PROPERTY_NAME,
1060            PageCreationDate::PROPERTY_NAME,
1061            ModificationDate::PROPERTY_NAME,
1062            PagePublicationDate::PROPERTY_NAME,
1063            StartDate::PROPERTY_NAME,
1064            EndDate::PROPERTY_NAME,
1065            PageLayout::PROPERTY_NAME,
1066            // Dokuwiki id is deprecated for path, no more advertised
1067            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE
1068        ];
1069
1070        foreach ($metadataNames as $metadataName) {
1071            $metadata = Metadata::getForName($metadataName);
1072            if ($metadata === null) {
1073                LogUtility::msg("The metadata ($metadata) should be defined");
1074                continue;
1075            }
1076            /**
1077             * The Value or Default is returned
1078             *
1079             * Because the title/h1 should never be null
1080             * otherwise a template link such as [[$path|$title]] will return a link without an description
1081             * and therefore will be not visible
1082             *
1083             * ToStoreValue to get the string format of date/boolean in the {@link PipelineUtility}
1084             * If we want the native value, we need to change the pipeline
1085             */
1086            $value = $metadata
1087                ->setResource($this)
1088                ->setWriteStore(TemplateStore::class)
1089                ->toStoreValueOrDefault();
1090            if ($metadata->getDataType() === DataType::TEXT_TYPE_VALUE) {
1091
1092                /**
1093                 * Hack: Replace every " by a ' to be able to detect/parse the title/h1 on a pipeline
1094                 * @see {@link \syntax_plugin_combo_pipeline}
1095                 */
1096                $value = str_replace('"', "'", $value);
1097            }
1098            $array[$metadataName] = $value;
1099        }
1100        $array["url"] = $this->getCanonicalUrl();
1101        return $array;
1102
1103    }
1104
1105    public
1106    function __toString()
1107    {
1108        return $this->dokuPath->toUriString();
1109    }
1110
1111
1112    public
1113    function getPublishedTimeAsString(): ?string
1114    {
1115        return $this->getPublishedTime() !== null ? $this->getPublishedTime()->format(Iso8601Date::getFormat()) : null;
1116    }
1117
1118    public
1119    function getEndDateAsString(): ?string
1120    {
1121        return $this->getEndDate() !== null ? $this->getEndDate()->format(Iso8601Date::getFormat()) : null;
1122    }
1123
1124    public
1125    function getEndDate(): ?DateTime
1126    {
1127        return $this->endDate->getValueFromStore();
1128    }
1129
1130    public
1131    function getStartDateAsString(): ?string
1132    {
1133        return $this->getStartDate() !== null ? $this->getStartDate()->format(Iso8601Date::getFormat()) : null;
1134    }
1135
1136    public
1137    function getStartDate(): ?DateTime
1138    {
1139        return $this->startDate->getValueFromStore();
1140    }
1141
1142    /**
1143     * A page id or null if the page id does not exists
1144     * @return string|null
1145     */
1146    public
1147    function getPageId(): ?string
1148    {
1149
1150        return $this->pageId->getValue();
1151
1152    }
1153
1154
1155    public
1156    function getAnalyticsDocument(): AnalyticsDocument
1157    {
1158        return new AnalyticsDocument($this);
1159    }
1160
1161    public
1162    function getDatabasePage(): DatabasePageRow
1163    {
1164        if ($this->databasePage == null) {
1165            $this->databasePage = DatabasePageRow::createFromPageObject($this);
1166        }
1167        return $this->databasePage;
1168    }
1169
1170    public
1171    function canBeUpdatedByCurrentUser(): bool
1172    {
1173        return Identity::isWriter($this->getDokuwikiId());
1174    }
1175
1176
1177    public
1178    function isRootHomePage(): bool
1179    {
1180        global $conf;
1181        $startPageName = $conf['start'];
1182        return $this->getPath()->toString() === ":$startPageName";
1183
1184    }
1185
1186    /**
1187     * Used when the page is moved to take the Page Id of the source
1188     * @param string|null $pageId
1189     * @return Page
1190     * @throws ExceptionCombo
1191     */
1192    public
1193    function setPageId(?string $pageId): Page
1194    {
1195
1196        $this->pageId
1197            ->setValue($pageId)
1198            ->sendToWriteStore();
1199
1200        return $this;
1201
1202    }
1203
1204    public
1205    function getPageType(): ?string
1206    {
1207        return $this->type->getValueFromStore();
1208    }
1209
1210    public
1211    function getCanonical(): ?string
1212    {
1213        return $this->canonical->getValueFromStore();
1214    }
1215
1216    /**
1217     * Create a canonical from the last page path part.
1218     *
1219     * @return string|null
1220     */
1221    public
1222    function getDefaultCanonical(): ?string
1223    {
1224        return $this->canonical->getDefaultValue();
1225    }
1226
1227    public
1228    function getLayout()
1229    {
1230        return $this->layout->getValueFromStore();
1231    }
1232
1233    public
1234    function getDefaultPageName(): string
1235    {
1236        return $this->pageName->getDefaultValue();
1237    }
1238
1239    public
1240    function getDefaultTitle(): ?string
1241    {
1242        return $this->title->getDefaultValue();
1243    }
1244
1245    public
1246    function getDefaultH1()
1247    {
1248        return $this->h1->getValueOrDefault();
1249    }
1250
1251    public
1252    function getDefaultType(): string
1253    {
1254        return $this->type->getDefaultValue();
1255    }
1256
1257    public
1258    function getDefaultLayout(): string
1259    {
1260        return $this->layout->getDefaultValue();
1261    }
1262
1263
1264    /**
1265     * @throws ExceptionCombo
1266     */
1267    public
1268    function setLowQualityIndicatorCalculation($bool): Page
1269    {
1270        return $this->setQualityIndicatorAndDeleteCacheIfNeeded($this->lowQualityIndicatorCalculated, $bool);
1271    }
1272
1273
1274    /**
1275     * Change the quality indicator
1276     * and if the quality level has become low
1277     * and that the protection is on, delete the cache
1278     * @param MetadataBoolean $lowQualityAttributeName
1279     * @param bool $value
1280     * @return Page
1281     * @throws ExceptionCombo
1282     */
1283    private
1284    function setQualityIndicatorAndDeleteCacheIfNeeded(MetadataBoolean $lowQualityAttributeName, bool $value): Page
1285    {
1286        $actualValue = $lowQualityAttributeName->getValue();
1287        if ($actualValue === null || $value !== $actualValue) {
1288            $lowQualityAttributeName
1289                ->setValue($value)
1290                ->persist();
1291        }
1292        return $this;
1293    }
1294
1295
1296    public
1297    function getLowQualityIndicatorCalculated()
1298    {
1299
1300        return $this->lowQualityIndicatorCalculated->getValueOrDefault();
1301
1302    }
1303
1304    /**
1305     * @return PageImage[]
1306     */
1307    public
1308    function getPageImages(): ?array
1309    {
1310        return $this->pageImages->getValueAsPageImages();
1311    }
1312
1313
1314    /**
1315     * @return array|null
1316     * @deprecated for {@link LdJson}
1317     */
1318    public
1319    function getLdJson(): ?string
1320    {
1321        return $this->ldJson->getValue();
1322
1323    }
1324
1325    /**
1326     * @param array|string $jsonLd
1327     * @return $this
1328     * @throws ExceptionCombo
1329     * @deprecated for {@link LdJson}
1330     */
1331    public
1332    function setJsonLd($jsonLd): Page
1333    {
1334        $this->ldJson
1335            ->setValue($jsonLd)
1336            ->sendToWriteStore();
1337        return $this;
1338    }
1339
1340    /**
1341     * @throws ExceptionCombo
1342     */
1343    public
1344    function setPageType(string $value): Page
1345    {
1346        $this->type
1347            ->setValue($value)
1348            ->sendToWriteStore();
1349        return $this;
1350    }
1351
1352
1353    /**
1354     * @param $aliasPath
1355     * @param string $aliasType
1356     * @return Alias
1357     * @deprecated for {@link Aliases}
1358     */
1359    public
1360    function addAndGetAlias($aliasPath, string $aliasType = AliasType::REDIRECT): Alias
1361    {
1362
1363        return $this->aliases->addAndGetAlias($aliasPath, $aliasType);
1364
1365    }
1366
1367
1368    /**
1369     * @return Alias[]
1370     */
1371    public
1372    function getAliases(): ?array
1373    {
1374        return $this->aliases->getValueAsAlias();
1375    }
1376
1377    /**
1378     * @return string|null
1379     *
1380     */
1381    public
1382    function getSlugOrDefault(): ?string
1383    {
1384
1385        if ($this->getSlug() !== null) {
1386            return $this->getSlug();
1387        }
1388        return $this->getDefaultSlug();
1389    }
1390
1391    /**
1392     *
1393     * @return string|null
1394     *
1395     */
1396    public
1397    function getDefaultSlug(): ?string
1398    {
1399        return $this->slug->getDefaultValue();
1400    }
1401
1402    /**
1403     * The parent page is the parent in the page tree directory
1404     *
1405     * If the page is at the root, the parent page is the root home
1406     * Only the root home does not have any parent page and return null.
1407     *
1408     * @return Page|null
1409     */
1410    public
1411    function getParentPage(): ?Page
1412    {
1413
1414        $names = $this->getPath()->getNames();
1415        if (sizeof($names) == 0) {
1416            return null;
1417        }
1418        $slice = 1;
1419        if ($this->isHomePage()) {
1420            /**
1421             * The parent of a home page
1422             * is in the parent directory
1423             */
1424            $slice = 2;
1425        }
1426        /**
1427         * Delete the last or the two last parts
1428         */
1429        if (sizeof($names) < $slice) {
1430            return null;
1431        }
1432        /**
1433         * Get the actual directory for a page
1434         * or the parent directory for a home page
1435         */
1436        $parentNames = array_slice($names, 0, sizeof($names) - $slice);
1437        /**
1438         * Create the parent namespace id
1439         */
1440        $parentNamespaceId = implode(DokuPath::PATH_SEPARATOR, $parentNames);
1441        return self::getHomePageFromNamespace($parentNamespaceId);
1442
1443    }
1444
1445    /**
1446     * @throws ExceptionCombo
1447     */
1448    public
1449    function setDescription($description): Page
1450    {
1451
1452        $this->description
1453            ->setValue($description)
1454            ->sendToWriteStore();
1455        return $this;
1456    }
1457
1458    /**
1459     * @throws ExceptionCombo
1460     * @deprecated uses {@link EndDate} instead
1461     */
1462    public
1463    function setEndDate($value): Page
1464    {
1465        $this->endDate
1466            ->setFromStoreValue($value)
1467            ->sendToWriteStore();
1468        return $this;
1469    }
1470
1471    /**
1472     * @throws ExceptionCombo
1473     * @deprecated uses {@link StartDate} instead
1474     */
1475    public
1476    function setStartDate($value): Page
1477    {
1478        $this->startDate
1479            ->setFromStoreValue($value)
1480            ->sendToWriteStore();
1481        return $this;
1482    }
1483
1484    /**
1485     * @throws ExceptionCombo
1486     */
1487    public
1488    function setPublishedDate($value): Page
1489    {
1490        $this->publishedDate
1491            ->setFromStoreValue($value)
1492            ->sendToWriteStore();
1493        return $this;
1494    }
1495
1496    /**
1497     * Utility to {@link ResourceName::setValue()}
1498     * Used mostly to create page in test
1499     * @throws ExceptionCombo
1500     */
1501    public
1502    function setPageName($value): Page
1503    {
1504        $this->pageName
1505            ->setValue($value)
1506            ->sendToWriteStore();
1507        return $this;
1508    }
1509
1510
1511    /**
1512     * @throws ExceptionCombo
1513     */
1514    public
1515    function setTitle($value): Page
1516    {
1517        $this->title
1518            ->setValue($value)
1519            ->sendToWriteStore();
1520        return $this;
1521    }
1522
1523    /**
1524     * @throws ExceptionCombo
1525     */
1526    public
1527    function setH1($value): Page
1528    {
1529        $this->h1
1530            ->setValue($value)
1531            ->sendToWriteStore();
1532        return $this;
1533    }
1534
1535    /**
1536     * @throws Exception
1537     */
1538    public
1539    function setRegion($value): Page
1540    {
1541        $this->region
1542            ->setFromStoreValue($value)
1543            ->sendToWriteStore();
1544        return $this;
1545    }
1546
1547    /**
1548     * @throws ExceptionCombo
1549     */
1550    public
1551    function setLang($value): Page
1552    {
1553
1554        $this->lang
1555            ->setFromStoreValue($value)
1556            ->sendToWriteStore();
1557        return $this;
1558    }
1559
1560    /**
1561     * @throws ExceptionCombo
1562     */
1563    public
1564    function setLayout($value): Page
1565    {
1566        $this->layout
1567            ->setValue($value)
1568            ->sendToWriteStore();
1569        return $this;
1570    }
1571
1572
1573    /**
1574     *
1575     * We manage the properties by setter and getter
1576     *
1577     * Why ?
1578     *   * Because we can capture the updates
1579     *   * Because setter are the entry point to good quality data
1580     *   * Because dokuwiki may cache the metadata (see below)
1581     *
1582     * Note all properties have been migrated
1583     * but they should be initialized below
1584     *
1585     * Dokuwiki cache: the data may be cached without our consent
1586     * The method {@link p_get_metadata()} does it with this logic
1587     * ```
1588     * $cache = ($ID == $id);
1589     * $meta = p_read_metadata($id, $cache);
1590     * ```
1591     */
1592    private
1593    function buildPropertiesFromFileSystem()
1594    {
1595
1596        /**
1597         * New meta system
1598         * Even if it does not exist, the metadata object should be instantiated
1599         * otherwise, there is a null exception
1600         */
1601        $this->cacheExpirationDate = CacheExpirationDate::createForPage($this);
1602        $this->aliases = Aliases::createForPage($this);
1603        $this->pageImages = PageImages::createForPage($this);
1604        $this->pageName = ResourceName::createForResource($this);
1605        $this->cacheExpirationFrequency = CacheExpirationFrequency::createForPage($this);
1606        $this->ldJson = LdJson::createForPage($this);
1607        $this->canonical = Canonical::createForPage($this);
1608        $this->pageId = PageId::createForPage($this);
1609        $this->description = PageDescription::createForPage($this);
1610        $this->h1 = PageH1::createForPage($this);
1611        $this->type = PageType::createForPage($this);
1612        $this->creationTime = PageCreationDate::createForPage($this);
1613        $this->title = PageTitle::createForPage($this);
1614        $this->keywords = PageKeywords::createForPage($this);
1615        $this->publishedDate = PagePublicationDate::createFromPage($this);
1616        $this->startDate = StartDate::createFromPage($this);
1617        $this->endDate = EndDate::createFromPage($this);
1618        $this->locale = Locale::createForPage($this);
1619        $this->lang = Lang::createForPage($this);
1620        $this->region = Region::createForPage($this);
1621        $this->slug = Slug::createForPage($this);
1622        $this->canBeOfLowQuality = LowQualityPageOverwrite::createForPage($this);
1623        $this->lowQualityIndicatorCalculated = LowQualityCalculatedIndicator::createFromPage($this);
1624        $this->qualityMonitoringIndicator = QualityDynamicMonitoringOverwrite::createFromPage($this);
1625        $this->modifiedTime = ModificationDate::createForPage($this);
1626        $this->pageUrlPath = PageUrlPath::createForPage($this);
1627        $this->layout = PageLayout::createFromPage($this);
1628
1629    }
1630
1631
1632    function getPageIdAbbr()
1633    {
1634
1635        if ($this->getPageId() === null) return null;
1636        return substr($this->getPageId(), 0, PageId::PAGE_ID_ABBREV_LENGTH);
1637
1638    }
1639
1640    public
1641    function setDatabasePage(DatabasePageRow $databasePage): Page
1642    {
1643        $this->databasePage = $databasePage;
1644        return $this;
1645    }
1646
1647    /**
1648     *
1649     * TODO: Move to {@link HtmlDocument} ?
1650     */
1651    public function getUrlPath(): string
1652    {
1653
1654        return $this->pageUrlPath->getValueOrDefault();
1655
1656    }
1657
1658
1659    /**
1660     * @return string|null
1661     *
1662     */
1663    public
1664    function getSlug(): ?string
1665    {
1666        return $this->slug->getValue();
1667    }
1668
1669
1670    /**
1671     * @throws ExceptionCombo
1672     */
1673    public
1674    function setSlug($slug): Page
1675    {
1676        $this->slug
1677            ->setFromStoreValue($slug)
1678            ->sendToWriteStore();
1679        return $this;
1680    }
1681
1682
1683    public
1684    function getUrlId()
1685    {
1686        return DokuPath::toDokuwikiId($this->getUrlPath());
1687    }
1688
1689
1690    /**
1691     * @throws ExceptionCombo
1692     */
1693    public
1694    function setQualityMonitoringIndicator($boolean): Page
1695    {
1696        $this->qualityMonitoringIndicator
1697            ->setFromStoreValue($boolean)
1698            ->sendToWriteStore();
1699        return $this;
1700    }
1701
1702    /**
1703     *
1704     * @param $aliasPath - third information - the alias used to build this page
1705     */
1706    public
1707    function setBuildAliasPath($aliasPath)
1708    {
1709        $this->buildAliasPath = $aliasPath;
1710    }
1711
1712    public
1713    function getBuildAlias(): ?Alias
1714    {
1715        if ($this->buildAliasPath === null) return null;
1716        foreach ($this->getAliases() as $alias) {
1717            if ($alias->getPath() === $this->buildAliasPath) {
1718                return $alias;
1719            }
1720        }
1721        return null;
1722    }
1723
1724    public
1725    function isDynamicQualityMonitored(): bool
1726    {
1727        if ($this->getQualityMonitoringIndicator() !== null) {
1728            return $this->getQualityMonitoringIndicator();
1729        }
1730        return $this->getDefaultQualityMonitoring();
1731    }
1732
1733    public
1734    function getDefaultQualityMonitoring(): bool
1735    {
1736        if (PluginUtility::getConfValue(action_plugin_combo_qualitymessage::CONF_DISABLE_QUALITY_MONITORING) === 1) {
1737            return false;
1738        } else {
1739            return true;
1740        }
1741    }
1742
1743    /**
1744     * @param MetadataStore|string $store
1745     * @return $this
1746     */
1747    public
1748    function setReadStore($store): Page
1749    {
1750        $this->readStore = $store;
1751        return $this;
1752    }
1753
1754
1755    /**
1756     * @param array $usages
1757     * @return Image[]
1758     */
1759    public
1760    function getImagesOrDefaultForTheFollowingUsages(array $usages): array
1761    {
1762        $usages = array_merge($usages, [PageImageUsage::ALL]);
1763        $images = [];
1764        foreach ($this->getPageImagesOrDefault() as $pageImage) {
1765            foreach ($usages as $usage) {
1766                if (in_array($usage, $pageImage->getUsages())) {
1767                    $images[] = $pageImage->getImage();
1768                    continue 2;
1769                }
1770            }
1771        }
1772        return $images;
1773
1774    }
1775
1776
1777    public
1778    function getKeywords(): ?array
1779    {
1780        return $this->keywords->getValues();
1781    }
1782
1783    public
1784    function getKeywordsOrDefault(): array
1785    {
1786        return $this->keywords->getValueOrDefaults();
1787    }
1788
1789
1790    /**
1791     * @throws ExceptionCombo
1792     */
1793    public
1794    function setKeywords($value): Page
1795    {
1796        $this->keywords
1797            ->setFromStoreValue($value)
1798            ->sendToWriteStore();
1799        return $this;
1800    }
1801
1802    /**
1803     * @return DateTime|null
1804     * @deprecated for {@link CacheExpirationDate}
1805     */
1806    public
1807    function getCacheExpirationDate(): ?DateTime
1808    {
1809        return $this->cacheExpirationDate->getValue();
1810    }
1811
1812    /**
1813     * @return DateTime|null
1814     * @deprecated for {@link CacheExpirationDate}
1815     */
1816    public
1817    function getDefaultCacheExpirationDate(): ?DateTime
1818    {
1819        return $this->cacheExpirationDate->getDefaultValue();
1820    }
1821
1822    /**
1823     * @return string|null
1824     * @deprecated for {@link CacheExpirationFrequency}
1825     */
1826    public
1827    function getCacheExpirationFrequency(): ?string
1828    {
1829        return $this->cacheExpirationFrequency->getValue();
1830    }
1831
1832
1833    /**
1834     * @param DateTime $cacheExpirationDate
1835     * @return $this
1836     * @deprecated for {@link CacheExpirationDate}
1837     */
1838    public
1839    function setCacheExpirationDate(DateTime $cacheExpirationDate): Page
1840    {
1841        $this->cacheExpirationDate->setValue($cacheExpirationDate);
1842        return $this;
1843    }
1844
1845    /**
1846     * @return bool - true if the page has changed
1847     * @deprecated use {@link Page::getInstructionsDocument()} instead
1848     */
1849    public
1850    function isParseCacheUsable(): bool
1851    {
1852        return $this->getInstructionsDocument()->shouldProcess() === false;
1853    }
1854
1855    /**
1856     * @return $this
1857     * @deprecated use {@link Page::getInstructionsDocument()} instead
1858     * Parse a page and put the instructions in the cache
1859     */
1860    public
1861    function parse(): Page
1862    {
1863
1864        $this->getInstructionsDocument()
1865            ->process();
1866
1867        return $this;
1868
1869    }
1870
1871    /**
1872     *
1873     */
1874    public
1875    function getInstructionsDocument(): InstructionsDocument
1876    {
1877        if ($this->instructionsDocument === null) {
1878            $this->instructionsDocument = new InstructionsDocument($this);
1879        }
1880        return $this->instructionsDocument;
1881
1882    }
1883
1884    public
1885    function delete()
1886    {
1887
1888        Index::getOrCreate()->deletePage($this);
1889        saveWikiText($this->getDokuwikiId(), "", "Delete");
1890
1891    }
1892
1893    /**
1894     * @return string|null -the absolute canonical url
1895     */
1896    public
1897    function getAbsoluteCanonicalUrl(): ?string
1898    {
1899        return $this->getCanonicalUrl([], true);
1900    }
1901
1902
1903    public
1904    function getReadStoreOrDefault(): MetadataStore
1905    {
1906        if ($this->readStore === null) {
1907            /**
1908             * No cache please if not set
1909             * Cache should be in the MetadataDokuWikiStore
1910             * that is page requested scoped and not by slot
1911             */
1912            return MetadataDokuWikiStore::getOrCreateFromResource($this);
1913        }
1914        if (!($this->readStore instanceof MetadataStore)) {
1915            $this->readStore = MetadataStoreAbs::toMetadataStore($this->readStore, $this);
1916        }
1917        return $this->readStore;
1918    }
1919
1920    /**
1921     * @return DokuPath
1922     */
1923    public
1924    function getPath(): Path
1925    {
1926        return $this->dokuPath;
1927    }
1928
1929
1930    /**
1931     * A shortcut for {@link Page::getPath()::getDokuwikiId()}
1932     */
1933    public
1934    function getDokuwikiId()
1935    {
1936        return $this->getPath()->getDokuwikiId();
1937    }
1938
1939    public
1940    function getUid(): Metadata
1941    {
1942        return $this->pageId;
1943    }
1944
1945
1946    public
1947    function getAbsolutePath(): string
1948    {
1949        return DokuPath::PATH_SEPARATOR . $this->getDokuwikiId();
1950    }
1951
1952    function getType(): string
1953    {
1954        return self::TYPE;
1955    }
1956
1957    public
1958    function getUrlPathObject(): PageUrlPath
1959    {
1960        return $this->pageUrlPath;
1961    }
1962
1963    public function getMainFooterSlot(): ?Page
1964    {
1965        if ($this->isSecondarySlot() || $this->isRootHomePage()) {
1966            return null;
1967        }
1968
1969        try {
1970            Site::loadStrapUtilityTemplateIfPresentAndSameVersion();
1971        } catch (ExceptionCombo $e) {
1972            LogUtility::msg("We can't load strap. The nearest main footer slot could not be detected, Error: {$e->getMessage()}");
1973            return null;
1974        }
1975
1976        $nearestMainFooter = $this->findNearest(TplUtility::SLOT_MAIN_FOOTER_NAME);
1977        if ($nearestMainFooter === false) {
1978            return null;
1979        }
1980        return Page::createPageFromId($nearestMainFooter);
1981
1982
1983    }
1984
1985    public function getSideSlot(): ?Page
1986    {
1987        if ($this->isSecondarySlot() || $this->isRootHomePage()) {
1988            return null;
1989        }
1990
1991        $nearestMainFooter = $this->findNearest(Site::getSidebarName());
1992        if ($nearestMainFooter === false) {
1993            return null;
1994        }
1995        return Page::createPageFromId($nearestMainFooter);
1996
1997
1998    }
1999
2000    /**
2001     * @param $pageName
2002     * @return false|string
2003     */
2004    private function findNearest($pageName)
2005    {
2006        global $ID;
2007        $keep = $ID;
2008        try {
2009            $ID = $this->getDokuwikiId();
2010            return page_findnearest($pageName);
2011        } finally {
2012            $ID = $keep;
2013        }
2014
2015    }
2016
2017    /**
2018     * @return Page[]
2019     */
2020    public function getSecondarySlots(): array
2021    {
2022        $secondarySlots = [];
2023        $sideSlot = $this->getSideSlot();
2024        if ($sideSlot !== null) {
2025            $secondarySlots[] = $sideSlot;
2026        }
2027        $footerSlot = $this->getMainFooterSlot();
2028        if ($footerSlot !== null) {
2029            $secondarySlots[] = $footerSlot;
2030        }
2031        return $secondarySlots;
2032    }
2033
2034
2035    public function isHidden(): bool
2036    {
2037        return isHiddenPage($this->getDokuwikiId());
2038    }
2039
2040
2041}
2042