1<?php
2
3namespace ComboStrap;
4
5
6use action_plugin_combo_qualitymessage;
7use DateTime;
8use dokuwiki\Cache\CacheInstructions;
9use dokuwiki\Cache\CacheRenderer;
10use dokuwiki\Extension\SyntaxPlugin;
11use renderer_plugin_combo_analytics;
12use RuntimeException;
13
14
15/**
16 * Page
17 */
18require_once(__DIR__ . '/PluginUtility.php');
19
20/**
21 *
22 * Class Page
23 * @package ComboStrap
24 *
25 * This is just a wrapper around a file with the mime Dokuwiki
26 * that has a doku path (ie with the `:` separator)
27 */
28class Page extends DokuPath
29{
30    const CANONICAL_PROPERTY = 'canonical';
31    const TITLE_META_PROPERTY = 'title';
32
33    const CONF_DISABLE_FIRST_IMAGE_AS_PAGE_IMAGE = "disableFirstImageAsPageImage";
34
35    const FIRST_IMAGE_META_RELATION = "firstimage";
36
37    /**
38     * An indicator in the meta
39     * that set a boolean to true or false
40     * to categorize a page as low quality
41     * It can be set manually via the {@link \syntax_plugin_combo_frontmatter front matter}
42     * otherwise the {@link \renderer_plugin_combo_analytics}
43     * will do it
44     */
45    const LOW_QUALITY_PAGE_INDICATOR = 'low_quality_page';
46
47    /**
48     * The default page type
49     */
50    const CONF_DEFAULT_PAGE_TYPE = "defaultPageType";
51    const WEBSITE_TYPE = "website";
52    const ARTICLE_TYPE = "article";
53    const EVENT_TYPE = "event";
54    const ORGANIZATION_TYPE = "organization";
55    const NEWS_TYPE = "news";
56    const BLOG_TYPE = "blog";
57    const NAME_PROPERTY = "name";
58    const DESCRIPTION_PROPERTY = "description";
59    const TYPE_META_PROPERTY = "type";
60
61    /**
62     * The scope is the namespace used to store the cache
63     *
64     * It can be set by a component via the {@link p_set_metadata()}
65     * in a {@link SyntaxPlugin::handle()} function
66     *
67     * This is mostly used on side slots to
68     * have several output of a list {@link \syntax_plugin_combo_pageexplorer navigation pane}
69     * for different namespace (ie there is one cache by namespace)
70     *
71     * The special value current means the namespace of the requested page
72     */
73    const SCOPE_KEY = "scope";
74    /**
75     * The special scope value current means the namespace of the requested page
76     * The real scope value is then calculated before retrieving the cache
77     */
78    const SCOPE_CURRENT_VALUE = "current";
79
80
81    const CURRENT_METADATA = "current";
82    const PERSISTENT_METADATA = "persistent";
83    const IMAGE_META_PROPERTY = 'image';
84    const COUNTRY_META_PROPERTY = "country";
85    const LANG_META_PROPERTY = "lang";
86
87
88    private $canonical;
89
90
91    /**
92     * @var array|array[]
93     */
94    private $metadatas;
95    /**
96     * @var string|null - the description (the origin is in the $descriptionOrigin)
97     */
98    private $description;
99    /**
100     * @var string - the dokuwiki
101     */
102    private $descriptionOrigin;
103
104
105    /**
106     * @var bool Indicator to say if this is a sidebar (or sidekick bar)
107     */
108    private $isSideSlot = false;
109
110    /**
111     * The id requested (ie the main page)
112     * The page may be a slot
113     * @var string
114     */
115    private $requestedId;
116
117    /**
118     * Page constructor.
119     * @param $absolutePath - the qualified path (may be not relative)
120     *
121     */
122    public function __construct($absolutePath)
123    {
124
125        /**
126         * Bars have a logical reasoning (ie such as a virtual, alias)
127         * They are logically located in the same namespace
128         * but the file may be located on the parent
129         *
130         * This block of code is processing this case
131         */
132        global $conf;
133        $sidebars = array($conf['sidebar']);
134        $strapTemplateName = 'strap';
135        if ($conf['template'] === $strapTemplateName) {
136            $sidebars[] = $conf['tpl'][$strapTemplateName]['sidekickbar'];
137        }
138        $lastPathPart = DokuPath::getLastPart($absolutePath);
139        if (in_array($lastPathPart, $sidebars)) {
140
141            $this->isSideSlot = true;
142
143            /**
144             * Find the first physical file
145             * Don't use ACL otherwise the ACL protection event 'AUTH_ACL_CHECK' will kick in
146             * and we got then a recursive problem
147             * with the {@link \action_plugin_combo_pageprotection}
148             */
149            $useAcl = false;
150            $id = page_findnearest($lastPathPart, $useAcl);
151            if ($id !== false) {
152                $absolutePath = DokuPath::PATH_SEPARATOR . $id;
153            }
154
155        }
156
157        global $ID;
158        $this->requestedId = $ID;
159
160        parent::__construct($absolutePath, DokuPath::PAGE_TYPE);
161
162    }
163
164    public static function createPageFromCurrentId()
165    {
166        global $ID;
167        return self::createPageFromId($ID);
168    }
169
170    public static function createPageFromId($id)
171    {
172        return new Page(DokuPath::PATH_SEPARATOR . $id);
173    }
174
175    public static function createPageFromNonQualifiedPath($pathOrId)
176    {
177        global $ID;
178        $qualifiedId = $pathOrId;
179        resolve_pageid(getNS($ID), $qualifiedId, $exists);
180        /**
181         * Root correction
182         * yeah no root functionality in the {@link resolve_pageid resolution}
183         * meaning that we get an empty string
184         * they correct it in the link creation {@link wl()}
185         */
186        if ($qualifiedId === '') {
187            global $conf;
188            $qualifiedId = $conf['start'];
189        }
190        return Page::createPageFromId($qualifiedId);
191
192    }
193
194    /**
195     * @return Page - the requested page
196     */
197    public static function createPageFromRequestedPage()
198    {
199        $mainPageId = FsWikiUtility::getMainPageId();
200        return self::createPageFromId($mainPageId);
201    }
202
203
204    /**
205     * @var string the logical id is used with slots.
206     *
207     * A slot may exist in several node of the file system tree
208     * but they can be rendered for a page in a lowest level
209     * listing the page of the current namespace
210     *
211     * The slot is physically stored in one place but is equivalent
212     * physically to the same slot in all sub-node.
213     *
214     * This logical id does take into account this aspect.
215     *
216     * This is used also to store the HTML output in the cache
217     * If this is not a slot the logical id is the {@link DokuPath::getId()}
218     */
219    public function getLogicalId()
220    {
221        /**
222         * Delete the first separator
223         */
224        return substr($this->getLogicalPath(), 1);
225    }
226
227    public function getLogicalPath()
228    {
229
230        /**
231         * Set the logical id
232         * When no $ID is set (for instance, test),
233         * the logical id is the id
234         *
235         * The logical id depends on the namespace attribute of the {@link \syntax_plugin_combo_pageexplorer}
236         * stored in the `scope` metadata.
237         */
238        $scopePath = $this->getScope();
239        if ($scopePath !== null) {
240
241            if ($scopePath == Page::SCOPE_CURRENT_VALUE) {
242                $requestPage = Page::createRequestedPageFromEnvironment();
243                $scopePath = $requestPage->getNamespacePath();
244            }
245
246            if ($scopePath !== ":") {
247                return $scopePath . DokuPath::PATH_SEPARATOR . $this->getName();
248            } else {
249                return DokuPath::PATH_SEPARATOR . $this->getName();
250            }
251
252
253        } else {
254
255            return $this->getAbsolutePath();
256
257        }
258
259
260    }
261
262
263    /**
264     *
265     *
266     * Dokuwiki Methodology taken from {@link tpl_metaheaders()}
267     * @return string - the Dokuwiki URL
268     */
269    public
270    function getUrl()
271    {
272        if ($this->isHomePage()) {
273            $url = DOKU_URL;
274        } else {
275            $url = wl($this->getId(), '', true, '&');
276        }
277        return $url;
278    }
279
280    public
281    static function createRequestedPageFromEnvironment()
282    {
283        $pageId = PluginUtility::getPageId();
284        if ($pageId != null) {
285            return Page::createPageFromId($pageId);
286        } else {
287            LogUtility::msg("We were unable to determine the page from the variables environment", LogUtility::LVL_MSG_ERROR);
288            return null;
289        }
290    }
291
292
293    /**
294     * Does the page is known in the pages table
295     * @return array
296     */
297    function getRow()
298    {
299
300        $sqlite = Sqlite::getSqlite();
301        $res = $sqlite->query("SELECT * FROM pages where id = ?", $this->getId());
302        if (!$res) {
303            throw new RuntimeException("An exception has occurred with the select pages query");
304        }
305        $res2arr = $sqlite->res2row($res);
306        $sqlite->res_close($res);
307        return $res2arr;
308
309
310    }
311
312    /**
313     * Delete Page
314     */
315    function deleteInDb()
316    {
317
318        $res = Sqlite::getSqlite()->query('delete from pages where id = ?', $this->getId());
319        if (!$res) {
320            LogUtility::msg("Something went wrong when deleting a page");
321        }
322
323    }
324
325
326    /**
327     * Does the page is known in the pages table
328     * @return int
329     */
330    function existInDb()
331    {
332        $sqlite = Sqlite::getSqlite();
333        $res = $sqlite->query("SELECT count(*) FROM pages where id = ?", $this->getId());
334        $count = $sqlite->res2single($res);
335        $sqlite->res_close($res);
336        return $count;
337
338    }
339
340    /**
341     * Exist in FS
342     * @return bool
343     * @deprecated use {@link DokuPath::exists()} instead
344     */
345    function existInFs()
346    {
347        return $this->exists();
348    }
349
350    private
351    function persistPageAlias($canonical, $alias)
352    {
353
354        $row = array(
355            "CANONICAL" => $canonical,
356            "ALIAS" => $alias
357        );
358
359        // Page has change of location
360        // Creation of an alias
361        $sqlite = Sqlite::getSqlite();
362        $res = $sqlite->query("select count(*) from pages_alias where CANONICAL = ? and ALIAS = ?", $row);
363        if (!$res) {
364            throw new RuntimeException("An exception has occurred with the alia selection query");
365        }
366        $aliasInDb = $sqlite->res2single($res);
367        $sqlite->res_close($res);
368        if ($aliasInDb == 0) {
369
370            $res = $sqlite->storeEntry('pages_alias', $row);
371            if (!$res) {
372                LogUtility::msg("There was a problem during pages_alias insertion");
373            }
374        }
375
376    }
377
378
379    static function createPageFromQualifiedPath($qualifiedPath)
380    {
381        return new Page($qualifiedPath);
382    }
383
384    /**
385     * @param $canonical
386     * @return Page - an id of an existing page
387     */
388    static function createPageFromCanonical($canonical)
389    {
390
391        // Canonical
392        $sqlite = Sqlite::getSqlite();
393        $res = $sqlite->query("select * from pages where CANONICAL = ? ", $canonical);
394        if (!$res) {
395            LogUtility::msg("An exception has occurred with the pages selection query");
396        }
397        $res2arr = $sqlite->res2arr($res);
398        $sqlite->res_close($res);
399        foreach ($res2arr as $row) {
400            $id = $row['ID'];
401            return self::createPageFromId($id)->setCanonical($canonical);
402        }
403
404
405        // If the function comes here, it means that the page id was not found in the pages table
406        // Alias ?
407        // Canonical
408        $res = $sqlite->query("select p.ID from pages p, PAGES_ALIAS pa where p.CANONICAL = pa.CANONICAL and pa.ALIAS = ? ", $canonical);
409        if (!$res) {
410            throw new RuntimeException("An exception has occurred with the alias selection query");
411        }
412        $res2arr = $sqlite->res2arr($res);
413        $sqlite->res_close($res);
414        foreach ($res2arr as $row) {
415            $id = $row['ID'];
416            return self::createPageFromId($id)
417                ->setCanonical($canonical);
418        }
419
420        return self::createPageFromId($canonical);
421
422    }
423
424    /**
425     * Persist a page in the database
426     */
427    function processAndPersistInDb(): Page
428    {
429
430        $canonical = p_get_metadata($this->getId(), Page::CANONICAL_PROPERTY);
431        if ($canonical != "") {
432
433            // Do we have a page attached to this canonical
434            $sqlite = Sqlite::getSqlite();
435            $res = $sqlite->query("select ID from pages where CANONICAL = ?", $canonical);
436            if (!$res) {
437                LogUtility::msg("An exception has occurred with the search id from canonical");
438            }
439            $idInDb = $sqlite->res2single($res);
440            $sqlite->res_close($res);
441            if ($idInDb && $idInDb != $this->getId()) {
442                // If the page does not exist anymore we delete it
443                if (!page_exists($idInDb)) {
444                    $res = $sqlite->query("delete from pages where ID = ?", $idInDb);
445                    if (!$res) {
446                        LogUtility::msg("An exception has occurred during the deletion of the page");
447                    }
448                    $sqlite->res_close($res);
449
450                } else {
451                    LogUtility::msg("The page ($this) and the page ($idInDb) have the same canonical ($canonical)", LogUtility::LVL_MSG_ERROR, "url:manager");
452                    /**
453                     * Check if the error may come from the auto-canonical
454                     * (Never ever save generated data)
455                     */
456                    $canonicalLastNamesCount = PluginUtility::getConfValue(\action_plugin_combo_metacanonical::CANONICAL_LAST_NAMES_COUNT_CONF);
457                    if ($canonicalLastNamesCount > 0) {
458                        $this->unsetMetadata(Page::CANONICAL_PROPERTY);
459                        Page::createPageFromQualifiedPath($idInDb)->unsetMetadata(Page::CANONICAL_PROPERTY);
460                    }
461                }
462                $this->persistPageAlias($canonical, $idInDb);
463            }
464
465            // Do we have a canonical on this page
466            $res = $sqlite->query("select canonical from pages where ID = ?", $this->getId());
467            if (!$res) {
468                LogUtility::msg("An exception has occurred with the query");
469            }
470            $canonicalInDb = $sqlite->res2single($res);
471            $sqlite->res_close($res);
472
473            $row = array(
474                "CANONICAL" => $canonical,
475                "ID" => $this->getId()
476            );
477            if ($canonicalInDb && $canonicalInDb != $canonical) {
478
479                // Persist alias
480                $this->persistPageAlias($canonical, $this->getId());
481
482                // Update
483                $statement = 'update pages set canonical = ? where id = ?';
484                $res = $sqlite->query($statement, $row);
485                if (!$res) {
486                    LogUtility::msg("There was a problem during page update");
487                }
488                $sqlite->res_close($res);
489
490            } else {
491
492                if ($canonicalInDb == false) {
493                    $res = $sqlite->storeEntry('pages', $row);
494                    if (!$res) {
495                        LogUtility::msg("There was a problem during pages insertion");
496                    }
497                    $sqlite->res_close($res);
498                }
499
500            }
501
502
503        }
504        return $this;
505    }
506
507    private
508    function setCanonical($canonical): Page
509    {
510        $this->canonical = $canonical;
511        return $this;
512    }
513
514
515    public
516    function isSlot()
517    {
518        global $conf;
519        $barsName = array($conf['sidebar']);
520        $strapTemplateName = 'strap';
521        if ($conf['template'] === $strapTemplateName) {
522            $loaded = PluginUtility::loadStrapUtilityTemplateIfPresentAndSameVersion();
523            if ($loaded) {
524                $barsName[] = TplUtility::getHeaderSlotPageName();
525                $barsName[] = TplUtility::getFooterSlotPageName();
526                $barsName[] = TplUtility::getSideKickSlotPageName();
527            }
528        }
529        return in_array($this->getName(), $barsName);
530    }
531
532    public
533    function isStrapSideSlot()
534    {
535
536        return $this->isSideSlot && Site::isStrapTemplate();
537
538    }
539
540
541    public
542    function isStartPage()
543    {
544        global $conf;
545        return $this->getName() == $conf['start'];
546    }
547
548    /**
549     * Return a canonical if set
550     * otherwise derive it from the id
551     * by taking the last two parts
552     *
553     * @return string
554     */
555    public
556    function getCanonical()
557    {
558        if (empty($this->canonical)) {
559
560            $this->canonical = $this->getPersistentMetadata(Page::CANONICAL_PROPERTY);
561
562            /**
563             * The last part of the id as canonical
564             */
565            // How many last parts are taken into account in the canonical processing (2 by default)
566            $canonicalLastNamesCount = PluginUtility::getConfValue(\action_plugin_combo_metacanonical::CANONICAL_LAST_NAMES_COUNT_CONF);
567            if (empty($this->canonical) && $canonicalLastNamesCount > 0) {
568                /**
569                 * Takes the last names part
570                 */
571                $namesOriginal = $this->getNames();
572                /**
573                 * Delete the identical names at the end
574                 * To resolve this problem
575                 * The page (viz:viz) and the page (data:viz:viz) have the same canonical.
576                 * The page (viz:viz) will get the canonical viz
577                 * The page (data:viz) will get the canonical  data:viz
578                 */
579                $i = sizeof($namesOriginal) - 1;
580                $names = $namesOriginal;
581                while ($namesOriginal[$i] == $namesOriginal[$i - 1]) {
582                    unset($names[$i]);
583                    $i--;
584                    if ($i <= 0) {
585                        break;
586                    }
587                }
588                /**
589                 * Minimal length check
590                 */
591                $namesLength = sizeof($names);
592                if ($namesLength > $canonicalLastNamesCount) {
593                    $names = array_slice($names, $namesLength - $canonicalLastNamesCount);
594                }
595                /**
596                 * If this is a start page, delete the name
597                 * ie javascript:start will become javascript
598                 */
599                if ($this->isStartPage()) {
600                    $names = array_slice($names, 0, $namesLength - 1);
601                }
602                $this->canonical = implode(":", $names);
603                p_set_metadata($this->getId(), array(Page::CANONICAL_PROPERTY => $this->canonical));
604            }
605
606        }
607        return $this->canonical;
608    }
609
610    /**
611     * @return array|null the analytics array or null if not in db
612     */
613    public
614    function getAnalyticsFromDb()
615    {
616        $sqlite = Sqlite::getSqlite();
617        if ($sqlite == null) {
618            return array();
619        }
620        $res = $sqlite->query("select ANALYTICS from pages where ID = ? ", $this->getId());
621        if (!$res) {
622            LogUtility::msg("An exception has occurred with the pages selection query");
623        }
624        $jsonString = trim($sqlite->res2single($res));
625        $sqlite->res_close($res);
626        if (!empty($jsonString)) {
627            return json_decode($jsonString, true);
628        } else {
629            return null;
630        }
631
632    }
633
634    /**
635     * Return the metadata stored in the file system
636     * @return array|array[]
637     */
638    public
639    function getMetadatas()
640    {
641
642        /**
643         * Read / not {@link p_get_metadata()}
644         * because it can trigger a rendering of the meta again)
645         *
646         * This is not a {@link Page::renderMetadata()}
647         */
648        if ($this->metadatas == null) {
649            $this->metadatas = p_read_metadata($this->getId());
650        }
651        return $this->metadatas;
652
653    }
654
655    /**
656     *
657     * @return mixed the internal links or null
658     */
659    public
660    function getInternalLinksFromMeta()
661    {
662        $metadata = $this->getMetadatas();
663        if (key_exists(self::CURRENT_METADATA, $metadata)) {
664            $current = $metadata[self::CURRENT_METADATA];
665            if (key_exists('relation', $current)) {
666                $relation = $current['relation'];
667                if (is_array($relation)) {
668                    if (key_exists('references', $relation)) {
669                        return $relation['references'];
670                    }
671                }
672            }
673        }
674        return null;
675    }
676
677    public
678    function persistAnalytics(array $analytics)
679    {
680
681        $sqlite = Sqlite::getSqlite();
682        if ($sqlite != null) {
683            /**
684             * Sqlite Plugin installed
685             */
686            $json = json_encode($analytics, JSON_PRETTY_PRINT);
687            /**
688             * Same data as {@link Page::getMetadataForRendering()}
689             */
690            $entry = array(
691                'CANONICAL' => $this->getCanonical(),
692                'ANALYTICS' => $json,
693                'PATH' => $this->getAbsolutePath(),
694                'NAME' => $this->getName(),
695                'TITLE' => $this->getTitleNotEmpty(),
696                'H1' => $this->getH1NotEmpty(),
697                'DATE_CREATED' => $this->getCreatedDateString(),
698                'DATE_MODIFIED' => $this->getModifiedDateString(),
699                'DATE_PUBLISHED' => $this->getPublishedTimeAsString(),
700                'DATE_START' => $this->getEndDateAsString(),
701                'DATE_END' => $this->getStartDateAsString(),
702                'COUNTRY' => $this->getCountry(),
703                'LANG' => $this->getLang(),
704                'IS_LOW_QUALITY' => ($this->isLowQualityPage() === true ? 1 : 0),
705                'TYPE' => $this->getType(),
706                'WORD_COUNT' => $analytics[Analytics::WORD_COUNT],
707                'BACKLINK_COUNT' => $analytics[Analytics::INTERNAL_BACKLINK_COUNT],
708                'ID' => $this->getId(),
709            );
710            $res = $sqlite->query("SELECT count(*) FROM PAGES where ID = ?", $this->getId());
711            if ($sqlite->res2single($res) == 1) {
712                // Upset not supported on all version
713                //$upsert = 'insert into PAGES (ID,CANONICAL,ANALYTICS) values (?,?,?) on conflict (ID,CANONICAL) do update set ANALYTICS = EXCLUDED.ANALYTICS';
714                $update = <<<EOF
715update
716    PAGES
717SET
718    CANONICAL = ?,
719    ANALYTICS = ?,
720    PATH = ?,
721    NAME = ?,
722    TITLE = ?,
723    H1 = ?,
724    DATE_CREATED = ?,
725    DATE_MODIFIED = ?,
726    DATE_PUBLISHED = ?,
727    DATE_START = ?,
728    DATE_END = ?,
729    COUNTRY = ?,
730    LANG = ?,
731    IS_LOW_QUALITY = ?,
732    TYPE = ?,
733    WORD_COUNT = ?,
734    BACKLINK_COUNT = ?
735where
736    ID=?
737EOF;
738                $res = $sqlite->query($update, $entry);
739            } else {
740                $res = $sqlite->storeEntry('PAGES', $entry);
741            }
742            if (!$res) {
743                LogUtility::msg("There was a problem during the upsert: {$sqlite->getAdapter()->getDb()->errorInfo()}");
744            }
745            $sqlite->res_close($res);
746        }
747
748    }
749
750    /**
751     * @param string $mode delete the cache for the format XHTML and {@link renderer_plugin_combo_analytics::RENDERER_NAME_MODE}
752     */
753    public
754    function deleteCache($mode = "xhtml")
755    {
756
757        if ($this->exists()) {
758
759
760            $cache = $this->getInstructionsCache();
761            $cache->removeCache();
762
763            $cache = $this->getRenderCache($mode);
764            $cache->removeCache();
765
766        }
767    }
768
769
770    public
771    function isAnalyticsCached()
772    {
773
774        $cache = new CacheRenderer($this->getId(), $this->getFileSystemPath(), renderer_plugin_combo_analytics::RENDERER_NAME_MODE);
775        $cacheFile = $cache->cache;
776        return file_exists($cacheFile);
777    }
778
779    /**
780     *
781     * @return string - the full path to the meta file
782     */
783    public
784    function getMetaFile()
785    {
786        return metaFN($this->getId(), '.meta');
787    }
788
789    /**
790     * @param $reason - a string with the reason
791     */
792    public
793    function deleteCacheAndAskAnalyticsRefresh($reason)
794    {
795        $this->deleteCache(renderer_plugin_combo_analytics::RENDERER_NAME_MODE);
796        $sqlite = Sqlite::getSqlite();
797        if ($sqlite != null) {
798
799            /**
800             * Check if exists
801             */
802            $res = $sqlite->query("select count(1) from ANALYTICS_TO_REFRESH where ID = ?", array('ID' => $this->getId()));
803            if (!$res) {
804                LogUtility::msg("There was a problem during the insert: {$sqlite->getAdapter()->getDb()->errorInfo()}");
805            }
806            $result = $sqlite->res2single($res);
807            $sqlite->res_close($res);
808
809            /**
810             * If not insert
811             */
812            if ($result != 1) {
813                $entry = array(
814                    "ID" => $this->getId(),
815                    "TIMESTAMP" => date('Y-m-d H:i:s', time()),
816                    "REASON" => $reason
817                );
818                $res = $sqlite->storeEntry('ANALYTICS_TO_REFRESH', $entry);
819                if (!$res) {
820                    LogUtility::msg("There was a problem during the insert: {$sqlite->getAdapter()->getDb()->errorInfo()}");
821                }
822                $sqlite->res_close($res);
823            }
824
825        }
826
827    }
828
829    public
830    function isAnalyticsStale()
831    {
832        $sqlite = Sqlite::getSqlite();
833        $res = $sqlite->query("SELECT count(*) FROM ANALYTICS_TO_REFRESH where ID = ?", $this->getId());
834        if (!$res) {
835            LogUtility::msg("There was a problem during the select: {$sqlite->getAdapter()->getDb()->errorInfo()}");
836        }
837        $value = $sqlite->res2single($res);
838        $sqlite->res_close($res);
839        return $value === "1";
840
841    }
842
843    /**
844     * Delete the cache, process the analytics
845     * and return it
846     * If you want the analytics from the cache use {@link Page::getAnalyticsFromFs()}
847     * instead
848     * @return mixed analytics as array
849     */
850    public
851    function processAnalytics()
852    {
853
854        /**
855         * Refresh and cache
856         * (The delete is normally not needed, just to be sure)
857         */
858        $this->deleteCache(renderer_plugin_combo_analytics::RENDERER_NAME_MODE);
859        $analytics = Analytics::processAndGetDataAsArray($this->getId(), true);
860
861        /**
862         * Delete from the table
863         */
864        $sqlite = Sqlite::getSqlite();
865        if ($sqlite != null) {
866            $res = $sqlite->query("DELETE FROM ANALYTICS_TO_REFRESH where ID = ?", $this->getId());
867            if (!$res) {
868                LogUtility::msg("There was a problem during the delete: {$sqlite->getAdapter()->getDb()->errorInfo()}");
869            }
870            $sqlite->res_close($res);
871
872        }
873        return $analytics;
874
875    }
876
877    /**
878     * @param bool $cache
879     * @return mixed
880     *
881     */
882    public
883    function getAnalyticsFromFs($cache = true)
884    {
885        if ($cache) {
886            /**
887             * Note for dev: because cache is off in dev environment,
888             * you will get it always processed
889             */
890            return Analytics::processAndGetDataAsArray($this->getId(), $cache);
891        } else {
892            /**
893             * Process analytics delete at the same a asked refresh
894             */
895            return $this->processAnalytics();
896        }
897    }
898
899    /**
900     * Set the page quality
901     * @param boolean $newIndicator true if this is a low quality page rank false otherwise
902     */
903
904    public
905    function setLowQualityIndicator(bool $newIndicator)
906    {
907        $actualIndicator = $this->getLowQualityIndicator();
908        if ($actualIndicator === null || $actualIndicator !== $newIndicator) {
909
910            /**
911             * Don't change the type of the value to a string
912             * otherwise dokuwiki will not see a change
913             * between true and a string and will not persist the value
914             */
915            p_set_metadata($this->getId(), array(self::LOW_QUALITY_PAGE_INDICATOR => $newIndicator));
916
917            /**
918             * Delete the cache to rewrite the links
919             * if the protection is on
920             */
921            if (PluginUtility::getConfValue(LowQualityPage::CONF_LOW_QUALITY_PAGE_PROTECTION_ENABLE) === 1) {
922                foreach ($this->getBacklinks() as $backlink) {
923                    $backlink->deleteCache("xhtml");
924                }
925            }
926
927        }
928
929
930    }
931
932    /**
933     * @return Page[] the backlinks
934     * Duplicate of related
935     */
936    public
937    function getBacklinks()
938    {
939        $backlinks = array();
940        foreach (ft_backlinks($this->getId()) as $backlinkId) {
941            $backlinks[] = Page::createPageFromId($backlinkId);
942        }
943        return $backlinks;
944    }
945
946
947    /**
948     * Low page quality
949     * @return bool true if this is a low internal page rank
950     */
951    function isLowQualityPage(): bool
952    {
953
954        $lowQualityIndicator = $this->getLowQualityIndicator();
955        if ($lowQualityIndicator == null) {
956            /**
957             * By default, if a file has not been through
958             * a {@link \renderer_plugin_combo_analytics}
959             * analysis, this is not a low page
960             */
961            return false;
962        } else {
963            return $lowQualityIndicator === true;
964        }
965
966    }
967
968
969    public
970    function getLowQualityIndicator()
971    {
972
973        $low = p_get_metadata($this->getId(), self::LOW_QUALITY_PAGE_INDICATOR, METADATA_DONT_RENDER);
974        if ($low === null) {
975            return null;
976        } else {
977            return filter_var($low, FILTER_VALIDATE_BOOLEAN);
978        }
979
980    }
981
982    /**
983     * @return bool - if a {@link Page::processAnalytics()} for the page should occurs
984     */
985    public
986    function shouldAnalyticsProcessOccurs()
987    {
988        /**
989         * If cache is on
990         */
991        global $conf;
992        if ($conf['cachetime'] !== -1) {
993            /**
994             * If there is no cache
995             */
996            if (!$this->isAnalyticsCached()) {
997                return true;
998            }
999        }
1000
1001        /**
1002         * Check Db
1003         */
1004        $sqlite = Sqlite::getSqlite();
1005        if ($sqlite != null) {
1006
1007            $res = $sqlite->query("select count(1) from pages where ID = ? and ANALYTICS is null", $this->getId());
1008            if (!$res) {
1009                LogUtility::msg("An exception has occurred with the analytics detection");
1010            }
1011            $count = intval($sqlite->res2single($res));
1012            $sqlite->res_close($res);
1013            if ($count >= 1) {
1014                return true;
1015            }
1016        }
1017
1018        /**
1019         * Check the refresh table
1020         */
1021        if ($sqlite != null) {
1022            $res = $sqlite->query("SELECT count(*) FROM ANALYTICS_TO_REFRESH where ID = ?", $this->getId());
1023            if (!$res) {
1024                LogUtility::msg("There was a problem during the delete: {$sqlite->getAdapter()->getDb()->errorInfo()}");
1025            }
1026            $count = $sqlite->res2single($res);
1027            $sqlite->res_close($res);
1028            return $count >= 1;
1029        }
1030
1031        return false;
1032    }
1033
1034
1035    public
1036    function getH1()
1037    {
1038
1039        $heading = p_get_metadata($this->getId(), Analytics::H1, METADATA_DONT_RENDER);
1040        if (!blank($heading)) {
1041            return $heading;
1042        } else {
1043            return null;
1044        }
1045
1046    }
1047
1048    /**
1049     * Return the Title
1050     */
1051    public
1052    function getTitle()
1053    {
1054
1055        $id = $this->getId();
1056        $title = p_get_metadata($id, Analytics::TITLE, METADATA_RENDER_USING_SIMPLE_CACHE);
1057        if (!blank($title)) {
1058            return $title;
1059        } else {
1060            return $id;
1061        }
1062
1063    }
1064
1065    /**
1066     * If true, the page is quality monitored (a note is shown to the writer)
1067     * @return bool|mixed
1068     */
1069    public
1070    function isQualityMonitored()
1071    {
1072        $dynamicQualityIndicator = p_get_metadata($this->getId(), action_plugin_combo_qualitymessage::DISABLE_INDICATOR, METADATA_RENDER_USING_SIMPLE_CACHE);
1073        if ($dynamicQualityIndicator === null) {
1074            return true;
1075        } else {
1076            return filter_var($dynamicQualityIndicator, FILTER_VALIDATE_BOOLEAN);
1077        }
1078    }
1079
1080    /**
1081     * @return string|null the title, or h1 if empty or the id if empty
1082     */
1083    public
1084    function getTitleNotEmpty()
1085    {
1086        $pageTitle = $this->getTitle();
1087        if ($pageTitle == null) {
1088            if (!empty($this->getH1())) {
1089                $pageTitle = $this->getH1();
1090            } else {
1091                $pageTitle = $this->getId();
1092            }
1093        }
1094        return $pageTitle;
1095
1096    }
1097
1098    public
1099    function getH1NotEmpty()
1100    {
1101
1102        $h1Title = $this->getH1();
1103        if ($h1Title == null) {
1104            if (!empty($this->getTitle())) {
1105                $h1Title = $this->getTitle();
1106            } else {
1107                $h1Title = $this->getPageNameNotEmpty();
1108            }
1109        }
1110        return $h1Title;
1111
1112    }
1113
1114    public
1115    function getDescription()
1116    {
1117
1118        $this->processDescriptionIfNeeded();
1119        if ($this->descriptionOrigin == \syntax_plugin_combo_frontmatter::CANONICAL) {
1120            return $this->description;
1121        } else {
1122            return null;
1123        }
1124
1125
1126    }
1127
1128
1129    /**
1130     * @return string - the description or the dokuwiki generated description
1131     */
1132    public
1133    function getDescriptionOrElseDokuWiki()
1134    {
1135        $this->processDescriptionIfNeeded();
1136        return $this->description;
1137    }
1138
1139
1140    public
1141    function getContent()
1142    {
1143        /**
1144         * use {@link io_readWikiPage(wikiFN($id, $rev), $id, $rev)};
1145         */
1146        return rawWiki($this->getId());
1147    }
1148
1149
1150    public
1151    function isInIndex()
1152    {
1153        $Indexer = idx_get_indexer();
1154        $pages = $Indexer->getPages();
1155        $return = array_search($this->getId(), $pages, true);
1156        return $return !== false;
1157    }
1158
1159
1160    public
1161    function upsertContent($content, $summary = "Default")
1162    {
1163        saveWikiText($this->getId(), $content, $summary);
1164        return $this;
1165    }
1166
1167    public
1168    function addToIndex()
1169    {
1170        idx_addPage($this->getId());
1171    }
1172
1173    public
1174    function getType()
1175    {
1176        $type = $this->getPersistentMetadata(self::TYPE_META_PROPERTY);
1177        if (isset($type)) {
1178            return $type;
1179        } else {
1180            if ($this->isHomePage()) {
1181                return self::WEBSITE_TYPE;
1182            } else {
1183                $defaultPageTypeConf = PluginUtility::getConfValue(self::CONF_DEFAULT_PAGE_TYPE);
1184                if (!empty($defaultPageTypeConf)) {
1185                    return $defaultPageTypeConf;
1186                } else {
1187                    return null;
1188                }
1189            }
1190        }
1191    }
1192
1193
1194    public
1195    function getFirstImage()
1196    {
1197
1198        $relation = $this->getCurrentMetadata('relation');
1199        if (isset($relation[Page::FIRST_IMAGE_META_RELATION])) {
1200            $firstImageId = $relation[Page::FIRST_IMAGE_META_RELATION];
1201            if (empty($firstImageId)) {
1202                return null;
1203            } else {
1204                // The  metadata store the Id or the url
1205                // We transform them to a path id
1206                $pathId = $firstImageId;
1207                if (!media_isexternal($firstImageId)) {
1208                    $pathId = DokuPath::PATH_SEPARATOR . $firstImageId;
1209                }
1210                return Image::createImageFromAbsolutePath($pathId);
1211            }
1212        }
1213        return null;
1214
1215    }
1216
1217    /**
1218     * Return the media found in the index
1219     *
1220     * They are saved via the function {@link \Doku_Renderer_metadata::_recordMediaUsage()}
1221     * called by the {@link \Doku_Renderer_metadata::internalmedia()}
1222     *
1223     *
1224     * {@link \Doku_Renderer_metadata::externalmedia()} does not save them
1225     */
1226    public function getExistingInternalMediaIdFromTheIndex()
1227    {
1228
1229        $medias = [];
1230        $relation = $this->getCurrentMetadata('relation');
1231        if (isset($relation['media'])) {
1232            /**
1233             * The relation is
1234             * $this->meta['relation']['media'][$src] = $exists;
1235             *
1236             */
1237            foreach ($relation['media'] as $src => $exists) {
1238                if ($exists) {
1239                    $medias[] = $src;
1240                }
1241            }
1242        }
1243        return $medias;
1244
1245    }
1246
1247    /**
1248     * An array of local/internal images that represents the same image
1249     * but in different dimension and ratio
1250     * (may be empty)
1251     * @return Image[]
1252     */
1253    public
1254    function getLocalImageSet(): array
1255    {
1256
1257        /**
1258         * Google accepts several images dimension and ratios
1259         * for the same image
1260         * We may get an array then
1261         */
1262        $imageMeta = $this->getMetadata(self::IMAGE_META_PROPERTY);
1263        $images = array();
1264        if (!empty($imageMeta)) {
1265            if (is_array($imageMeta)) {
1266                foreach ($imageMeta as $key => $imageIdFromMeta) {
1267                    DokuPath::addRootSeparatorIfNotPresent($imageIdFromMeta);
1268                    $images[$key] = Image::createImageFromAbsolutePath($imageIdFromMeta);
1269                }
1270            } else {
1271                DokuPath::addRootSeparatorIfNotPresent($imageMeta);
1272                $images = array(Image::createImageFromAbsolutePath($imageMeta));
1273            }
1274        } else {
1275            if (!PluginUtility::getConfValue(self::CONF_DISABLE_FIRST_IMAGE_AS_PAGE_IMAGE)) {
1276                $firstImage = $this->getFirstImage();
1277                if ($firstImage != null) {
1278                    if ($firstImage->getScheme() == DokuPath::LOCAL_SCHEME) {
1279                        $images = array($firstImage);
1280                    }
1281                }
1282            }
1283        }
1284        return $images;
1285
1286    }
1287
1288
1289    /**
1290     * @return Image
1291     */
1292    public
1293    function getImage(): ?Image
1294    {
1295
1296        $images = $this->getLocalImageSet();
1297        if (sizeof($images) >= 1) {
1298            return $images[0];
1299        } else {
1300            return null;
1301        }
1302
1303    }
1304
1305    /**
1306     * Get author name
1307     *
1308     * @return string
1309     */
1310    public
1311    function getAuthor()
1312    {
1313        $author = $this->getPersistentMetadata('creator');
1314        return ($author ? $author : null);
1315    }
1316
1317    /**
1318     * Get author ID
1319     *
1320     * @return string
1321     */
1322    public
1323    function getAuthorID()
1324    {
1325        $user = $this->getPersistentMetadata('user');
1326        return ($user ? $user : null);
1327    }
1328
1329
1330    private
1331    function getPersistentMetadata($key)
1332    {
1333        if (isset($this->getMetadatas()['persistent'][$key])) {
1334            return $this->getMetadatas()['persistent'][$key];
1335        } else {
1336            return null;
1337        }
1338    }
1339
1340    public
1341    function getPersistentMetadatas()
1342    {
1343        return $this->getMetadatas()['persistent'];
1344    }
1345
1346    /**
1347     * The modified date is the last modification date
1348     * the first time, this is the creation date
1349     * @return string|null
1350     */
1351    public
1352    function getModifiedDateString()
1353    {
1354        $modified = $this->getModifiedTime();
1355        return $modified != null ? $modified->format(Iso8601Date::getFormat()) : null;
1356
1357    }
1358
1359    private
1360    function getCurrentMetadata($key)
1361    {
1362        $key = $this->getMetadatas()[self::CURRENT_METADATA][$key];
1363        return ($key ? $key : null);
1364    }
1365
1366    /**
1367     * Get the create date of page
1368     *
1369     * @return DateTime
1370     */
1371    public function getCreatedTime(): ?DateTime
1372    {
1373        $createdMeta = $this->getPersistentMetadata('date')['created'];
1374        if (empty($createdMeta)) {
1375            return null;
1376        } else {
1377            $datetime = new DateTime();
1378            $datetime->setTimestamp($createdMeta);
1379            return $datetime;
1380        }
1381    }
1382
1383    /**
1384     * Get the modified date of page
1385     *
1386     * The modified date is the last modification date
1387     * the first time, this is the creation date
1388     *
1389     * @return DateTime
1390     */
1391    public function getModifiedTime(): \DateTime
1392    {
1393        $modified = $this->getCurrentMetadata('date')['modified'];
1394        if (empty($modified)) {
1395            return parent::getModifiedTime();
1396        } else {
1397            $datetime = new DateTime();
1398            $datetime->setTimestamp($modified);
1399            return $datetime;
1400        }
1401    }
1402
1403    /**
1404     * Creation date can not be null
1405     * @return null|string
1406     */
1407    public
1408    function getCreatedDateString()
1409    {
1410
1411        $created = $this->getCreatedTime();
1412        return $created != null ? $created->format(Iso8601Date::getFormat()) : null;
1413
1414    }
1415
1416    /**
1417     * Refresh the metadata (used only in test)
1418     */
1419    public
1420    function renderMetadata()
1421    {
1422
1423        if ($this->metadatas == null) {
1424            /**
1425             * Read the metadata from the file
1426             */
1427            $this->metadatas = $this->getMetadatas();
1428        }
1429
1430        /**
1431         * Read/render the metadata from the file
1432         * with parsing
1433         */
1434        $this->metadatas = p_render_metadata($this->getId(), $this->metadatas);
1435
1436        /**
1437         * ReInitialize
1438         */
1439        $this->descriptionOrigin = null;
1440        $this->description = null;
1441
1442        /**
1443         * Return
1444         */
1445        return $this;
1446
1447    }
1448
1449    public
1450    function getCountry()
1451    {
1452
1453        $country = $this->getPersistentMetadata(self::COUNTRY_META_PROPERTY);
1454        if (!empty($country)) {
1455            if (!StringUtility::match($country, "[a-zA-Z]{2}")) {
1456                LogUtility::msg("The country value ($country) for the page (" . $this->getId() . ") does not have two letters (ISO 3166 alpha-2 country code)", LogUtility::LVL_MSG_ERROR, "country");
1457            }
1458            return $country;
1459        } else {
1460
1461            return Site::getCountry();
1462
1463        }
1464
1465    }
1466
1467    public
1468    function getLang()
1469    {
1470        $lang = $this->getPersistentMetadata(self::LANG_META_PROPERTY);
1471        if (empty($lang)) {
1472            global $conf;
1473            if (isset($conf["lang"])) {
1474                $lang = $conf["lang"];
1475            }
1476        }
1477        return $lang;
1478    }
1479
1480    /**
1481     * Adapted from {@link FsWikiUtility::getHomePagePath()}
1482     * @return bool
1483     */
1484    public
1485    function isHomePage()
1486    {
1487        global $conf;
1488        $startPageName = $conf['start'];
1489        if ($this->getName() == $startPageName) {
1490            return true;
1491        } else {
1492            $namespaceName = noNS(cleanID($this->getNamespacePath()));
1493            if ($namespaceName == $this->getName()) {
1494                /**
1495                 * page named like the NS inside the NS
1496                 * ie ns:ns
1497                 */
1498                $startPage = Page::createPageFromId(DokuPath::absolutePathToId($this->getNamespacePath()) . DokuPath::PATH_SEPARATOR . $startPageName);
1499                if (!$startPage->exists()) {
1500                    return true;
1501                }
1502            }
1503        }
1504        return false;
1505    }
1506
1507
1508    public
1509    function getMetadata($key, $default = null)
1510    {
1511        $persistentMetadata = $this->getPersistentMetadata($key);
1512        if (empty($persistentMetadata)) {
1513            $persistentMetadata = $this->getCurrentMetadata($key);
1514        }
1515        if ($persistentMetadata == null) {
1516            return $default;
1517        } else {
1518            return $persistentMetadata;
1519        }
1520    }
1521
1522    public
1523    function getPublishedTime(): ?DateTime
1524    {
1525        $property = Publication::DATE_PUBLISHED;
1526        $persistentMetadata = $this->getPersistentMetadata($property);
1527        if (empty($persistentMetadata)) {
1528            /**
1529             * Old metadata key
1530             */
1531            $persistentMetadata = $this->getPersistentMetadata("published");
1532            if (empty($persistentMetadata)) {
1533                return null;
1534            }
1535        }
1536        // Ms level parsing
1537        $dateTime = DateTime::createFromFormat(DateTime::ISO8601, $persistentMetadata);
1538        if ($dateTime === false) {
1539            /**
1540             * Should not happen as the data is validate in entry
1541             * at the {@link \syntax_plugin_combo_frontmatter}
1542             */
1543            LogUtility::msg("The published date property ($property) of the page ($this) has a value  ($persistentMetadata) that is not valid.", LogUtility::LVL_MSG_ERROR, Iso8601Date::CANONICAL);
1544            return null;
1545        }
1546        return $dateTime;
1547    }
1548
1549
1550    /**
1551     * @return DateTime
1552     */
1553    public
1554    function getPublishedElseCreationTime()
1555    {
1556        $publishedDate = $this->getPublishedTime();
1557        if (empty($publishedDate)) {
1558            $publishedDate = $this->getCreatedTime();
1559        }
1560        return $publishedDate;
1561    }
1562
1563
1564    public
1565    function isLatePublication()
1566    {
1567        return $this->getPublishedElseCreationTime() > new DateTime('now');
1568    }
1569
1570    public
1571    function getCanonicalUrl()
1572    {
1573        if (!empty($this->getCanonical())) {
1574            return getBaseURL(true) . strtr($this->getCanonical(), ':', '/');
1575        }
1576        return null;
1577    }
1578
1579    public
1580    function getCanonicalUrlOrDefault()
1581    {
1582        $url = $this->getCanonicalUrl();
1583        if (empty($url)) {
1584            $url = $this->getUrl();
1585        }
1586        return $url;
1587    }
1588
1589    /**
1590     *
1591     * @return string|null - the locale facebook way
1592     */
1593    public
1594    function getLocale($default = null): ?string
1595    {
1596        $lang = $this->getLang();
1597        if (!empty($lang)) {
1598
1599            $country = $this->getCountry();
1600            if (empty($country)) {
1601                $country = $lang;
1602            }
1603            return $lang . "_" . strtoupper($country);
1604        }
1605        return $default;
1606    }
1607
1608    private
1609    function processDescriptionIfNeeded()
1610    {
1611
1612        if ($this->descriptionOrigin == null) {
1613            $descriptionArray = $this->getMetadata(Page::DESCRIPTION_PROPERTY);
1614            if (!empty($descriptionArray)) {
1615                if (array_key_exists('abstract', $descriptionArray)) {
1616
1617                    $temporaryDescription = $descriptionArray['abstract'];
1618
1619                    $this->descriptionOrigin = "dokuwiki";
1620                    if (array_key_exists('origin', $descriptionArray)) {
1621                        $this->descriptionOrigin = $descriptionArray['origin'];
1622                    }
1623
1624                    if ($this->descriptionOrigin == "dokuwiki") {
1625
1626                        // suppress the carriage return
1627                        $temporaryDescription = str_replace("\n", " ", $descriptionArray['abstract']);
1628                        // suppress the h1
1629                        $temporaryDescription = str_replace($this->getH1(), "", $temporaryDescription);
1630                        // Suppress the star, the tab, About
1631                        $temporaryDescription = preg_replace('/(\*|\t|About)/im', "", $temporaryDescription);
1632                        // Suppress all double space and trim
1633                        $temporaryDescription = trim(preg_replace('/  /m', " ", $temporaryDescription));
1634                        $this->description = $temporaryDescription;
1635
1636                    } else {
1637
1638                        $this->description = $temporaryDescription;
1639
1640                    }
1641                }
1642
1643            }
1644        }
1645
1646    }
1647
1648    public
1649    function hasXhtmlCache()
1650    {
1651
1652        $renderCache = $this->getRenderCache("xhtml");
1653        return file_exists($renderCache->cache);
1654
1655    }
1656
1657    public
1658    function hasInstructionCache()
1659    {
1660
1661        $instructionCache = $this->getInstructionsCache();
1662        /**
1663         * $cache->cache is the file
1664         */
1665        return file_exists($instructionCache->cache);
1666
1667    }
1668
1669    public
1670    function render()
1671    {
1672
1673        if (!$this->isStrapSideSlot()) {
1674            $template = Site::getTemplate();
1675            LogUtility::msg("This function renders only sidebar for the " . PluginUtility::getUrl("strap", "strap template") . ". (Actual page: $this, actual template: $template)", LogUtility::LVL_MSG_ERROR);
1676            return "";
1677        }
1678
1679
1680        /**
1681         * Global ID is the ID of the HTTP request
1682         * (ie the page id)
1683         * We change it for the run
1684         * And restore it at the end
1685         */
1686        global $ID;
1687        $keep = $ID;
1688        $ID = $this->getId();
1689
1690        /**
1691         * The code below is adapted from {@link p_cached_output()}
1692         * $ret = p_cached_output($file, 'xhtml', $pageid);
1693         *
1694         * We don't use {@link CacheRenderer}
1695         * because the cache key is the physical file
1696         */
1697        global $conf;
1698        $format = 'xhtml';
1699
1700        $renderCache = $this->getRenderCache($format);
1701        if ($renderCache->useCache()) {
1702            $xhtml = $renderCache->retrieveCache(false);
1703            if (($conf['allowdebug'] || PluginUtility::isDevOrTest()) && $format == 'xhtml') {
1704                $logicalId = $this->getLogicalId();
1705                $scope = $this->getScope();
1706                $xhtml = "<div id=\"{$this->getCacheHtmlId()}\" style=\"display:none;\" data-logical-Id=\"$logicalId\" data-scope=\"$scope\" data-cache-op=\"hit\" data-cache-file=\"{$renderCache->cache}\"></div>" . $xhtml;
1707            }
1708        } else {
1709
1710            /**
1711             * Get the instructions
1712             * Adapted from {@link p_cached_instructions()}
1713             */
1714            $instructionsCache = $this->getInstructionsCache();
1715            if ($instructionsCache->useCache()) {
1716                $instructions = $instructionsCache->retrieveCache();
1717            } else {
1718                // no cache - do some work
1719                $instructions = p_get_instructions($this->getContent());
1720                if (!$instructionsCache->storeCache($instructions)) {
1721                    $message = 'Unable to save cache file. Hint: disk full; file permissions; safe_mode setting ?';
1722                    msg($message, -1);
1723                    // close restore ID
1724                    $ID = $keep;
1725                    return "<div class=\"text-warning\">$message</div>";
1726                }
1727            }
1728
1729            /**
1730             * Due to the instructions parsing, they may have been changed
1731             * by a component
1732             */
1733            $logicalId = $this->getLogicalId();
1734            $scope = $this->getScope();
1735
1736            /**
1737             * Render
1738             */
1739            $xhtml = p_render($format, $instructions, $info);
1740            if ($info['cache'] && $renderCache->storeCache($xhtml)) {
1741                if (($conf['allowdebug'] || PluginUtility::isDevOrTest()) && $format == 'xhtml') {
1742                    $xhtml = "<div id=\"{$this->getCacheHtmlId()}\" style=\"display:none;\" data-logical-Id=\"$logicalId\" data-scope=\"$scope\" data-cache-op=\"created\" data-cache-file=\"{$renderCache->cache}\"></div>" . $xhtml;
1743                }
1744            } else {
1745                $renderCache->removeCache();   //   try to delete cachefile
1746                if (($conf['allowdebug'] || PluginUtility::isDevOrTest()) && $format == 'xhtml') {
1747                    $xhtml = "<div id=\"{$this->getCacheHtmlId()}\" style=\"display:none;\" data-logical-Id=\"$logicalId\" data-scope=\"$scope\" data-cache-op=\"forbidden\"></div>" . $xhtml;
1748                }
1749            }
1750        }
1751
1752        // restore ID
1753        $ID = $keep;
1754        return $xhtml;
1755
1756    }
1757
1758    /**
1759     * @param string $outputFormat For instance, "xhtml" or {@links Analytics::RENDERER_NAME_MODE}
1760     * @return \dokuwiki\Cache\Cache the cache of the page
1761     *
1762     * Output of {@link DokuWiki_Syntax_Plugin::render()}
1763     *
1764     */
1765    private
1766    function getRenderCache($outputFormat)
1767    {
1768
1769        if ($this->isStrapSideSlot()) {
1770
1771            /**
1772             * Logical cache based on scope (ie logical id) is the scope and part of the key
1773             */
1774            return new CacheByLogicalKey($this, $outputFormat);
1775
1776        } else {
1777
1778            return new CacheRenderer($this->getId(), $this->getFileSystemPath(), $outputFormat);
1779
1780        }
1781    }
1782
1783    /**
1784     * @return CacheInstructions
1785     * The cache of the {@link CallStack call stack} (ie list of output of {@link DokuWiki_Syntax_Plugin::handle})
1786     */
1787    private
1788    function getInstructionsCache()
1789    {
1790
1791        if ($this->isStrapSideSlot()) {
1792
1793            /**
1794             * @noinspection PhpIncompatibleReturnTypeInspection
1795             * No inspection because this is not the same object interface
1796             * because we can't overide the constructor of {@link CacheInstructions}
1797             * but they should used the same interface (ie manipulate array data)
1798             */
1799            return new CacheInstructionsByLogicalKey($this);
1800
1801        } else {
1802
1803            return new CacheInstructions($this->getId(), $this->getFileSystemPath());
1804
1805        }
1806
1807    }
1808
1809    public
1810    function deleteXhtmlCache()
1811    {
1812        $this->deleteCache("xhtml");
1813    }
1814
1815    public
1816    function getAnchorLink()
1817    {
1818        $url = $this->getCanonicalUrlOrDefault();
1819        $title = $this->getTitle();
1820        return "<a href=\"$url\">$title</a>";
1821    }
1822
1823
1824    /**
1825     * Without the `:` at the end
1826     * @return string
1827     */
1828    public
1829    function getNamespacePath()
1830    {
1831        $ns = getNS($this->getId());
1832        /**
1833         * False means root namespace
1834         */
1835        if ($ns == false) {
1836            return ":";
1837        } else {
1838            return ":$ns";
1839        }
1840    }
1841
1842
1843    public
1844    function getScope()
1845    {
1846        /**
1847         * The scope may change
1848         * during a run, we then read the metadata file
1849         * each time
1850         */
1851        if (isset(p_read_metadata($this->getId())["persistent"][Page::SCOPE_KEY])) {
1852            return p_read_metadata($this->getId())["persistent"][Page::SCOPE_KEY];
1853        } else {
1854            return null;
1855        }
1856    }
1857
1858    /**
1859     * Return the id of the div HTML
1860     * element that is added for cache debugging
1861     */
1862    public
1863    function getCacheHtmlId()
1864    {
1865        return "cache-" . str_replace(":", "-", $this->getId());
1866    }
1867
1868    public
1869    function deleteMetadatas()
1870    {
1871        $meta = [Page::CURRENT_METADATA => [], Page::PERSISTENT_METADATA => []];
1872        p_save_metadata($this->getId(), $meta);
1873        return $this;
1874    }
1875
1876    public
1877    function getPageName()
1878    {
1879        return p_get_metadata($this->getId(), self::NAME_PROPERTY, METADATA_RENDER_USING_SIMPLE_CACHE);
1880
1881    }
1882
1883    public
1884    function getPageNameNotEmpty()
1885    {
1886        $name = $this->getPageName();
1887        if (!blank($name)) {
1888            return $name;
1889        } else {
1890            return $this->getName();
1891        }
1892    }
1893
1894    /**
1895     * @param $property
1896     */
1897    private function unsetMetadata($property)
1898    {
1899        $meta = p_read_metadata($this->getId());
1900        if (isset($meta['persistent'][$property])) {
1901            unset($meta['persistent'][$property]);
1902        }
1903        p_save_metadata($this->getId(), $meta);
1904
1905    }
1906
1907    /**
1908     * @return array - return the standard / generated metadata
1909     * used in templating
1910     */
1911    public function getMetadataForRendering()
1912    {
1913
1914
1915        /**
1916         * The title/h1 should never be null
1917         * otherwise a template link such as [[$path|$title]] will return a link without an description
1918         * and therefore will be not visible
1919         * We render at least the id
1920         */
1921        $array[Analytics::H1] = $this->getH1NotEmpty();
1922        $title = $this->getTitleNotEmpty();
1923        /**
1924         * Hack: Replace every " by a ' to be able to detect/parse the title/h1 on a pipeline
1925         * @see {@link \syntax_plugin_combo_pipeline}
1926         */
1927        $title = str_replace('"', "'", $title);
1928        $array[Analytics::TITLE] = $title;
1929        $array[Analytics::PATH] = $this->getAbsolutePath();
1930        $array[Analytics::DESCRIPTION] = $this->getDescriptionOrElseDokuWiki();
1931        $array[Analytics::NAME] = $this->getPageNameNotEmpty();
1932        $array[self::TYPE_META_PROPERTY] = $this->getType() !== null ? $this->getType() : "";
1933
1934        /**
1935         * When creating a page, the file
1936         * may not be saved, causing a
1937         * filemtime(): stat failed for pages/test.txt in lib\plugins\combo\ComboStrap\File.php on line 62
1938         *
1939         */
1940        if($this->exists()) {
1941            $array[Analytics::DATE_CREATED] = $this->getCreatedDateString();
1942            $array[Analytics::DATE_MODIFIED] = $this->getModifiedDateString();
1943        }
1944
1945        $array[Publication::DATE_PUBLISHED] = $this->getPublishedTimeAsString();
1946
1947        return $array;
1948
1949    }
1950
1951    public function __toString()
1952    {
1953        return $this->getId();
1954    }
1955
1956    public function setMetadata($key, $value)
1957    {
1958        p_set_metadata($this->getId(),
1959            [
1960                $key => $value
1961            ]
1962        );
1963    }
1964
1965    private function getPublishedTimeAsString(): ?string
1966    {
1967        return $this->getPublishedTime() !== null ? $this->getPublishedTime()->format(Iso8601Date::getFormat()) : null;
1968    }
1969
1970    public function getEndDateAsString(): ?string
1971    {
1972        return $this->getEndDate() !== null ? $this->getEndDate()->format(Iso8601Date::getFormat()) : null;
1973    }
1974
1975    public function getEndDate(): ?DateTime
1976    {
1977        $dateEndProperty = Analytics::DATE_END;
1978        $persistentMetadata = $this->getPersistentMetadata($dateEndProperty);
1979        if (empty($persistentMetadata)) {
1980            return null;
1981        }
1982
1983        // Ms level parsing
1984        $dateTime = DateTime::createFromFormat(Iso8601Date::getFormat(), $persistentMetadata);
1985        if ($dateTime === false) {
1986            /**
1987             * Should not happen as the data is validate in entry
1988             * at the {@link \syntax_plugin_combo_frontmatter}
1989             */
1990            LogUtility::msg("The property $dateEndProperty of the page ($this) has a value ($persistentMetadata) that is not valid.", LogUtility::LVL_MSG_ERROR, Iso8601Date::CANONICAL);
1991            return null;
1992        }
1993        return $dateTime;
1994    }
1995
1996    public function getStartDateAsString(): ?string
1997    {
1998        return $this->getStartDate() !== null ? $this->getStartDate()->format(Iso8601Date::getFormat()) : null;
1999    }
2000
2001    public function getStartDate(): ?DateTime
2002    {
2003        $dateStartProperty = Analytics::DATE_START;
2004        $persistentMetadata = $this->getPersistentMetadata($dateStartProperty);
2005        if (empty($persistentMetadata)) {
2006            return null;
2007        }
2008
2009        // Ms level parsing
2010        $dateTime = DateTime::createFromFormat(Iso8601Date::getFormat(), $persistentMetadata);
2011        if ($dateTime === false) {
2012            /**
2013             * Should not happen as the data is validate in entry
2014             * at the {@link \syntax_plugin_combo_frontmatter}
2015             */
2016            LogUtility::msg("The start date property $dateStartProperty of the page ($this) has a value ($persistentMetadata) that is not valid.", LogUtility::LVL_MSG_ERROR, Iso8601Date::CANONICAL);
2017            return null;
2018        }
2019        return $dateTime;
2020    }
2021
2022
2023}
2024