xref: /plugin/combo/ComboStrap/DatabasePageRow.php (revision 2d4863144e14613de4de42a4560d2738664cbc0e)
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
205031d4b49Sgerardnico    public static function createFromRowId(string $rowId): DatabasePageRow
206031d4b49Sgerardnico    {
207031d4b49Sgerardnico
208031d4b49Sgerardnico        $databasePage = new DatabasePageRow();
209031d4b49Sgerardnico        try {
210031d4b49Sgerardnico            $row = $databasePage->getDatabaseRowFromRowId($rowId);
211031d4b49Sgerardnico            $databasePage->setRow($row);
212031d4b49Sgerardnico        } catch (ExceptionNotFound|ExceptionSqliteNotAvailable $e) {
213031d4b49Sgerardnico            // not found
214031d4b49Sgerardnico        }
215031d4b49Sgerardnico        return $databasePage;
216031d4b49Sgerardnico
217031d4b49Sgerardnico    }
218031d4b49Sgerardnico
21904fd306cSNickeau    /**
22004fd306cSNickeau     * @param MarkupPath $page
22104fd306cSNickeau     * @return DatabasePageRow
22204fd306cSNickeau     * @throws ExceptionSqliteNotAvailable - if there is no sqlite available
22304fd306cSNickeau     * @noinspection PhpDocRedundantThrowsInspection
22404fd306cSNickeau     */
22504fd306cSNickeau    public static function getOrCreateFromPageObject(MarkupPath $page): DatabasePageRow
226c3437056SNickeau    {
227c3437056SNickeau
228c3437056SNickeau        $databasePage = new DatabasePageRow();
22904fd306cSNickeau        try {
230c3437056SNickeau            $row = $databasePage->getDatabaseRowFromPage($page);
231c3437056SNickeau            $databasePage->setRow($row);
232c3437056SNickeau            return $databasePage;
23304fd306cSNickeau        } catch (ExceptionNotExists $e) {
23404fd306cSNickeau            // page copied on the local system
23504fd306cSNickeau            try {
23604fd306cSNickeau                ComboFs::createIfNotExists($page);
23704fd306cSNickeau                $row = $databasePage->getDatabaseRowFromPage($page);
23804fd306cSNickeau                $databasePage->setRow($row);
23904fd306cSNickeau                return $databasePage;
24004fd306cSNickeau            } catch (ExceptionNotExists $e) {
24104fd306cSNickeau                throw ExceptionRuntimeInternal::withMessageAndError("The row should exists as we created it specifically", $e);
24204fd306cSNickeau            }
243c3437056SNickeau        }
244c3437056SNickeau
24504fd306cSNickeau    }
24604fd306cSNickeau
24704fd306cSNickeau
24804fd306cSNickeau    /**
24904fd306cSNickeau     *
25004fd306cSNickeau     */
25104fd306cSNickeau    public
25204fd306cSNickeau    static function createFromPageIdAbbr(string $pageIdAbbr): DatabasePageRow
253c3437056SNickeau    {
254c3437056SNickeau        $databasePage = new DatabasePageRow();
25504fd306cSNickeau        try {
256c3437056SNickeau            $row = $databasePage->getDatabaseRowFromAttribute(PageId::PAGE_ID_ABBR_ATTRIBUTE, $pageIdAbbr);
257c3437056SNickeau            $databasePage->setRow($row);
25804fd306cSNickeau        } catch (ExceptionNotFound $e) {
25904fd306cSNickeau            // ok
260c3437056SNickeau        }
261c3437056SNickeau        return $databasePage;
262c3437056SNickeau
263c3437056SNickeau    }
264c3437056SNickeau
265c3437056SNickeau    /**
266c3437056SNickeau     * @param $canonical
267c3437056SNickeau     * @return DatabasePageRow
268c3437056SNickeau     */
26904fd306cSNickeau    public
27004fd306cSNickeau    static function createFromCanonical($canonical): DatabasePageRow
271c3437056SNickeau    {
272c3437056SNickeau
27304fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($canonical);
274c3437056SNickeau        $databasePage = new DatabasePageRow();
27504fd306cSNickeau        try {
276c3437056SNickeau            $row = $databasePage->getDatabaseRowFromAttribute(Canonical::PROPERTY_NAME, $canonical);
277c3437056SNickeau            $databasePage->setRow($row);
27804fd306cSNickeau        } catch (ExceptionNotFound $e) {
27904fd306cSNickeau            // ok
280c3437056SNickeau        }
281c3437056SNickeau        return $databasePage;
282c3437056SNickeau
283c3437056SNickeau
284c3437056SNickeau    }
285c3437056SNickeau
28604fd306cSNickeau    public
28704fd306cSNickeau    static function createFromAlias($alias): DatabasePageRow
288c3437056SNickeau    {
289c3437056SNickeau
29004fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($alias);
291c3437056SNickeau        $databasePage = new DatabasePageRow();
292c3437056SNickeau        $row = $databasePage->getDatabaseRowFromAlias($alias);
293c3437056SNickeau        if ($row != null) {
294c3437056SNickeau            $databasePage->setRow($row);
29504fd306cSNickeau            $page = $databasePage->getMarkupPath();
29658317768Sgerardnico            if ($page !== null) {
29758317768Sgerardnico                // page may be null in production
29858317768Sgerardnico                // PHP Fatal error:  Uncaught Error: Call to a member function setBuildAliasPath() on null in
29958317768Sgerardnico                // /opt/www/bytle/farmer.bytle.net/lib/plugins/combo/ComboStrap/DatabasePageRow.php:220
30058317768Sgerardnico                $page->setBuildAliasPath($alias);
30158317768Sgerardnico            }
302c3437056SNickeau        }
303c3437056SNickeau        return $databasePage;
304c3437056SNickeau
305c3437056SNickeau    }
306c3437056SNickeau
30704fd306cSNickeau    /**
30804fd306cSNickeau     * @throws ExceptionNotFound
30904fd306cSNickeau     */
31004fd306cSNickeau    public
31104fd306cSNickeau    static function getFromDokuWikiId($id): DatabasePageRow
312c3437056SNickeau    {
313c3437056SNickeau        $databasePage = new DatabasePageRow();
3144ebc3257Sgerardnico        $databasePage->markupPath = MarkupPath::createMarkupFromId($id);
315c3437056SNickeau        $row = $databasePage->getDatabaseRowFromDokuWikiId($id);
316c3437056SNickeau        $databasePage->setRow($row);
317c3437056SNickeau        return $databasePage;
318c3437056SNickeau    }
319c3437056SNickeau
32004fd306cSNickeau    public
32104fd306cSNickeau    function getPageId()
322c3437056SNickeau    {
323c3437056SNickeau        return $this->getFromRow(PageId::PROPERTY_NAME);
324c3437056SNickeau    }
325c3437056SNickeau
326c3437056SNickeau
327c3437056SNickeau    public
328c3437056SNickeau    function shouldReplicate(): bool
329c3437056SNickeau    {
330c3437056SNickeau
33104fd306cSNickeau
3324cadd4f8SNickeau        $dateReplication = $this->getReplicationDate();
3334cadd4f8SNickeau        if ($dateReplication === null) {
3344cadd4f8SNickeau            return true;
3354cadd4f8SNickeau        }
3364cadd4f8SNickeau
3374cadd4f8SNickeau        /**
3384cadd4f8SNickeau         * When the replication date is older than the actual document
3394cadd4f8SNickeau         */
34004fd306cSNickeau        try {
34104fd306cSNickeau            $modifiedTime = FileSystems::getModifiedTime($this->markupPath->getPathObject());
3424cadd4f8SNickeau            if ($modifiedTime > $dateReplication) {
3434cadd4f8SNickeau                return true;
3444cadd4f8SNickeau            }
34504fd306cSNickeau        } catch (ExceptionNotFound $e) {
34604fd306cSNickeau            return false;
34704fd306cSNickeau        }
34804fd306cSNickeau
34904fd306cSNickeau
35004fd306cSNickeau        $path = $this->markupPath->fetchAnalyticsPath();
3514cadd4f8SNickeau
352c3437056SNickeau        /**
353c3437056SNickeau         * When the file does not exist
354c3437056SNickeau         */
35504fd306cSNickeau        $exist = FileSystems::exists($path);
356c3437056SNickeau        if (!$exist) {
357c3437056SNickeau            return true;
358c3437056SNickeau        }
359c3437056SNickeau
360c3437056SNickeau        /**
3614cadd4f8SNickeau         * When the analytics document is older
362c3437056SNickeau         */
36304fd306cSNickeau        try {
36404fd306cSNickeau
36504fd306cSNickeau            $modifiedTime = FileSystems::getModifiedTime($path);
366c3437056SNickeau            if ($modifiedTime > $dateReplication) {
367c3437056SNickeau                return true;
368c3437056SNickeau            }
36904fd306cSNickeau        } catch (ExceptionNotFound $e) {
37004fd306cSNickeau            //
37104fd306cSNickeau        }
372c3437056SNickeau
3734cadd4f8SNickeau
374c3437056SNickeau        /**
375c3437056SNickeau         * When the database version file is higher
376c3437056SNickeau         */
37704fd306cSNickeau        $version = LocalPath::createFromPathString(__DIR__ . "/../db/latest.version");
37804fd306cSNickeau        try {
379c3437056SNickeau            $versionModifiedTime = FileSystems::getModifiedTime($version);
38004fd306cSNickeau        } catch (ExceptionNotFound $e) {
38104fd306cSNickeau            return false;
38204fd306cSNickeau        }
383c3437056SNickeau        if ($versionModifiedTime > $dateReplication) {
384c3437056SNickeau            return true;
385c3437056SNickeau        }
386c3437056SNickeau
387c3437056SNickeau        /**
388c3437056SNickeau         * When the class date time is higher
389c3437056SNickeau         */
39004fd306cSNickeau        $code = LocalPath::createFromPathString(__DIR__ . "/DatabasePageRow.php");
39104fd306cSNickeau        try {
392c3437056SNickeau            $codeModified = FileSystems::getModifiedTime($code);
39304fd306cSNickeau        } catch (ExceptionNotFound $e) {
39404fd306cSNickeau            throw new ExceptionRuntime("The database file does not exist");
39504fd306cSNickeau        }
396c3437056SNickeau        if ($codeModified > $dateReplication) {
397c3437056SNickeau            return true;
398c3437056SNickeau        }
399c3437056SNickeau
400c3437056SNickeau        return false;
401c3437056SNickeau
402c3437056SNickeau    }
403c3437056SNickeau
404c3437056SNickeau    public
405c3437056SNickeau    function delete()
406c3437056SNickeau    {
407031d4b49Sgerardnico        $rowId = $this->getRowId();
40804fd306cSNickeau        $request = $this->sqlite
409c3437056SNickeau            ->createRequest()
410031d4b49Sgerardnico            ->setQueryParametrized('delete from pages where rowid = ?', [$rowId]);
411c3437056SNickeau        try {
412c3437056SNickeau            $request->execute();
41304fd306cSNickeau        } catch (ExceptionCompile $e) {
414031d4b49Sgerardnico            LogUtility::error("Something went wrong when deleting the page ({$this->markupPath}) from the database with the rowid $rowId", self::CANONICAL, $e);
415c3437056SNickeau        } finally {
416c3437056SNickeau            $request->close();
417c3437056SNickeau        }
418031d4b49Sgerardnico
419c3437056SNickeau        $this->buildInitObjectFields();
420c3437056SNickeau
421c3437056SNickeau    }
422c3437056SNickeau
423c3437056SNickeau    /**
42404fd306cSNickeau     * @return Json the analytics array or null if not in db
425c3437056SNickeau     */
426c3437056SNickeau    public
42704fd306cSNickeau    function getAnalyticsData(): Json
428c3437056SNickeau    {
429c3437056SNickeau
430c3437056SNickeau        $jsonString = $this->getFromRow(self::ANALYTICS_ATTRIBUTE);
431c3437056SNickeau        if ($jsonString === null) {
43204fd306cSNickeau            // we put an empty json to not get any problem with the json database function
43304fd306cSNickeau            // on an empty string / null (for sqlite)
43404fd306cSNickeau            return Json::createEmpty();
435c3437056SNickeau        }
436c3437056SNickeau        try {
437c3437056SNickeau            return Json::createFromString($jsonString);
43804fd306cSNickeau        } catch (ExceptionCompile $e) {
43904fd306cSNickeau            throw ExceptionRuntimeInternal::withMessageAndError("Error while building back the analytics JSON object. {$e->getMessage()}", $e);
440c3437056SNickeau        }
441c3437056SNickeau
442c3437056SNickeau    }
443c3437056SNickeau
444c3437056SNickeau    /**
445c3437056SNickeau     * Return the database row
446c3437056SNickeau     *
447c3437056SNickeau     *
44804fd306cSNickeau     * @throws ExceptionNotExists - if the row does not exists
449c3437056SNickeau     */
45004fd306cSNickeau    public
45104fd306cSNickeau    function getDatabaseRowFromPage(MarkupPath $markupPath): array
452c3437056SNickeau    {
453c3437056SNickeau
45404fd306cSNickeau        $this->setMarkupPath($markupPath);
455c3437056SNickeau
45604fd306cSNickeau        /**
45704fd306cSNickeau         * Generated identifier
45804fd306cSNickeau         */
45904fd306cSNickeau        try {
46004fd306cSNickeau            $pageId = $markupPath->getPageId();
46104fd306cSNickeau            return $this->getDatabaseRowFromPageId($pageId);
46204fd306cSNickeau        } catch (ExceptionNotFound $e) {
46304fd306cSNickeau            // no page id
464c3437056SNickeau        }
465c3437056SNickeau
466c3437056SNickeau        /**
46704fd306cSNickeau         * Named identifier: path
468c3437056SNickeau         */
46904fd306cSNickeau        try {
47004fd306cSNickeau            $path = $markupPath->getPathObject();
47104fd306cSNickeau            return $this->getDatabaseRowFromPath($path);
47204fd306cSNickeau        } catch (ExceptionNotFound $e) {
47304fd306cSNickeau            // not found
47404fd306cSNickeau        }
47504fd306cSNickeau
47604fd306cSNickeau        /**
47704fd306cSNickeau         * Named identifier: id (ie path)
47804fd306cSNickeau         */
47904fd306cSNickeau        try {
48004fd306cSNickeau            $id = $markupPath->getPathObject()->toWikiPath()->getWikiId();
481c3437056SNickeau            return $this->getDatabaseRowFromDokuWikiId($id);
48204fd306cSNickeau        } catch (ExceptionCast|ExceptionNotFound $e) {
48304fd306cSNickeau        }
48404fd306cSNickeau
48504fd306cSNickeau        /**
48604fd306cSNickeau         * Named identifier: canonical
48704fd306cSNickeau         * (Note that canonical should become a soft link and therefore a path)
48804fd306cSNickeau         */
48904fd306cSNickeau        try {
49004fd306cSNickeau            $canonical = Canonical::createForPage($markupPath)->getValue();
49104fd306cSNickeau            return $this->getDatabaseRowFromCanonical($canonical->toAbsoluteId());
49204fd306cSNickeau        } catch (ExceptionNotFound $e) {
49304fd306cSNickeau            // no canonical
49404fd306cSNickeau        }
49504fd306cSNickeau
49604fd306cSNickeau        // we send a not exist
49704fd306cSNickeau        throw new ExceptionNotExists("No row could be found");
498c3437056SNickeau
499c3437056SNickeau
500c3437056SNickeau    }
501c3437056SNickeau
502c3437056SNickeau
5034cadd4f8SNickeau    /**
5044cadd4f8SNickeau     * @return DateTime|null
5054cadd4f8SNickeau     */
50604fd306cSNickeau    public
50704fd306cSNickeau    function getReplicationDate(): ?DateTime
508c3437056SNickeau    {
50904fd306cSNickeau        $dateString = $this->getFromRow(ReplicationDate::getPersistentName());
510c3437056SNickeau        if ($dateString === null) {
511c3437056SNickeau            return null;
512c3437056SNickeau        }
513c3437056SNickeau        try {
514c3437056SNickeau            return Iso8601Date::createFromString($dateString)->getDateTime();
51504fd306cSNickeau        } catch (ExceptionCompile $e) {
516c3437056SNickeau            LogUtility::msg("Error while reading the replication date in the database. {$e->getMessage()}");
517c3437056SNickeau            return null;
518c3437056SNickeau        }
519c3437056SNickeau
520c3437056SNickeau    }
521c3437056SNickeau
522c3437056SNickeau    /**
52304fd306cSNickeau     *
52404fd306cSNickeau     * @throws ExceptionBadState
52504fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
526c3437056SNickeau     */
52704fd306cSNickeau    public
52804fd306cSNickeau    function replicatePage(): void
529c3437056SNickeau    {
530c3437056SNickeau
53104fd306cSNickeau        if (!FileSystems::exists($this->markupPath)) {
53204fd306cSNickeau            throw new ExceptionBadState("You can't replicate the page ($this->markupPath) because it does not exists.");
533c3437056SNickeau        }
534c3437056SNickeau
535c3437056SNickeau        /**
536c3437056SNickeau         * Replication Date
537c3437056SNickeau         */
53804fd306cSNickeau        $replicationDate = ReplicationDate::createFromPage($this->markupPath)
539c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
5404cadd4f8SNickeau            ->setValue(new DateTime());
541c3437056SNickeau
542c3437056SNickeau        /**
54304fd306cSNickeau         * Same data as {@link MarkupPath::getMetadataForRendering()}
544c3437056SNickeau         */
545c3437056SNickeau        $record = $this->getMetaRecord();
546c3437056SNickeau        $record[$replicationDate::getPersistentName()] = $replicationDate->toStoreValue();
54704fd306cSNickeau        $this->upsertAttributes($record);
548c3437056SNickeau
549c3437056SNickeau    }
550c3437056SNickeau
551c3437056SNickeau
552c3437056SNickeau    /**
55304fd306cSNickeau     *
554c3437056SNickeau     *
555c3437056SNickeau     * Attribute that are scalar / modifiable in the database
556c3437056SNickeau     * (not aliases or json data for instance)
55704fd306cSNickeau     *
55804fd306cSNickeau     * @throws ExceptionBadState
55904fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
560c3437056SNickeau     */
56104fd306cSNickeau    public
56204fd306cSNickeau    function replicateMetaAttributes(): void
563c3437056SNickeau    {
564c3437056SNickeau
56504fd306cSNickeau        $this->upsertAttributes($this->getMetaRecord());
566c3437056SNickeau
567c3437056SNickeau    }
568c3437056SNickeau
56904fd306cSNickeau    /**
57004fd306cSNickeau     * @throws ExceptionBadState - if the array is empty
57104fd306cSNickeau     */
57204fd306cSNickeau    public
57304fd306cSNickeau    function upsertAttributes(array $attributes): void
574c3437056SNickeau    {
575c3437056SNickeau
576c3437056SNickeau        if (empty($attributes)) {
57704fd306cSNickeau            throw new ExceptionBadState("The page database attribute passed should not be empty");
578c3437056SNickeau        }
579c3437056SNickeau
580c3437056SNickeau        $values = [];
581c3437056SNickeau        $columnClauses = [];
582c3437056SNickeau        foreach ($attributes as $key => $value) {
583c3437056SNickeau            if (is_array($value)) {
58404fd306cSNickeau                throw new ExceptionRuntime("The attribute ($key) has value that is an array (" . implode(", ", $value) . ")");
585c3437056SNickeau            }
586c3437056SNickeau            $columnClauses[] = "$key = ?";
587c3437056SNickeau            $values[$key] = $value;
588c3437056SNickeau        }
589c3437056SNickeau
590c3437056SNickeau        /**
591c3437056SNickeau         * Primary key has moved during the time
592c3437056SNickeau         * It should be the UUID but not for older version
593c3437056SNickeau         *
594c3437056SNickeau         * If the primary key is null, no record was found
595c3437056SNickeau         */
596c3437056SNickeau        $rowId = $this->getRowId();
597c3437056SNickeau        if ($rowId !== null) {
598c3437056SNickeau            /**
599c3437056SNickeau             * We just add the primary key
600c3437056SNickeau             * otherwise as this is a associative
601c3437056SNickeau             * array, we will miss a value for the update statement
602c3437056SNickeau             */
603c3437056SNickeau            $values[] = $rowId;
604c3437056SNickeau
605c3437056SNickeau            $updateStatement = "update PAGES SET " . implode(", ", $columnClauses) . " where ROWID = ?";
606c3437056SNickeau            $request = $this->sqlite
607c3437056SNickeau                ->createRequest()
608c3437056SNickeau                ->setQueryParametrized($updateStatement, $values);
609c3437056SNickeau            $countChanges = 0;
610c3437056SNickeau            try {
611c3437056SNickeau                $countChanges = $request
612c3437056SNickeau                    ->execute()
613c3437056SNickeau                    ->getChangeCount();
61404fd306cSNickeau            } catch (ExceptionCompile $e) {
61504fd306cSNickeau                throw new ExceptionBadState("There was a problem during the page attribute updates. : {$e->getMessage()}");
616c3437056SNickeau            } finally {
617c3437056SNickeau                $request->close();
618c3437056SNickeau            }
619c3437056SNickeau            if ($countChanges !== 1) {
62004fd306cSNickeau                // internal error
62104fd306cSNickeau                LogUtility::error("The database replication has not updated exactly 1 record but ($countChanges) record", \action_plugin_combo_indexer::CANONICAL);
622c3437056SNickeau            }
623c3437056SNickeau
624c3437056SNickeau        } else {
625c3437056SNickeau
626c3437056SNickeau            /**
627c3437056SNickeau             * Creation
628c3437056SNickeau             */
62904fd306cSNickeau            if ($this->markupPath === null) {
63004fd306cSNickeau                throw new ExceptionBadState("The page should be defined to create a page database row");
631c3437056SNickeau            }
63204fd306cSNickeau
63304fd306cSNickeau            /**
63404fd306cSNickeau             * If the user copy a frontmatter with the same page id abbr, we got a problem
63504fd306cSNickeau             */
636be61a7dfSgerardnico            $pageIdAbbr = $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] ?? null;
63704fd306cSNickeau            if ($pageIdAbbr == null) {
6388dfa0a7aSNico                $pageId = $values[PageId::getPersistentName()] ?? null;
63904fd306cSNickeau                if ($pageId === null) {
64004fd306cSNickeau                    throw new ExceptionBadState("You can't insert a page in the database without a page id");
64104fd306cSNickeau                }
64204fd306cSNickeau                $pageIdAbbr = PageId::getAbbreviated($pageId);
64304fd306cSNickeau                $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $pageIdAbbr;
64404fd306cSNickeau            }
64504fd306cSNickeau
64604fd306cSNickeau            $databasePage = DatabasePageRow::createFromPageIdAbbr($pageIdAbbr);
64704fd306cSNickeau            if ($databasePage->exists()) {
64804fd306cSNickeau                $duplicatePage = $databasePage->getMarkupPath();
64904fd306cSNickeau                if ($duplicatePage->getPathObject()->toUriString() === $this->markupPath->toUriString()) {
65004fd306cSNickeau                    $message = "The page ($this->markupPath) is already in the database with the uid ($pageIdAbbr).";
65104fd306cSNickeau                } else {
65204fd306cSNickeau                    $message = "The page ($this->markupPath) cannot be replicated to the database because it has the same page id abbreviation ($pageIdAbbr) than the page ($duplicatePage)";
65304fd306cSNickeau                }
65404fd306cSNickeau                throw new ExceptionBadState($message);
65504fd306cSNickeau            }
65604fd306cSNickeau
65704fd306cSNickeau            $values[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] = $this->markupPath->getPathObject()->getWikiId();
65804fd306cSNickeau            $values[PagePath::PROPERTY_NAME] = $this->markupPath->getPathObject()->toAbsolutePath()->toAbsoluteId();
659c3437056SNickeau            /**
660c3437056SNickeau             * Default implements the auto-canonical feature
661c3437056SNickeau             */
66204fd306cSNickeau            try {
663*2d486314SNico                $values[Canonical::PROPERTY_NAME] = Canonical::createForPage($this->markupPath)->getValueOrDefault()->toAbsoluteId();
66404fd306cSNickeau            } catch (ExceptionNotFound $e) {
66504fd306cSNickeau                $values[Canonical::PROPERTY_NAME] = null;
66604fd306cSNickeau            }
667c3437056SNickeau
668c3437056SNickeau            /**
669c3437056SNickeau             * Analytics
670c3437056SNickeau             */
671be61a7dfSgerardnico            $analyticsAttributeValue = $values[self::ANALYTICS_ATTRIBUTE] ?? null;
672be61a7dfSgerardnico            if (!isset($analyticsAttributeValue)) {
673c3437056SNickeau                // otherwise we get an empty string
674c3437056SNickeau                // and a json function will not work
675c3437056SNickeau                $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString();
676c3437056SNickeau            }
677c3437056SNickeau
678c3437056SNickeau            /**
679c3437056SNickeau             * Page Id / Abbr are mandatory for url redirection
680c3437056SNickeau             */
681c3437056SNickeau            $this->addPageIdAttributeIfNeeded($values);
682c3437056SNickeau
683c3437056SNickeau            $request = $this->sqlite
684c3437056SNickeau                ->createRequest()
685c3437056SNickeau                ->setTableRow('PAGES', $values);
686c3437056SNickeau            try {
687c3437056SNickeau                /**
688c3437056SNickeau                 * rowid is used in {@link DatabasePageRow::exists()}
689c3437056SNickeau                 * to check if the page exists in the database
690c3437056SNickeau                 * We update it
691c3437056SNickeau                 */
692c3437056SNickeau                $this->row[self::ROWID] = $request
693c3437056SNickeau                    ->execute()
694c3437056SNickeau                    ->getInsertId();
695c3437056SNickeau                $this->row = array_merge($values, $this->row);
69604fd306cSNickeau            } catch (ExceptionCompile $e) {
69704fd306cSNickeau                throw new ExceptionBadState("There was a problem during the updateAttributes insert. : {$e->getMessage()}");
698c3437056SNickeau            } finally {
699c3437056SNickeau                $request->close();
700c3437056SNickeau            }
701c3437056SNickeau
702c3437056SNickeau        }
703c3437056SNickeau
704c3437056SNickeau    }
705c3437056SNickeau
706c3437056SNickeau    public
707c3437056SNickeau    function getDescription()
708c3437056SNickeau    {
709c3437056SNickeau        return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY);
710c3437056SNickeau    }
711c3437056SNickeau
712c3437056SNickeau
713c3437056SNickeau    public
714c3437056SNickeau    function getPageName()
715c3437056SNickeau    {
716c3437056SNickeau        return $this->getFromRow(ResourceName::PROPERTY_NAME);
717c3437056SNickeau    }
718c3437056SNickeau
719c3437056SNickeau    public
720c3437056SNickeau    function exists(): bool
721c3437056SNickeau    {
722c3437056SNickeau        return $this->getFromRow(self::ROWID) !== null;
723c3437056SNickeau    }
724c3437056SNickeau
725c3437056SNickeau    /**
726c3437056SNickeau     * Called when a page is moved
727c3437056SNickeau     * @param $targetId
728c3437056SNickeau     */
729c3437056SNickeau    public
730c3437056SNickeau    function updatePathAndDokuwikiId($targetId)
731c3437056SNickeau    {
732c3437056SNickeau        if (!$this->exists()) {
73304fd306cSNickeau            LogUtility::error("The `database` page ($this) does not exist and cannot be moved to ($targetId)");
734c3437056SNickeau        }
735c3437056SNickeau
736c3437056SNickeau        $path = $targetId;
73704fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
738c3437056SNickeau        $attributes = [
739c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId,
740c3437056SNickeau            PagePath::PROPERTY_NAME => $path
741c3437056SNickeau        ];
742c3437056SNickeau
743c3437056SNickeau        $this->upsertAttributes($attributes);
744c3437056SNickeau
745c3437056SNickeau    }
746c3437056SNickeau
747c3437056SNickeau    public
748c3437056SNickeau    function __toString()
749c3437056SNickeau    {
75004fd306cSNickeau        return $this->markupPath->__toString();
751c3437056SNickeau    }
752c3437056SNickeau
753c3437056SNickeau
754c3437056SNickeau    /**
755c3437056SNickeau     * Redirect are now added during a move
756c3437056SNickeau     * Not when a duplicate is found.
757c3437056SNickeau     * With the advent of the page id, it should never occurs anymore
75804fd306cSNickeau     * @param MarkupPath $page
759c3437056SNickeau     * @deprecated 2012-10-28
760c3437056SNickeau     */
761c3437056SNickeau    private
76204fd306cSNickeau    function deleteIfExistsAndAddRedirectAlias(MarkupPath $page): void
763c3437056SNickeau    {
764c3437056SNickeau
76504fd306cSNickeau        if ($this->markupPath != null) {
766c3437056SNickeau            $page->getDatabasePage()->deleteIfExist();
767c3437056SNickeau            $this->addRedirectAliasWhileBuildingRow($page);
768c3437056SNickeau        }
769c3437056SNickeau
770c3437056SNickeau    }
771c3437056SNickeau
772c3437056SNickeau    public
773c3437056SNickeau    function getCanonical()
774c3437056SNickeau    {
775c3437056SNickeau        return $this->getFromRow(Canonical::PROPERTY_NAME);
776c3437056SNickeau    }
777c3437056SNickeau
778c3437056SNickeau    /**
779c3437056SNickeau     * Set the field to their values
780c3437056SNickeau     * @param $row
781c3437056SNickeau     */
782c3437056SNickeau    public
783c3437056SNickeau    function setRow($row)
784c3437056SNickeau    {
785c3437056SNickeau        if ($row === null) {
786c3437056SNickeau            LogUtility::msg("A row should not be null");
787c3437056SNickeau            return;
788c3437056SNickeau        }
789c3437056SNickeau        if (!is_array($row)) {
790c3437056SNickeau            LogUtility::msg("A row should be an array");
791c3437056SNickeau            return;
792c3437056SNickeau        }
793c3437056SNickeau
794c3437056SNickeau        /**
795c3437056SNickeau         * All get function lookup the row
796c3437056SNickeau         */
797c3437056SNickeau        $this->row = $row;
798c3437056SNickeau
799c3437056SNickeau
800c3437056SNickeau    }
801c3437056SNickeau
802c3437056SNickeau    private
803c3437056SNickeau    function buildInitObjectFields()
804c3437056SNickeau    {
805c3437056SNickeau        $this->row = null;
806c3437056SNickeau
807c3437056SNickeau    }
808c3437056SNickeau
809c3437056SNickeau    public
810c3437056SNickeau    function rebuild(): DatabasePageRow
811c3437056SNickeau    {
812c3437056SNickeau
81304fd306cSNickeau        if ($this->markupPath != null) {
81404fd306cSNickeau            $this->markupPath->rebuild();
81504fd306cSNickeau            try {
81604fd306cSNickeau                $row = $this->getDatabaseRowFromPage($this->markupPath);
817c3437056SNickeau                $this->setRow($row);
81804fd306cSNickeau            } catch (ExceptionNotExists $e) {
81904fd306cSNickeau                // ok
820c3437056SNickeau            }
821c3437056SNickeau        }
822c3437056SNickeau        return $this;
823c3437056SNickeau
824c3437056SNickeau    }
825c3437056SNickeau
826c3437056SNickeau    /**
827c3437056SNickeau     * @return array - an array of the fix page metadata (ie not derived)
828c3437056SNickeau     * Therefore quick to insert/update
829c3437056SNickeau     *
830c3437056SNickeau     */
831c3437056SNickeau    private
832c3437056SNickeau    function getMetaRecord(): array
833c3437056SNickeau    {
83404fd306cSNickeau        $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
83504fd306cSNickeau        $targetStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
836c3437056SNickeau
837c3437056SNickeau        $record = array(
838c3437056SNickeau            Canonical::PROPERTY_NAME,
839c3437056SNickeau            PagePath::PROPERTY_NAME,
840c3437056SNickeau            ResourceName::PROPERTY_NAME,
841c3437056SNickeau            PageTitle::TITLE,
842c3437056SNickeau            PageH1::PROPERTY_NAME,
843c3437056SNickeau            PageDescription::PROPERTY_NAME,
84404fd306cSNickeau            CreationDate::PROPERTY_NAME,
845c3437056SNickeau            ModificationDate::PROPERTY_NAME,
846c3437056SNickeau            PagePublicationDate::PROPERTY_NAME,
847c3437056SNickeau            StartDate::PROPERTY_NAME,
848c3437056SNickeau            EndDate::PROPERTY_NAME,
849c3437056SNickeau            Region::PROPERTY_NAME,
850c3437056SNickeau            Lang::PROPERTY_NAME,
851c3437056SNickeau            PageType::PROPERTY_NAME,
852c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
85304fd306cSNickeau            PageLevel::PROPERTY_NAME
854c3437056SNickeau        );
855c3437056SNickeau        $metaRecord = [];
856c3437056SNickeau        foreach ($record as $name) {
85704fd306cSNickeau            try {
85804fd306cSNickeau                $metadata = Meta\Api\MetadataSystem::getForName($name);
85904fd306cSNickeau            } catch (ExceptionNotFound $e) {
86004fd306cSNickeau                LogUtility::internalError("The metadata ($name) is unknown", self::CANONICAL);
86104fd306cSNickeau                continue;
862c3437056SNickeau            }
863c3437056SNickeau            $metaRecord[$name] = $metadata
86404fd306cSNickeau                ->setResource($this->markupPath)
865c3437056SNickeau                ->setReadStore($sourceStore)
866c3437056SNickeau                ->buildFromReadStore()
867c3437056SNickeau                ->setWriteStore($targetStore)
868c3437056SNickeau                ->toStoreValueOrDefault(); // used by the template, the value is or default
86904fd306cSNickeau
870c3437056SNickeau        }
871c3437056SNickeau
87204fd306cSNickeau        try {
873c3437056SNickeau            $this->addPageIdMeta($metaRecord);
87404fd306cSNickeau        } catch (ExceptionNotExists $e) {
87504fd306cSNickeau            // no page id for non-existent page ok
876c3437056SNickeau        }
87704fd306cSNickeau
87804fd306cSNickeau        // Is index
87904fd306cSNickeau        $metaRecord[self::IS_INDEX_COLUMN] = ($this->markupPath->isIndexPage() === true ? 1 : 0);
88004fd306cSNickeau
881c3437056SNickeau        return $metaRecord;
882c3437056SNickeau    }
883c3437056SNickeau
884c3437056SNickeau    public
885c3437056SNickeau    function deleteIfExist(): DatabasePageRow
886c3437056SNickeau    {
887c3437056SNickeau        if ($this->exists()) {
888c3437056SNickeau            $this->delete();
889c3437056SNickeau        }
890c3437056SNickeau        return $this;
891c3437056SNickeau    }
892c3437056SNickeau
893c3437056SNickeau    public
894c3437056SNickeau    function getRowId()
895c3437056SNickeau    {
896c3437056SNickeau        return $this->getFromRow(self::ROWID);
897c3437056SNickeau    }
898c3437056SNickeau
89904fd306cSNickeau    /**
90004fd306cSNickeau     * @throws ExceptionNotFound
901031d4b49Sgerardnico     * @throws ExceptionSqliteNotAvailable
90204fd306cSNickeau     */
903c3437056SNickeau    private
90404fd306cSNickeau    function getDatabaseRowFromPageId(string $pageIdValue)
905c3437056SNickeau    {
906c3437056SNickeau
907c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
908c3437056SNickeau        $query = $this->getParametrizedLookupQuery($pageIdAttribute);
909c3437056SNickeau        $request = Sqlite::createOrGetSqlite()
910c3437056SNickeau            ->createRequest()
91104fd306cSNickeau            ->setQueryParametrized($query, [$pageIdValue]);
912c3437056SNickeau        $rows = [];
913c3437056SNickeau        try {
914c3437056SNickeau            $rows = $request
915c3437056SNickeau                ->execute()
916c3437056SNickeau                ->getRows();
91704fd306cSNickeau        } catch (ExceptionCompile $e) {
91804fd306cSNickeau            throw new ExceptionRuntimeInternal("Error while retrieving the object by id", self::CANONICAL, 1, $e);
919c3437056SNickeau        } finally {
920c3437056SNickeau            $request->close();
921c3437056SNickeau        }
922c3437056SNickeau
923c3437056SNickeau        switch (sizeof($rows)) {
924c3437056SNickeau            case 0:
92504fd306cSNickeau                throw new ExceptionNotFound("No object by page id");
926c3437056SNickeau            case 1:
927c3437056SNickeau                /**
928c3437056SNickeau                 * Page Id Collision detection
929c3437056SNickeau                 */
93004fd306cSNickeau                $rowId = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
93104fd306cSNickeau                $this->checkCollision($rowId, $pageIdAttribute, $pageIdValue);
932c3437056SNickeau                return $rows[0];
933c3437056SNickeau            default:
934c3437056SNickeau                $existingPages = implode(", ", $rows);
93504fd306cSNickeau                $message = "The pages ($existingPages) have all the same page id ($pageIdValue)";
93604fd306cSNickeau                throw new ExceptionRuntimeInternal($message, self::CANONICAL);
937c3437056SNickeau        }
938c3437056SNickeau
939c3437056SNickeau    }
940c3437056SNickeau
941c3437056SNickeau
942c3437056SNickeau    private
94304fd306cSNickeau    function getParametrizedLookupQuery(string $attributeName): string
944c3437056SNickeau    {
945c3437056SNickeau        $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES);
94604fd306cSNickeau        return "$select where $attributeName = ?";
947c3437056SNickeau    }
948c3437056SNickeau
949c3437056SNickeau
95004fd306cSNickeau    public
95104fd306cSNickeau    function setMarkupPath(MarkupPath $page)
952c3437056SNickeau    {
95304fd306cSNickeau        $this->markupPath = $page;
954c3437056SNickeau        return $this;
955c3437056SNickeau    }
956c3437056SNickeau
95704fd306cSNickeau    /**
95804fd306cSNickeau     * @throws ExceptionNotFound
95904fd306cSNickeau     */
960c3437056SNickeau    private
96104fd306cSNickeau    function getDatabaseRowFromCanonical($canonicalValue)
962c3437056SNickeau    {
96304fd306cSNickeau        $canoncialName = Canonical::PROPERTY_NAME;
96404fd306cSNickeau        $query = $this->getParametrizedLookupQuery($canoncialName);
965c3437056SNickeau        $request = $this->sqlite
966c3437056SNickeau            ->createRequest()
96704fd306cSNickeau            ->setQueryParametrized($query, [$canonicalValue]);
968c3437056SNickeau        $rows = [];
969c3437056SNickeau        try {
970c3437056SNickeau            $rows = $request
971c3437056SNickeau                ->execute()
972c3437056SNickeau                ->getRows();
97304fd306cSNickeau        } catch (ExceptionCompile $e) {
97404fd306cSNickeau            throw new ExceptionRuntime("An exception has occurred with the page search from CANONICAL. " . $e->getMessage());
975c3437056SNickeau        } finally {
976c3437056SNickeau            $request->close();
977c3437056SNickeau        }
978c3437056SNickeau
979c3437056SNickeau        switch (sizeof($rows)) {
980c3437056SNickeau            case 0:
98104fd306cSNickeau                throw new ExceptionNotFound("No canonical row was found");
982c3437056SNickeau            case 1:
983c3437056SNickeau                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
98404fd306cSNickeau                $this->checkCollision($id, $canoncialName, $canonicalValue);
985c3437056SNickeau                return $rows[0];
986c3437056SNickeau            default:
987c3437056SNickeau                $existingPages = [];
988c3437056SNickeau                foreach ($rows as $row) {
989c3437056SNickeau                    $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
99004fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($id);
991c3437056SNickeau                    if (!$duplicatePage->exists()) {
992c3437056SNickeau
993c3437056SNickeau                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
994c3437056SNickeau
995c3437056SNickeau                    } else {
996c3437056SNickeau
997c3437056SNickeau                        /**
998c3437056SNickeau                         * Check if the error may come from the auto-canonical
999c3437056SNickeau                         * (Never ever save generated data)
1000c3437056SNickeau                         */
100104fd306cSNickeau                        $canonicalLastNamesCount = SiteConfig::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0);
1002c3437056SNickeau                        if ($canonicalLastNamesCount > 0) {
100304fd306cSNickeau                            $this->markupPath->unsetMetadata($canoncialName);
100404fd306cSNickeau                            $duplicatePage->unsetMetadata($canoncialName);
1005c3437056SNickeau                        }
1006c3437056SNickeau
1007c3437056SNickeau                        $existingPages[] = $row;
1008c3437056SNickeau                    }
1009c3437056SNickeau                }
101004fd306cSNickeau                if (sizeof($existingPages) > 1) {
1011c3437056SNickeau                    $existingPages = implode(", ", $existingPages);
101204fd306cSNickeau                    $message = "The existing pages ($existingPages) have all the same canonical ($canonicalValue), return the first one";
101304fd306cSNickeau                    LogUtility::error($message, self::CANONICAL);
1014c3437056SNickeau                }
101504fd306cSNickeau                return $existingPages[0];
1016c3437056SNickeau        }
1017c3437056SNickeau    }
1018c3437056SNickeau
101904fd306cSNickeau    /**
102004fd306cSNickeau     * @throws ExceptionNotFound
102104fd306cSNickeau     */
1022c3437056SNickeau    private
1023c3437056SNickeau    function getDatabaseRowFromPath(string $path): ?array
1024c3437056SNickeau    {
102504fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
1026c3437056SNickeau        return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path);
1027c3437056SNickeau    }
1028c3437056SNickeau
102904fd306cSNickeau    /**
103004fd306cSNickeau     * @throws ExceptionNotFound
103104fd306cSNickeau     */
1032c3437056SNickeau    private
103304fd306cSNickeau    function getDatabaseRowFromDokuWikiId(string $id): array
1034c3437056SNickeau    {
1035c3437056SNickeau        return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
1036c3437056SNickeau    }
1037c3437056SNickeau
103804fd306cSNickeau    /**
103904fd306cSNickeau     * @throws ExceptionNotFound
104004fd306cSNickeau     */
1041c3437056SNickeau    public
1042031d4b49Sgerardnico    function getDatabaseRowFromAttribute(string $attribute, string $attributeValue)
1043c3437056SNickeau    {
1044c3437056SNickeau        $query = $this->getParametrizedLookupQuery($attribute);
1045c3437056SNickeau        $request = $this->sqlite
1046c3437056SNickeau            ->createRequest()
1047031d4b49Sgerardnico            ->setQueryParametrized($query, [$attributeValue]);
1048c3437056SNickeau        $rows = [];
1049c3437056SNickeau        try {
1050c3437056SNickeau            $rows = $request
1051c3437056SNickeau                ->execute()
1052c3437056SNickeau                ->getRows();
105304fd306cSNickeau        } catch (ExceptionCompile $e) {
105404fd306cSNickeau            $message = "Internal Error: An exception has occurred with the page search from a PATH: " . $e->getMessage();
105504fd306cSNickeau            LogUtility::log2file($message);
105604fd306cSNickeau            throw new ExceptionNotFound($message);
1057c3437056SNickeau        } finally {
1058c3437056SNickeau            $request->close();
1059c3437056SNickeau        }
1060c3437056SNickeau
1061031d4b49Sgerardnico        $rowCount = sizeof($rows);
1062031d4b49Sgerardnico        switch ($rowCount) {
1063c3437056SNickeau            case 0:
106404fd306cSNickeau                throw new ExceptionNotFound("No database row found for the page");
1065c3437056SNickeau            case 1:
1066031d4b49Sgerardnico                $attributeValue = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1067031d4b49Sgerardnico                try {
106811d09b86Sgerardnico                    if ($this->markupPath != null && $attributeValue !== $this->markupPath->getWikiId()) {
1069031d4b49Sgerardnico                        $duplicatePage = MarkupPath::createMarkupFromId($attributeValue);
1070c3437056SNickeau                        if (!$duplicatePage->exists()) {
1071c3437056SNickeau                            $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1072c3437056SNickeau                        } else {
1073031d4b49Sgerardnico                            LogUtility::error("The page ($this->markupPath) and the page ($attributeValue) have the same $attribute ($attributeValue)");
1074c3437056SNickeau                        }
1075c3437056SNickeau                    }
107611d09b86Sgerardnico                } catch (ExceptionBadArgument $e) {
107711d09b86Sgerardnico                    throw new ExceptionRuntimeInternal("The wiki id should be available");
107811d09b86Sgerardnico                }
1079c3437056SNickeau                return $rows[0];
1080c3437056SNickeau            default:
1081031d4b49Sgerardnico                LogUtility::warning("Error: More than 1 rows ($rowCount) found for attribute ($attribute) with the value ($attributeValue)");
1082031d4b49Sgerardnico
1083031d4b49Sgerardnico                /**
1084031d4b49Sgerardnico                 * Trying to delete the bad one
1085031d4b49Sgerardnico                 */
1086c3437056SNickeau                $existingPages = [];
1087c3437056SNickeau                foreach ($rows as $row) {
1088c3437056SNickeau
1089031d4b49Sgerardnico                    $rowId = $row[self::ROWID] ?? null;
1090031d4b49Sgerardnico                    if ($rowId === null) {
1091031d4b49Sgerardnico                        LogUtility::error("A row id should be present");
1092c3437056SNickeau                        $existingPages[] = $row;
1093031d4b49Sgerardnico                        continue;
1094c3437056SNickeau                    }
1095031d4b49Sgerardnico                    // page id throw for several id
1096031d4b49Sgerardnico                    $duplicateRow = DatabasePageRow::createFromRowId($rowId);
1097031d4b49Sgerardnico                    $duplicateMarkupPath = $duplicateRow->getMarkupPath();
1098031d4b49Sgerardnico                    if (!FileSystems::exists($duplicateMarkupPath)) {
1099031d4b49Sgerardnico                        LogUtility::warning("The duplicate page ($duplicateMarkupPath) does not exists. Delete Row ({$rowId})");
1100031d4b49Sgerardnico                        $duplicateRow->delete();
1101031d4b49Sgerardnico                        $this->addRedirectAliasWhileBuildingRow($duplicateMarkupPath);
1102031d4b49Sgerardnico                        continue;
1103031d4b49Sgerardnico                    }
1104031d4b49Sgerardnico                    $existingPages[] = $row;
1105c3437056SNickeau                }
1106c3437056SNickeau                if (sizeof($existingPages) === 1) {
1107c3437056SNickeau                    return $existingPages[0];
1108c3437056SNickeau                } else {
110904fd306cSNickeau                    $existingPageIds = array_map(
111004fd306cSNickeau                        function ($row) {
111104fd306cSNickeau                            return $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
111204fd306cSNickeau                        },
111304fd306cSNickeau                        $existingPages);
111404fd306cSNickeau                    $existingPages = implode(", ", $existingPageIds);
1115031d4b49Sgerardnico                    throw new ExceptionNotFound("The existing pages ($existingPages) have all the same attribute $attribute with the value ($attributeValue)", LogUtility::LVL_MSG_ERROR);
1116c3437056SNickeau                }
1117c3437056SNickeau        }
1118c3437056SNickeau    }
1119c3437056SNickeau
1120c3437056SNickeau    public
112104fd306cSNickeau    function getMarkupPath(): ?MarkupPath
1122c3437056SNickeau    {
112370bbd7f1Sgerardnico        if ($this->row === null) {
112470bbd7f1Sgerardnico            return null;
112570bbd7f1Sgerardnico        }
1126c3437056SNickeau        if (
112704fd306cSNickeau            $this->markupPath === null
1128c3437056SNickeau            && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null
1129c3437056SNickeau        ) {
113004fd306cSNickeau            $this->markupPath = MarkupPath::createMarkupFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]);
1131c3437056SNickeau        }
113204fd306cSNickeau        return $this->markupPath;
1133c3437056SNickeau    }
1134c3437056SNickeau
1135c3437056SNickeau    private
1136c3437056SNickeau    function getDatabaseRowFromAlias($alias): ?array
1137c3437056SNickeau    {
1138c3437056SNickeau
1139c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
1140c3437056SNickeau        $buildFields = self::PAGE_BUILD_ATTRIBUTES;
1141c3437056SNickeau        $fields = array_reduce($buildFields, function ($carry, $element) {
1142c3437056SNickeau            if ($carry !== null) {
1143c3437056SNickeau                return "$carry, p.{$element}";
1144c3437056SNickeau            } else {
1145c3437056SNickeau                return "p.{$element}";
1146c3437056SNickeau            }
1147c3437056SNickeau        }, null);
114804fd306cSNickeau        /** @noinspection SqlResolve */
1149c3437056SNickeau        $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? ";
1150c3437056SNickeau        $request = $this->sqlite
1151c3437056SNickeau            ->createRequest()
1152c3437056SNickeau            ->setQueryParametrized($query, [$alias]);
1153c3437056SNickeau        $rows = [];
1154c3437056SNickeau        try {
1155c3437056SNickeau            $rows = $request
1156c3437056SNickeau                ->execute()
1157c3437056SNickeau                ->getRows();
115804fd306cSNickeau        } catch (ExceptionCompile $e) {
1159c3437056SNickeau            LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}");
1160c3437056SNickeau            return null;
1161c3437056SNickeau        } finally {
1162c3437056SNickeau            $request->close();
1163c3437056SNickeau        }
1164c3437056SNickeau        switch (sizeof($rows)) {
1165c3437056SNickeau            case 0:
1166c3437056SNickeau                return null;
1167c3437056SNickeau            case 1:
1168c3437056SNickeau                return $rows[0];
1169c3437056SNickeau            default:
1170c3437056SNickeau                $id = $rows[0]['ID'];
1171c3437056SNickeau                $pages = implode(",",
1172c3437056SNickeau                    array_map(
1173c3437056SNickeau                        function ($row) {
1174c3437056SNickeau                            return $row['ID'];
1175c3437056SNickeau                        },
1176c3437056SNickeau                        $rows
1177c3437056SNickeau                    )
1178c3437056SNickeau                );
1179c3437056SNickeau                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);
1180c3437056SNickeau                return $rows[0];
1181c3437056SNickeau        }
1182c3437056SNickeau    }
1183c3437056SNickeau
1184c3437056SNickeau
1185c3437056SNickeau    /**
1186c3437056SNickeau     * Utility function
118704fd306cSNickeau     * @param MarkupPath $pageAlias
1188c3437056SNickeau     */
1189c3437056SNickeau    private
119004fd306cSNickeau    function addRedirectAliasWhileBuildingRow(MarkupPath $pageAlias)
1191c3437056SNickeau    {
1192c3437056SNickeau
119304fd306cSNickeau        $aliasPath = $pageAlias->getPathObject()->toAbsoluteId();
1194d899a2a6Sgerardnico        LogUtility::info("Add alias ($aliasPath) for page ({$this->markupPath})");
1195c3437056SNickeau        try {
119604fd306cSNickeau            Aliases::createForPage($this->markupPath)
1197c3437056SNickeau                ->addAlias($aliasPath)
1198c3437056SNickeau                ->sendToWriteStore();
119904fd306cSNickeau        } catch (ExceptionCompile $e) {
1200c3437056SNickeau            // we don't throw while getting
120104fd306cSNickeau            LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->markupPath)");
1202c3437056SNickeau        }
1203c3437056SNickeau
1204c3437056SNickeau    }
1205c3437056SNickeau
1206c3437056SNickeau    private
1207c3437056SNickeau    function addPageIdAttributeIfNeeded(array &$values)
1208c3437056SNickeau    {
1209c3437056SNickeau        if (!isset($values[PageId::getPersistentName()])) {
121004fd306cSNickeau            $values[PageId::getPersistentName()] = $this->markupPath->getPageId();
1211c3437056SNickeau        }
1212c3437056SNickeau        if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) {
121304fd306cSNickeau            $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->markupPath->getPageIdAbbr();
1214c3437056SNickeau        }
1215c3437056SNickeau    }
1216c3437056SNickeau
1217c3437056SNickeau    public
1218c3437056SNickeau    function getFromRow(string $attribute)
1219c3437056SNickeau    {
1220c3437056SNickeau        if ($this->row === null) {
1221c3437056SNickeau            return null;
1222c3437056SNickeau        }
1223c3437056SNickeau
1224c3437056SNickeau        if (!array_key_exists($attribute, $this->row)) {
1225c3437056SNickeau            /**
1226c3437056SNickeau             * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES}
1227c3437056SNickeau             * or in the table
1228c3437056SNickeau             */
122904fd306cSNickeau            throw new ExceptionRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical());
1230c3437056SNickeau        }
1231c3437056SNickeau
1232c3437056SNickeau        $value = $this->row[$attribute];
1233c3437056SNickeau
1234c3437056SNickeau        if ($value !== null) {
1235c3437056SNickeau            return $value;
1236c3437056SNickeau        }
1237c3437056SNickeau
1238c3437056SNickeau        // don't know why but the sqlite plugin returns them uppercase
1239c3437056SNickeau        // rowid is returned lowercase from the sqlite plugin
1240c3437056SNickeau        $upperAttribute = strtoupper($attribute);
124170bbd7f1Sgerardnico        return $this->row[$upperAttribute] ?? null;
1242c3437056SNickeau
1243c3437056SNickeau    }
1244c3437056SNickeau
1245c3437056SNickeau
12464cadd4f8SNickeau    /**
124704fd306cSNickeau     * @throws ExceptionCompile
12484cadd4f8SNickeau     */
124904fd306cSNickeau    public
125004fd306cSNickeau    function replicateAnalytics()
1251c3437056SNickeau    {
1252c3437056SNickeau
1253c3437056SNickeau        try {
125404fd306cSNickeau            $fetchPath = $this->markupPath->fetchAnalyticsPath();
125504fd306cSNickeau            $analyticsJson = Json::createFromPath($fetchPath);
125604fd306cSNickeau        } catch (ExceptionCompile $e) {
125704fd306cSNickeau            if (PluginUtility::isDevOrTest()) {
125804fd306cSNickeau                throw $e;
125904fd306cSNickeau            }
126004fd306cSNickeau            throw new ExceptionCompile("Unable to get the analytics document", self::CANONICAL, 0, $e);
1261c3437056SNickeau        }
1262c3437056SNickeau
1263c3437056SNickeau        /**
1264c3437056SNickeau         * Replication Date
1265c3437056SNickeau         */
126604fd306cSNickeau        $replicationDateMeta = ReplicationDate::createFromPage($this->markupPath)
1267c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
12684cadd4f8SNickeau            ->setValue(new DateTime());
1269c3437056SNickeau
1270c3437056SNickeau        /**
1271c3437056SNickeau         * Analytics
1272c3437056SNickeau         */
1273c3437056SNickeau        $analyticsJsonAsString = $analyticsJson->toPrettyJsonString();
1274c3437056SNickeau        $analyticsJsonAsArray = $analyticsJson->toArray();
1275c3437056SNickeau
1276c3437056SNickeau        /**
1277c3437056SNickeau         * Record
1278c3437056SNickeau         */
1279c3437056SNickeau        $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString;
128004fd306cSNickeau        $record['IS_LOW_QUALITY'] = ($this->markupPath->isLowQualityPage() === true ? 1 : 0);
128104fd306cSNickeau        $record['WORD_COUNT'] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][renderer_plugin_combo_analytics::WORD_COUNT];
128204fd306cSNickeau        $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][BacklinkCount::getPersistentName()];
1283c3437056SNickeau        $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue();
1284c3437056SNickeau        $this->upsertAttributes($record);
1285c3437056SNickeau    }
1286c3437056SNickeau
128704fd306cSNickeau    private
128804fd306cSNickeau    function checkCollision($wikiIdInDatabase, $attribute, $value)
128904fd306cSNickeau    {
129004fd306cSNickeau        if ($this->markupPath === null) {
129104fd306cSNickeau            return;
129204fd306cSNickeau        }
129304fd306cSNickeau        try {
129404fd306cSNickeau            $markupWikiId = $this->markupPath->toWikiPath()->getWikiId();
129504fd306cSNickeau        } catch (ExceptionCast $e) {
129604fd306cSNickeau            return;
129704fd306cSNickeau        }
129804fd306cSNickeau        if ($wikiIdInDatabase !== $markupWikiId) {
129904fd306cSNickeau            $duplicatePage = MarkupPath::createMarkupFromId($wikiIdInDatabase);
130004fd306cSNickeau            if (!FileSystems::exists($duplicatePage)) {
130104fd306cSNickeau                // Move
130204fd306cSNickeau                LogUtility::info("The non-existing duplicate page ($wikiIdInDatabase) has been added as redirect alias for the page ($this->markupPath)", self::CANONICAL);
130304fd306cSNickeau                $this->addRedirectAliasWhileBuildingRow($duplicatePage);
130404fd306cSNickeau            } else {
130504fd306cSNickeau                // This can happens if two page were created not on the same website
130604fd306cSNickeau                // of if the sqlite database was deleted and rebuilt.
130704fd306cSNickeau                // The chance is really, really low
130804fd306cSNickeau                $errorMessage = "The page ($this->markupPath) and the page ($wikiIdInDatabase) have the same $attribute value ($value)";
130904fd306cSNickeau                throw new ExceptionRuntimeInternal($errorMessage, self::CANONICAL);
131004fd306cSNickeau                // What to do ?
131104fd306cSNickeau                // The database does not allow two page id with the same value
131204fd306cSNickeau                // If it happens, ugh, ugh, ..., a replication process between website may be.
131304fd306cSNickeau            }
131404fd306cSNickeau        }
131504fd306cSNickeau    }
131604fd306cSNickeau
1317031d4b49Sgerardnico    /**
1318031d4b49Sgerardnico     * @throws ExceptionSqliteNotAvailable
1319031d4b49Sgerardnico     * @throws ExceptionNotFound
1320031d4b49Sgerardnico     */
1321031d4b49Sgerardnico    private function getDatabaseRowFromRowId(string $rowId)
1322031d4b49Sgerardnico    {
1323031d4b49Sgerardnico
1324031d4b49Sgerardnico        $query = $this->getParametrizedLookupQuery(self::ROWID);
1325031d4b49Sgerardnico        $request = Sqlite::createOrGetSqlite()
1326031d4b49Sgerardnico            ->createRequest()
1327031d4b49Sgerardnico            ->setQueryParametrized($query, [$rowId]);
1328031d4b49Sgerardnico        $rows = [];
1329031d4b49Sgerardnico        try {
1330031d4b49Sgerardnico            $rows = $request
1331031d4b49Sgerardnico                ->execute()
1332031d4b49Sgerardnico                ->getRows();
1333031d4b49Sgerardnico        } catch (ExceptionCompile $e) {
1334031d4b49Sgerardnico            throw new ExceptionRuntimeInternal("Error while retrieving the object by rowid ($rowId)", self::CANONICAL, 1, $e);
1335031d4b49Sgerardnico        } finally {
1336031d4b49Sgerardnico            $request->close();
1337031d4b49Sgerardnico        }
1338031d4b49Sgerardnico
1339031d4b49Sgerardnico        $rowCount = sizeof($rows);
1340031d4b49Sgerardnico        switch ($rowCount) {
1341031d4b49Sgerardnico            case 0:
1342031d4b49Sgerardnico                throw new ExceptionNotFound("No object by row id ($rowId)");
1343031d4b49Sgerardnico            case 1:
1344031d4b49Sgerardnico                return $rows[0];
1345031d4b49Sgerardnico            default:
1346031d4b49Sgerardnico                throw new ExceptionRuntimeInternal("Too much record ($rowCount) for the rowid ($rowId)", self::CANONICAL);
1347031d4b49Sgerardnico        }
1348031d4b49Sgerardnico
1349031d4b49Sgerardnico    }
1350031d4b49Sgerardnico
1351c3437056SNickeau
1352c3437056SNickeau}
1353