xref: /plugin/combo/ComboStrap/DatabasePageRow.php (revision c0705265c76e59176d9f02515cf7dcf68d9c4862)
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
657*c0705265SNico            try {
658*c0705265SNico                $wikiPath = $this->markupPath->toWikiPath();
659*c0705265SNico            } catch (ExceptionCast $e) {
660*c0705265SNico                // should not happen but yeah
661*c0705265SNico                throw new ExceptionBadState("The markup path {$this->markupPath} could not be transformed as wiki path");
662*c0705265SNico            }
663*c0705265SNico            $values[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] = $wikiPath->getWikiId();
664*c0705265SNico            $values[PagePath::PROPERTY_NAME] = $wikiPath->toAbsoluteId();
665c3437056SNickeau            /**
666c3437056SNickeau             * Default implements the auto-canonical feature
667c3437056SNickeau             */
66804fd306cSNickeau            try {
6692d486314SNico                $values[Canonical::PROPERTY_NAME] = Canonical::createForPage($this->markupPath)->getValueOrDefault()->toAbsoluteId();
67004fd306cSNickeau            } catch (ExceptionNotFound $e) {
67104fd306cSNickeau                $values[Canonical::PROPERTY_NAME] = null;
67204fd306cSNickeau            }
673c3437056SNickeau
674c3437056SNickeau            /**
675c3437056SNickeau             * Analytics
676c3437056SNickeau             */
677be61a7dfSgerardnico            $analyticsAttributeValue = $values[self::ANALYTICS_ATTRIBUTE] ?? null;
678be61a7dfSgerardnico            if (!isset($analyticsAttributeValue)) {
679c3437056SNickeau                // otherwise we get an empty string
680c3437056SNickeau                // and a json function will not work
681c3437056SNickeau                $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString();
682c3437056SNickeau            }
683c3437056SNickeau
684c3437056SNickeau            /**
685c3437056SNickeau             * Page Id / Abbr are mandatory for url redirection
686c3437056SNickeau             */
687c3437056SNickeau            $this->addPageIdAttributeIfNeeded($values);
688c3437056SNickeau
689c3437056SNickeau            $request = $this->sqlite
690c3437056SNickeau                ->createRequest()
691c3437056SNickeau                ->setTableRow('PAGES', $values);
692c3437056SNickeau            try {
693c3437056SNickeau                /**
694c3437056SNickeau                 * rowid is used in {@link DatabasePageRow::exists()}
695c3437056SNickeau                 * to check if the page exists in the database
696c3437056SNickeau                 * We update it
697c3437056SNickeau                 */
698c3437056SNickeau                $this->row[self::ROWID] = $request
699c3437056SNickeau                    ->execute()
700c3437056SNickeau                    ->getInsertId();
701c3437056SNickeau                $this->row = array_merge($values, $this->row);
70204fd306cSNickeau            } catch (ExceptionCompile $e) {
70304fd306cSNickeau                throw new ExceptionBadState("There was a problem during the updateAttributes insert. : {$e->getMessage()}");
704c3437056SNickeau            } finally {
705c3437056SNickeau                $request->close();
706c3437056SNickeau            }
707c3437056SNickeau
708c3437056SNickeau        }
709c3437056SNickeau
710c3437056SNickeau    }
711c3437056SNickeau
712c3437056SNickeau    public
713c3437056SNickeau    function getDescription()
714c3437056SNickeau    {
715c3437056SNickeau        return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY);
716c3437056SNickeau    }
717c3437056SNickeau
718c3437056SNickeau
719c3437056SNickeau    public
720c3437056SNickeau    function getPageName()
721c3437056SNickeau    {
722c3437056SNickeau        return $this->getFromRow(ResourceName::PROPERTY_NAME);
723c3437056SNickeau    }
724c3437056SNickeau
725c3437056SNickeau    public
726c3437056SNickeau    function exists(): bool
727c3437056SNickeau    {
728c3437056SNickeau        return $this->getFromRow(self::ROWID) !== null;
729c3437056SNickeau    }
730c3437056SNickeau
731c3437056SNickeau    /**
732c3437056SNickeau     * Called when a page is moved
733c3437056SNickeau     * @param $targetId
734c3437056SNickeau     */
735c3437056SNickeau    public
736c3437056SNickeau    function updatePathAndDokuwikiId($targetId)
737c3437056SNickeau    {
738c3437056SNickeau        if (!$this->exists()) {
73904fd306cSNickeau            LogUtility::error("The `database` page ($this) does not exist and cannot be moved to ($targetId)");
740c3437056SNickeau        }
741c3437056SNickeau
742c3437056SNickeau        $path = $targetId;
74304fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
744c3437056SNickeau        $attributes = [
745c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId,
746c3437056SNickeau            PagePath::PROPERTY_NAME => $path
747c3437056SNickeau        ];
748c3437056SNickeau
749c3437056SNickeau        $this->upsertAttributes($attributes);
750c3437056SNickeau
751c3437056SNickeau    }
752c3437056SNickeau
753c3437056SNickeau    public
754c3437056SNickeau    function __toString()
755c3437056SNickeau    {
75604fd306cSNickeau        return $this->markupPath->__toString();
757c3437056SNickeau    }
758c3437056SNickeau
759c3437056SNickeau
760c3437056SNickeau    /**
761c3437056SNickeau     * Redirect are now added during a move
762c3437056SNickeau     * Not when a duplicate is found.
763c3437056SNickeau     * With the advent of the page id, it should never occurs anymore
76404fd306cSNickeau     * @param MarkupPath $page
765c3437056SNickeau     * @deprecated 2012-10-28
766c3437056SNickeau     */
767c3437056SNickeau    private
76804fd306cSNickeau    function deleteIfExistsAndAddRedirectAlias(MarkupPath $page): void
769c3437056SNickeau    {
770c3437056SNickeau
77104fd306cSNickeau        if ($this->markupPath != null) {
772c3437056SNickeau            $page->getDatabasePage()->deleteIfExist();
773c3437056SNickeau            $this->addRedirectAliasWhileBuildingRow($page);
774c3437056SNickeau        }
775c3437056SNickeau
776c3437056SNickeau    }
777c3437056SNickeau
778c3437056SNickeau    public
779c3437056SNickeau    function getCanonical()
780c3437056SNickeau    {
781c3437056SNickeau        return $this->getFromRow(Canonical::PROPERTY_NAME);
782c3437056SNickeau    }
783c3437056SNickeau
784c3437056SNickeau    /**
785c3437056SNickeau     * Set the field to their values
786c3437056SNickeau     * @param $row
787c3437056SNickeau     */
788c3437056SNickeau    public
789c3437056SNickeau    function setRow($row)
790c3437056SNickeau    {
791c3437056SNickeau        if ($row === null) {
792c3437056SNickeau            LogUtility::msg("A row should not be null");
793c3437056SNickeau            return;
794c3437056SNickeau        }
795c3437056SNickeau        if (!is_array($row)) {
796c3437056SNickeau            LogUtility::msg("A row should be an array");
797c3437056SNickeau            return;
798c3437056SNickeau        }
799c3437056SNickeau
800c3437056SNickeau        /**
801c3437056SNickeau         * All get function lookup the row
802c3437056SNickeau         */
803c3437056SNickeau        $this->row = $row;
804c3437056SNickeau
805c3437056SNickeau
806c3437056SNickeau    }
807c3437056SNickeau
808c3437056SNickeau    private
809c3437056SNickeau    function buildInitObjectFields()
810c3437056SNickeau    {
811c3437056SNickeau        $this->row = null;
812c3437056SNickeau
813c3437056SNickeau    }
814c3437056SNickeau
815c3437056SNickeau    public
816c3437056SNickeau    function rebuild(): DatabasePageRow
817c3437056SNickeau    {
818c3437056SNickeau
81904fd306cSNickeau        if ($this->markupPath != null) {
82004fd306cSNickeau            $this->markupPath->rebuild();
82104fd306cSNickeau            try {
82204fd306cSNickeau                $row = $this->getDatabaseRowFromPage($this->markupPath);
823c3437056SNickeau                $this->setRow($row);
82404fd306cSNickeau            } catch (ExceptionNotExists $e) {
82504fd306cSNickeau                // ok
826c3437056SNickeau            }
827c3437056SNickeau        }
828c3437056SNickeau        return $this;
829c3437056SNickeau
830c3437056SNickeau    }
831c3437056SNickeau
832c3437056SNickeau    /**
833c3437056SNickeau     * @return array - an array of the fix page metadata (ie not derived)
834c3437056SNickeau     * Therefore quick to insert/update
835c3437056SNickeau     *
836c3437056SNickeau     */
837c3437056SNickeau    private
838c3437056SNickeau    function getMetaRecord(): array
839c3437056SNickeau    {
84004fd306cSNickeau        $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
84104fd306cSNickeau        $targetStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
842c3437056SNickeau
843c3437056SNickeau        $record = array(
844c3437056SNickeau            Canonical::PROPERTY_NAME,
845c3437056SNickeau            PagePath::PROPERTY_NAME,
846c3437056SNickeau            ResourceName::PROPERTY_NAME,
847c3437056SNickeau            PageTitle::TITLE,
848c3437056SNickeau            PageH1::PROPERTY_NAME,
849c3437056SNickeau            PageDescription::PROPERTY_NAME,
85004fd306cSNickeau            CreationDate::PROPERTY_NAME,
851c3437056SNickeau            ModificationDate::PROPERTY_NAME,
852c3437056SNickeau            PagePublicationDate::PROPERTY_NAME,
853c3437056SNickeau            StartDate::PROPERTY_NAME,
854c3437056SNickeau            EndDate::PROPERTY_NAME,
855c3437056SNickeau            Region::PROPERTY_NAME,
856c3437056SNickeau            Lang::PROPERTY_NAME,
857c3437056SNickeau            PageType::PROPERTY_NAME,
858c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
85904fd306cSNickeau            PageLevel::PROPERTY_NAME
860c3437056SNickeau        );
861c3437056SNickeau        $metaRecord = [];
862c3437056SNickeau        foreach ($record as $name) {
86304fd306cSNickeau            try {
86404fd306cSNickeau                $metadata = Meta\Api\MetadataSystem::getForName($name);
86504fd306cSNickeau            } catch (ExceptionNotFound $e) {
86604fd306cSNickeau                LogUtility::internalError("The metadata ($name) is unknown", self::CANONICAL);
86704fd306cSNickeau                continue;
868c3437056SNickeau            }
869c3437056SNickeau            $metaRecord[$name] = $metadata
87004fd306cSNickeau                ->setResource($this->markupPath)
871c3437056SNickeau                ->setReadStore($sourceStore)
872c3437056SNickeau                ->buildFromReadStore()
873c3437056SNickeau                ->setWriteStore($targetStore)
874c3437056SNickeau                ->toStoreValueOrDefault(); // used by the template, the value is or default
87504fd306cSNickeau
876c3437056SNickeau        }
877c3437056SNickeau
87804fd306cSNickeau        try {
879c3437056SNickeau            $this->addPageIdMeta($metaRecord);
88004fd306cSNickeau        } catch (ExceptionNotExists $e) {
88104fd306cSNickeau            // no page id for non-existent page ok
882c3437056SNickeau        }
88304fd306cSNickeau
88404fd306cSNickeau        // Is index
88504fd306cSNickeau        $metaRecord[self::IS_INDEX_COLUMN] = ($this->markupPath->isIndexPage() === true ? 1 : 0);
88604fd306cSNickeau
887c3437056SNickeau        return $metaRecord;
888c3437056SNickeau    }
889c3437056SNickeau
890c3437056SNickeau    public
891c3437056SNickeau    function deleteIfExist(): DatabasePageRow
892c3437056SNickeau    {
893c3437056SNickeau        if ($this->exists()) {
894c3437056SNickeau            $this->delete();
895c3437056SNickeau        }
896c3437056SNickeau        return $this;
897c3437056SNickeau    }
898c3437056SNickeau
899c3437056SNickeau    public
900c3437056SNickeau    function getRowId()
901c3437056SNickeau    {
902c3437056SNickeau        return $this->getFromRow(self::ROWID);
903c3437056SNickeau    }
904c3437056SNickeau
90504fd306cSNickeau    /**
90604fd306cSNickeau     * @throws ExceptionNotFound
907031d4b49Sgerardnico     * @throws ExceptionSqliteNotAvailable
90804fd306cSNickeau     */
909c3437056SNickeau    private
91004fd306cSNickeau    function getDatabaseRowFromPageId(string $pageIdValue)
911c3437056SNickeau    {
912c3437056SNickeau
913c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
914c3437056SNickeau        $query = $this->getParametrizedLookupQuery($pageIdAttribute);
915c3437056SNickeau        $request = Sqlite::createOrGetSqlite()
916c3437056SNickeau            ->createRequest()
91704fd306cSNickeau            ->setQueryParametrized($query, [$pageIdValue]);
918c3437056SNickeau        $rows = [];
919c3437056SNickeau        try {
920c3437056SNickeau            $rows = $request
921c3437056SNickeau                ->execute()
922c3437056SNickeau                ->getRows();
92304fd306cSNickeau        } catch (ExceptionCompile $e) {
92404fd306cSNickeau            throw new ExceptionRuntimeInternal("Error while retrieving the object by id", self::CANONICAL, 1, $e);
925c3437056SNickeau        } finally {
926c3437056SNickeau            $request->close();
927c3437056SNickeau        }
928c3437056SNickeau
929c3437056SNickeau        switch (sizeof($rows)) {
930c3437056SNickeau            case 0:
93104fd306cSNickeau                throw new ExceptionNotFound("No object by page id");
932c3437056SNickeau            case 1:
933c3437056SNickeau                /**
934c3437056SNickeau                 * Page Id Collision detection
935c3437056SNickeau                 */
93604fd306cSNickeau                $rowId = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
93704fd306cSNickeau                $this->checkCollision($rowId, $pageIdAttribute, $pageIdValue);
938c3437056SNickeau                return $rows[0];
939c3437056SNickeau            default:
940c3437056SNickeau                $existingPages = implode(", ", $rows);
94104fd306cSNickeau                $message = "The pages ($existingPages) have all the same page id ($pageIdValue)";
94204fd306cSNickeau                throw new ExceptionRuntimeInternal($message, self::CANONICAL);
943c3437056SNickeau        }
944c3437056SNickeau
945c3437056SNickeau    }
946c3437056SNickeau
947c3437056SNickeau
948c3437056SNickeau    private
94904fd306cSNickeau    function getParametrizedLookupQuery(string $attributeName): string
950c3437056SNickeau    {
951c3437056SNickeau        $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES);
95204fd306cSNickeau        return "$select where $attributeName = ?";
953c3437056SNickeau    }
954c3437056SNickeau
955c3437056SNickeau
95604fd306cSNickeau    public
95704fd306cSNickeau    function setMarkupPath(MarkupPath $page)
958c3437056SNickeau    {
95904fd306cSNickeau        $this->markupPath = $page;
960c3437056SNickeau        return $this;
961c3437056SNickeau    }
962c3437056SNickeau
96304fd306cSNickeau    /**
96404fd306cSNickeau     * @throws ExceptionNotFound
96504fd306cSNickeau     */
966c3437056SNickeau    private
96704fd306cSNickeau    function getDatabaseRowFromCanonical($canonicalValue)
968c3437056SNickeau    {
969*c0705265SNico        $canonicalName = Canonical::PROPERTY_NAME;
970*c0705265SNico        $query = $this->getParametrizedLookupQuery($canonicalName);
971c3437056SNickeau        $request = $this->sqlite
972c3437056SNickeau            ->createRequest()
97304fd306cSNickeau            ->setQueryParametrized($query, [$canonicalValue]);
974c3437056SNickeau        $rows = [];
975c3437056SNickeau        try {
976c3437056SNickeau            $rows = $request
977c3437056SNickeau                ->execute()
978c3437056SNickeau                ->getRows();
97904fd306cSNickeau        } catch (ExceptionCompile $e) {
98004fd306cSNickeau            throw new ExceptionRuntime("An exception has occurred with the page search from CANONICAL. " . $e->getMessage());
981c3437056SNickeau        } finally {
982c3437056SNickeau            $request->close();
983c3437056SNickeau        }
984c3437056SNickeau
985c3437056SNickeau        switch (sizeof($rows)) {
986c3437056SNickeau            case 0:
98704fd306cSNickeau                throw new ExceptionNotFound("No canonical row was found");
988c3437056SNickeau            case 1:
989c3437056SNickeau                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
990*c0705265SNico                $this->checkCollision($id, $canonicalName, $canonicalValue);
991c3437056SNickeau                return $rows[0];
992c3437056SNickeau            default:
993c3437056SNickeau                $existingPages = [];
994c3437056SNickeau                foreach ($rows as $row) {
995c3437056SNickeau                    $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
99604fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($id);
997c3437056SNickeau                    if (!$duplicatePage->exists()) {
998c3437056SNickeau
999c3437056SNickeau                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
1000c3437056SNickeau
1001c3437056SNickeau                    } else {
1002c3437056SNickeau
1003c3437056SNickeau                        /**
1004c3437056SNickeau                         * Check if the error may come from the auto-canonical
1005c3437056SNickeau                         * (Never ever save generated data)
1006c3437056SNickeau                         */
100704fd306cSNickeau                        $canonicalLastNamesCount = SiteConfig::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0);
1008c3437056SNickeau                        if ($canonicalLastNamesCount > 0) {
1009*c0705265SNico                            $this->markupPath->unsetMetadata($canonicalName);
1010*c0705265SNico                            $duplicatePage->unsetMetadata($canonicalName);
1011c3437056SNickeau                        }
1012c3437056SNickeau
1013c3437056SNickeau                        $existingPages[] = $row;
1014c3437056SNickeau                    }
1015c3437056SNickeau                }
101604fd306cSNickeau                if (sizeof($existingPages) > 1) {
1017c3437056SNickeau                    $existingPages = implode(", ", $existingPages);
101804fd306cSNickeau                    $message = "The existing pages ($existingPages) have all the same canonical ($canonicalValue), return the first one";
101904fd306cSNickeau                    LogUtility::error($message, self::CANONICAL);
1020c3437056SNickeau                }
102104fd306cSNickeau                return $existingPages[0];
1022c3437056SNickeau        }
1023c3437056SNickeau    }
1024c3437056SNickeau
102504fd306cSNickeau    /**
102604fd306cSNickeau     * @throws ExceptionNotFound
102704fd306cSNickeau     */
1028c3437056SNickeau    private
1029c3437056SNickeau    function getDatabaseRowFromPath(string $path): ?array
1030c3437056SNickeau    {
103104fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
1032c3437056SNickeau        return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path);
1033c3437056SNickeau    }
1034c3437056SNickeau
103504fd306cSNickeau    /**
103604fd306cSNickeau     * @throws ExceptionNotFound
103704fd306cSNickeau     */
1038c3437056SNickeau    private
103904fd306cSNickeau    function getDatabaseRowFromDokuWikiId(string $id): array
1040c3437056SNickeau    {
1041c3437056SNickeau        return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
1042c3437056SNickeau    }
1043c3437056SNickeau
104404fd306cSNickeau    /**
104504fd306cSNickeau     * @throws ExceptionNotFound
104604fd306cSNickeau     */
1047c3437056SNickeau    public
1048031d4b49Sgerardnico    function getDatabaseRowFromAttribute(string $attribute, string $attributeValue)
1049c3437056SNickeau    {
1050c3437056SNickeau        $query = $this->getParametrizedLookupQuery($attribute);
1051c3437056SNickeau        $request = $this->sqlite
1052c3437056SNickeau            ->createRequest()
1053031d4b49Sgerardnico            ->setQueryParametrized($query, [$attributeValue]);
1054c3437056SNickeau        $rows = [];
1055c3437056SNickeau        try {
1056c3437056SNickeau            $rows = $request
1057c3437056SNickeau                ->execute()
1058c3437056SNickeau                ->getRows();
105904fd306cSNickeau        } catch (ExceptionCompile $e) {
106004fd306cSNickeau            $message = "Internal Error: An exception has occurred with the page search from a PATH: " . $e->getMessage();
106104fd306cSNickeau            LogUtility::log2file($message);
106204fd306cSNickeau            throw new ExceptionNotFound($message);
1063c3437056SNickeau        } finally {
1064c3437056SNickeau            $request->close();
1065c3437056SNickeau        }
1066c3437056SNickeau
1067031d4b49Sgerardnico        $rowCount = sizeof($rows);
1068031d4b49Sgerardnico        switch ($rowCount) {
1069c3437056SNickeau            case 0:
107004fd306cSNickeau                throw new ExceptionNotFound("No database row found for the page");
1071c3437056SNickeau            case 1:
1072031d4b49Sgerardnico                $attributeValue = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1073031d4b49Sgerardnico                try {
107411d09b86Sgerardnico                    if ($this->markupPath != null && $attributeValue !== $this->markupPath->getWikiId()) {
1075031d4b49Sgerardnico                        $duplicatePage = MarkupPath::createMarkupFromId($attributeValue);
1076c3437056SNickeau                        if (!$duplicatePage->exists()) {
1077c3437056SNickeau                            $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1078c3437056SNickeau                        } else {
1079031d4b49Sgerardnico                            LogUtility::error("The page ($this->markupPath) and the page ($attributeValue) have the same $attribute ($attributeValue)");
1080c3437056SNickeau                        }
1081c3437056SNickeau                    }
108211d09b86Sgerardnico                } catch (ExceptionBadArgument $e) {
108311d09b86Sgerardnico                    throw new ExceptionRuntimeInternal("The wiki id should be available");
108411d09b86Sgerardnico                }
1085c3437056SNickeau                return $rows[0];
1086c3437056SNickeau            default:
1087031d4b49Sgerardnico                LogUtility::warning("Error: More than 1 rows ($rowCount) found for attribute ($attribute) with the value ($attributeValue)");
1088031d4b49Sgerardnico
1089031d4b49Sgerardnico                /**
1090031d4b49Sgerardnico                 * Trying to delete the bad one
1091031d4b49Sgerardnico                 */
1092c3437056SNickeau                $existingPages = [];
1093c3437056SNickeau                foreach ($rows as $row) {
1094c3437056SNickeau
1095031d4b49Sgerardnico                    $rowId = $row[self::ROWID] ?? null;
1096031d4b49Sgerardnico                    if ($rowId === null) {
1097031d4b49Sgerardnico                        LogUtility::error("A row id should be present");
1098c3437056SNickeau                        $existingPages[] = $row;
1099031d4b49Sgerardnico                        continue;
1100c3437056SNickeau                    }
1101031d4b49Sgerardnico                    // page id throw for several id
1102031d4b49Sgerardnico                    $duplicateRow = DatabasePageRow::createFromRowId($rowId);
1103031d4b49Sgerardnico                    $duplicateMarkupPath = $duplicateRow->getMarkupPath();
1104031d4b49Sgerardnico                    if (!FileSystems::exists($duplicateMarkupPath)) {
1105031d4b49Sgerardnico                        LogUtility::warning("The duplicate page ($duplicateMarkupPath) does not exists. Delete Row ({$rowId})");
1106031d4b49Sgerardnico                        $duplicateRow->delete();
1107031d4b49Sgerardnico                        $this->addRedirectAliasWhileBuildingRow($duplicateMarkupPath);
1108031d4b49Sgerardnico                        continue;
1109031d4b49Sgerardnico                    }
1110031d4b49Sgerardnico                    $existingPages[] = $row;
1111c3437056SNickeau                }
1112c3437056SNickeau                if (sizeof($existingPages) === 1) {
1113c3437056SNickeau                    return $existingPages[0];
1114c3437056SNickeau                } else {
111504fd306cSNickeau                    $existingPageIds = array_map(
111604fd306cSNickeau                        function ($row) {
111704fd306cSNickeau                            return $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
111804fd306cSNickeau                        },
111904fd306cSNickeau                        $existingPages);
112004fd306cSNickeau                    $existingPages = implode(", ", $existingPageIds);
1121031d4b49Sgerardnico                    throw new ExceptionNotFound("The existing pages ($existingPages) have all the same attribute $attribute with the value ($attributeValue)", LogUtility::LVL_MSG_ERROR);
1122c3437056SNickeau                }
1123c3437056SNickeau        }
1124c3437056SNickeau    }
1125c3437056SNickeau
1126c3437056SNickeau    public
112704fd306cSNickeau    function getMarkupPath(): ?MarkupPath
1128c3437056SNickeau    {
112970bbd7f1Sgerardnico        if ($this->row === null) {
113070bbd7f1Sgerardnico            return null;
113170bbd7f1Sgerardnico        }
1132c3437056SNickeau        if (
113304fd306cSNickeau            $this->markupPath === null
1134c3437056SNickeau            && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null
1135c3437056SNickeau        ) {
113604fd306cSNickeau            $this->markupPath = MarkupPath::createMarkupFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]);
1137c3437056SNickeau        }
113804fd306cSNickeau        return $this->markupPath;
1139c3437056SNickeau    }
1140c3437056SNickeau
1141c3437056SNickeau    private
1142c3437056SNickeau    function getDatabaseRowFromAlias($alias): ?array
1143c3437056SNickeau    {
1144c3437056SNickeau
1145c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
1146c3437056SNickeau        $buildFields = self::PAGE_BUILD_ATTRIBUTES;
1147c3437056SNickeau        $fields = array_reduce($buildFields, function ($carry, $element) {
1148c3437056SNickeau            if ($carry !== null) {
1149c3437056SNickeau                return "$carry, p.{$element}";
1150c3437056SNickeau            } else {
1151c3437056SNickeau                return "p.{$element}";
1152c3437056SNickeau            }
1153c3437056SNickeau        }, null);
115404fd306cSNickeau        /** @noinspection SqlResolve */
1155c3437056SNickeau        $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? ";
1156c3437056SNickeau        $request = $this->sqlite
1157c3437056SNickeau            ->createRequest()
1158c3437056SNickeau            ->setQueryParametrized($query, [$alias]);
1159c3437056SNickeau        $rows = [];
1160c3437056SNickeau        try {
1161c3437056SNickeau            $rows = $request
1162c3437056SNickeau                ->execute()
1163c3437056SNickeau                ->getRows();
116404fd306cSNickeau        } catch (ExceptionCompile $e) {
1165c3437056SNickeau            LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}");
1166c3437056SNickeau            return null;
1167c3437056SNickeau        } finally {
1168c3437056SNickeau            $request->close();
1169c3437056SNickeau        }
1170c3437056SNickeau        switch (sizeof($rows)) {
1171c3437056SNickeau            case 0:
1172c3437056SNickeau                return null;
1173c3437056SNickeau            case 1:
1174c3437056SNickeau                return $rows[0];
1175c3437056SNickeau            default:
1176c3437056SNickeau                $id = $rows[0]['ID'];
1177c3437056SNickeau                $pages = implode(",",
1178c3437056SNickeau                    array_map(
1179c3437056SNickeau                        function ($row) {
1180c3437056SNickeau                            return $row['ID'];
1181c3437056SNickeau                        },
1182c3437056SNickeau                        $rows
1183c3437056SNickeau                    )
1184c3437056SNickeau                );
1185c3437056SNickeau                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);
1186c3437056SNickeau                return $rows[0];
1187c3437056SNickeau        }
1188c3437056SNickeau    }
1189c3437056SNickeau
1190c3437056SNickeau
1191c3437056SNickeau    /**
1192c3437056SNickeau     * Utility function
119304fd306cSNickeau     * @param MarkupPath $pageAlias
1194c3437056SNickeau     */
1195c3437056SNickeau    private
119604fd306cSNickeau    function addRedirectAliasWhileBuildingRow(MarkupPath $pageAlias)
1197c3437056SNickeau    {
1198c3437056SNickeau
119904fd306cSNickeau        $aliasPath = $pageAlias->getPathObject()->toAbsoluteId();
1200d899a2a6Sgerardnico        LogUtility::info("Add alias ($aliasPath) for page ({$this->markupPath})");
1201c3437056SNickeau        try {
120204fd306cSNickeau            Aliases::createForPage($this->markupPath)
1203c3437056SNickeau                ->addAlias($aliasPath)
1204c3437056SNickeau                ->sendToWriteStore();
120504fd306cSNickeau        } catch (ExceptionCompile $e) {
1206c3437056SNickeau            // we don't throw while getting
120704fd306cSNickeau            LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->markupPath)");
1208c3437056SNickeau        }
1209c3437056SNickeau
1210c3437056SNickeau    }
1211c3437056SNickeau
1212c3437056SNickeau    private
1213c3437056SNickeau    function addPageIdAttributeIfNeeded(array &$values)
1214c3437056SNickeau    {
1215c3437056SNickeau        if (!isset($values[PageId::getPersistentName()])) {
121604fd306cSNickeau            $values[PageId::getPersistentName()] = $this->markupPath->getPageId();
1217c3437056SNickeau        }
1218c3437056SNickeau        if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) {
121904fd306cSNickeau            $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->markupPath->getPageIdAbbr();
1220c3437056SNickeau        }
1221c3437056SNickeau    }
1222c3437056SNickeau
1223c3437056SNickeau    public
1224c3437056SNickeau    function getFromRow(string $attribute)
1225c3437056SNickeau    {
1226c3437056SNickeau        if ($this->row === null) {
1227c3437056SNickeau            return null;
1228c3437056SNickeau        }
1229c3437056SNickeau
1230c3437056SNickeau        if (!array_key_exists($attribute, $this->row)) {
1231c3437056SNickeau            /**
1232c3437056SNickeau             * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES}
1233c3437056SNickeau             * or in the table
1234c3437056SNickeau             */
123504fd306cSNickeau            throw new ExceptionRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical());
1236c3437056SNickeau        }
1237c3437056SNickeau
1238c3437056SNickeau        $value = $this->row[$attribute];
1239c3437056SNickeau
1240c3437056SNickeau        if ($value !== null) {
1241c3437056SNickeau            return $value;
1242c3437056SNickeau        }
1243c3437056SNickeau
1244c3437056SNickeau        // don't know why but the sqlite plugin returns them uppercase
1245c3437056SNickeau        // rowid is returned lowercase from the sqlite plugin
1246c3437056SNickeau        $upperAttribute = strtoupper($attribute);
124770bbd7f1Sgerardnico        return $this->row[$upperAttribute] ?? null;
1248c3437056SNickeau
1249c3437056SNickeau    }
1250c3437056SNickeau
1251c3437056SNickeau
12524cadd4f8SNickeau    /**
125304fd306cSNickeau     * @throws ExceptionCompile
12544cadd4f8SNickeau     */
125504fd306cSNickeau    public
125604fd306cSNickeau    function replicateAnalytics()
1257c3437056SNickeau    {
1258c3437056SNickeau
1259c3437056SNickeau        try {
126004fd306cSNickeau            $fetchPath = $this->markupPath->fetchAnalyticsPath();
126104fd306cSNickeau            $analyticsJson = Json::createFromPath($fetchPath);
126204fd306cSNickeau        } catch (ExceptionCompile $e) {
126304fd306cSNickeau            if (PluginUtility::isDevOrTest()) {
126404fd306cSNickeau                throw $e;
126504fd306cSNickeau            }
126604fd306cSNickeau            throw new ExceptionCompile("Unable to get the analytics document", self::CANONICAL, 0, $e);
1267c3437056SNickeau        }
1268c3437056SNickeau
1269c3437056SNickeau        /**
1270c3437056SNickeau         * Replication Date
1271c3437056SNickeau         */
127204fd306cSNickeau        $replicationDateMeta = ReplicationDate::createFromPage($this->markupPath)
1273c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
12744cadd4f8SNickeau            ->setValue(new DateTime());
1275c3437056SNickeau
1276c3437056SNickeau        /**
1277c3437056SNickeau         * Analytics
1278c3437056SNickeau         */
1279c3437056SNickeau        $analyticsJsonAsString = $analyticsJson->toPrettyJsonString();
1280c3437056SNickeau        $analyticsJsonAsArray = $analyticsJson->toArray();
1281c3437056SNickeau
1282c3437056SNickeau        /**
1283c3437056SNickeau         * Record
1284c3437056SNickeau         */
1285c3437056SNickeau        $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString;
128604fd306cSNickeau        $record['IS_LOW_QUALITY'] = ($this->markupPath->isLowQualityPage() === true ? 1 : 0);
128704fd306cSNickeau        $record['WORD_COUNT'] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][renderer_plugin_combo_analytics::WORD_COUNT];
128804fd306cSNickeau        $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][BacklinkCount::getPersistentName()];
1289c3437056SNickeau        $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue();
1290c3437056SNickeau        $this->upsertAttributes($record);
1291c3437056SNickeau    }
1292c3437056SNickeau
129304fd306cSNickeau    private
1294*c0705265SNico    function checkCollision($wikiIdInDatabase, $attribute, $value): void
129504fd306cSNickeau    {
129604fd306cSNickeau        if ($this->markupPath === null) {
129704fd306cSNickeau            return;
129804fd306cSNickeau        }
129904fd306cSNickeau        try {
130004fd306cSNickeau            $markupWikiId = $this->markupPath->toWikiPath()->getWikiId();
130104fd306cSNickeau        } catch (ExceptionCast $e) {
130204fd306cSNickeau            return;
130304fd306cSNickeau        }
130404fd306cSNickeau        if ($wikiIdInDatabase !== $markupWikiId) {
130504fd306cSNickeau            $duplicatePage = MarkupPath::createMarkupFromId($wikiIdInDatabase);
130604fd306cSNickeau            if (!FileSystems::exists($duplicatePage)) {
130704fd306cSNickeau                // Move
130804fd306cSNickeau                LogUtility::info("The non-existing duplicate page ($wikiIdInDatabase) has been added as redirect alias for the page ($this->markupPath)", self::CANONICAL);
130904fd306cSNickeau                $this->addRedirectAliasWhileBuildingRow($duplicatePage);
131004fd306cSNickeau            } else {
131104fd306cSNickeau                // This can happens if two page were created not on the same website
131204fd306cSNickeau                // of if the sqlite database was deleted and rebuilt.
131304fd306cSNickeau                // The chance is really, really low
131404fd306cSNickeau                $errorMessage = "The page ($this->markupPath) and the page ($wikiIdInDatabase) have the same $attribute value ($value)";
1315*c0705265SNico                LogUtility::error($errorMessage);
131604fd306cSNickeau                // What to do ?
131704fd306cSNickeau                // The database does not allow two page id with the same value
131804fd306cSNickeau                // If it happens, ugh, ugh, ..., a replication process between website may be.
131904fd306cSNickeau            }
132004fd306cSNickeau        }
132104fd306cSNickeau    }
132204fd306cSNickeau
1323031d4b49Sgerardnico    /**
1324031d4b49Sgerardnico     * @throws ExceptionSqliteNotAvailable
1325031d4b49Sgerardnico     * @throws ExceptionNotFound
1326031d4b49Sgerardnico     */
1327031d4b49Sgerardnico    private function getDatabaseRowFromRowId(string $rowId)
1328031d4b49Sgerardnico    {
1329031d4b49Sgerardnico
1330031d4b49Sgerardnico        $query = $this->getParametrizedLookupQuery(self::ROWID);
1331031d4b49Sgerardnico        $request = Sqlite::createOrGetSqlite()
1332031d4b49Sgerardnico            ->createRequest()
1333031d4b49Sgerardnico            ->setQueryParametrized($query, [$rowId]);
1334031d4b49Sgerardnico        $rows = [];
1335031d4b49Sgerardnico        try {
1336031d4b49Sgerardnico            $rows = $request
1337031d4b49Sgerardnico                ->execute()
1338031d4b49Sgerardnico                ->getRows();
1339031d4b49Sgerardnico        } catch (ExceptionCompile $e) {
1340031d4b49Sgerardnico            throw new ExceptionRuntimeInternal("Error while retrieving the object by rowid ($rowId)", self::CANONICAL, 1, $e);
1341031d4b49Sgerardnico        } finally {
1342031d4b49Sgerardnico            $request->close();
1343031d4b49Sgerardnico        }
1344031d4b49Sgerardnico
1345031d4b49Sgerardnico        $rowCount = sizeof($rows);
1346031d4b49Sgerardnico        switch ($rowCount) {
1347031d4b49Sgerardnico            case 0:
1348031d4b49Sgerardnico                throw new ExceptionNotFound("No object by row id ($rowId)");
1349031d4b49Sgerardnico            case 1:
1350031d4b49Sgerardnico                return $rows[0];
1351031d4b49Sgerardnico            default:
1352031d4b49Sgerardnico                throw new ExceptionRuntimeInternal("Too much record ($rowCount) for the rowid ($rowId)", self::CANONICAL);
1353031d4b49Sgerardnico        }
1354031d4b49Sgerardnico
1355031d4b49Sgerardnico    }
1356031d4b49Sgerardnico
1357c3437056SNickeau
1358c3437056SNickeau}
1359