xref: /plugin/combo/ComboStrap/DatabasePageRow.php (revision 70bbd7f1f72440223cc13f3495efdcb2b0a11514)
1c3437056SNickeau<?php
2c3437056SNickeau
3c3437056SNickeau
4c3437056SNickeaunamespace ComboStrap;
5c3437056SNickeau
604fd306cSNickeauuse ComboStrap\Meta\Api\Metadata;
704fd306cSNickeauuse ComboStrap\Meta\Api\MetadataStore;
804fd306cSNickeauuse ComboStrap\Meta\Field\Aliases;
904fd306cSNickeauuse ComboStrap\Meta\Field\BacklinkCount;
1004fd306cSNickeauuse ComboStrap\Meta\Field\PageH1;
1104fd306cSNickeauuse ComboStrap\Meta\Field\Region;
1204fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDbStore;
1304fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore;
144cadd4f8SNickeauuse DateTime;
1504fd306cSNickeauuse renderer_plugin_combo_analytics;
16c3437056SNickeau
17c3437056SNickeau/**
18c3437056SNickeau * The class that manage the replication
19c3437056SNickeau * Class Replicate
20c3437056SNickeau * @package ComboStrap
21c3437056SNickeau *
22c3437056SNickeau * The database can also be seen as a {@link MetadataStore}
23c3437056SNickeau * and an {@link Index}
24c3437056SNickeau */
25c3437056SNickeauclass DatabasePageRow
26c3437056SNickeau{
27c3437056SNickeau
28c3437056SNickeau
29c3437056SNickeau    /**
30c3437056SNickeau     * The list of attributes that are set
31c3437056SNickeau     * at build time
32c3437056SNickeau     * used in the build functions such as {@link DatabasePageRow::getDatabaseRowFromPage()}
33c3437056SNickeau     * to build the sql
34c3437056SNickeau     */
35c3437056SNickeau    private const PAGE_BUILD_ATTRIBUTES =
36c3437056SNickeau        [
37c3437056SNickeau            self::ROWID,
38c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
39c3437056SNickeau            self::ANALYTICS_ATTRIBUTE,
40c3437056SNickeau            PageDescription::PROPERTY_NAME,
41c3437056SNickeau            Canonical::PROPERTY_NAME,
42c3437056SNickeau            ResourceName::PROPERTY_NAME,
43c3437056SNickeau            PageTitle::TITLE,
44c3437056SNickeau            PageH1::PROPERTY_NAME,
45c3437056SNickeau            PagePublicationDate::PROPERTY_NAME,
46c3437056SNickeau            ModificationDate::PROPERTY_NAME,
4704fd306cSNickeau            CreationDate::PROPERTY_NAME,
48c3437056SNickeau            PagePath::PROPERTY_NAME,
49c3437056SNickeau            StartDate::PROPERTY_NAME,
50c3437056SNickeau            EndDate::PROPERTY_NAME,
51c3437056SNickeau            Region::PROPERTY_NAME,
52c3437056SNickeau            Lang::PROPERTY_NAME,
53c3437056SNickeau            PageType::PROPERTY_NAME,
54c3437056SNickeau            PageId::PROPERTY_NAME,
55c3437056SNickeau            PageId::PAGE_ID_ABBR_ATTRIBUTE,
5604fd306cSNickeau            ReplicationDate::PROPERTY_NAME,
57c3437056SNickeau            BacklinkCount::PROPERTY_NAME
58c3437056SNickeau        ];
59c3437056SNickeau    const ANALYTICS_ATTRIBUTE = "analytics";
60c3437056SNickeau
61c3437056SNickeau    /**
62c3437056SNickeau     * For whatever reason, the row id is lowercase
63c3437056SNickeau     */
64c3437056SNickeau    const ROWID = "rowid";
65c3437056SNickeau
66c3437056SNickeau    const CANONICAL = MetadataDbStore::CANONICAL;
67c3437056SNickeau
6804fd306cSNickeau    const IS_HOME_COLUMN = "is_home";
6904fd306cSNickeau    const IS_INDEX_COLUMN = "is_index";
7004fd306cSNickeau
71c3437056SNickeau    /**
7204fd306cSNickeau     * @var MarkupPath
73c3437056SNickeau     */
7404fd306cSNickeau    private $markupPath;
75c3437056SNickeau    /**
76c3437056SNickeau     * @var Sqlite|null
77c3437056SNickeau     */
78c3437056SNickeau    private $sqlite;
79c3437056SNickeau
80c3437056SNickeau    /**
81c3437056SNickeau     * @var array
82c3437056SNickeau     */
83c3437056SNickeau    private $row;
84c3437056SNickeau
85c3437056SNickeau    /**
86c3437056SNickeau     * Replicate constructor.
8704fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
88c3437056SNickeau     */
89c3437056SNickeau    public function __construct()
90c3437056SNickeau    {
91c3437056SNickeau        /**
92c3437056SNickeau         * Persist on the DB
93c3437056SNickeau         */
94c3437056SNickeau        $this->sqlite = Sqlite::createOrGetSqlite();
95c3437056SNickeau
96c3437056SNickeau
97c3437056SNickeau    }
98c3437056SNickeau
9904fd306cSNickeau    public static function getFromPageObject(MarkupPath $page): DatabasePageRow
10004fd306cSNickeau    {
10104fd306cSNickeau        $databasePage = new DatabasePageRow();
10204fd306cSNickeau        try {
10304fd306cSNickeau            $row = $databasePage->getDatabaseRowFromPage($page);
10404fd306cSNickeau            $databasePage->setRow($row);
10504fd306cSNickeau            return $databasePage;
10604fd306cSNickeau        } catch (ExceptionNotExists $e) {
10704fd306cSNickeau            //
10804fd306cSNickeau        }
10904fd306cSNickeau        return $databasePage;
11004fd306cSNickeau    }
11104fd306cSNickeau
112c3437056SNickeau    /**
113c3437056SNickeau     * Delete the cache,
114c3437056SNickeau     * Process the analytics
115c3437056SNickeau     * Save it in the Db
116c3437056SNickeau     * Delete from the page to refresh if any
117c3437056SNickeau     *
118c3437056SNickeau     * If you want the analytics:
119c3437056SNickeau     *   * from the cache use {@link self::getAnalyticsFromFs()}
120c3437056SNickeau     *   * from the db use {@link self::getAnalyticsFromDb()}
121c3437056SNickeau     *
122c3437056SNickeau     *
12304fd306cSNickeau     * @throws ExceptionCompile
124c3437056SNickeau     */
125c3437056SNickeau    public function replicate(): DatabasePageRow
126c3437056SNickeau    {
127c3437056SNickeau
12804fd306cSNickeau        if (!FileSystems::exists($this->markupPath)) {
12904fd306cSNickeau            throw new ExceptionCompile("You can't replicate the non-existing page ($this->markupPath) on the file system");
130c3437056SNickeau        }
131c3437056SNickeau
132c3437056SNickeau
133c3437056SNickeau        /**
134c3437056SNickeau         * Page Replication should appears
135c3437056SNickeau         */
136c3437056SNickeau        $this->replicatePage();
137c3437056SNickeau
138c3437056SNickeau        /**
139c3437056SNickeau         * @var Metadata $tabularMetadataToSync
140c3437056SNickeau         */
141c3437056SNickeau        $tabularMetadataToSync = [
142c3437056SNickeau            (new References()),
143c3437056SNickeau            (new Aliases())
144c3437056SNickeau        ];
14504fd306cSNickeau        $fsStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
14604fd306cSNickeau        $dbStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
147c3437056SNickeau        foreach ($tabularMetadataToSync as $tabular) {
148c3437056SNickeau            $tabular
14904fd306cSNickeau                ->setResource($this->markupPath)
150c3437056SNickeau                ->setReadStore($fsStore)
151c3437056SNickeau                ->buildFromReadStore()
152c3437056SNickeau                ->setWriteStore($dbStore)
153c3437056SNickeau                ->persist();
154c3437056SNickeau        }
155c3437056SNickeau
156c3437056SNickeau        /**
157c3437056SNickeau         * Analytics (derived)
158c3437056SNickeau         * Should appear at the end of the replication because it is based
159c3437056SNickeau         * on the previous replication (ie backlink count)
160c3437056SNickeau         */
161c3437056SNickeau        $this->replicateAnalytics();
162c3437056SNickeau
163c3437056SNickeau
164c3437056SNickeau        return $this;
165c3437056SNickeau
166c3437056SNickeau    }
167c3437056SNickeau
168c3437056SNickeau    /**
16904fd306cSNickeau     * @throws ExceptionCompile
170c3437056SNickeau     */
17104fd306cSNickeau    public function replicateAndRebuild(): DatabasePageRow
172c3437056SNickeau    {
173c3437056SNickeau        $this->replicate();
174c3437056SNickeau        $this->rebuild();
175c3437056SNickeau        return $this;
176c3437056SNickeau    }
177c3437056SNickeau
17804fd306cSNickeau    /**
17904fd306cSNickeau     * @throws ExceptionNotExists - no page id to add
18004fd306cSNickeau     */
181c3437056SNickeau    private function addPageIdMeta(array &$metaRecord)
182c3437056SNickeau    {
18304fd306cSNickeau        try {
18404fd306cSNickeau            $pageId = $this->markupPath->getPageId();
18504fd306cSNickeau        } catch (ExceptionNotFound $e) {
18604fd306cSNickeau            $pageId = PageId::generateAndStorePageId($this->markupPath);
18704fd306cSNickeau        }
18804fd306cSNickeau        $metaRecord[PageId::PROPERTY_NAME] = $pageId;
18904fd306cSNickeau        $metaRecord[PageId::PAGE_ID_ABBR_ATTRIBUTE] = PageId::getAbbreviated($pageId);
190c3437056SNickeau    }
191c3437056SNickeau
192c3437056SNickeau    public static function createFromPageId(string $pageId): DatabasePageRow
193c3437056SNickeau    {
194c3437056SNickeau        $databasePage = new DatabasePageRow();
19504fd306cSNickeau        try {
196c3437056SNickeau            $row = $databasePage->getDatabaseRowFromPageId($pageId);
197c3437056SNickeau            $databasePage->setRow($row);
19804fd306cSNickeau        } catch (ExceptionNotFound|ExceptionSqliteNotAvailable $e) {
19904fd306cSNickeau            // not found
200c3437056SNickeau        }
20104fd306cSNickeau
202c3437056SNickeau        return $databasePage;
203c3437056SNickeau    }
204c3437056SNickeau
20504fd306cSNickeau    /**
20604fd306cSNickeau     * @param MarkupPath $page
20704fd306cSNickeau     * @return DatabasePageRow
20804fd306cSNickeau     * @throws ExceptionSqliteNotAvailable - if there is no sqlite available
20904fd306cSNickeau     * @noinspection PhpDocRedundantThrowsInspection
21004fd306cSNickeau     */
21104fd306cSNickeau    public static function getOrCreateFromPageObject(MarkupPath $page): DatabasePageRow
212c3437056SNickeau    {
213c3437056SNickeau
214c3437056SNickeau        $databasePage = new DatabasePageRow();
21504fd306cSNickeau        try {
216c3437056SNickeau            $row = $databasePage->getDatabaseRowFromPage($page);
217c3437056SNickeau            $databasePage->setRow($row);
218c3437056SNickeau            return $databasePage;
21904fd306cSNickeau        } catch (ExceptionNotExists $e) {
22004fd306cSNickeau            // page copied on the local system
22104fd306cSNickeau            try {
22204fd306cSNickeau                ComboFs::createIfNotExists($page);
22304fd306cSNickeau                $row = $databasePage->getDatabaseRowFromPage($page);
22404fd306cSNickeau                $databasePage->setRow($row);
22504fd306cSNickeau                return $databasePage;
22604fd306cSNickeau            } catch (ExceptionNotExists $e) {
22704fd306cSNickeau                throw ExceptionRuntimeInternal::withMessageAndError("The row should exists as we created it specifically", $e);
22804fd306cSNickeau            }
229c3437056SNickeau        }
230c3437056SNickeau
23104fd306cSNickeau    }
23204fd306cSNickeau
23304fd306cSNickeau
23404fd306cSNickeau    /**
23504fd306cSNickeau     *
23604fd306cSNickeau     */
23704fd306cSNickeau    public
23804fd306cSNickeau    static function createFromPageIdAbbr(string $pageIdAbbr): DatabasePageRow
239c3437056SNickeau    {
240c3437056SNickeau        $databasePage = new DatabasePageRow();
24104fd306cSNickeau        try {
242c3437056SNickeau            $row = $databasePage->getDatabaseRowFromAttribute(PageId::PAGE_ID_ABBR_ATTRIBUTE, $pageIdAbbr);
243c3437056SNickeau            $databasePage->setRow($row);
24404fd306cSNickeau        } catch (ExceptionNotFound $e) {
24504fd306cSNickeau            // ok
246c3437056SNickeau        }
247c3437056SNickeau        return $databasePage;
248c3437056SNickeau
249c3437056SNickeau    }
250c3437056SNickeau
251c3437056SNickeau    /**
252c3437056SNickeau     * @param $canonical
253c3437056SNickeau     * @return DatabasePageRow
254c3437056SNickeau     */
25504fd306cSNickeau    public
25604fd306cSNickeau    static function createFromCanonical($canonical): DatabasePageRow
257c3437056SNickeau    {
258c3437056SNickeau
25904fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($canonical);
260c3437056SNickeau        $databasePage = new DatabasePageRow();
26104fd306cSNickeau        try {
262c3437056SNickeau            $row = $databasePage->getDatabaseRowFromAttribute(Canonical::PROPERTY_NAME, $canonical);
263c3437056SNickeau            $databasePage->setRow($row);
26404fd306cSNickeau        } catch (ExceptionNotFound $e) {
26504fd306cSNickeau            // ok
266c3437056SNickeau        }
267c3437056SNickeau        return $databasePage;
268c3437056SNickeau
269c3437056SNickeau
270c3437056SNickeau    }
271c3437056SNickeau
27204fd306cSNickeau    public
27304fd306cSNickeau    static function createFromAlias($alias): DatabasePageRow
274c3437056SNickeau    {
275c3437056SNickeau
27604fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($alias);
277c3437056SNickeau        $databasePage = new DatabasePageRow();
278c3437056SNickeau        $row = $databasePage->getDatabaseRowFromAlias($alias);
279c3437056SNickeau        if ($row != null) {
280c3437056SNickeau            $databasePage->setRow($row);
28104fd306cSNickeau            $page = $databasePage->getMarkupPath();
28258317768Sgerardnico            if ($page !== null) {
28358317768Sgerardnico                // page may be null in production
28458317768Sgerardnico                // PHP Fatal error:  Uncaught Error: Call to a member function setBuildAliasPath() on null in
28558317768Sgerardnico                // /opt/www/bytle/farmer.bytle.net/lib/plugins/combo/ComboStrap/DatabasePageRow.php:220
28658317768Sgerardnico                $page->setBuildAliasPath($alias);
28758317768Sgerardnico            }
288c3437056SNickeau        }
289c3437056SNickeau        return $databasePage;
290c3437056SNickeau
291c3437056SNickeau    }
292c3437056SNickeau
29304fd306cSNickeau    /**
29404fd306cSNickeau     * @throws ExceptionNotFound
29504fd306cSNickeau     */
29604fd306cSNickeau    public
29704fd306cSNickeau    static function getFromDokuWikiId($id): DatabasePageRow
298c3437056SNickeau    {
299c3437056SNickeau        $databasePage = new DatabasePageRow();
3004ebc3257Sgerardnico        $databasePage->markupPath = MarkupPath::createMarkupFromId($id);
301c3437056SNickeau        $row = $databasePage->getDatabaseRowFromDokuWikiId($id);
302c3437056SNickeau        $databasePage->setRow($row);
303c3437056SNickeau        return $databasePage;
304c3437056SNickeau    }
305c3437056SNickeau
30604fd306cSNickeau    public
30704fd306cSNickeau    function getPageId()
308c3437056SNickeau    {
309c3437056SNickeau        return $this->getFromRow(PageId::PROPERTY_NAME);
310c3437056SNickeau    }
311c3437056SNickeau
312c3437056SNickeau
313c3437056SNickeau    public
314c3437056SNickeau    function shouldReplicate(): bool
315c3437056SNickeau    {
316c3437056SNickeau
31704fd306cSNickeau
3184cadd4f8SNickeau        $dateReplication = $this->getReplicationDate();
3194cadd4f8SNickeau        if ($dateReplication === null) {
3204cadd4f8SNickeau            return true;
3214cadd4f8SNickeau        }
3224cadd4f8SNickeau
3234cadd4f8SNickeau        /**
3244cadd4f8SNickeau         * When the replication date is older than the actual document
3254cadd4f8SNickeau         */
32604fd306cSNickeau        try {
32704fd306cSNickeau            $modifiedTime = FileSystems::getModifiedTime($this->markupPath->getPathObject());
3284cadd4f8SNickeau            if ($modifiedTime > $dateReplication) {
3294cadd4f8SNickeau                return true;
3304cadd4f8SNickeau            }
33104fd306cSNickeau        } catch (ExceptionNotFound $e) {
33204fd306cSNickeau            return false;
33304fd306cSNickeau        }
33404fd306cSNickeau
33504fd306cSNickeau
33604fd306cSNickeau        $path = $this->markupPath->fetchAnalyticsPath();
3374cadd4f8SNickeau
338c3437056SNickeau        /**
339c3437056SNickeau         * When the file does not exist
340c3437056SNickeau         */
34104fd306cSNickeau        $exist = FileSystems::exists($path);
342c3437056SNickeau        if (!$exist) {
343c3437056SNickeau            return true;
344c3437056SNickeau        }
345c3437056SNickeau
346c3437056SNickeau        /**
3474cadd4f8SNickeau         * When the analytics document is older
348c3437056SNickeau         */
34904fd306cSNickeau        try {
35004fd306cSNickeau
35104fd306cSNickeau            $modifiedTime = FileSystems::getModifiedTime($path);
352c3437056SNickeau            if ($modifiedTime > $dateReplication) {
353c3437056SNickeau                return true;
354c3437056SNickeau            }
35504fd306cSNickeau        } catch (ExceptionNotFound $e) {
35604fd306cSNickeau            //
35704fd306cSNickeau        }
358c3437056SNickeau
3594cadd4f8SNickeau
360c3437056SNickeau        /**
361c3437056SNickeau         * When the database version file is higher
362c3437056SNickeau         */
36304fd306cSNickeau        $version = LocalPath::createFromPathString(__DIR__ . "/../db/latest.version");
36404fd306cSNickeau        try {
365c3437056SNickeau            $versionModifiedTime = FileSystems::getModifiedTime($version);
36604fd306cSNickeau        } catch (ExceptionNotFound $e) {
36704fd306cSNickeau            return false;
36804fd306cSNickeau        }
369c3437056SNickeau        if ($versionModifiedTime > $dateReplication) {
370c3437056SNickeau            return true;
371c3437056SNickeau        }
372c3437056SNickeau
373c3437056SNickeau        /**
374c3437056SNickeau         * When the class date time is higher
375c3437056SNickeau         */
37604fd306cSNickeau        $code = LocalPath::createFromPathString(__DIR__ . "/DatabasePageRow.php");
37704fd306cSNickeau        try {
378c3437056SNickeau            $codeModified = FileSystems::getModifiedTime($code);
37904fd306cSNickeau        } catch (ExceptionNotFound $e) {
38004fd306cSNickeau            throw new ExceptionRuntime("The database file does not exist");
38104fd306cSNickeau        }
382c3437056SNickeau        if ($codeModified > $dateReplication) {
383c3437056SNickeau            return true;
384c3437056SNickeau        }
385c3437056SNickeau
386c3437056SNickeau        return false;
387c3437056SNickeau
388c3437056SNickeau    }
389c3437056SNickeau
390c3437056SNickeau    public
391c3437056SNickeau    function delete()
392c3437056SNickeau    {
393c3437056SNickeau
39404fd306cSNickeau        $request = $this->sqlite
395c3437056SNickeau            ->createRequest()
39604fd306cSNickeau            ->setQueryParametrized('delete from pages where id = ?', [$this->markupPath->getWikiId()]);
397c3437056SNickeau        try {
398c3437056SNickeau            $request->execute();
39904fd306cSNickeau        } catch (ExceptionCompile $e) {
40004fd306cSNickeau            LogUtility::msg("Something went wrong when deleting the page ({$this->markupPath}) from the database");
401c3437056SNickeau        } finally {
402c3437056SNickeau            $request->close();
403c3437056SNickeau        }
404c3437056SNickeau        $this->buildInitObjectFields();
405c3437056SNickeau
406c3437056SNickeau    }
407c3437056SNickeau
408c3437056SNickeau    /**
40904fd306cSNickeau     * @return Json the analytics array or null if not in db
410c3437056SNickeau     */
411c3437056SNickeau    public
41204fd306cSNickeau    function getAnalyticsData(): Json
413c3437056SNickeau    {
414c3437056SNickeau
415c3437056SNickeau        $jsonString = $this->getFromRow(self::ANALYTICS_ATTRIBUTE);
416c3437056SNickeau        if ($jsonString === null) {
41704fd306cSNickeau            // we put an empty json to not get any problem with the json database function
41804fd306cSNickeau            // on an empty string / null (for sqlite)
41904fd306cSNickeau            return Json::createEmpty();
420c3437056SNickeau        }
421c3437056SNickeau        try {
422c3437056SNickeau            return Json::createFromString($jsonString);
42304fd306cSNickeau        } catch (ExceptionCompile $e) {
42404fd306cSNickeau            throw ExceptionRuntimeInternal::withMessageAndError("Error while building back the analytics JSON object. {$e->getMessage()}", $e);
425c3437056SNickeau        }
426c3437056SNickeau
427c3437056SNickeau    }
428c3437056SNickeau
429c3437056SNickeau    /**
430c3437056SNickeau     * Return the database row
431c3437056SNickeau     *
432c3437056SNickeau     *
43304fd306cSNickeau     * @throws ExceptionNotExists - if the row does not exists
434c3437056SNickeau     */
43504fd306cSNickeau    public
43604fd306cSNickeau    function getDatabaseRowFromPage(MarkupPath $markupPath): array
437c3437056SNickeau    {
438c3437056SNickeau
43904fd306cSNickeau        $this->setMarkupPath($markupPath);
440c3437056SNickeau
44104fd306cSNickeau        /**
44204fd306cSNickeau         * Generated identifier
44304fd306cSNickeau         */
44404fd306cSNickeau        try {
44504fd306cSNickeau            $pageId = $markupPath->getPageId();
44604fd306cSNickeau            return $this->getDatabaseRowFromPageId($pageId);
44704fd306cSNickeau        } catch (ExceptionNotFound $e) {
44804fd306cSNickeau            // no page id
449c3437056SNickeau        }
450c3437056SNickeau
451c3437056SNickeau        /**
45204fd306cSNickeau         * Named identifier: path
453c3437056SNickeau         */
45404fd306cSNickeau        try {
45504fd306cSNickeau            $path = $markupPath->getPathObject();
45604fd306cSNickeau            return $this->getDatabaseRowFromPath($path);
45704fd306cSNickeau        } catch (ExceptionNotFound $e) {
45804fd306cSNickeau            // not found
45904fd306cSNickeau        }
46004fd306cSNickeau
46104fd306cSNickeau        /**
46204fd306cSNickeau         * Named identifier: id (ie path)
46304fd306cSNickeau         */
46404fd306cSNickeau        try {
46504fd306cSNickeau            $id = $markupPath->getPathObject()->toWikiPath()->getWikiId();
466c3437056SNickeau            return $this->getDatabaseRowFromDokuWikiId($id);
46704fd306cSNickeau        } catch (ExceptionCast|ExceptionNotFound $e) {
46804fd306cSNickeau        }
46904fd306cSNickeau
47004fd306cSNickeau        /**
47104fd306cSNickeau         * Named identifier: canonical
47204fd306cSNickeau         * (Note that canonical should become a soft link and therefore a path)
47304fd306cSNickeau         */
47404fd306cSNickeau        try {
47504fd306cSNickeau            $canonical = Canonical::createForPage($markupPath)->getValue();
47604fd306cSNickeau            return $this->getDatabaseRowFromCanonical($canonical->toAbsoluteId());
47704fd306cSNickeau        } catch (ExceptionNotFound $e) {
47804fd306cSNickeau            // no canonical
47904fd306cSNickeau        }
48004fd306cSNickeau
48104fd306cSNickeau        // we send a not exist
48204fd306cSNickeau        throw new ExceptionNotExists("No row could be found");
483c3437056SNickeau
484c3437056SNickeau
485c3437056SNickeau    }
486c3437056SNickeau
487c3437056SNickeau
4884cadd4f8SNickeau    /**
4894cadd4f8SNickeau     * @return DateTime|null
4904cadd4f8SNickeau     */
49104fd306cSNickeau    public
49204fd306cSNickeau    function getReplicationDate(): ?DateTime
493c3437056SNickeau    {
49404fd306cSNickeau        $dateString = $this->getFromRow(ReplicationDate::getPersistentName());
495c3437056SNickeau        if ($dateString === null) {
496c3437056SNickeau            return null;
497c3437056SNickeau        }
498c3437056SNickeau        try {
499c3437056SNickeau            return Iso8601Date::createFromString($dateString)->getDateTime();
50004fd306cSNickeau        } catch (ExceptionCompile $e) {
501c3437056SNickeau            LogUtility::msg("Error while reading the replication date in the database. {$e->getMessage()}");
502c3437056SNickeau            return null;
503c3437056SNickeau        }
504c3437056SNickeau
505c3437056SNickeau    }
506c3437056SNickeau
507c3437056SNickeau    /**
50804fd306cSNickeau     *
50904fd306cSNickeau     * @throws ExceptionBadState
51004fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
511c3437056SNickeau     */
51204fd306cSNickeau    public
51304fd306cSNickeau    function replicatePage(): void
514c3437056SNickeau    {
515c3437056SNickeau
51604fd306cSNickeau        if (!FileSystems::exists($this->markupPath)) {
51704fd306cSNickeau            throw new ExceptionBadState("You can't replicate the page ($this->markupPath) because it does not exists.");
518c3437056SNickeau        }
519c3437056SNickeau
520c3437056SNickeau        /**
521c3437056SNickeau         * Replication Date
522c3437056SNickeau         */
52304fd306cSNickeau        $replicationDate = ReplicationDate::createFromPage($this->markupPath)
524c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
5254cadd4f8SNickeau            ->setValue(new DateTime());
526c3437056SNickeau
527c3437056SNickeau        /**
52804fd306cSNickeau         * Same data as {@link MarkupPath::getMetadataForRendering()}
529c3437056SNickeau         */
530c3437056SNickeau        $record = $this->getMetaRecord();
531c3437056SNickeau        $record[$replicationDate::getPersistentName()] = $replicationDate->toStoreValue();
53204fd306cSNickeau        $this->upsertAttributes($record);
533c3437056SNickeau
534c3437056SNickeau    }
535c3437056SNickeau
536c3437056SNickeau
537c3437056SNickeau    /**
53804fd306cSNickeau     *
539c3437056SNickeau     *
540c3437056SNickeau     * Attribute that are scalar / modifiable in the database
541c3437056SNickeau     * (not aliases or json data for instance)
54204fd306cSNickeau     *
54304fd306cSNickeau     * @throws ExceptionBadState
54404fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
545c3437056SNickeau     */
54604fd306cSNickeau    public
54704fd306cSNickeau    function replicateMetaAttributes(): void
548c3437056SNickeau    {
549c3437056SNickeau
55004fd306cSNickeau        $this->upsertAttributes($this->getMetaRecord());
551c3437056SNickeau
552c3437056SNickeau    }
553c3437056SNickeau
55404fd306cSNickeau    /**
55504fd306cSNickeau     * @throws ExceptionBadState - if the array is empty
55604fd306cSNickeau     */
55704fd306cSNickeau    public
55804fd306cSNickeau    function upsertAttributes(array $attributes): void
559c3437056SNickeau    {
560c3437056SNickeau
561c3437056SNickeau        if (empty($attributes)) {
56204fd306cSNickeau            throw new ExceptionBadState("The page database attribute passed should not be empty");
563c3437056SNickeau        }
564c3437056SNickeau
565c3437056SNickeau        $values = [];
566c3437056SNickeau        $columnClauses = [];
567c3437056SNickeau        foreach ($attributes as $key => $value) {
568c3437056SNickeau            if (is_array($value)) {
56904fd306cSNickeau                throw new ExceptionRuntime("The attribute ($key) has value that is an array (" . implode(", ", $value) . ")");
570c3437056SNickeau            }
571c3437056SNickeau            $columnClauses[] = "$key = ?";
572c3437056SNickeau            $values[$key] = $value;
573c3437056SNickeau        }
574c3437056SNickeau
575c3437056SNickeau        /**
576c3437056SNickeau         * Primary key has moved during the time
577c3437056SNickeau         * It should be the UUID but not for older version
578c3437056SNickeau         *
579c3437056SNickeau         * If the primary key is null, no record was found
580c3437056SNickeau         */
581c3437056SNickeau        $rowId = $this->getRowId();
582c3437056SNickeau        if ($rowId !== null) {
583c3437056SNickeau            /**
584c3437056SNickeau             * We just add the primary key
585c3437056SNickeau             * otherwise as this is a associative
586c3437056SNickeau             * array, we will miss a value for the update statement
587c3437056SNickeau             */
588c3437056SNickeau            $values[] = $rowId;
589c3437056SNickeau
590c3437056SNickeau            $updateStatement = "update PAGES SET " . implode(", ", $columnClauses) . " where ROWID = ?";
591c3437056SNickeau            $request = $this->sqlite
592c3437056SNickeau                ->createRequest()
593c3437056SNickeau                ->setQueryParametrized($updateStatement, $values);
594c3437056SNickeau            $countChanges = 0;
595c3437056SNickeau            try {
596c3437056SNickeau                $countChanges = $request
597c3437056SNickeau                    ->execute()
598c3437056SNickeau                    ->getChangeCount();
59904fd306cSNickeau            } catch (ExceptionCompile $e) {
60004fd306cSNickeau                throw new ExceptionBadState("There was a problem during the page attribute updates. : {$e->getMessage()}");
601c3437056SNickeau            } finally {
602c3437056SNickeau                $request->close();
603c3437056SNickeau            }
604c3437056SNickeau            if ($countChanges !== 1) {
60504fd306cSNickeau                // internal error
60604fd306cSNickeau                LogUtility::error("The database replication has not updated exactly 1 record but ($countChanges) record", \action_plugin_combo_indexer::CANONICAL);
607c3437056SNickeau            }
608c3437056SNickeau
609c3437056SNickeau        } else {
610c3437056SNickeau
611c3437056SNickeau            /**
612c3437056SNickeau             * Creation
613c3437056SNickeau             */
61404fd306cSNickeau            if ($this->markupPath === null) {
61504fd306cSNickeau                throw new ExceptionBadState("The page should be defined to create a page database row");
616c3437056SNickeau            }
61704fd306cSNickeau
61804fd306cSNickeau            /**
61904fd306cSNickeau             * If the user copy a frontmatter with the same page id abbr, we got a problem
62004fd306cSNickeau             */
621be61a7dfSgerardnico            $pageIdAbbr = $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] ?? null;
62204fd306cSNickeau            if ($pageIdAbbr == null) {
62304fd306cSNickeau                $pageId = $values[PageId::getPersistentName()];
62404fd306cSNickeau                if ($pageId === null) {
62504fd306cSNickeau                    throw new ExceptionBadState("You can't insert a page in the database without a page id");
62604fd306cSNickeau                }
62704fd306cSNickeau                $pageIdAbbr = PageId::getAbbreviated($pageId);
62804fd306cSNickeau                $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $pageIdAbbr;
62904fd306cSNickeau            }
63004fd306cSNickeau
63104fd306cSNickeau            $databasePage = DatabasePageRow::createFromPageIdAbbr($pageIdAbbr);
63204fd306cSNickeau            if ($databasePage->exists()) {
63304fd306cSNickeau                $duplicatePage = $databasePage->getMarkupPath();
63404fd306cSNickeau                if ($duplicatePage->getPathObject()->toUriString() === $this->markupPath->toUriString()) {
63504fd306cSNickeau                    $message = "The page ($this->markupPath) is already in the database with the uid ($pageIdAbbr).";
63604fd306cSNickeau                } else {
63704fd306cSNickeau                    $message = "The page ($this->markupPath) cannot be replicated to the database because it has the same page id abbreviation ($pageIdAbbr) than the page ($duplicatePage)";
63804fd306cSNickeau                }
63904fd306cSNickeau                throw new ExceptionBadState($message);
64004fd306cSNickeau            }
64104fd306cSNickeau
64204fd306cSNickeau            $values[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] = $this->markupPath->getPathObject()->getWikiId();
64304fd306cSNickeau            $values[PagePath::PROPERTY_NAME] = $this->markupPath->getPathObject()->toAbsolutePath()->toAbsoluteId();
644c3437056SNickeau            /**
645c3437056SNickeau             * Default implements the auto-canonical feature
646c3437056SNickeau             */
64704fd306cSNickeau            try {
64804fd306cSNickeau                $values[Canonical::PROPERTY_NAME] = $this->markupPath->getCanonicalOrDefault();
64904fd306cSNickeau            } catch (ExceptionNotFound $e) {
65004fd306cSNickeau                $values[Canonical::PROPERTY_NAME] = null;
65104fd306cSNickeau            }
652c3437056SNickeau
653c3437056SNickeau            /**
654c3437056SNickeau             * Analytics
655c3437056SNickeau             */
656be61a7dfSgerardnico            $analyticsAttributeValue = $values[self::ANALYTICS_ATTRIBUTE] ?? null;
657be61a7dfSgerardnico            if (!isset($analyticsAttributeValue)) {
658c3437056SNickeau                // otherwise we get an empty string
659c3437056SNickeau                // and a json function will not work
660c3437056SNickeau                $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString();
661c3437056SNickeau            }
662c3437056SNickeau
663c3437056SNickeau            /**
664c3437056SNickeau             * Page Id / Abbr are mandatory for url redirection
665c3437056SNickeau             */
666c3437056SNickeau            $this->addPageIdAttributeIfNeeded($values);
667c3437056SNickeau
668c3437056SNickeau            $request = $this->sqlite
669c3437056SNickeau                ->createRequest()
670c3437056SNickeau                ->setTableRow('PAGES', $values);
671c3437056SNickeau            try {
672c3437056SNickeau                /**
673c3437056SNickeau                 * rowid is used in {@link DatabasePageRow::exists()}
674c3437056SNickeau                 * to check if the page exists in the database
675c3437056SNickeau                 * We update it
676c3437056SNickeau                 */
677c3437056SNickeau                $this->row[self::ROWID] = $request
678c3437056SNickeau                    ->execute()
679c3437056SNickeau                    ->getInsertId();
680c3437056SNickeau                $this->row = array_merge($values, $this->row);
68104fd306cSNickeau            } catch (ExceptionCompile $e) {
68204fd306cSNickeau                throw new ExceptionBadState("There was a problem during the updateAttributes insert. : {$e->getMessage()}");
683c3437056SNickeau            } finally {
684c3437056SNickeau                $request->close();
685c3437056SNickeau            }
686c3437056SNickeau
687c3437056SNickeau        }
688c3437056SNickeau
689c3437056SNickeau    }
690c3437056SNickeau
691c3437056SNickeau    public
692c3437056SNickeau    function getDescription()
693c3437056SNickeau    {
694c3437056SNickeau        return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY);
695c3437056SNickeau    }
696c3437056SNickeau
697c3437056SNickeau
698c3437056SNickeau    public
699c3437056SNickeau    function getPageName()
700c3437056SNickeau    {
701c3437056SNickeau        return $this->getFromRow(ResourceName::PROPERTY_NAME);
702c3437056SNickeau    }
703c3437056SNickeau
704c3437056SNickeau    public
705c3437056SNickeau    function exists(): bool
706c3437056SNickeau    {
707c3437056SNickeau        return $this->getFromRow(self::ROWID) !== null;
708c3437056SNickeau    }
709c3437056SNickeau
710c3437056SNickeau    /**
711c3437056SNickeau     * Called when a page is moved
712c3437056SNickeau     * @param $targetId
713c3437056SNickeau     */
714c3437056SNickeau    public
715c3437056SNickeau    function updatePathAndDokuwikiId($targetId)
716c3437056SNickeau    {
717c3437056SNickeau        if (!$this->exists()) {
71804fd306cSNickeau            LogUtility::error("The `database` page ($this) does not exist and cannot be moved to ($targetId)");
719c3437056SNickeau        }
720c3437056SNickeau
721c3437056SNickeau        $path = $targetId;
72204fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
723c3437056SNickeau        $attributes = [
724c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId,
725c3437056SNickeau            PagePath::PROPERTY_NAME => $path
726c3437056SNickeau        ];
727c3437056SNickeau
728c3437056SNickeau        $this->upsertAttributes($attributes);
729c3437056SNickeau
730c3437056SNickeau    }
731c3437056SNickeau
732c3437056SNickeau    public
733c3437056SNickeau    function __toString()
734c3437056SNickeau    {
73504fd306cSNickeau        return $this->markupPath->__toString();
736c3437056SNickeau    }
737c3437056SNickeau
738c3437056SNickeau
739c3437056SNickeau    /**
740c3437056SNickeau     * Redirect are now added during a move
741c3437056SNickeau     * Not when a duplicate is found.
742c3437056SNickeau     * With the advent of the page id, it should never occurs anymore
74304fd306cSNickeau     * @param MarkupPath $page
744c3437056SNickeau     * @deprecated 2012-10-28
745c3437056SNickeau     */
746c3437056SNickeau    private
74704fd306cSNickeau    function deleteIfExistsAndAddRedirectAlias(MarkupPath $page): void
748c3437056SNickeau    {
749c3437056SNickeau
75004fd306cSNickeau        if ($this->markupPath != null) {
751c3437056SNickeau            $page->getDatabasePage()->deleteIfExist();
752c3437056SNickeau            $this->addRedirectAliasWhileBuildingRow($page);
753c3437056SNickeau        }
754c3437056SNickeau
755c3437056SNickeau    }
756c3437056SNickeau
757c3437056SNickeau    public
758c3437056SNickeau    function getCanonical()
759c3437056SNickeau    {
760c3437056SNickeau        return $this->getFromRow(Canonical::PROPERTY_NAME);
761c3437056SNickeau    }
762c3437056SNickeau
763c3437056SNickeau    /**
764c3437056SNickeau     * Set the field to their values
765c3437056SNickeau     * @param $row
766c3437056SNickeau     */
767c3437056SNickeau    public
768c3437056SNickeau    function setRow($row)
769c3437056SNickeau    {
770c3437056SNickeau        if ($row === null) {
771c3437056SNickeau            LogUtility::msg("A row should not be null");
772c3437056SNickeau            return;
773c3437056SNickeau        }
774c3437056SNickeau        if (!is_array($row)) {
775c3437056SNickeau            LogUtility::msg("A row should be an array");
776c3437056SNickeau            return;
777c3437056SNickeau        }
778c3437056SNickeau
779c3437056SNickeau        /**
780c3437056SNickeau         * All get function lookup the row
781c3437056SNickeau         */
782c3437056SNickeau        $this->row = $row;
783c3437056SNickeau
784c3437056SNickeau
785c3437056SNickeau    }
786c3437056SNickeau
787c3437056SNickeau    private
788c3437056SNickeau    function buildInitObjectFields()
789c3437056SNickeau    {
790c3437056SNickeau        $this->row = null;
791c3437056SNickeau
792c3437056SNickeau    }
793c3437056SNickeau
794c3437056SNickeau    public
795c3437056SNickeau    function rebuild(): DatabasePageRow
796c3437056SNickeau    {
797c3437056SNickeau
79804fd306cSNickeau        if ($this->markupPath != null) {
79904fd306cSNickeau            $this->markupPath->rebuild();
80004fd306cSNickeau            try {
80104fd306cSNickeau                $row = $this->getDatabaseRowFromPage($this->markupPath);
802c3437056SNickeau                $this->setRow($row);
80304fd306cSNickeau            } catch (ExceptionNotExists $e) {
80404fd306cSNickeau                // ok
805c3437056SNickeau            }
806c3437056SNickeau        }
807c3437056SNickeau        return $this;
808c3437056SNickeau
809c3437056SNickeau    }
810c3437056SNickeau
811c3437056SNickeau    /**
812c3437056SNickeau     * @return array - an array of the fix page metadata (ie not derived)
813c3437056SNickeau     * Therefore quick to insert/update
814c3437056SNickeau     *
815c3437056SNickeau     */
816c3437056SNickeau    private
817c3437056SNickeau    function getMetaRecord(): array
818c3437056SNickeau    {
81904fd306cSNickeau        $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
82004fd306cSNickeau        $targetStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
821c3437056SNickeau
822c3437056SNickeau        $record = array(
823c3437056SNickeau            Canonical::PROPERTY_NAME,
824c3437056SNickeau            PagePath::PROPERTY_NAME,
825c3437056SNickeau            ResourceName::PROPERTY_NAME,
826c3437056SNickeau            PageTitle::TITLE,
827c3437056SNickeau            PageH1::PROPERTY_NAME,
828c3437056SNickeau            PageDescription::PROPERTY_NAME,
82904fd306cSNickeau            CreationDate::PROPERTY_NAME,
830c3437056SNickeau            ModificationDate::PROPERTY_NAME,
831c3437056SNickeau            PagePublicationDate::PROPERTY_NAME,
832c3437056SNickeau            StartDate::PROPERTY_NAME,
833c3437056SNickeau            EndDate::PROPERTY_NAME,
834c3437056SNickeau            Region::PROPERTY_NAME,
835c3437056SNickeau            Lang::PROPERTY_NAME,
836c3437056SNickeau            PageType::PROPERTY_NAME,
837c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
83804fd306cSNickeau            PageLevel::PROPERTY_NAME
839c3437056SNickeau        );
840c3437056SNickeau        $metaRecord = [];
841c3437056SNickeau        foreach ($record as $name) {
84204fd306cSNickeau            try {
84304fd306cSNickeau                $metadata = Meta\Api\MetadataSystem::getForName($name);
84404fd306cSNickeau            } catch (ExceptionNotFound $e) {
84504fd306cSNickeau                LogUtility::internalError("The metadata ($name) is unknown", self::CANONICAL);
84604fd306cSNickeau                continue;
847c3437056SNickeau            }
848c3437056SNickeau            $metaRecord[$name] = $metadata
84904fd306cSNickeau                ->setResource($this->markupPath)
850c3437056SNickeau                ->setReadStore($sourceStore)
851c3437056SNickeau                ->buildFromReadStore()
852c3437056SNickeau                ->setWriteStore($targetStore)
853c3437056SNickeau                ->toStoreValueOrDefault(); // used by the template, the value is or default
85404fd306cSNickeau
855c3437056SNickeau        }
856c3437056SNickeau
85704fd306cSNickeau        try {
858c3437056SNickeau            $this->addPageIdMeta($metaRecord);
85904fd306cSNickeau        } catch (ExceptionNotExists $e) {
86004fd306cSNickeau            // no page id for non-existent page ok
861c3437056SNickeau        }
86204fd306cSNickeau
86304fd306cSNickeau        // Is index
86404fd306cSNickeau        $metaRecord[self::IS_INDEX_COLUMN] = ($this->markupPath->isIndexPage() === true ? 1 : 0);
86504fd306cSNickeau
866c3437056SNickeau        return $metaRecord;
867c3437056SNickeau    }
868c3437056SNickeau
869c3437056SNickeau    public
870c3437056SNickeau    function deleteIfExist(): DatabasePageRow
871c3437056SNickeau    {
872c3437056SNickeau        if ($this->exists()) {
873c3437056SNickeau            $this->delete();
874c3437056SNickeau        }
875c3437056SNickeau        return $this;
876c3437056SNickeau    }
877c3437056SNickeau
878c3437056SNickeau    public
879c3437056SNickeau    function getRowId()
880c3437056SNickeau    {
881c3437056SNickeau        return $this->getFromRow(self::ROWID);
882c3437056SNickeau    }
883c3437056SNickeau
88404fd306cSNickeau    /**
88504fd306cSNickeau     * @throws ExceptionNotFound
88604fd306cSNickeau     */
887c3437056SNickeau    private
88804fd306cSNickeau    function getDatabaseRowFromPageId(string $pageIdValue)
889c3437056SNickeau    {
890c3437056SNickeau
891c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
892c3437056SNickeau        $query = $this->getParametrizedLookupQuery($pageIdAttribute);
893c3437056SNickeau        $request = Sqlite::createOrGetSqlite()
894c3437056SNickeau            ->createRequest()
89504fd306cSNickeau            ->setQueryParametrized($query, [$pageIdValue]);
896c3437056SNickeau        $rows = [];
897c3437056SNickeau        try {
898c3437056SNickeau            $rows = $request
899c3437056SNickeau                ->execute()
900c3437056SNickeau                ->getRows();
90104fd306cSNickeau        } catch (ExceptionCompile $e) {
90204fd306cSNickeau            throw new ExceptionRuntimeInternal("Error while retrieving the object by id", self::CANONICAL, 1, $e);
903c3437056SNickeau        } finally {
904c3437056SNickeau            $request->close();
905c3437056SNickeau        }
906c3437056SNickeau
907c3437056SNickeau        switch (sizeof($rows)) {
908c3437056SNickeau            case 0:
90904fd306cSNickeau                throw new ExceptionNotFound("No object by page id");
910c3437056SNickeau            case 1:
911c3437056SNickeau                /**
912c3437056SNickeau                 * Page Id Collision detection
913c3437056SNickeau                 */
91404fd306cSNickeau                $rowId = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
91504fd306cSNickeau                $this->checkCollision($rowId, $pageIdAttribute, $pageIdValue);
916c3437056SNickeau                return $rows[0];
917c3437056SNickeau            default:
918c3437056SNickeau                $existingPages = implode(", ", $rows);
91904fd306cSNickeau                $message = "The pages ($existingPages) have all the same page id ($pageIdValue)";
92004fd306cSNickeau                throw new ExceptionRuntimeInternal($message, self::CANONICAL);
921c3437056SNickeau        }
922c3437056SNickeau
923c3437056SNickeau    }
924c3437056SNickeau
925c3437056SNickeau
926c3437056SNickeau    private
92704fd306cSNickeau    function getParametrizedLookupQuery(string $attributeName): string
928c3437056SNickeau    {
929c3437056SNickeau        $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES);
93004fd306cSNickeau        return "$select where $attributeName = ?";
931c3437056SNickeau    }
932c3437056SNickeau
933c3437056SNickeau
93404fd306cSNickeau    public
93504fd306cSNickeau    function setMarkupPath(MarkupPath $page)
936c3437056SNickeau    {
93704fd306cSNickeau        $this->markupPath = $page;
938c3437056SNickeau        return $this;
939c3437056SNickeau    }
940c3437056SNickeau
94104fd306cSNickeau    /**
94204fd306cSNickeau     * @throws ExceptionNotFound
94304fd306cSNickeau     */
944c3437056SNickeau    private
94504fd306cSNickeau    function getDatabaseRowFromCanonical($canonicalValue)
946c3437056SNickeau    {
94704fd306cSNickeau        $canoncialName = Canonical::PROPERTY_NAME;
94804fd306cSNickeau        $query = $this->getParametrizedLookupQuery($canoncialName);
949c3437056SNickeau        $request = $this->sqlite
950c3437056SNickeau            ->createRequest()
95104fd306cSNickeau            ->setQueryParametrized($query, [$canonicalValue]);
952c3437056SNickeau        $rows = [];
953c3437056SNickeau        try {
954c3437056SNickeau            $rows = $request
955c3437056SNickeau                ->execute()
956c3437056SNickeau                ->getRows();
95704fd306cSNickeau        } catch (ExceptionCompile $e) {
95804fd306cSNickeau            throw new ExceptionRuntime("An exception has occurred with the page search from CANONICAL. " . $e->getMessage());
959c3437056SNickeau        } finally {
960c3437056SNickeau            $request->close();
961c3437056SNickeau        }
962c3437056SNickeau
963c3437056SNickeau        switch (sizeof($rows)) {
964c3437056SNickeau            case 0:
96504fd306cSNickeau                throw new ExceptionNotFound("No canonical row was found");
966c3437056SNickeau            case 1:
967c3437056SNickeau                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
96804fd306cSNickeau                $this->checkCollision($id, $canoncialName, $canonicalValue);
969c3437056SNickeau                return $rows[0];
970c3437056SNickeau            default:
971c3437056SNickeau                $existingPages = [];
972c3437056SNickeau                foreach ($rows as $row) {
973c3437056SNickeau                    $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
97404fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($id);
975c3437056SNickeau                    if (!$duplicatePage->exists()) {
976c3437056SNickeau
977c3437056SNickeau                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
978c3437056SNickeau
979c3437056SNickeau                    } else {
980c3437056SNickeau
981c3437056SNickeau                        /**
982c3437056SNickeau                         * Check if the error may come from the auto-canonical
983c3437056SNickeau                         * (Never ever save generated data)
984c3437056SNickeau                         */
98504fd306cSNickeau                        $canonicalLastNamesCount = SiteConfig::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0);
986c3437056SNickeau                        if ($canonicalLastNamesCount > 0) {
98704fd306cSNickeau                            $this->markupPath->unsetMetadata($canoncialName);
98804fd306cSNickeau                            $duplicatePage->unsetMetadata($canoncialName);
989c3437056SNickeau                        }
990c3437056SNickeau
991c3437056SNickeau                        $existingPages[] = $row;
992c3437056SNickeau                    }
993c3437056SNickeau                }
99404fd306cSNickeau                if (sizeof($existingPages) > 1) {
995c3437056SNickeau                    $existingPages = implode(", ", $existingPages);
99604fd306cSNickeau                    $message = "The existing pages ($existingPages) have all the same canonical ($canonicalValue), return the first one";
99704fd306cSNickeau                    LogUtility::error($message, self::CANONICAL);
998c3437056SNickeau                }
99904fd306cSNickeau                return $existingPages[0];
1000c3437056SNickeau        }
1001c3437056SNickeau    }
1002c3437056SNickeau
100304fd306cSNickeau    /**
100404fd306cSNickeau     * @throws ExceptionNotFound
100504fd306cSNickeau     */
1006c3437056SNickeau    private
1007c3437056SNickeau    function getDatabaseRowFromPath(string $path): ?array
1008c3437056SNickeau    {
100904fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
1010c3437056SNickeau        return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path);
1011c3437056SNickeau    }
1012c3437056SNickeau
101304fd306cSNickeau    /**
101404fd306cSNickeau     * @throws ExceptionNotFound
101504fd306cSNickeau     */
1016c3437056SNickeau    private
101704fd306cSNickeau    function getDatabaseRowFromDokuWikiId(string $id): array
1018c3437056SNickeau    {
1019c3437056SNickeau        return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
1020c3437056SNickeau    }
1021c3437056SNickeau
102204fd306cSNickeau    /**
102304fd306cSNickeau     * @throws ExceptionNotFound
102404fd306cSNickeau     */
1025c3437056SNickeau    public
1026c3437056SNickeau    function getDatabaseRowFromAttribute(string $attribute, string $value)
1027c3437056SNickeau    {
1028c3437056SNickeau        $query = $this->getParametrizedLookupQuery($attribute);
1029c3437056SNickeau        $request = $this->sqlite
1030c3437056SNickeau            ->createRequest()
1031c3437056SNickeau            ->setQueryParametrized($query, [$value]);
1032c3437056SNickeau        $rows = [];
1033c3437056SNickeau        try {
1034c3437056SNickeau            $rows = $request
1035c3437056SNickeau                ->execute()
1036c3437056SNickeau                ->getRows();
103704fd306cSNickeau        } catch (ExceptionCompile $e) {
103804fd306cSNickeau            $message = "Internal Error: An exception has occurred with the page search from a PATH: " . $e->getMessage();
103904fd306cSNickeau            LogUtility::log2file($message);
104004fd306cSNickeau            throw new ExceptionNotFound($message);
1041c3437056SNickeau        } finally {
1042c3437056SNickeau            $request->close();
1043c3437056SNickeau        }
1044c3437056SNickeau
1045c3437056SNickeau        switch (sizeof($rows)) {
1046c3437056SNickeau            case 0:
104704fd306cSNickeau                throw new ExceptionNotFound("No database row found for the page");
1048c3437056SNickeau            case 1:
1049c3437056SNickeau                $value = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
105004fd306cSNickeau                if ($this->markupPath != null && $value !== $this->markupPath->getWikiId()) {
105104fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($value);
1052c3437056SNickeau                    if (!$duplicatePage->exists()) {
1053c3437056SNickeau                        $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1054c3437056SNickeau                    } else {
105504fd306cSNickeau                        LogUtility::msg("The page ($this->markupPath) and the page ($value) have the same $attribute ($value)", LogUtility::LVL_MSG_ERROR);
1056c3437056SNickeau                    }
1057c3437056SNickeau                }
1058c3437056SNickeau                return $rows[0];
1059c3437056SNickeau            default:
1060c3437056SNickeau                $existingPages = [];
1061c3437056SNickeau                foreach ($rows as $row) {
1062c3437056SNickeau                    $value = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
106304fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($value);
1064c3437056SNickeau                    if (!$duplicatePage->exists()) {
1065c3437056SNickeau
1066c3437056SNickeau                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
1067c3437056SNickeau
1068c3437056SNickeau                    } else {
1069c3437056SNickeau                        $existingPages[] = $row;
1070c3437056SNickeau                    }
1071c3437056SNickeau                }
1072c3437056SNickeau                if (sizeof($existingPages) === 1) {
1073c3437056SNickeau                    return $existingPages[0];
1074c3437056SNickeau                } else {
107504fd306cSNickeau                    $existingPageIds = array_map(
107604fd306cSNickeau                        function ($row) {
107704fd306cSNickeau                            return $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
107804fd306cSNickeau                        },
107904fd306cSNickeau                        $existingPages);
108004fd306cSNickeau                    $existingPages = implode(", ", $existingPageIds);
108104fd306cSNickeau                    throw new ExceptionNotFound("The existing pages ($existingPages) have all the same attribute $attribute with the value ($value)", LogUtility::LVL_MSG_ERROR);
1082c3437056SNickeau                }
1083c3437056SNickeau        }
1084c3437056SNickeau    }
1085c3437056SNickeau
1086c3437056SNickeau    public
108704fd306cSNickeau    function getMarkupPath(): ?MarkupPath
1088c3437056SNickeau    {
1089*70bbd7f1Sgerardnico        if ($this->row === null) {
1090*70bbd7f1Sgerardnico            return null;
1091*70bbd7f1Sgerardnico        }
1092c3437056SNickeau        if (
109304fd306cSNickeau            $this->markupPath === null
1094c3437056SNickeau            && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null
1095c3437056SNickeau        ) {
109604fd306cSNickeau            $this->markupPath = MarkupPath::createMarkupFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]);
1097c3437056SNickeau        }
109804fd306cSNickeau        return $this->markupPath;
1099c3437056SNickeau    }
1100c3437056SNickeau
1101c3437056SNickeau    private
1102c3437056SNickeau    function getDatabaseRowFromAlias($alias): ?array
1103c3437056SNickeau    {
1104c3437056SNickeau
1105c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
1106c3437056SNickeau        $buildFields = self::PAGE_BUILD_ATTRIBUTES;
1107c3437056SNickeau        $fields = array_reduce($buildFields, function ($carry, $element) {
1108c3437056SNickeau            if ($carry !== null) {
1109c3437056SNickeau                return "$carry, p.{$element}";
1110c3437056SNickeau            } else {
1111c3437056SNickeau                return "p.{$element}";
1112c3437056SNickeau            }
1113c3437056SNickeau        }, null);
111404fd306cSNickeau        /** @noinspection SqlResolve */
1115c3437056SNickeau        $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? ";
1116c3437056SNickeau        $request = $this->sqlite
1117c3437056SNickeau            ->createRequest()
1118c3437056SNickeau            ->setQueryParametrized($query, [$alias]);
1119c3437056SNickeau        $rows = [];
1120c3437056SNickeau        try {
1121c3437056SNickeau            $rows = $request
1122c3437056SNickeau                ->execute()
1123c3437056SNickeau                ->getRows();
112404fd306cSNickeau        } catch (ExceptionCompile $e) {
1125c3437056SNickeau            LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}");
1126c3437056SNickeau            return null;
1127c3437056SNickeau        } finally {
1128c3437056SNickeau            $request->close();
1129c3437056SNickeau        }
1130c3437056SNickeau        switch (sizeof($rows)) {
1131c3437056SNickeau            case 0:
1132c3437056SNickeau                return null;
1133c3437056SNickeau            case 1:
1134c3437056SNickeau                return $rows[0];
1135c3437056SNickeau            default:
1136c3437056SNickeau                $id = $rows[0]['ID'];
1137c3437056SNickeau                $pages = implode(",",
1138c3437056SNickeau                    array_map(
1139c3437056SNickeau                        function ($row) {
1140c3437056SNickeau                            return $row['ID'];
1141c3437056SNickeau                        },
1142c3437056SNickeau                        $rows
1143c3437056SNickeau                    )
1144c3437056SNickeau                );
1145c3437056SNickeau                LogUtility::msg("For the alias $alias, there is more than one page defined ($pages), the first one ($id) was used", LogUtility::LVL_MSG_ERROR, Aliases::PROPERTY_NAME);
1146c3437056SNickeau                return $rows[0];
1147c3437056SNickeau        }
1148c3437056SNickeau    }
1149c3437056SNickeau
1150c3437056SNickeau
1151c3437056SNickeau    /**
1152c3437056SNickeau     * Utility function
115304fd306cSNickeau     * @param MarkupPath $pageAlias
1154c3437056SNickeau     */
1155c3437056SNickeau    private
115604fd306cSNickeau    function addRedirectAliasWhileBuildingRow(MarkupPath $pageAlias)
1157c3437056SNickeau    {
1158c3437056SNickeau
115904fd306cSNickeau        $aliasPath = $pageAlias->getPathObject()->toAbsoluteId();
1160c3437056SNickeau        try {
116104fd306cSNickeau            Aliases::createForPage($this->markupPath)
1162c3437056SNickeau                ->addAlias($aliasPath)
1163c3437056SNickeau                ->sendToWriteStore();
116404fd306cSNickeau        } catch (ExceptionCompile $e) {
1165c3437056SNickeau            // we don't throw while getting
116604fd306cSNickeau            LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->markupPath)");
1167c3437056SNickeau        }
1168c3437056SNickeau
1169c3437056SNickeau    }
1170c3437056SNickeau
1171c3437056SNickeau    private
1172c3437056SNickeau    function addPageIdAttributeIfNeeded(array &$values)
1173c3437056SNickeau    {
1174c3437056SNickeau        if (!isset($values[PageId::getPersistentName()])) {
117504fd306cSNickeau            $values[PageId::getPersistentName()] = $this->markupPath->getPageId();
1176c3437056SNickeau        }
1177c3437056SNickeau        if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) {
117804fd306cSNickeau            $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->markupPath->getPageIdAbbr();
1179c3437056SNickeau        }
1180c3437056SNickeau    }
1181c3437056SNickeau
1182c3437056SNickeau    public
1183c3437056SNickeau    function getFromRow(string $attribute)
1184c3437056SNickeau    {
1185c3437056SNickeau        if ($this->row === null) {
1186c3437056SNickeau            return null;
1187c3437056SNickeau        }
1188c3437056SNickeau
1189c3437056SNickeau        if (!array_key_exists($attribute, $this->row)) {
1190c3437056SNickeau            /**
1191c3437056SNickeau             * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES}
1192c3437056SNickeau             * or in the table
1193c3437056SNickeau             */
119404fd306cSNickeau            throw new ExceptionRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical());
1195c3437056SNickeau        }
1196c3437056SNickeau
1197c3437056SNickeau        $value = $this->row[$attribute];
1198c3437056SNickeau
1199c3437056SNickeau        if ($value !== null) {
1200c3437056SNickeau            return $value;
1201c3437056SNickeau        }
1202c3437056SNickeau
1203c3437056SNickeau        // don't know why but the sqlite plugin returns them uppercase
1204c3437056SNickeau        // rowid is returned lowercase from the sqlite plugin
1205c3437056SNickeau        $upperAttribute = strtoupper($attribute);
1206*70bbd7f1Sgerardnico        return $this->row[$upperAttribute] ?? null;
1207c3437056SNickeau
1208c3437056SNickeau    }
1209c3437056SNickeau
1210c3437056SNickeau
12114cadd4f8SNickeau    /**
121204fd306cSNickeau     * @throws ExceptionCompile
12134cadd4f8SNickeau     */
121404fd306cSNickeau    public
121504fd306cSNickeau    function replicateAnalytics()
1216c3437056SNickeau    {
1217c3437056SNickeau
1218c3437056SNickeau        try {
121904fd306cSNickeau            $fetchPath = $this->markupPath->fetchAnalyticsPath();
122004fd306cSNickeau            $analyticsJson = Json::createFromPath($fetchPath);
122104fd306cSNickeau        } catch (ExceptionCompile $e) {
122204fd306cSNickeau            if (PluginUtility::isDevOrTest()) {
122304fd306cSNickeau                throw $e;
122404fd306cSNickeau            }
122504fd306cSNickeau            throw new ExceptionCompile("Unable to get the analytics document", self::CANONICAL, 0, $e);
1226c3437056SNickeau        }
1227c3437056SNickeau
1228c3437056SNickeau        /**
1229c3437056SNickeau         * Replication Date
1230c3437056SNickeau         */
123104fd306cSNickeau        $replicationDateMeta = ReplicationDate::createFromPage($this->markupPath)
1232c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
12334cadd4f8SNickeau            ->setValue(new DateTime());
1234c3437056SNickeau
1235c3437056SNickeau        /**
1236c3437056SNickeau         * Analytics
1237c3437056SNickeau         */
1238c3437056SNickeau        $analyticsJsonAsString = $analyticsJson->toPrettyJsonString();
1239c3437056SNickeau        $analyticsJsonAsArray = $analyticsJson->toArray();
1240c3437056SNickeau
1241c3437056SNickeau        /**
1242c3437056SNickeau         * Record
1243c3437056SNickeau         */
1244c3437056SNickeau        $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString;
124504fd306cSNickeau        $record['IS_LOW_QUALITY'] = ($this->markupPath->isLowQualityPage() === true ? 1 : 0);
124604fd306cSNickeau        $record['WORD_COUNT'] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][renderer_plugin_combo_analytics::WORD_COUNT];
124704fd306cSNickeau        $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][BacklinkCount::getPersistentName()];
1248c3437056SNickeau        $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue();
1249c3437056SNickeau        $this->upsertAttributes($record);
1250c3437056SNickeau    }
1251c3437056SNickeau
125204fd306cSNickeau    private
125304fd306cSNickeau    function checkCollision($wikiIdInDatabase, $attribute, $value)
125404fd306cSNickeau    {
125504fd306cSNickeau        if ($this->markupPath === null) {
125604fd306cSNickeau            return;
125704fd306cSNickeau        }
125804fd306cSNickeau        try {
125904fd306cSNickeau            $markupWikiId = $this->markupPath->toWikiPath()->getWikiId();
126004fd306cSNickeau        } catch (ExceptionCast $e) {
126104fd306cSNickeau            return;
126204fd306cSNickeau        }
126304fd306cSNickeau        if ($wikiIdInDatabase !== $markupWikiId) {
126404fd306cSNickeau            $duplicatePage = MarkupPath::createMarkupFromId($wikiIdInDatabase);
126504fd306cSNickeau            if (!FileSystems::exists($duplicatePage)) {
126604fd306cSNickeau                // Move
126704fd306cSNickeau                LogUtility::info("The non-existing duplicate page ($wikiIdInDatabase) has been added as redirect alias for the page ($this->markupPath)", self::CANONICAL);
126804fd306cSNickeau                $this->addRedirectAliasWhileBuildingRow($duplicatePage);
126904fd306cSNickeau            } else {
127004fd306cSNickeau                // This can happens if two page were created not on the same website
127104fd306cSNickeau                // of if the sqlite database was deleted and rebuilt.
127204fd306cSNickeau                // The chance is really, really low
127304fd306cSNickeau                $errorMessage = "The page ($this->markupPath) and the page ($wikiIdInDatabase) have the same $attribute value ($value)";
127404fd306cSNickeau                throw new ExceptionRuntimeInternal($errorMessage, self::CANONICAL);
127504fd306cSNickeau                // What to do ?
127604fd306cSNickeau                // The database does not allow two page id with the same value
127704fd306cSNickeau                // If it happens, ugh, ugh, ..., a replication process between website may be.
127804fd306cSNickeau            }
127904fd306cSNickeau        }
128004fd306cSNickeau    }
128104fd306cSNickeau
1282c3437056SNickeau
1283c3437056SNickeau}
1284