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