xref: /plugin/combo/ComboStrap/DatabasePageRow.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1c3437056SNickeau<?php
2c3437056SNickeau
3c3437056SNickeau
4c3437056SNickeaunamespace ComboStrap;
5c3437056SNickeau
6*04fd306cSNickeauuse ComboStrap\Meta\Api\Metadata;
7*04fd306cSNickeauuse ComboStrap\Meta\Api\MetadataStore;
8*04fd306cSNickeauuse ComboStrap\Meta\Field\Aliases;
9*04fd306cSNickeauuse ComboStrap\Meta\Field\BacklinkCount;
10*04fd306cSNickeauuse ComboStrap\Meta\Field\PageH1;
11*04fd306cSNickeauuse ComboStrap\Meta\Field\Region;
12*04fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDbStore;
13*04fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore;
144cadd4f8SNickeauuse DateTime;
15*04fd306cSNickeauuse 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,
47*04fd306cSNickeau            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,
56*04fd306cSNickeau            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
68*04fd306cSNickeau    const IS_HOME_COLUMN = "is_home";
69*04fd306cSNickeau    const IS_INDEX_COLUMN = "is_index";
70*04fd306cSNickeau
71c3437056SNickeau    /**
72*04fd306cSNickeau     * @var MarkupPath
73c3437056SNickeau     */
74*04fd306cSNickeau    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.
87*04fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
88c3437056SNickeau     */
89c3437056SNickeau    public function __construct()
90c3437056SNickeau    {
91c3437056SNickeau        /**
92c3437056SNickeau         * Persist on the DB
93c3437056SNickeau         */
94c3437056SNickeau        $this->sqlite = Sqlite::createOrGetSqlite();
95c3437056SNickeau
96c3437056SNickeau
97c3437056SNickeau    }
98c3437056SNickeau
99*04fd306cSNickeau    public static function getFromPageObject(MarkupPath $page): DatabasePageRow
100*04fd306cSNickeau    {
101*04fd306cSNickeau        $databasePage = new DatabasePageRow();
102*04fd306cSNickeau        try {
103*04fd306cSNickeau            $row = $databasePage->getDatabaseRowFromPage($page);
104*04fd306cSNickeau            $databasePage->setRow($row);
105*04fd306cSNickeau            return $databasePage;
106*04fd306cSNickeau        } catch (ExceptionNotExists $e) {
107*04fd306cSNickeau            //
108*04fd306cSNickeau        }
109*04fd306cSNickeau        return $databasePage;
110*04fd306cSNickeau    }
111*04fd306cSNickeau
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     *
123*04fd306cSNickeau     * @throws ExceptionCompile
124c3437056SNickeau     */
125c3437056SNickeau    public function replicate(): DatabasePageRow
126c3437056SNickeau    {
127c3437056SNickeau
128*04fd306cSNickeau        if (!FileSystems::exists($this->markupPath)) {
129*04fd306cSNickeau            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        ];
145*04fd306cSNickeau        $fsStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
146*04fd306cSNickeau        $dbStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
147c3437056SNickeau        foreach ($tabularMetadataToSync as $tabular) {
148c3437056SNickeau            $tabular
149*04fd306cSNickeau                ->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    /**
169*04fd306cSNickeau     * @throws ExceptionCompile
170c3437056SNickeau     */
171*04fd306cSNickeau    public function replicateAndRebuild(): DatabasePageRow
172c3437056SNickeau    {
173c3437056SNickeau        $this->replicate();
174c3437056SNickeau        $this->rebuild();
175c3437056SNickeau        return $this;
176c3437056SNickeau    }
177c3437056SNickeau
178*04fd306cSNickeau    /**
179*04fd306cSNickeau     * @throws ExceptionNotExists - no page id to add
180*04fd306cSNickeau     */
181c3437056SNickeau    private function addPageIdMeta(array &$metaRecord)
182c3437056SNickeau    {
183*04fd306cSNickeau        try {
184*04fd306cSNickeau            $pageId = $this->markupPath->getPageId();
185*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
186*04fd306cSNickeau            $pageId = PageId::generateAndStorePageId($this->markupPath);
187*04fd306cSNickeau        }
188*04fd306cSNickeau        $metaRecord[PageId::PROPERTY_NAME] = $pageId;
189*04fd306cSNickeau        $metaRecord[PageId::PAGE_ID_ABBR_ATTRIBUTE] = PageId::getAbbreviated($pageId);
190c3437056SNickeau    }
191c3437056SNickeau
192c3437056SNickeau    public static function createFromPageId(string $pageId): DatabasePageRow
193c3437056SNickeau    {
194c3437056SNickeau        $databasePage = new DatabasePageRow();
195*04fd306cSNickeau        try {
196c3437056SNickeau            $row = $databasePage->getDatabaseRowFromPageId($pageId);
197c3437056SNickeau            $databasePage->setRow($row);
198*04fd306cSNickeau        } catch (ExceptionNotFound|ExceptionSqliteNotAvailable $e) {
199*04fd306cSNickeau            // not found
200c3437056SNickeau        }
201*04fd306cSNickeau
202c3437056SNickeau        return $databasePage;
203c3437056SNickeau    }
204c3437056SNickeau
205*04fd306cSNickeau    /**
206*04fd306cSNickeau     * @param MarkupPath $page
207*04fd306cSNickeau     * @return DatabasePageRow
208*04fd306cSNickeau     * @throws ExceptionSqliteNotAvailable - if there is no sqlite available
209*04fd306cSNickeau     * @noinspection PhpDocRedundantThrowsInspection
210*04fd306cSNickeau     */
211*04fd306cSNickeau    public static function getOrCreateFromPageObject(MarkupPath $page): DatabasePageRow
212c3437056SNickeau    {
213c3437056SNickeau
214c3437056SNickeau        $databasePage = new DatabasePageRow();
215*04fd306cSNickeau        try {
216c3437056SNickeau            $row = $databasePage->getDatabaseRowFromPage($page);
217c3437056SNickeau            $databasePage->setRow($row);
218c3437056SNickeau            return $databasePage;
219*04fd306cSNickeau        } catch (ExceptionNotExists $e) {
220*04fd306cSNickeau            // page copied on the local system
221*04fd306cSNickeau            try {
222*04fd306cSNickeau                ComboFs::createIfNotExists($page);
223*04fd306cSNickeau                $row = $databasePage->getDatabaseRowFromPage($page);
224*04fd306cSNickeau                $databasePage->setRow($row);
225*04fd306cSNickeau                return $databasePage;
226*04fd306cSNickeau            } catch (ExceptionNotExists $e) {
227*04fd306cSNickeau                throw ExceptionRuntimeInternal::withMessageAndError("The row should exists as we created it specifically", $e);
228*04fd306cSNickeau            }
229c3437056SNickeau        }
230c3437056SNickeau
231*04fd306cSNickeau    }
232*04fd306cSNickeau
233*04fd306cSNickeau
234*04fd306cSNickeau    /**
235*04fd306cSNickeau     *
236*04fd306cSNickeau     */
237*04fd306cSNickeau    public
238*04fd306cSNickeau    static function createFromPageIdAbbr(string $pageIdAbbr): DatabasePageRow
239c3437056SNickeau    {
240c3437056SNickeau        $databasePage = new DatabasePageRow();
241*04fd306cSNickeau        try {
242c3437056SNickeau            $row = $databasePage->getDatabaseRowFromAttribute(PageId::PAGE_ID_ABBR_ATTRIBUTE, $pageIdAbbr);
243c3437056SNickeau            $databasePage->setRow($row);
244*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
245*04fd306cSNickeau            // ok
246c3437056SNickeau        }
247c3437056SNickeau        return $databasePage;
248c3437056SNickeau
249c3437056SNickeau    }
250c3437056SNickeau
251c3437056SNickeau    /**
252c3437056SNickeau     * @param $canonical
253c3437056SNickeau     * @return DatabasePageRow
254c3437056SNickeau     */
255*04fd306cSNickeau    public
256*04fd306cSNickeau    static function createFromCanonical($canonical): DatabasePageRow
257c3437056SNickeau    {
258c3437056SNickeau
259*04fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($canonical);
260c3437056SNickeau        $databasePage = new DatabasePageRow();
261*04fd306cSNickeau        try {
262c3437056SNickeau            $row = $databasePage->getDatabaseRowFromAttribute(Canonical::PROPERTY_NAME, $canonical);
263c3437056SNickeau            $databasePage->setRow($row);
264*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
265*04fd306cSNickeau            // ok
266c3437056SNickeau        }
267c3437056SNickeau        return $databasePage;
268c3437056SNickeau
269c3437056SNickeau
270c3437056SNickeau    }
271c3437056SNickeau
272*04fd306cSNickeau    public
273*04fd306cSNickeau    static function createFromAlias($alias): DatabasePageRow
274c3437056SNickeau    {
275c3437056SNickeau
276*04fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($alias);
277c3437056SNickeau        $databasePage = new DatabasePageRow();
278c3437056SNickeau        $row = $databasePage->getDatabaseRowFromAlias($alias);
279c3437056SNickeau        if ($row != null) {
280c3437056SNickeau            $databasePage->setRow($row);
281*04fd306cSNickeau            $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
293*04fd306cSNickeau    /**
294*04fd306cSNickeau     * @throws ExceptionNotFound
295*04fd306cSNickeau     */
296*04fd306cSNickeau    public
297*04fd306cSNickeau    static function getFromDokuWikiId($id): DatabasePageRow
298c3437056SNickeau    {
299c3437056SNickeau        $databasePage = new DatabasePageRow();
300c3437056SNickeau        $row = $databasePage->getDatabaseRowFromDokuWikiId($id);
301c3437056SNickeau        $databasePage->setRow($row);
302c3437056SNickeau        return $databasePage;
303c3437056SNickeau    }
304c3437056SNickeau
305*04fd306cSNickeau    public
306*04fd306cSNickeau    function getPageId()
307c3437056SNickeau    {
308c3437056SNickeau        return $this->getFromRow(PageId::PROPERTY_NAME);
309c3437056SNickeau    }
310c3437056SNickeau
311c3437056SNickeau
312c3437056SNickeau    public
313c3437056SNickeau    function shouldReplicate(): bool
314c3437056SNickeau    {
315c3437056SNickeau
316*04fd306cSNickeau
3174cadd4f8SNickeau        $dateReplication = $this->getReplicationDate();
3184cadd4f8SNickeau        if ($dateReplication === null) {
3194cadd4f8SNickeau            return true;
3204cadd4f8SNickeau        }
3214cadd4f8SNickeau
3224cadd4f8SNickeau        /**
3234cadd4f8SNickeau         * When the replication date is older than the actual document
3244cadd4f8SNickeau         */
325*04fd306cSNickeau        try {
326*04fd306cSNickeau            $modifiedTime = FileSystems::getModifiedTime($this->markupPath->getPathObject());
3274cadd4f8SNickeau            if ($modifiedTime > $dateReplication) {
3284cadd4f8SNickeau                return true;
3294cadd4f8SNickeau            }
330*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
331*04fd306cSNickeau            return false;
332*04fd306cSNickeau        }
333*04fd306cSNickeau
334*04fd306cSNickeau
335*04fd306cSNickeau        $path = $this->markupPath->fetchAnalyticsPath();
3364cadd4f8SNickeau
337c3437056SNickeau        /**
338c3437056SNickeau         * When the file does not exist
339c3437056SNickeau         */
340*04fd306cSNickeau        $exist = FileSystems::exists($path);
341c3437056SNickeau        if (!$exist) {
342c3437056SNickeau            return true;
343c3437056SNickeau        }
344c3437056SNickeau
345c3437056SNickeau        /**
3464cadd4f8SNickeau         * When the analytics document is older
347c3437056SNickeau         */
348*04fd306cSNickeau        try {
349*04fd306cSNickeau
350*04fd306cSNickeau            $modifiedTime = FileSystems::getModifiedTime($path);
351c3437056SNickeau            if ($modifiedTime > $dateReplication) {
352c3437056SNickeau                return true;
353c3437056SNickeau            }
354*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
355*04fd306cSNickeau            //
356*04fd306cSNickeau        }
357c3437056SNickeau
3584cadd4f8SNickeau
359c3437056SNickeau        /**
360c3437056SNickeau         * When the database version file is higher
361c3437056SNickeau         */
362*04fd306cSNickeau        $version = LocalPath::createFromPathString(__DIR__ . "/../db/latest.version");
363*04fd306cSNickeau        try {
364c3437056SNickeau            $versionModifiedTime = FileSystems::getModifiedTime($version);
365*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
366*04fd306cSNickeau            return false;
367*04fd306cSNickeau        }
368c3437056SNickeau        if ($versionModifiedTime > $dateReplication) {
369c3437056SNickeau            return true;
370c3437056SNickeau        }
371c3437056SNickeau
372c3437056SNickeau        /**
373c3437056SNickeau         * When the class date time is higher
374c3437056SNickeau         */
375*04fd306cSNickeau        $code = LocalPath::createFromPathString(__DIR__ . "/DatabasePageRow.php");
376*04fd306cSNickeau        try {
377c3437056SNickeau            $codeModified = FileSystems::getModifiedTime($code);
378*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
379*04fd306cSNickeau            throw new ExceptionRuntime("The database file does not exist");
380*04fd306cSNickeau        }
381c3437056SNickeau        if ($codeModified > $dateReplication) {
382c3437056SNickeau            return true;
383c3437056SNickeau        }
384c3437056SNickeau
385c3437056SNickeau        return false;
386c3437056SNickeau
387c3437056SNickeau    }
388c3437056SNickeau
389c3437056SNickeau    public
390c3437056SNickeau    function delete()
391c3437056SNickeau    {
392c3437056SNickeau
393*04fd306cSNickeau        $request = $this->sqlite
394c3437056SNickeau            ->createRequest()
395*04fd306cSNickeau            ->setQueryParametrized('delete from pages where id = ?', [$this->markupPath->getWikiId()]);
396c3437056SNickeau        try {
397c3437056SNickeau            $request->execute();
398*04fd306cSNickeau        } catch (ExceptionCompile $e) {
399*04fd306cSNickeau            LogUtility::msg("Something went wrong when deleting the page ({$this->markupPath}) from the database");
400c3437056SNickeau        } finally {
401c3437056SNickeau            $request->close();
402c3437056SNickeau        }
403c3437056SNickeau        $this->buildInitObjectFields();
404c3437056SNickeau
405c3437056SNickeau    }
406c3437056SNickeau
407c3437056SNickeau    /**
408*04fd306cSNickeau     * @return Json the analytics array or null if not in db
409c3437056SNickeau     */
410c3437056SNickeau    public
411*04fd306cSNickeau    function getAnalyticsData(): Json
412c3437056SNickeau    {
413c3437056SNickeau
414c3437056SNickeau        $jsonString = $this->getFromRow(self::ANALYTICS_ATTRIBUTE);
415c3437056SNickeau        if ($jsonString === null) {
416*04fd306cSNickeau            // we put an empty json to not get any problem with the json database function
417*04fd306cSNickeau            // on an empty string / null (for sqlite)
418*04fd306cSNickeau            return Json::createEmpty();
419c3437056SNickeau        }
420c3437056SNickeau        try {
421c3437056SNickeau            return Json::createFromString($jsonString);
422*04fd306cSNickeau        } catch (ExceptionCompile $e) {
423*04fd306cSNickeau            throw ExceptionRuntimeInternal::withMessageAndError("Error while building back the analytics JSON object. {$e->getMessage()}", $e);
424c3437056SNickeau        }
425c3437056SNickeau
426c3437056SNickeau    }
427c3437056SNickeau
428c3437056SNickeau    /**
429c3437056SNickeau     * Return the database row
430c3437056SNickeau     *
431c3437056SNickeau     *
432*04fd306cSNickeau     * @throws ExceptionNotExists - if the row does not exists
433c3437056SNickeau     */
434*04fd306cSNickeau    public
435*04fd306cSNickeau    function getDatabaseRowFromPage(MarkupPath $markupPath): array
436c3437056SNickeau    {
437c3437056SNickeau
438*04fd306cSNickeau        $this->setMarkupPath($markupPath);
439c3437056SNickeau
440*04fd306cSNickeau        /**
441*04fd306cSNickeau         * Generated identifier
442*04fd306cSNickeau         */
443*04fd306cSNickeau        try {
444*04fd306cSNickeau            $pageId = $markupPath->getPageId();
445*04fd306cSNickeau            return $this->getDatabaseRowFromPageId($pageId);
446*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
447*04fd306cSNickeau            // no page id
448c3437056SNickeau        }
449c3437056SNickeau
450c3437056SNickeau        /**
451*04fd306cSNickeau         * Named identifier: path
452c3437056SNickeau         */
453*04fd306cSNickeau        try {
454*04fd306cSNickeau            $path = $markupPath->getPathObject();
455*04fd306cSNickeau            return $this->getDatabaseRowFromPath($path);
456*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
457*04fd306cSNickeau            // not found
458*04fd306cSNickeau        }
459*04fd306cSNickeau
460*04fd306cSNickeau        /**
461*04fd306cSNickeau         * Named identifier: id (ie path)
462*04fd306cSNickeau         */
463*04fd306cSNickeau        try {
464*04fd306cSNickeau            $id = $markupPath->getPathObject()->toWikiPath()->getWikiId();
465c3437056SNickeau            return $this->getDatabaseRowFromDokuWikiId($id);
466*04fd306cSNickeau        } catch (ExceptionCast|ExceptionNotFound $e) {
467*04fd306cSNickeau        }
468*04fd306cSNickeau
469*04fd306cSNickeau        /**
470*04fd306cSNickeau         * Named identifier: canonical
471*04fd306cSNickeau         * (Note that canonical should become a soft link and therefore a path)
472*04fd306cSNickeau         */
473*04fd306cSNickeau        try {
474*04fd306cSNickeau            $canonical = Canonical::createForPage($markupPath)->getValue();
475*04fd306cSNickeau            return $this->getDatabaseRowFromCanonical($canonical->toAbsoluteId());
476*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
477*04fd306cSNickeau            // no canonical
478*04fd306cSNickeau        }
479*04fd306cSNickeau
480*04fd306cSNickeau        // we send a not exist
481*04fd306cSNickeau        throw new ExceptionNotExists("No row could be found");
482c3437056SNickeau
483c3437056SNickeau
484c3437056SNickeau    }
485c3437056SNickeau
486c3437056SNickeau
4874cadd4f8SNickeau    /**
4884cadd4f8SNickeau     * @return DateTime|null
4894cadd4f8SNickeau     */
490*04fd306cSNickeau    public
491*04fd306cSNickeau    function getReplicationDate(): ?DateTime
492c3437056SNickeau    {
493*04fd306cSNickeau        $dateString = $this->getFromRow(ReplicationDate::getPersistentName());
494c3437056SNickeau        if ($dateString === null) {
495c3437056SNickeau            return null;
496c3437056SNickeau        }
497c3437056SNickeau        try {
498c3437056SNickeau            return Iso8601Date::createFromString($dateString)->getDateTime();
499*04fd306cSNickeau        } catch (ExceptionCompile $e) {
500c3437056SNickeau            LogUtility::msg("Error while reading the replication date in the database. {$e->getMessage()}");
501c3437056SNickeau            return null;
502c3437056SNickeau        }
503c3437056SNickeau
504c3437056SNickeau    }
505c3437056SNickeau
506c3437056SNickeau    /**
507*04fd306cSNickeau     *
508*04fd306cSNickeau     * @throws ExceptionBadState
509*04fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
510c3437056SNickeau     */
511*04fd306cSNickeau    public
512*04fd306cSNickeau    function replicatePage(): void
513c3437056SNickeau    {
514c3437056SNickeau
515*04fd306cSNickeau        if (!FileSystems::exists($this->markupPath)) {
516*04fd306cSNickeau            throw new ExceptionBadState("You can't replicate the page ($this->markupPath) because it does not exists.");
517c3437056SNickeau        }
518c3437056SNickeau
519c3437056SNickeau        /**
520c3437056SNickeau         * Replication Date
521c3437056SNickeau         */
522*04fd306cSNickeau        $replicationDate = ReplicationDate::createFromPage($this->markupPath)
523c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
5244cadd4f8SNickeau            ->setValue(new DateTime());
525c3437056SNickeau
526c3437056SNickeau        /**
527*04fd306cSNickeau         * Same data as {@link MarkupPath::getMetadataForRendering()}
528c3437056SNickeau         */
529c3437056SNickeau        $record = $this->getMetaRecord();
530c3437056SNickeau        $record[$replicationDate::getPersistentName()] = $replicationDate->toStoreValue();
531*04fd306cSNickeau        $this->upsertAttributes($record);
532c3437056SNickeau
533c3437056SNickeau    }
534c3437056SNickeau
535c3437056SNickeau
536c3437056SNickeau    /**
537*04fd306cSNickeau     *
538c3437056SNickeau     *
539c3437056SNickeau     * Attribute that are scalar / modifiable in the database
540c3437056SNickeau     * (not aliases or json data for instance)
541*04fd306cSNickeau     *
542*04fd306cSNickeau     * @throws ExceptionBadState
543*04fd306cSNickeau     * @throws ExceptionSqliteNotAvailable
544c3437056SNickeau     */
545*04fd306cSNickeau    public
546*04fd306cSNickeau    function replicateMetaAttributes(): void
547c3437056SNickeau    {
548c3437056SNickeau
549*04fd306cSNickeau        $this->upsertAttributes($this->getMetaRecord());
550c3437056SNickeau
551c3437056SNickeau    }
552c3437056SNickeau
553*04fd306cSNickeau    /**
554*04fd306cSNickeau     * @throws ExceptionBadState - if the array is empty
555*04fd306cSNickeau     */
556*04fd306cSNickeau    public
557*04fd306cSNickeau    function upsertAttributes(array $attributes): void
558c3437056SNickeau    {
559c3437056SNickeau
560c3437056SNickeau        if (empty($attributes)) {
561*04fd306cSNickeau            throw new ExceptionBadState("The page database attribute passed should not be empty");
562c3437056SNickeau        }
563c3437056SNickeau
564c3437056SNickeau        $values = [];
565c3437056SNickeau        $columnClauses = [];
566c3437056SNickeau        foreach ($attributes as $key => $value) {
567c3437056SNickeau            if (is_array($value)) {
568*04fd306cSNickeau                throw new ExceptionRuntime("The attribute ($key) has value that is an array (" . implode(", ", $value) . ")");
569c3437056SNickeau            }
570c3437056SNickeau            $columnClauses[] = "$key = ?";
571c3437056SNickeau            $values[$key] = $value;
572c3437056SNickeau        }
573c3437056SNickeau
574c3437056SNickeau        /**
575c3437056SNickeau         * Primary key has moved during the time
576c3437056SNickeau         * It should be the UUID but not for older version
577c3437056SNickeau         *
578c3437056SNickeau         * If the primary key is null, no record was found
579c3437056SNickeau         */
580c3437056SNickeau        $rowId = $this->getRowId();
581c3437056SNickeau        if ($rowId !== null) {
582c3437056SNickeau            /**
583c3437056SNickeau             * We just add the primary key
584c3437056SNickeau             * otherwise as this is a associative
585c3437056SNickeau             * array, we will miss a value for the update statement
586c3437056SNickeau             */
587c3437056SNickeau            $values[] = $rowId;
588c3437056SNickeau
589c3437056SNickeau            $updateStatement = "update PAGES SET " . implode(", ", $columnClauses) . " where ROWID = ?";
590c3437056SNickeau            $request = $this->sqlite
591c3437056SNickeau                ->createRequest()
592c3437056SNickeau                ->setQueryParametrized($updateStatement, $values);
593c3437056SNickeau            $countChanges = 0;
594c3437056SNickeau            try {
595c3437056SNickeau                $countChanges = $request
596c3437056SNickeau                    ->execute()
597c3437056SNickeau                    ->getChangeCount();
598*04fd306cSNickeau            } catch (ExceptionCompile $e) {
599*04fd306cSNickeau                throw new ExceptionBadState("There was a problem during the page attribute updates. : {$e->getMessage()}");
600c3437056SNickeau            } finally {
601c3437056SNickeau                $request->close();
602c3437056SNickeau            }
603c3437056SNickeau            if ($countChanges !== 1) {
604*04fd306cSNickeau                // internal error
605*04fd306cSNickeau                LogUtility::error("The database replication has not updated exactly 1 record but ($countChanges) record", \action_plugin_combo_indexer::CANONICAL);
606c3437056SNickeau            }
607c3437056SNickeau
608c3437056SNickeau        } else {
609c3437056SNickeau
610c3437056SNickeau            /**
611c3437056SNickeau             * Creation
612c3437056SNickeau             */
613*04fd306cSNickeau            if ($this->markupPath === null) {
614*04fd306cSNickeau                throw new ExceptionBadState("The page should be defined to create a page database row");
615c3437056SNickeau            }
616*04fd306cSNickeau
617*04fd306cSNickeau            /**
618*04fd306cSNickeau             * If the user copy a frontmatter with the same page id abbr, we got a problem
619*04fd306cSNickeau             */
620*04fd306cSNickeau            $pageIdAbbr = $values[PageId::PAGE_ID_ABBR_ATTRIBUTE];
621*04fd306cSNickeau            if ($pageIdAbbr == null) {
622*04fd306cSNickeau                $pageId = $values[PageId::getPersistentName()];
623*04fd306cSNickeau                if ($pageId === null) {
624*04fd306cSNickeau                    throw new ExceptionBadState("You can't insert a page in the database without a page id");
625*04fd306cSNickeau                }
626*04fd306cSNickeau                $pageIdAbbr = PageId::getAbbreviated($pageId);
627*04fd306cSNickeau                $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $pageIdAbbr;
628*04fd306cSNickeau            }
629*04fd306cSNickeau
630*04fd306cSNickeau            $databasePage = DatabasePageRow::createFromPageIdAbbr($pageIdAbbr);
631*04fd306cSNickeau            if ($databasePage->exists()) {
632*04fd306cSNickeau                $duplicatePage = $databasePage->getMarkupPath();
633*04fd306cSNickeau                if ($duplicatePage->getPathObject()->toUriString() === $this->markupPath->toUriString()) {
634*04fd306cSNickeau                    $message = "The page ($this->markupPath) is already in the database with the uid ($pageIdAbbr).";
635*04fd306cSNickeau                } else {
636*04fd306cSNickeau                    $message = "The page ($this->markupPath) cannot be replicated to the database because it has the same page id abbreviation ($pageIdAbbr) than the page ($duplicatePage)";
637*04fd306cSNickeau                }
638*04fd306cSNickeau                throw new ExceptionBadState($message);
639*04fd306cSNickeau            }
640*04fd306cSNickeau
641*04fd306cSNickeau            $values[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] = $this->markupPath->getPathObject()->getWikiId();
642*04fd306cSNickeau            $values[PagePath::PROPERTY_NAME] = $this->markupPath->getPathObject()->toAbsolutePath()->toAbsoluteId();
643c3437056SNickeau            /**
644c3437056SNickeau             * Default implements the auto-canonical feature
645c3437056SNickeau             */
646*04fd306cSNickeau            try {
647*04fd306cSNickeau                $values[Canonical::PROPERTY_NAME] = $this->markupPath->getCanonicalOrDefault();
648*04fd306cSNickeau            } catch (ExceptionNotFound $e) {
649*04fd306cSNickeau                $values[Canonical::PROPERTY_NAME] = null;
650*04fd306cSNickeau            }
651c3437056SNickeau
652c3437056SNickeau            /**
653c3437056SNickeau             * Analytics
654c3437056SNickeau             */
655c3437056SNickeau            if (!isset($values[self::ANALYTICS_ATTRIBUTE])) {
656c3437056SNickeau                // otherwise we get an empty string
657c3437056SNickeau                // and a json function will not work
658c3437056SNickeau                $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString();
659c3437056SNickeau            }
660c3437056SNickeau
661c3437056SNickeau            /**
662c3437056SNickeau             * Page Id / Abbr are mandatory for url redirection
663c3437056SNickeau             */
664c3437056SNickeau            $this->addPageIdAttributeIfNeeded($values);
665c3437056SNickeau
666c3437056SNickeau            $request = $this->sqlite
667c3437056SNickeau                ->createRequest()
668c3437056SNickeau                ->setTableRow('PAGES', $values);
669c3437056SNickeau            try {
670c3437056SNickeau                /**
671c3437056SNickeau                 * rowid is used in {@link DatabasePageRow::exists()}
672c3437056SNickeau                 * to check if the page exists in the database
673c3437056SNickeau                 * We update it
674c3437056SNickeau                 */
675c3437056SNickeau                $this->row[self::ROWID] = $request
676c3437056SNickeau                    ->execute()
677c3437056SNickeau                    ->getInsertId();
678c3437056SNickeau                $this->row = array_merge($values, $this->row);
679*04fd306cSNickeau            } catch (ExceptionCompile $e) {
680*04fd306cSNickeau                throw new ExceptionBadState("There was a problem during the updateAttributes insert. : {$e->getMessage()}");
681c3437056SNickeau            } finally {
682c3437056SNickeau                $request->close();
683c3437056SNickeau            }
684c3437056SNickeau
685c3437056SNickeau        }
686c3437056SNickeau
687c3437056SNickeau    }
688c3437056SNickeau
689c3437056SNickeau    public
690c3437056SNickeau    function getDescription()
691c3437056SNickeau    {
692c3437056SNickeau        return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY);
693c3437056SNickeau    }
694c3437056SNickeau
695c3437056SNickeau
696c3437056SNickeau    public
697c3437056SNickeau    function getPageName()
698c3437056SNickeau    {
699c3437056SNickeau        return $this->getFromRow(ResourceName::PROPERTY_NAME);
700c3437056SNickeau    }
701c3437056SNickeau
702c3437056SNickeau    public
703c3437056SNickeau    function exists(): bool
704c3437056SNickeau    {
705c3437056SNickeau        return $this->getFromRow(self::ROWID) !== null;
706c3437056SNickeau    }
707c3437056SNickeau
708c3437056SNickeau    /**
709c3437056SNickeau     * Called when a page is moved
710c3437056SNickeau     * @param $targetId
711c3437056SNickeau     */
712c3437056SNickeau    public
713c3437056SNickeau    function updatePathAndDokuwikiId($targetId)
714c3437056SNickeau    {
715c3437056SNickeau        if (!$this->exists()) {
716*04fd306cSNickeau            LogUtility::error("The `database` page ($this) does not exist and cannot be moved to ($targetId)");
717c3437056SNickeau        }
718c3437056SNickeau
719c3437056SNickeau        $path = $targetId;
720*04fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
721c3437056SNickeau        $attributes = [
722c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId,
723c3437056SNickeau            PagePath::PROPERTY_NAME => $path
724c3437056SNickeau        ];
725c3437056SNickeau
726c3437056SNickeau        $this->upsertAttributes($attributes);
727c3437056SNickeau
728c3437056SNickeau    }
729c3437056SNickeau
730c3437056SNickeau    public
731c3437056SNickeau    function __toString()
732c3437056SNickeau    {
733*04fd306cSNickeau        return $this->markupPath->__toString();
734c3437056SNickeau    }
735c3437056SNickeau
736c3437056SNickeau
737c3437056SNickeau    /**
738c3437056SNickeau     * Redirect are now added during a move
739c3437056SNickeau     * Not when a duplicate is found.
740c3437056SNickeau     * With the advent of the page id, it should never occurs anymore
741*04fd306cSNickeau     * @param MarkupPath $page
742c3437056SNickeau     * @deprecated 2012-10-28
743c3437056SNickeau     */
744c3437056SNickeau    private
745*04fd306cSNickeau    function deleteIfExistsAndAddRedirectAlias(MarkupPath $page): void
746c3437056SNickeau    {
747c3437056SNickeau
748*04fd306cSNickeau        if ($this->markupPath != null) {
749c3437056SNickeau            $page->getDatabasePage()->deleteIfExist();
750c3437056SNickeau            $this->addRedirectAliasWhileBuildingRow($page);
751c3437056SNickeau        }
752c3437056SNickeau
753c3437056SNickeau    }
754c3437056SNickeau
755c3437056SNickeau    public
756c3437056SNickeau    function getCanonical()
757c3437056SNickeau    {
758c3437056SNickeau        return $this->getFromRow(Canonical::PROPERTY_NAME);
759c3437056SNickeau    }
760c3437056SNickeau
761c3437056SNickeau    /**
762c3437056SNickeau     * Set the field to their values
763c3437056SNickeau     * @param $row
764c3437056SNickeau     */
765c3437056SNickeau    public
766c3437056SNickeau    function setRow($row)
767c3437056SNickeau    {
768c3437056SNickeau        if ($row === null) {
769c3437056SNickeau            LogUtility::msg("A row should not be null");
770c3437056SNickeau            return;
771c3437056SNickeau        }
772c3437056SNickeau        if (!is_array($row)) {
773c3437056SNickeau            LogUtility::msg("A row should be an array");
774c3437056SNickeau            return;
775c3437056SNickeau        }
776c3437056SNickeau
777c3437056SNickeau        /**
778c3437056SNickeau         * All get function lookup the row
779c3437056SNickeau         */
780c3437056SNickeau        $this->row = $row;
781c3437056SNickeau
782c3437056SNickeau
783c3437056SNickeau    }
784c3437056SNickeau
785c3437056SNickeau    private
786c3437056SNickeau    function buildInitObjectFields()
787c3437056SNickeau    {
788c3437056SNickeau        $this->row = null;
789c3437056SNickeau
790c3437056SNickeau    }
791c3437056SNickeau
792c3437056SNickeau    public
793c3437056SNickeau    function rebuild(): DatabasePageRow
794c3437056SNickeau    {
795c3437056SNickeau
796*04fd306cSNickeau        if ($this->markupPath != null) {
797*04fd306cSNickeau            $this->markupPath->rebuild();
798*04fd306cSNickeau            try {
799*04fd306cSNickeau                $row = $this->getDatabaseRowFromPage($this->markupPath);
800c3437056SNickeau                $this->setRow($row);
801*04fd306cSNickeau            } catch (ExceptionNotExists $e) {
802*04fd306cSNickeau                // ok
803c3437056SNickeau            }
804c3437056SNickeau        }
805c3437056SNickeau        return $this;
806c3437056SNickeau
807c3437056SNickeau    }
808c3437056SNickeau
809c3437056SNickeau    /**
810c3437056SNickeau     * @return array - an array of the fix page metadata (ie not derived)
811c3437056SNickeau     * Therefore quick to insert/update
812c3437056SNickeau     *
813c3437056SNickeau     */
814c3437056SNickeau    private
815c3437056SNickeau    function getMetaRecord(): array
816c3437056SNickeau    {
817*04fd306cSNickeau        $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
818*04fd306cSNickeau        $targetStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
819c3437056SNickeau
820c3437056SNickeau        $record = array(
821c3437056SNickeau            Canonical::PROPERTY_NAME,
822c3437056SNickeau            PagePath::PROPERTY_NAME,
823c3437056SNickeau            ResourceName::PROPERTY_NAME,
824c3437056SNickeau            PageTitle::TITLE,
825c3437056SNickeau            PageH1::PROPERTY_NAME,
826c3437056SNickeau            PageDescription::PROPERTY_NAME,
827*04fd306cSNickeau            CreationDate::PROPERTY_NAME,
828c3437056SNickeau            ModificationDate::PROPERTY_NAME,
829c3437056SNickeau            PagePublicationDate::PROPERTY_NAME,
830c3437056SNickeau            StartDate::PROPERTY_NAME,
831c3437056SNickeau            EndDate::PROPERTY_NAME,
832c3437056SNickeau            Region::PROPERTY_NAME,
833c3437056SNickeau            Lang::PROPERTY_NAME,
834c3437056SNickeau            PageType::PROPERTY_NAME,
835c3437056SNickeau            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
836*04fd306cSNickeau            PageLevel::PROPERTY_NAME
837c3437056SNickeau        );
838c3437056SNickeau        $metaRecord = [];
839c3437056SNickeau        foreach ($record as $name) {
840*04fd306cSNickeau            try {
841*04fd306cSNickeau                $metadata = Meta\Api\MetadataSystem::getForName($name);
842*04fd306cSNickeau            } catch (ExceptionNotFound $e) {
843*04fd306cSNickeau                LogUtility::internalError("The metadata ($name) is unknown", self::CANONICAL);
844*04fd306cSNickeau                continue;
845c3437056SNickeau            }
846c3437056SNickeau            $metaRecord[$name] = $metadata
847*04fd306cSNickeau                ->setResource($this->markupPath)
848c3437056SNickeau                ->setReadStore($sourceStore)
849c3437056SNickeau                ->buildFromReadStore()
850c3437056SNickeau                ->setWriteStore($targetStore)
851c3437056SNickeau                ->toStoreValueOrDefault(); // used by the template, the value is or default
852*04fd306cSNickeau
853c3437056SNickeau        }
854c3437056SNickeau
855*04fd306cSNickeau        try {
856c3437056SNickeau            $this->addPageIdMeta($metaRecord);
857*04fd306cSNickeau        } catch (ExceptionNotExists $e) {
858*04fd306cSNickeau            // no page id for non-existent page ok
859c3437056SNickeau        }
860*04fd306cSNickeau
861*04fd306cSNickeau        // Is index
862*04fd306cSNickeau        $metaRecord[self::IS_INDEX_COLUMN] = ($this->markupPath->isIndexPage() === true ? 1 : 0);
863*04fd306cSNickeau
864c3437056SNickeau        return $metaRecord;
865c3437056SNickeau    }
866c3437056SNickeau
867c3437056SNickeau    public
868c3437056SNickeau    function deleteIfExist(): DatabasePageRow
869c3437056SNickeau    {
870c3437056SNickeau        if ($this->exists()) {
871c3437056SNickeau            $this->delete();
872c3437056SNickeau        }
873c3437056SNickeau        return $this;
874c3437056SNickeau    }
875c3437056SNickeau
876c3437056SNickeau    public
877c3437056SNickeau    function getRowId()
878c3437056SNickeau    {
879c3437056SNickeau        return $this->getFromRow(self::ROWID);
880c3437056SNickeau    }
881c3437056SNickeau
882*04fd306cSNickeau    /**
883*04fd306cSNickeau     * @throws ExceptionNotFound
884*04fd306cSNickeau     */
885c3437056SNickeau    private
886*04fd306cSNickeau    function getDatabaseRowFromPageId(string $pageIdValue)
887c3437056SNickeau    {
888c3437056SNickeau
889c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
890c3437056SNickeau        $query = $this->getParametrizedLookupQuery($pageIdAttribute);
891c3437056SNickeau        $request = Sqlite::createOrGetSqlite()
892c3437056SNickeau            ->createRequest()
893*04fd306cSNickeau            ->setQueryParametrized($query, [$pageIdValue]);
894c3437056SNickeau        $rows = [];
895c3437056SNickeau        try {
896c3437056SNickeau            $rows = $request
897c3437056SNickeau                ->execute()
898c3437056SNickeau                ->getRows();
899*04fd306cSNickeau        } catch (ExceptionCompile $e) {
900*04fd306cSNickeau            throw new ExceptionRuntimeInternal("Error while retrieving the object by id", self::CANONICAL, 1, $e);
901c3437056SNickeau        } finally {
902c3437056SNickeau            $request->close();
903c3437056SNickeau        }
904c3437056SNickeau
905c3437056SNickeau        switch (sizeof($rows)) {
906c3437056SNickeau            case 0:
907*04fd306cSNickeau                throw new ExceptionNotFound("No object by page id");
908c3437056SNickeau            case 1:
909c3437056SNickeau                /**
910c3437056SNickeau                 * Page Id Collision detection
911c3437056SNickeau                 */
912*04fd306cSNickeau                $rowId = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
913*04fd306cSNickeau                $this->checkCollision($rowId, $pageIdAttribute, $pageIdValue);
914c3437056SNickeau                return $rows[0];
915c3437056SNickeau            default:
916c3437056SNickeau                $existingPages = implode(", ", $rows);
917*04fd306cSNickeau                $message = "The pages ($existingPages) have all the same page id ($pageIdValue)";
918*04fd306cSNickeau                throw new ExceptionRuntimeInternal($message, self::CANONICAL);
919c3437056SNickeau        }
920c3437056SNickeau
921c3437056SNickeau    }
922c3437056SNickeau
923c3437056SNickeau
924c3437056SNickeau    private
925*04fd306cSNickeau    function getParametrizedLookupQuery(string $attributeName): string
926c3437056SNickeau    {
927c3437056SNickeau        $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES);
928*04fd306cSNickeau        return "$select where $attributeName = ?";
929c3437056SNickeau    }
930c3437056SNickeau
931c3437056SNickeau
932*04fd306cSNickeau    public
933*04fd306cSNickeau    function setMarkupPath(MarkupPath $page)
934c3437056SNickeau    {
935*04fd306cSNickeau        $this->markupPath = $page;
936c3437056SNickeau        return $this;
937c3437056SNickeau    }
938c3437056SNickeau
939*04fd306cSNickeau    /**
940*04fd306cSNickeau     * @throws ExceptionNotFound
941*04fd306cSNickeau     */
942c3437056SNickeau    private
943*04fd306cSNickeau    function getDatabaseRowFromCanonical($canonicalValue)
944c3437056SNickeau    {
945*04fd306cSNickeau        $canoncialName = Canonical::PROPERTY_NAME;
946*04fd306cSNickeau        $query = $this->getParametrizedLookupQuery($canoncialName);
947c3437056SNickeau        $request = $this->sqlite
948c3437056SNickeau            ->createRequest()
949*04fd306cSNickeau            ->setQueryParametrized($query, [$canonicalValue]);
950c3437056SNickeau        $rows = [];
951c3437056SNickeau        try {
952c3437056SNickeau            $rows = $request
953c3437056SNickeau                ->execute()
954c3437056SNickeau                ->getRows();
955*04fd306cSNickeau        } catch (ExceptionCompile $e) {
956*04fd306cSNickeau            throw new ExceptionRuntime("An exception has occurred with the page search from CANONICAL. " . $e->getMessage());
957c3437056SNickeau        } finally {
958c3437056SNickeau            $request->close();
959c3437056SNickeau        }
960c3437056SNickeau
961c3437056SNickeau        switch (sizeof($rows)) {
962c3437056SNickeau            case 0:
963*04fd306cSNickeau                throw new ExceptionNotFound("No canonical row was found");
964c3437056SNickeau            case 1:
965c3437056SNickeau                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
966*04fd306cSNickeau                $this->checkCollision($id, $canoncialName, $canonicalValue);
967c3437056SNickeau                return $rows[0];
968c3437056SNickeau            default:
969c3437056SNickeau                $existingPages = [];
970c3437056SNickeau                foreach ($rows as $row) {
971c3437056SNickeau                    $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
972*04fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($id);
973c3437056SNickeau                    if (!$duplicatePage->exists()) {
974c3437056SNickeau
975c3437056SNickeau                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
976c3437056SNickeau
977c3437056SNickeau                    } else {
978c3437056SNickeau
979c3437056SNickeau                        /**
980c3437056SNickeau                         * Check if the error may come from the auto-canonical
981c3437056SNickeau                         * (Never ever save generated data)
982c3437056SNickeau                         */
983*04fd306cSNickeau                        $canonicalLastNamesCount = SiteConfig::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0);
984c3437056SNickeau                        if ($canonicalLastNamesCount > 0) {
985*04fd306cSNickeau                            $this->markupPath->unsetMetadata($canoncialName);
986*04fd306cSNickeau                            $duplicatePage->unsetMetadata($canoncialName);
987c3437056SNickeau                        }
988c3437056SNickeau
989c3437056SNickeau                        $existingPages[] = $row;
990c3437056SNickeau                    }
991c3437056SNickeau                }
992*04fd306cSNickeau                if (sizeof($existingPages) > 1) {
993c3437056SNickeau                    $existingPages = implode(", ", $existingPages);
994*04fd306cSNickeau                    $message = "The existing pages ($existingPages) have all the same canonical ($canonicalValue), return the first one";
995*04fd306cSNickeau                    LogUtility::error($message, self::CANONICAL);
996c3437056SNickeau                }
997*04fd306cSNickeau                return $existingPages[0];
998c3437056SNickeau        }
999c3437056SNickeau    }
1000c3437056SNickeau
1001*04fd306cSNickeau    /**
1002*04fd306cSNickeau     * @throws ExceptionNotFound
1003*04fd306cSNickeau     */
1004c3437056SNickeau    private
1005c3437056SNickeau    function getDatabaseRowFromPath(string $path): ?array
1006c3437056SNickeau    {
1007*04fd306cSNickeau        WikiPath::addRootSeparatorIfNotPresent($path);
1008c3437056SNickeau        return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path);
1009c3437056SNickeau    }
1010c3437056SNickeau
1011*04fd306cSNickeau    /**
1012*04fd306cSNickeau     * @throws ExceptionNotFound
1013*04fd306cSNickeau     */
1014c3437056SNickeau    private
1015*04fd306cSNickeau    function getDatabaseRowFromDokuWikiId(string $id): array
1016c3437056SNickeau    {
1017c3437056SNickeau        return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
1018c3437056SNickeau    }
1019c3437056SNickeau
1020*04fd306cSNickeau    /**
1021*04fd306cSNickeau     * @throws ExceptionNotFound
1022*04fd306cSNickeau     */
1023c3437056SNickeau    public
1024c3437056SNickeau    function getDatabaseRowFromAttribute(string $attribute, string $value)
1025c3437056SNickeau    {
1026c3437056SNickeau        $query = $this->getParametrizedLookupQuery($attribute);
1027c3437056SNickeau        $request = $this->sqlite
1028c3437056SNickeau            ->createRequest()
1029c3437056SNickeau            ->setQueryParametrized($query, [$value]);
1030c3437056SNickeau        $rows = [];
1031c3437056SNickeau        try {
1032c3437056SNickeau            $rows = $request
1033c3437056SNickeau                ->execute()
1034c3437056SNickeau                ->getRows();
1035*04fd306cSNickeau        } catch (ExceptionCompile $e) {
1036*04fd306cSNickeau            $message = "Internal Error: An exception has occurred with the page search from a PATH: " . $e->getMessage();
1037*04fd306cSNickeau            LogUtility::log2file($message);
1038*04fd306cSNickeau            throw new ExceptionNotFound($message);
1039c3437056SNickeau        } finally {
1040c3437056SNickeau            $request->close();
1041c3437056SNickeau        }
1042c3437056SNickeau
1043c3437056SNickeau        switch (sizeof($rows)) {
1044c3437056SNickeau            case 0:
1045*04fd306cSNickeau                throw new ExceptionNotFound("No database row found for the page");
1046c3437056SNickeau            case 1:
1047c3437056SNickeau                $value = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1048*04fd306cSNickeau                if ($this->markupPath != null && $value !== $this->markupPath->getWikiId()) {
1049*04fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($value);
1050c3437056SNickeau                    if (!$duplicatePage->exists()) {
1051c3437056SNickeau                        $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1052c3437056SNickeau                    } else {
1053*04fd306cSNickeau                        LogUtility::msg("The page ($this->markupPath) and the page ($value) have the same $attribute ($value)", LogUtility::LVL_MSG_ERROR);
1054c3437056SNickeau                    }
1055c3437056SNickeau                }
1056c3437056SNickeau                return $rows[0];
1057c3437056SNickeau            default:
1058c3437056SNickeau                $existingPages = [];
1059c3437056SNickeau                foreach ($rows as $row) {
1060c3437056SNickeau                    $value = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1061*04fd306cSNickeau                    $duplicatePage = MarkupPath::createMarkupFromId($value);
1062c3437056SNickeau                    if (!$duplicatePage->exists()) {
1063c3437056SNickeau
1064c3437056SNickeau                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
1065c3437056SNickeau
1066c3437056SNickeau                    } else {
1067c3437056SNickeau                        $existingPages[] = $row;
1068c3437056SNickeau                    }
1069c3437056SNickeau                }
1070c3437056SNickeau                if (sizeof($existingPages) === 1) {
1071c3437056SNickeau                    return $existingPages[0];
1072c3437056SNickeau                } else {
1073*04fd306cSNickeau                    $existingPageIds = array_map(
1074*04fd306cSNickeau                        function ($row) {
1075*04fd306cSNickeau                            return $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1076*04fd306cSNickeau                        },
1077*04fd306cSNickeau                        $existingPages);
1078*04fd306cSNickeau                    $existingPages = implode(", ", $existingPageIds);
1079*04fd306cSNickeau                    throw new ExceptionNotFound("The existing pages ($existingPages) have all the same attribute $attribute with the value ($value)", LogUtility::LVL_MSG_ERROR);
1080c3437056SNickeau                }
1081c3437056SNickeau        }
1082c3437056SNickeau    }
1083c3437056SNickeau
1084c3437056SNickeau    public
1085*04fd306cSNickeau    function getMarkupPath(): ?MarkupPath
1086c3437056SNickeau    {
1087c3437056SNickeau        if (
1088*04fd306cSNickeau            $this->markupPath === null
1089c3437056SNickeau            && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null
1090c3437056SNickeau        ) {
1091*04fd306cSNickeau            $this->markupPath = MarkupPath::createMarkupFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]);
1092c3437056SNickeau        }
1093*04fd306cSNickeau        return $this->markupPath;
1094c3437056SNickeau    }
1095c3437056SNickeau
1096c3437056SNickeau    private
1097c3437056SNickeau    function getDatabaseRowFromAlias($alias): ?array
1098c3437056SNickeau    {
1099c3437056SNickeau
1100c3437056SNickeau        $pageIdAttribute = PageId::PROPERTY_NAME;
1101c3437056SNickeau        $buildFields = self::PAGE_BUILD_ATTRIBUTES;
1102c3437056SNickeau        $fields = array_reduce($buildFields, function ($carry, $element) {
1103c3437056SNickeau            if ($carry !== null) {
1104c3437056SNickeau                return "$carry, p.{$element}";
1105c3437056SNickeau            } else {
1106c3437056SNickeau                return "p.{$element}";
1107c3437056SNickeau            }
1108c3437056SNickeau        }, null);
1109*04fd306cSNickeau        /** @noinspection SqlResolve */
1110c3437056SNickeau        $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? ";
1111c3437056SNickeau        $request = $this->sqlite
1112c3437056SNickeau            ->createRequest()
1113c3437056SNickeau            ->setQueryParametrized($query, [$alias]);
1114c3437056SNickeau        $rows = [];
1115c3437056SNickeau        try {
1116c3437056SNickeau            $rows = $request
1117c3437056SNickeau                ->execute()
1118c3437056SNickeau                ->getRows();
1119*04fd306cSNickeau        } catch (ExceptionCompile $e) {
1120c3437056SNickeau            LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}");
1121c3437056SNickeau            return null;
1122c3437056SNickeau        } finally {
1123c3437056SNickeau            $request->close();
1124c3437056SNickeau        }
1125c3437056SNickeau        switch (sizeof($rows)) {
1126c3437056SNickeau            case 0:
1127c3437056SNickeau                return null;
1128c3437056SNickeau            case 1:
1129c3437056SNickeau                return $rows[0];
1130c3437056SNickeau            default:
1131c3437056SNickeau                $id = $rows[0]['ID'];
1132c3437056SNickeau                $pages = implode(",",
1133c3437056SNickeau                    array_map(
1134c3437056SNickeau                        function ($row) {
1135c3437056SNickeau                            return $row['ID'];
1136c3437056SNickeau                        },
1137c3437056SNickeau                        $rows
1138c3437056SNickeau                    )
1139c3437056SNickeau                );
1140c3437056SNickeau                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);
1141c3437056SNickeau                return $rows[0];
1142c3437056SNickeau        }
1143c3437056SNickeau    }
1144c3437056SNickeau
1145c3437056SNickeau
1146c3437056SNickeau    /**
1147c3437056SNickeau     * Utility function
1148*04fd306cSNickeau     * @param MarkupPath $pageAlias
1149c3437056SNickeau     */
1150c3437056SNickeau    private
1151*04fd306cSNickeau    function addRedirectAliasWhileBuildingRow(MarkupPath $pageAlias)
1152c3437056SNickeau    {
1153c3437056SNickeau
1154*04fd306cSNickeau        $aliasPath = $pageAlias->getPathObject()->toAbsoluteId();
1155c3437056SNickeau        try {
1156*04fd306cSNickeau            Aliases::createForPage($this->markupPath)
1157c3437056SNickeau                ->addAlias($aliasPath)
1158c3437056SNickeau                ->sendToWriteStore();
1159*04fd306cSNickeau        } catch (ExceptionCompile $e) {
1160c3437056SNickeau            // we don't throw while getting
1161*04fd306cSNickeau            LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->markupPath)");
1162c3437056SNickeau        }
1163c3437056SNickeau
1164c3437056SNickeau    }
1165c3437056SNickeau
1166c3437056SNickeau    private
1167c3437056SNickeau    function addPageIdAttributeIfNeeded(array &$values)
1168c3437056SNickeau    {
1169c3437056SNickeau        if (!isset($values[PageId::getPersistentName()])) {
1170*04fd306cSNickeau            $values[PageId::getPersistentName()] = $this->markupPath->getPageId();
1171c3437056SNickeau        }
1172c3437056SNickeau        if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) {
1173*04fd306cSNickeau            $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->markupPath->getPageIdAbbr();
1174c3437056SNickeau        }
1175c3437056SNickeau    }
1176c3437056SNickeau
1177c3437056SNickeau    public
1178c3437056SNickeau    function getFromRow(string $attribute)
1179c3437056SNickeau    {
1180c3437056SNickeau        if ($this->row === null) {
1181c3437056SNickeau            return null;
1182c3437056SNickeau        }
1183c3437056SNickeau
1184c3437056SNickeau        if (!array_key_exists($attribute, $this->row)) {
1185c3437056SNickeau            /**
1186c3437056SNickeau             * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES}
1187c3437056SNickeau             * or in the table
1188c3437056SNickeau             */
1189*04fd306cSNickeau            throw new ExceptionRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical());
1190c3437056SNickeau        }
1191c3437056SNickeau
1192c3437056SNickeau        $value = $this->row[$attribute];
1193c3437056SNickeau
1194c3437056SNickeau        if ($value !== null) {
1195c3437056SNickeau            return $value;
1196c3437056SNickeau        }
1197c3437056SNickeau
1198c3437056SNickeau        // don't know why but the sqlite plugin returns them uppercase
1199c3437056SNickeau        // rowid is returned lowercase from the sqlite plugin
1200c3437056SNickeau        $upperAttribute = strtoupper($attribute);
1201c3437056SNickeau        return $this->row[$upperAttribute];
1202c3437056SNickeau
1203c3437056SNickeau    }
1204c3437056SNickeau
1205c3437056SNickeau
12064cadd4f8SNickeau    /**
1207*04fd306cSNickeau     * @throws ExceptionCompile
12084cadd4f8SNickeau     */
1209*04fd306cSNickeau    public
1210*04fd306cSNickeau    function replicateAnalytics()
1211c3437056SNickeau    {
1212c3437056SNickeau
1213c3437056SNickeau        try {
1214*04fd306cSNickeau            $fetchPath = $this->markupPath->fetchAnalyticsPath();
1215*04fd306cSNickeau            $analyticsJson = Json::createFromPath($fetchPath);
1216*04fd306cSNickeau        } catch (ExceptionCompile $e) {
1217*04fd306cSNickeau            if (PluginUtility::isDevOrTest()) {
1218*04fd306cSNickeau                throw $e;
1219*04fd306cSNickeau            }
1220*04fd306cSNickeau            throw new ExceptionCompile("Unable to get the analytics document", self::CANONICAL, 0, $e);
1221c3437056SNickeau        }
1222c3437056SNickeau
1223c3437056SNickeau        /**
1224c3437056SNickeau         * Replication Date
1225c3437056SNickeau         */
1226*04fd306cSNickeau        $replicationDateMeta = ReplicationDate::createFromPage($this->markupPath)
1227c3437056SNickeau            ->setWriteStore(MetadataDbStore::class)
12284cadd4f8SNickeau            ->setValue(new DateTime());
1229c3437056SNickeau
1230c3437056SNickeau        /**
1231c3437056SNickeau         * Analytics
1232c3437056SNickeau         */
1233c3437056SNickeau        $analyticsJsonAsString = $analyticsJson->toPrettyJsonString();
1234c3437056SNickeau        $analyticsJsonAsArray = $analyticsJson->toArray();
1235c3437056SNickeau
1236c3437056SNickeau        /**
1237c3437056SNickeau         * Record
1238c3437056SNickeau         */
1239c3437056SNickeau        $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString;
1240*04fd306cSNickeau        $record['IS_LOW_QUALITY'] = ($this->markupPath->isLowQualityPage() === true ? 1 : 0);
1241*04fd306cSNickeau        $record['WORD_COUNT'] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][renderer_plugin_combo_analytics::WORD_COUNT];
1242*04fd306cSNickeau        $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][BacklinkCount::getPersistentName()];
1243c3437056SNickeau        $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue();
1244c3437056SNickeau        $this->upsertAttributes($record);
1245c3437056SNickeau    }
1246c3437056SNickeau
1247*04fd306cSNickeau    private
1248*04fd306cSNickeau    function checkCollision($wikiIdInDatabase, $attribute, $value)
1249*04fd306cSNickeau    {
1250*04fd306cSNickeau        if ($this->markupPath === null) {
1251*04fd306cSNickeau            return;
1252*04fd306cSNickeau        }
1253*04fd306cSNickeau        try {
1254*04fd306cSNickeau            $markupWikiId = $this->markupPath->toWikiPath()->getWikiId();
1255*04fd306cSNickeau        } catch (ExceptionCast $e) {
1256*04fd306cSNickeau            return;
1257*04fd306cSNickeau        }
1258*04fd306cSNickeau        if ($wikiIdInDatabase !== $markupWikiId) {
1259*04fd306cSNickeau            $duplicatePage = MarkupPath::createMarkupFromId($wikiIdInDatabase);
1260*04fd306cSNickeau            if (!FileSystems::exists($duplicatePage)) {
1261*04fd306cSNickeau                // Move
1262*04fd306cSNickeau                LogUtility::info("The non-existing duplicate page ($wikiIdInDatabase) has been added as redirect alias for the page ($this->markupPath)", self::CANONICAL);
1263*04fd306cSNickeau                $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1264*04fd306cSNickeau            } else {
1265*04fd306cSNickeau                // This can happens if two page were created not on the same website
1266*04fd306cSNickeau                // of if the sqlite database was deleted and rebuilt.
1267*04fd306cSNickeau                // The chance is really, really low
1268*04fd306cSNickeau                $errorMessage = "The page ($this->markupPath) and the page ($wikiIdInDatabase) have the same $attribute value ($value)";
1269*04fd306cSNickeau                throw new ExceptionRuntimeInternal($errorMessage, self::CANONICAL);
1270*04fd306cSNickeau                // What to do ?
1271*04fd306cSNickeau                // The database does not allow two page id with the same value
1272*04fd306cSNickeau                // If it happens, ugh, ugh, ..., a replication process between website may be.
1273*04fd306cSNickeau            }
1274*04fd306cSNickeau        }
1275*04fd306cSNickeau    }
1276*04fd306cSNickeau
1277c3437056SNickeau
1278c3437056SNickeau}
1279