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            PageKeywords::PROPERTY_NAME
1069        ];
1070
1071        foreach ($metadataNames as $metadataName) {
1072            $metadata = Metadata::getForName($metadataName);
1073            if ($metadata === null) {
1074                LogUtility::msg("The metadata ($metadata) should be defined");
1075                continue;
1076            }
1077            /**
1078             * The Value or Default is returned
1079             *
1080             * Because the title/h1 should never be null
1081             * otherwise a template link such as [[$path|$title]] will return a link without an description
1082             * and therefore will be not visible
1083             *
1084             * ToStoreValue to get the string format of date/boolean in the {@link PipelineUtility}
1085             * If we want the native value, we need to change the pipeline
1086             */
1087            $value = $metadata
1088                ->setResource($this)
1089                ->setWriteStore(TemplateStore::class)
1090                ->toStoreValueOrDefault();
1091            if ($metadata->getDataType() === DataType::TEXT_TYPE_VALUE) {
1092
1093                /**
1094                 * Hack: Replace every " by a ' to be able to detect/parse the title/h1 on a pipeline
1095                 * @see {@link \syntax_plugin_combo_pipeline}
1096                 */
1097                $value = str_replace('"', "'", $value);
1098            }
1099            $array[$metadataName] = $value;
1100        }
1101        $array["url"] = $this->getCanonicalUrl();
1102        return $array;
1103
1104    }
1105
1106    public
1107    function __toString()
1108    {
1109        return $this->dokuPath->toUriString();
1110    }
1111
1112
1113    public
1114    function getPublishedTimeAsString(): ?string
1115    {
1116        return $this->getPublishedTime() !== null ? $this->getPublishedTime()->format(Iso8601Date::getFormat()) : null;
1117    }
1118
1119    public
1120    function getEndDateAsString(): ?string
1121    {
1122        return $this->getEndDate() !== null ? $this->getEndDate()->format(Iso8601Date::getFormat()) : null;
1123    }
1124
1125    public
1126    function getEndDate(): ?DateTime
1127    {
1128        return $this->endDate->getValueFromStore();
1129    }
1130
1131    public
1132    function getStartDateAsString(): ?string
1133    {
1134        return $this->getStartDate() !== null ? $this->getStartDate()->format(Iso8601Date::getFormat()) : null;
1135    }
1136
1137    public
1138    function getStartDate(): ?DateTime
1139    {
1140        return $this->startDate->getValueFromStore();
1141    }
1142
1143    /**
1144     * A page id or null if the page id does not exists
1145     * @return string|null
1146     */
1147    public
1148    function getPageId(): ?string
1149    {
1150
1151        return $this->pageId->getValue();
1152
1153    }
1154
1155
1156    public
1157    function getAnalyticsDocument(): AnalyticsDocument
1158    {
1159        return new AnalyticsDocument($this);
1160    }
1161
1162    public
1163    function getDatabasePage(): DatabasePageRow
1164    {
1165        if ($this->databasePage == null) {
1166            $this->databasePage = DatabasePageRow::createFromPageObject($this);
1167        }
1168        return $this->databasePage;
1169    }
1170
1171    public
1172    function canBeUpdatedByCurrentUser(): bool
1173    {
1174        return Identity::isWriter($this->getDokuwikiId());
1175    }
1176
1177
1178    public
1179    function isRootHomePage(): bool
1180    {
1181        global $conf;
1182        $startPageName = $conf['start'];
1183        return $this->getPath()->toString() === ":$startPageName";
1184
1185    }
1186
1187    /**
1188     * Used when the page is moved to take the Page Id of the source
1189     * @param string|null $pageId
1190     * @return Page
1191     * @throws ExceptionCombo
1192     */
1193    public
1194    function setPageId(?string $pageId): Page
1195    {
1196
1197        $this->pageId
1198            ->setValue($pageId)
1199            ->sendToWriteStore();
1200
1201        return $this;
1202
1203    }
1204
1205    public
1206    function getPageType(): ?string
1207    {
1208        return $this->type->getValueFromStore();
1209    }
1210
1211    public
1212    function getCanonical(): ?string
1213    {
1214        return $this->canonical->getValueFromStore();
1215    }
1216
1217    /**
1218     * Create a canonical from the last page path part.
1219     *
1220     * @return string|null
1221     */
1222    public
1223    function getDefaultCanonical(): ?string
1224    {
1225        return $this->canonical->getDefaultValue();
1226    }
1227
1228    public
1229    function getLayout()
1230    {
1231        return $this->layout->getValueFromStore();
1232    }
1233
1234    public
1235    function getDefaultPageName(): string
1236    {
1237        return $this->pageName->getDefaultValue();
1238    }
1239
1240    public
1241    function getDefaultTitle(): ?string
1242    {
1243        return $this->title->getDefaultValue();
1244    }
1245
1246    public
1247    function getDefaultH1()
1248    {
1249        return $this->h1->getValueOrDefault();
1250    }
1251
1252    public
1253    function getDefaultType(): string
1254    {
1255        return $this->type->getDefaultValue();
1256    }
1257
1258    public
1259    function getDefaultLayout(): string
1260    {
1261        return $this->layout->getDefaultValue();
1262    }
1263
1264
1265    /**
1266     * @throws ExceptionCombo
1267     */
1268    public
1269    function setLowQualityIndicatorCalculation($bool): Page
1270    {
1271        return $this->setQualityIndicatorAndDeleteCacheIfNeeded($this->lowQualityIndicatorCalculated, $bool);
1272    }
1273
1274
1275    /**
1276     * Change the quality indicator
1277     * and if the quality level has become low
1278     * and that the protection is on, delete the cache
1279     * @param MetadataBoolean $lowQualityAttributeName
1280     * @param bool $value
1281     * @return Page
1282     * @throws ExceptionCombo
1283     */
1284    private
1285    function setQualityIndicatorAndDeleteCacheIfNeeded(MetadataBoolean $lowQualityAttributeName, bool $value): Page
1286    {
1287        $actualValue = $lowQualityAttributeName->getValue();
1288        if ($actualValue === null || $value !== $actualValue) {
1289            $lowQualityAttributeName
1290                ->setValue($value)
1291                ->persist();
1292        }
1293        return $this;
1294    }
1295
1296
1297    public
1298    function getLowQualityIndicatorCalculated()
1299    {
1300
1301        return $this->lowQualityIndicatorCalculated->getValueOrDefault();
1302
1303    }
1304
1305    /**
1306     * @return PageImage[]
1307     */
1308    public
1309    function getPageImages(): ?array
1310    {
1311        return $this->pageImages->getValueAsPageImages();
1312    }
1313
1314
1315    /**
1316     * @return array|null
1317     * @deprecated for {@link LdJson}
1318     */
1319    public
1320    function getLdJson(): ?string
1321    {
1322        return $this->ldJson->getValue();
1323
1324    }
1325
1326    /**
1327     * @param array|string $jsonLd
1328     * @return $this
1329     * @throws ExceptionCombo
1330     * @deprecated for {@link LdJson}
1331     */
1332    public
1333    function setJsonLd($jsonLd): Page
1334    {
1335        $this->ldJson
1336            ->setValue($jsonLd)
1337            ->sendToWriteStore();
1338        return $this;
1339    }
1340
1341    /**
1342     * @throws ExceptionCombo
1343     */
1344    public
1345    function setPageType(string $value): Page
1346    {
1347        $this->type
1348            ->setValue($value)
1349            ->sendToWriteStore();
1350        return $this;
1351    }
1352
1353
1354    /**
1355     * @param $aliasPath
1356     * @param string $aliasType
1357     * @return Alias
1358     * @deprecated for {@link Aliases}
1359     */
1360    public
1361    function addAndGetAlias($aliasPath, string $aliasType = AliasType::REDIRECT): Alias
1362    {
1363
1364        return $this->aliases->addAndGetAlias($aliasPath, $aliasType);
1365
1366    }
1367
1368
1369    /**
1370     * @return Alias[]
1371     */
1372    public
1373    function getAliases(): ?array
1374    {
1375        return $this->aliases->getValueAsAlias();
1376    }
1377
1378    /**
1379     * @return string|null
1380     *
1381     */
1382    public
1383    function getSlugOrDefault(): ?string
1384    {
1385
1386        if ($this->getSlug() !== null) {
1387            return $this->getSlug();
1388        }
1389        return $this->getDefaultSlug();
1390    }
1391
1392    /**
1393     *
1394     * @return string|null
1395     *
1396     */
1397    public
1398    function getDefaultSlug(): ?string
1399    {
1400        return $this->slug->getDefaultValue();
1401    }
1402
1403    /**
1404     * The parent page is the parent in the page tree directory
1405     *
1406     * If the page is at the root, the parent page is the root home
1407     * Only the root home does not have any parent page and return null.
1408     *
1409     * @return Page|null
1410     */
1411    public
1412    function getParentPage(): ?Page
1413    {
1414
1415        $names = $this->getPath()->getNames();
1416        if (sizeof($names) == 0) {
1417            return null;
1418        }
1419        $slice = 1;
1420        if ($this->isHomePage()) {
1421            /**
1422             * The parent of a home page
1423             * is in the parent directory
1424             */
1425            $slice = 2;
1426        }
1427        /**
1428         * Delete the last or the two last parts
1429         */
1430        if (sizeof($names) < $slice) {
1431            return null;
1432        }
1433        /**
1434         * Get the actual directory for a page
1435         * or the parent directory for a home page
1436         */
1437        $parentNames = array_slice($names, 0, sizeof($names) - $slice);
1438        /**
1439         * Create the parent namespace id
1440         */
1441        $parentNamespaceId = implode(DokuPath::PATH_SEPARATOR, $parentNames);
1442        return self::getHomePageFromNamespace($parentNamespaceId);
1443
1444    }
1445
1446    /**
1447     * @throws ExceptionCombo
1448     */
1449    public
1450    function setDescription($description): Page
1451    {
1452
1453        $this->description
1454            ->setValue($description)
1455            ->sendToWriteStore();
1456        return $this;
1457    }
1458
1459    /**
1460     * @throws ExceptionCombo
1461     * @deprecated uses {@link EndDate} instead
1462     */
1463    public
1464    function setEndDate($value): Page
1465    {
1466        $this->endDate
1467            ->setFromStoreValue($value)
1468            ->sendToWriteStore();
1469        return $this;
1470    }
1471
1472    /**
1473     * @throws ExceptionCombo
1474     * @deprecated uses {@link StartDate} instead
1475     */
1476    public
1477    function setStartDate($value): Page
1478    {
1479        $this->startDate
1480            ->setFromStoreValue($value)
1481            ->sendToWriteStore();
1482        return $this;
1483    }
1484
1485    /**
1486     * @throws ExceptionCombo
1487     */
1488    public
1489    function setPublishedDate($value): Page
1490    {
1491        $this->publishedDate
1492            ->setFromStoreValue($value)
1493            ->sendToWriteStore();
1494        return $this;
1495    }
1496
1497    /**
1498     * Utility to {@link ResourceName::setValue()}
1499     * Used mostly to create page in test
1500     * @throws ExceptionCombo
1501     */
1502    public
1503    function setPageName($value): Page
1504    {
1505        $this->pageName
1506            ->setValue($value)
1507            ->sendToWriteStore();
1508        return $this;
1509    }
1510
1511
1512    /**
1513     * @throws ExceptionCombo
1514     */
1515    public
1516    function setTitle($value): Page
1517    {
1518        $this->title
1519            ->setValue($value)
1520            ->sendToWriteStore();
1521        return $this;
1522    }
1523
1524    /**
1525     * @throws ExceptionCombo
1526     */
1527    public
1528    function setH1($value): Page
1529    {
1530        $this->h1
1531            ->setValue($value)
1532            ->sendToWriteStore();
1533        return $this;
1534    }
1535
1536    /**
1537     * @throws Exception
1538     */
1539    public
1540    function setRegion($value): Page
1541    {
1542        $this->region
1543            ->setFromStoreValue($value)
1544            ->sendToWriteStore();
1545        return $this;
1546    }
1547
1548    /**
1549     * @throws ExceptionCombo
1550     */
1551    public
1552    function setLang($value): Page
1553    {
1554
1555        $this->lang
1556            ->setFromStoreValue($value)
1557            ->sendToWriteStore();
1558        return $this;
1559    }
1560
1561    /**
1562     * @throws ExceptionCombo
1563     */
1564    public
1565    function setLayout($value): Page
1566    {
1567        $this->layout
1568            ->setValue($value)
1569            ->sendToWriteStore();
1570        return $this;
1571    }
1572
1573
1574    /**
1575     *
1576     * We manage the properties by setter and getter
1577     *
1578     * Why ?
1579     *   * Because we can capture the updates
1580     *   * Because setter are the entry point to good quality data
1581     *   * Because dokuwiki may cache the metadata (see below)
1582     *
1583     * Note all properties have been migrated
1584     * but they should be initialized below
1585     *
1586     * Dokuwiki cache: the data may be cached without our consent
1587     * The method {@link p_get_metadata()} does it with this logic
1588     * ```
1589     * $cache = ($ID == $id);
1590     * $meta = p_read_metadata($id, $cache);
1591     * ```
1592     */
1593    private
1594    function buildPropertiesFromFileSystem()
1595    {
1596
1597        /**
1598         * New meta system
1599         * Even if it does not exist, the metadata object should be instantiated
1600         * otherwise, there is a null exception
1601         */
1602        $this->cacheExpirationDate = CacheExpirationDate::createForPage($this);
1603        $this->aliases = Aliases::createForPage($this);
1604        $this->pageImages = PageImages::createForPage($this);
1605        $this->pageName = ResourceName::createForResource($this);
1606        $this->cacheExpirationFrequency = CacheExpirationFrequency::createForPage($this);
1607        $this->ldJson = LdJson::createForPage($this);
1608        $this->canonical = Canonical::createForPage($this);
1609        $this->pageId = PageId::createForPage($this);
1610        $this->description = PageDescription::createForPage($this);
1611        $this->h1 = PageH1::createForPage($this);
1612        $this->type = PageType::createForPage($this);
1613        $this->creationTime = PageCreationDate::createForPage($this);
1614        $this->title = PageTitle::createForPage($this);
1615        $this->keywords = PageKeywords::createForPage($this);
1616        $this->publishedDate = PagePublicationDate::createFromPage($this);
1617        $this->startDate = StartDate::createFromPage($this);
1618        $this->endDate = EndDate::createFromPage($this);
1619        $this->locale = Locale::createForPage($this);
1620        $this->lang = Lang::createForPage($this);
1621        $this->region = Region::createForPage($this);
1622        $this->slug = Slug::createForPage($this);
1623        $this->canBeOfLowQuality = LowQualityPageOverwrite::createForPage($this);
1624        $this->lowQualityIndicatorCalculated = LowQualityCalculatedIndicator::createFromPage($this);
1625        $this->qualityMonitoringIndicator = QualityDynamicMonitoringOverwrite::createFromPage($this);
1626        $this->modifiedTime = ModificationDate::createForPage($this);
1627        $this->pageUrlPath = PageUrlPath::createForPage($this);
1628        $this->layout = PageLayout::createFromPage($this);
1629
1630    }
1631
1632
1633    function getPageIdAbbr()
1634    {
1635
1636        if ($this->getPageId() === null) return null;
1637        return substr($this->getPageId(), 0, PageId::PAGE_ID_ABBREV_LENGTH);
1638
1639    }
1640
1641    public
1642    function setDatabasePage(DatabasePageRow $databasePage): Page
1643    {
1644        $this->databasePage = $databasePage;
1645        return $this;
1646    }
1647
1648    /**
1649     *
1650     * TODO: Move to {@link HtmlDocument} ?
1651     */
1652    public function getUrlPath(): string
1653    {
1654
1655        return $this->pageUrlPath->getValueOrDefault();
1656
1657    }
1658
1659
1660    /**
1661     * @return string|null
1662     *
1663     */
1664    public
1665    function getSlug(): ?string
1666    {
1667        return $this->slug->getValue();
1668    }
1669
1670
1671    /**
1672     * @throws ExceptionCombo
1673     */
1674    public
1675    function setSlug($slug): Page
1676    {
1677        $this->slug
1678            ->setFromStoreValue($slug)
1679            ->sendToWriteStore();
1680        return $this;
1681    }
1682
1683
1684    public
1685    function getUrlId()
1686    {
1687        return DokuPath::toDokuwikiId($this->getUrlPath());
1688    }
1689
1690
1691    /**
1692     * @throws ExceptionCombo
1693     */
1694    public
1695    function setQualityMonitoringIndicator($boolean): Page
1696    {
1697        $this->qualityMonitoringIndicator
1698            ->setFromStoreValue($boolean)
1699            ->sendToWriteStore();
1700        return $this;
1701    }
1702
1703    /**
1704     *
1705     * @param $aliasPath - third information - the alias used to build this page
1706     */
1707    public
1708    function setBuildAliasPath($aliasPath)
1709    {
1710        $this->buildAliasPath = $aliasPath;
1711    }
1712
1713    public
1714    function getBuildAlias(): ?Alias
1715    {
1716        if ($this->buildAliasPath === null) return null;
1717        foreach ($this->getAliases() as $alias) {
1718            if ($alias->getPath() === $this->buildAliasPath) {
1719                return $alias;
1720            }
1721        }
1722        return null;
1723    }
1724
1725    public
1726    function isDynamicQualityMonitored(): bool
1727    {
1728        if ($this->getQualityMonitoringIndicator() !== null) {
1729            return $this->getQualityMonitoringIndicator();
1730        }
1731        return $this->getDefaultQualityMonitoring();
1732    }
1733
1734    public
1735    function getDefaultQualityMonitoring(): bool
1736    {
1737        if (PluginUtility::getConfValue(action_plugin_combo_qualitymessage::CONF_DISABLE_QUALITY_MONITORING) === 1) {
1738            return false;
1739        } else {
1740            return true;
1741        }
1742    }
1743
1744    /**
1745     * @param MetadataStore|string $store
1746     * @return $this
1747     */
1748    public
1749    function setReadStore($store): Page
1750    {
1751        $this->readStore = $store;
1752        return $this;
1753    }
1754
1755
1756    /**
1757     * @param array $usages
1758     * @return Image[]
1759     */
1760    public
1761    function getImagesOrDefaultForTheFollowingUsages(array $usages): array
1762    {
1763        $usages = array_merge($usages, [PageImageUsage::ALL]);
1764        $images = [];
1765        foreach ($this->getPageImagesOrDefault() as $pageImage) {
1766            foreach ($usages as $usage) {
1767                if (in_array($usage, $pageImage->getUsages())) {
1768                    $images[] = $pageImage->getImage();
1769                    continue 2;
1770                }
1771            }
1772        }
1773        return $images;
1774
1775    }
1776
1777
1778    public
1779    function getKeywords(): ?array
1780    {
1781        return $this->keywords->getValue();
1782    }
1783
1784    public
1785    function getKeywordsOrDefault(): array
1786    {
1787        return $this->keywords->getValueOrDefaults();
1788    }
1789
1790
1791    /**
1792     * @throws ExceptionCombo
1793     */
1794    public
1795    function setKeywords($value): Page
1796    {
1797        $this->keywords
1798            ->setFromStoreValue($value)
1799            ->sendToWriteStore();
1800        return $this;
1801    }
1802
1803    /**
1804     * @return DateTime|null
1805     * @deprecated for {@link CacheExpirationDate}
1806     */
1807    public
1808    function getCacheExpirationDate(): ?DateTime
1809    {
1810        return $this->cacheExpirationDate->getValue();
1811    }
1812
1813    /**
1814     * @return DateTime|null
1815     * @deprecated for {@link CacheExpirationDate}
1816     */
1817    public
1818    function getDefaultCacheExpirationDate(): ?DateTime
1819    {
1820        return $this->cacheExpirationDate->getDefaultValue();
1821    }
1822
1823    /**
1824     * @return string|null
1825     * @deprecated for {@link CacheExpirationFrequency}
1826     */
1827    public
1828    function getCacheExpirationFrequency(): ?string
1829    {
1830        return $this->cacheExpirationFrequency->getValue();
1831    }
1832
1833
1834    /**
1835     * @param DateTime $cacheExpirationDate
1836     * @return $this
1837     * @deprecated for {@link CacheExpirationDate}
1838     */
1839    public
1840    function setCacheExpirationDate(DateTime $cacheExpirationDate): Page
1841    {
1842        $this->cacheExpirationDate->setValue($cacheExpirationDate);
1843        return $this;
1844    }
1845
1846    /**
1847     * @return bool - true if the page has changed
1848     * @deprecated use {@link Page::getInstructionsDocument()} instead
1849     */
1850    public
1851    function isParseCacheUsable(): bool
1852    {
1853        return $this->getInstructionsDocument()->shouldProcess() === false;
1854    }
1855
1856    /**
1857     * @return $this
1858     * @deprecated use {@link Page::getInstructionsDocument()} instead
1859     * Parse a page and put the instructions in the cache
1860     */
1861    public
1862    function parse(): Page
1863    {
1864
1865        $this->getInstructionsDocument()
1866            ->process();
1867
1868        return $this;
1869
1870    }
1871
1872    /**
1873     *
1874     */
1875    public
1876    function getInstructionsDocument(): InstructionsDocument
1877    {
1878        if ($this->instructionsDocument === null) {
1879            $this->instructionsDocument = new InstructionsDocument($this);
1880        }
1881        return $this->instructionsDocument;
1882
1883    }
1884
1885    public
1886    function delete()
1887    {
1888
1889        Index::getOrCreate()->deletePage($this);
1890        saveWikiText($this->getDokuwikiId(), "", "Delete");
1891
1892    }
1893
1894    /**
1895     * @return string|null -the absolute canonical url
1896     */
1897    public
1898    function getAbsoluteCanonicalUrl(): ?string
1899    {
1900        return $this->getCanonicalUrl([], true);
1901    }
1902
1903
1904    public
1905    function getReadStoreOrDefault(): MetadataStore
1906    {
1907        if ($this->readStore === null) {
1908            /**
1909             * No cache please if not set
1910             * Cache should be in the MetadataDokuWikiStore
1911             * that is page requested scoped and not by slot
1912             */
1913            return MetadataDokuWikiStore::getOrCreateFromResource($this);
1914        }
1915        if (!($this->readStore instanceof MetadataStore)) {
1916            $this->readStore = MetadataStoreAbs::toMetadataStore($this->readStore, $this);
1917        }
1918        return $this->readStore;
1919    }
1920
1921    /**
1922     * @return DokuPath
1923     */
1924    public
1925    function getPath(): Path
1926    {
1927        return $this->dokuPath;
1928    }
1929
1930
1931    /**
1932     * A shortcut for {@link Page::getPath()::getDokuwikiId()}
1933     */
1934    public
1935    function getDokuwikiId()
1936    {
1937        return $this->getPath()->getDokuwikiId();
1938    }
1939
1940    public
1941    function getUid(): Metadata
1942    {
1943        return $this->pageId;
1944    }
1945
1946
1947    public
1948    function getAbsolutePath(): string
1949    {
1950        return DokuPath::PATH_SEPARATOR . $this->getDokuwikiId();
1951    }
1952
1953    function getType(): string
1954    {
1955        return self::TYPE;
1956    }
1957
1958    public
1959    function getUrlPathObject(): PageUrlPath
1960    {
1961        return $this->pageUrlPath;
1962    }
1963
1964    public function getMainFooterSlot(): ?Page
1965    {
1966        if ($this->isSecondarySlot() || $this->isRootHomePage()) {
1967            return null;
1968        }
1969
1970        try {
1971            Site::loadStrapUtilityTemplateIfPresentAndSameVersion();
1972        } catch (ExceptionCombo $e) {
1973            LogUtility::msg("We can't load strap. The nearest main footer slot could not be detected, Error: {$e->getMessage()}");
1974            return null;
1975        }
1976
1977        $nearestMainFooter = $this->findNearest(TplUtility::SLOT_MAIN_FOOTER_NAME);
1978        if ($nearestMainFooter === false) {
1979            return null;
1980        }
1981        return Page::createPageFromId($nearestMainFooter);
1982
1983
1984    }
1985
1986    public function getSideSlot(): ?Page
1987    {
1988        if ($this->isSecondarySlot() || $this->isRootHomePage()) {
1989            return null;
1990        }
1991
1992        $nearestMainFooter = $this->findNearest(Site::getSidebarName());
1993        if ($nearestMainFooter === false) {
1994            return null;
1995        }
1996        return Page::createPageFromId($nearestMainFooter);
1997
1998
1999    }
2000
2001    /**
2002     * @param $pageName
2003     * @return false|string
2004     */
2005    private function findNearest($pageName)
2006    {
2007        global $ID;
2008        $keep = $ID;
2009        try {
2010            $ID = $this->getDokuwikiId();
2011            return page_findnearest($pageName);
2012        } finally {
2013            $ID = $keep;
2014        }
2015
2016    }
2017
2018    /**
2019     * @return Page[]
2020     */
2021    public function getSecondarySlots(): array
2022    {
2023        $secondarySlots = [];
2024        $sideSlot = $this->getSideSlot();
2025        if ($sideSlot !== null) {
2026            $secondarySlots[] = $sideSlot;
2027        }
2028        $footerSlot = $this->getMainFooterSlot();
2029        if ($footerSlot !== null) {
2030            $secondarySlots[] = $footerSlot;
2031        }
2032        return $secondarySlots;
2033    }
2034
2035
2036    public function isHidden(): bool
2037    {
2038        return isHiddenPage($this->getDokuwikiId());
2039    }
2040
2041
2042}
2043