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