1<?php
2
3
4namespace ComboStrap;
5
6use DateTime;
7use ModificationDate;
8
9/**
10 * The class that manage the replication
11 * Class Replicate
12 * @package ComboStrap
13 *
14 * The database can also be seen as a {@link MetadataStore}
15 * and an {@link Index}
16 */
17class DatabasePageRow
18{
19
20
21    /**
22     * The list of attributes that are set
23     * at build time
24     * used in the build functions such as {@link DatabasePageRow::getDatabaseRowFromPage()}
25     * to build the sql
26     */
27    private const PAGE_BUILD_ATTRIBUTES =
28        [
29            self::ROWID,
30            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
31            self::ANALYTICS_ATTRIBUTE,
32            PageDescription::PROPERTY_NAME,
33            Canonical::PROPERTY_NAME,
34            ResourceName::PROPERTY_NAME,
35            PageTitle::TITLE,
36            PageH1::PROPERTY_NAME,
37            PagePublicationDate::PROPERTY_NAME,
38            ModificationDate::PROPERTY_NAME,
39            PageCreationDate::PROPERTY_NAME,
40            PagePath::PROPERTY_NAME,
41            StartDate::PROPERTY_NAME,
42            EndDate::PROPERTY_NAME,
43            Region::PROPERTY_NAME,
44            Lang::PROPERTY_NAME,
45            PageType::PROPERTY_NAME,
46            PageId::PROPERTY_NAME,
47            PageId::PAGE_ID_ABBR_ATTRIBUTE,
48            \ReplicationDate::PROPERTY_NAME,
49            BacklinkCount::PROPERTY_NAME
50        ];
51    const ANALYTICS_ATTRIBUTE = "analytics";
52
53    /**
54     * For whatever reason, the row id is lowercase
55     */
56    const ROWID = "rowid";
57
58    const CANONICAL = MetadataDbStore::CANONICAL;
59
60    /**
61     * @var Page
62     */
63    private $page;
64    /**
65     * @var Sqlite|null
66     */
67    private $sqlite;
68
69    /**
70     * @var array
71     */
72    private $row;
73
74    /**
75     * Replicate constructor.
76     */
77    public function __construct()
78    {
79
80        /**
81         * Persist on the DB
82         */
83        $this->sqlite = Sqlite::createOrGetSqlite();
84
85
86    }
87
88    /**
89     * Delete the cache,
90     * Process the analytics
91     * Save it in the Db
92     * Delete from the page to refresh if any
93     *
94     * If you want the analytics:
95     *   * from the cache use {@link self::getAnalyticsFromFs()}
96     *   * from the db use {@link self::getAnalyticsFromDb()}
97     *
98     *
99     * @throws ExceptionCombo
100     */
101    public function replicate(): DatabasePageRow
102    {
103        if ($this->sqlite === null) {
104            throw new ExceptionCombo("Sqlite is mandatory for database replication");
105        }
106
107        if (!$this->page->exists()) {
108            throw new ExceptionCombo("You can't replicate the non-existing page ($this->page) on the file system");
109        }
110
111
112        /**
113         * Page Replication should appears
114         */
115        $this->replicatePage();
116
117        /**
118         * @var Metadata $tabularMetadataToSync
119         */
120        $tabularMetadataToSync = [
121            (new References()),
122            (new Aliases())
123        ];
124        $fsStore = MetadataDokuWikiStore::getOrCreateFromResource($this->page);
125        $dbStore = MetadataDbStore::getOrCreateFromResource($this->page);
126        foreach ($tabularMetadataToSync as $tabular) {
127            $tabular
128                ->setResource($this->page)
129                ->setReadStore($fsStore)
130                ->buildFromReadStore()
131                ->setWriteStore($dbStore)
132                ->persist();
133        }
134
135        /**
136         * Analytics (derived)
137         * Should appear at the end of the replication because it is based
138         * on the previous replication (ie backlink count)
139         */
140        $this->replicateAnalytics();
141
142
143        return $this;
144
145    }
146
147    /**
148     * @throws ExceptionCombo
149     */
150    public function replicateAndRebuild()
151    {
152        $this->replicate();
153        $this->rebuild();
154        return $this;
155    }
156
157    private function addPageIdMeta(array &$metaRecord)
158    {
159        $metaRecord[PageId::PROPERTY_NAME] = $this->page->getPageId();
160        $metaRecord[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->page->getPageIdAbbr();
161    }
162
163    public static function createFromPageId(string $pageId): DatabasePageRow
164    {
165        $databasePage = new DatabasePageRow();
166        $row = $databasePage->getDatabaseRowFromPageId($pageId);
167        if ($row != null) {
168            $databasePage->setRow($row);
169        }
170        return $databasePage;
171    }
172
173    public static function createFromPageObject(Page $page): DatabasePageRow
174    {
175
176        $databasePage = new DatabasePageRow();
177        $row = $databasePage->getDatabaseRowFromPage($page);
178        if ($row !== null) {
179            $databasePage->setRow($row);
180        }
181        return $databasePage;
182    }
183
184    public static function createFromPageIdAbbr(string $pageIdAbbr): DatabasePageRow
185    {
186        $databasePage = new DatabasePageRow();
187        $row = $databasePage->getDatabaseRowFromAttribute(PageId::PAGE_ID_ABBR_ATTRIBUTE, $pageIdAbbr);
188        if ($row != null) {
189            $databasePage->setRow($row);
190        }
191        return $databasePage;
192
193    }
194
195    /**
196     * @param $canonical
197     * @return DatabasePageRow
198     */
199    public static function createFromCanonical($canonical): DatabasePageRow
200    {
201
202        DokuPath::addRootSeparatorIfNotPresent($canonical);
203        $databasePage = new DatabasePageRow();
204        $row = $databasePage->getDatabaseRowFromAttribute(Canonical::PROPERTY_NAME, $canonical);
205        if ($row != null) {
206            $databasePage->setRow($row);
207        }
208        return $databasePage;
209
210
211    }
212
213    public static function createFromAlias($alias): DatabasePageRow
214    {
215
216        DokuPath::addRootSeparatorIfNotPresent($alias);
217        $databasePage = new DatabasePageRow();
218        $row = $databasePage->getDatabaseRowFromAlias($alias);
219        if ($row != null) {
220            $databasePage->setRow($row);
221            $page = $databasePage->getPage();
222            if ($page !== null) {
223                // page may be null in production
224                // PHP Fatal error:  Uncaught Error: Call to a member function setBuildAliasPath() on null in
225                // /opt/www/bytle/farmer.bytle.net/lib/plugins/combo/ComboStrap/DatabasePageRow.php:220
226                $page->setBuildAliasPath($alias);
227            }
228        }
229        return $databasePage;
230
231    }
232
233    public static function createFromDokuWikiId($id): DatabasePageRow
234    {
235        $databasePage = new DatabasePageRow();
236        $row = $databasePage->getDatabaseRowFromDokuWikiId($id);
237        if ($row !== null) {
238            $databasePage->setRow($row);
239        }
240        return $databasePage;
241    }
242
243    public function getPageId()
244    {
245        return $this->getFromRow(PageId::PROPERTY_NAME);
246    }
247
248
249    public
250    function shouldReplicate(): bool
251    {
252
253        $dateReplication = $this->getReplicationDate();
254        if ($dateReplication === null) {
255            return true;
256        }
257
258        /**
259         * When the replication date is older than the actual document
260         */
261        $modifiedTime = FileSystems::getModifiedTime($this->page->getPath());
262        if ($modifiedTime > $dateReplication) {
263            return true;
264        }
265
266        /**
267         * When the file does not exist
268         */
269        $exist = FileSystems::exists($this->page->getAnalyticsDocument()->getCachePath());
270        if (!$exist) {
271            return true;
272        }
273
274        /**
275         * When the analytics document is older
276         */
277        $modifiedTime = FileSystems::getModifiedTime($this->page->getAnalyticsDocument()->getCachePath());
278        if ($modifiedTime > $dateReplication) {
279            return true;
280        }
281
282
283        /**
284         * When the database version file is higher
285         */
286        $version = LocalPath::createFromPath(__DIR__ . "/../db/latest.version");
287        $versionModifiedTime = FileSystems::getModifiedTime($version);
288        if ($versionModifiedTime > $dateReplication) {
289            return true;
290        }
291
292        /**
293         * When the class date time is higher
294         */
295        $code = LocalPath::createFromPath(__DIR__ . "/DatabasePageRow.php");
296        $codeModified = FileSystems::getModifiedTime($code);
297        if ($codeModified > $dateReplication) {
298            return true;
299        }
300
301        return false;
302
303    }
304
305    public
306    function delete()
307    {
308
309        $request = Sqlite::createOrGetSqlite()
310            ->createRequest()
311            ->setQueryParametrized('delete from pages where id = ?', [$this->page->getDokuwikiId()]);
312        try {
313            $request->execute();
314        } catch (ExceptionCombo $e) {
315            LogUtility::msg("Something went wrong when deleting the page ({$this->page}) from the database");
316        } finally {
317            $request->close();
318        }
319        $this->buildInitObjectFields();
320
321    }
322
323    /**
324     * @return Json|null the analytics array or null if not in db
325     */
326    public
327    function getAnalyticsData(): ?Json
328    {
329
330        $jsonString = $this->getFromRow(self::ANALYTICS_ATTRIBUTE);
331        if ($jsonString === null) {
332            return null;
333        }
334        try {
335            return Json::createFromString($jsonString);
336        } catch (ExceptionCombo $e) {
337            LogUtility::msg("Error while building back the analytics JSON object. {$e->getMessage()}");
338            return null;
339        }
340
341    }
342
343    /**
344     * Return the database row
345     *
346     *
347     */
348    private
349    function getDatabaseRowFromPage(Page $page): ?array
350    {
351
352        $this->setPage($page);
353
354        if ($this->sqlite === null) return null;
355
356        // Do we have a page attached to this page id
357        $pageId = $page->getPageId();
358        if ($pageId !== null) {
359            $row = $this->getDatabaseRowFromPageId($pageId);
360            if ($row !== null) {
361                return $row;
362            }
363        }
364
365        // Do we have a page attached to the canonical
366        $canonical = $page->getCanonical();
367        if ($canonical != null) {
368            $row = $this->getDatabaseRowFromCanonical($canonical);
369            if ($row !== null) {
370                return $row;
371            }
372        }
373
374        // Do we have a page attached to the path
375        $path = $page->getPath();
376        $row = $this->getDatabaseRowFromPath($path);
377        if ($row !== null) { // the path may no yet updated in the db
378            return $row;
379        }
380
381        /**
382         * Do we have a page attached to this ID
383         */
384        $id = $page->getPath()->getDokuwikiId();
385        return $this->getDatabaseRowFromDokuWikiId($id);
386
387
388    }
389
390
391    /**
392     * @return DateTime|null
393     */
394    public function getReplicationDate(): ?DateTime
395    {
396        $dateString = $this->getFromRow(\ReplicationDate::getPersistentName());
397        if ($dateString === null) {
398            return null;
399        }
400        try {
401            return Iso8601Date::createFromString($dateString)->getDateTime();
402        } catch (ExceptionCombo $e) {
403            LogUtility::msg("Error while reading the replication date in the database. {$e->getMessage()}");
404            return null;
405        }
406
407    }
408
409    /**
410     * @return bool
411     * @throws ExceptionCombo
412     */
413    public function replicatePage(): bool
414    {
415
416        if (!$this->page->exists()) {
417            throw new ExceptionCombo("You can't replicate the page ($this->page) because it does not exists.");
418        }
419
420        /**
421         * Replication Date
422         */
423        $replicationDate = \ReplicationDate::createFromPage($this->page)
424            ->setWriteStore(MetadataDbStore::class)
425            ->setValue(new DateTime());
426
427        /**
428         * Convenient variable
429         */
430        $page = $this->page;
431
432
433        /**
434         * Same data as {@link Page::getMetadataForRendering()}
435         */
436        $record = $this->getMetaRecord();
437        $record['IS_HOME'] = ($page->isHomePage() === true ? 1 : 0);
438        $record[$replicationDate::getPersistentName()] = $replicationDate->toStoreValue();
439
440        return $this->upsertAttributes($record);
441
442    }
443
444
445    /**
446     * @return bool when an update as occurred
447     *
448     * Attribute that are scalar / modifiable in the database
449     * (not aliases or json data for instance)
450     */
451    public function replicateMetaAttributes(): bool
452    {
453
454        return $this->upsertAttributes($this->getMetaRecord());
455
456    }
457
458    public function upsertAttributes(array $attributes): bool
459    {
460
461        if ($this->sqlite === null) {
462            return false;
463        }
464
465        if (empty($attributes)) {
466            LogUtility::msg("The page database attribute passed should not be empty");
467            return false;
468        }
469
470        $values = [];
471        $columnClauses = [];
472        foreach ($attributes as $key => $value) {
473            if (is_array($value)) {
474                throw new ExceptionComboRuntime("The attribute ($key) has value that is an array (" . implode(", ", $value) . ")");
475            }
476            $columnClauses[] = "$key = ?";
477            $values[$key] = $value;
478        }
479
480        /**
481         * Primary key has moved during the time
482         * It should be the UUID but not for older version
483         *
484         * If the primary key is null, no record was found
485         */
486        $rowId = $this->getRowId();
487        if ($rowId !== null) {
488            /**
489             * We just add the primary key
490             * otherwise as this is a associative
491             * array, we will miss a value for the update statement
492             */
493            $values[] = $rowId;
494
495            $updateStatement = "update PAGES SET " . implode(", ", $columnClauses) . " where ROWID = ?";
496            $request = $this->sqlite
497                ->createRequest()
498                ->setQueryParametrized($updateStatement, $values);
499            $countChanges = 0;
500            try {
501                $countChanges = $request
502                    ->execute()
503                    ->getChangeCount();
504            } catch (ExceptionCombo $e) {
505                LogUtility::msg("There was a problem during the page attribute updates. : {$e->getMessage()}");
506                return false;
507            } finally {
508                $request->close();
509            }
510            if ($countChanges !== 1) {
511                LogUtility::msg("The database replication has not updated exactly 1 record but ($countChanges) record", LogUtility::LVL_MSG_ERROR, \action_plugin_combo_fulldatabasereplication::CANONICAL);
512            }
513
514        } else {
515
516            /**
517             * Creation
518             */
519            if ($this->page === null) {
520                LogUtility::msg("The page should be defined to create a page row");
521                return false;
522            }
523            $values[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] = $this->page->getPath()->getDokuwikiId();
524            $values[PagePath::PROPERTY_NAME] = $this->page->getPath()->toAbsolutePath()->toString();
525            /**
526             * Default implements the auto-canonical feature
527             */
528            $values[Canonical::PROPERTY_NAME] = $this->page->getCanonicalOrDefault();
529
530            /**
531             * Analytics
532             */
533            if (!isset($values[self::ANALYTICS_ATTRIBUTE])) {
534                // otherwise we get an empty string
535                // and a json function will not work
536                $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString();
537            }
538
539            /**
540             * Page Id / Abbr are mandatory for url redirection
541             */
542            $this->addPageIdAttributeIfNeeded($values);
543
544            $request = $this->sqlite
545                ->createRequest()
546                ->setTableRow('PAGES', $values);
547            try {
548                /**
549                 * rowid is used in {@link DatabasePageRow::exists()}
550                 * to check if the page exists in the database
551                 * We update it
552                 */
553                $this->row[self::ROWID] = $request
554                    ->execute()
555                    ->getInsertId();
556                $this->row = array_merge($values, $this->row);
557            } catch (ExceptionCombo $e) {
558                LogUtility::msg("There was a problem during the updateAttributes insert. : {$e->getMessage()}");
559                return false;
560            } finally {
561                $request->close();
562            }
563
564        }
565        return true;
566
567    }
568
569    public
570    function getDescription()
571    {
572        return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY);
573    }
574
575
576    public
577    function getPageName()
578    {
579        return $this->getFromRow(ResourceName::PROPERTY_NAME);
580    }
581
582    public
583    function exists(): bool
584    {
585        return $this->getFromRow(self::ROWID) !== null;
586    }
587
588    /**
589     * Called when a page is moved
590     * @param $targetId
591     */
592    public
593    function updatePathAndDokuwikiId($targetId)
594    {
595        if (!$this->exists()) {
596            LogUtility::msg("The `database` page ($this) does not exist and cannot be moved to ($targetId)", LogUtility::LVL_MSG_ERROR);
597        }
598
599        $path = $targetId;
600        DokuPath::addRootSeparatorIfNotPresent($path);
601        $attributes = [
602            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId,
603            PagePath::PROPERTY_NAME => $path
604        ];
605
606        $this->upsertAttributes($attributes);
607
608    }
609
610    public
611    function __toString()
612    {
613        return $this->page->__toString();
614    }
615
616
617    /**
618     * Redirect are now added during a move
619     * Not when a duplicate is found.
620     * With the advent of the page id, it should never occurs anymore
621     * @param Page $page
622     * @deprecated 2012-10-28
623     */
624    private
625    function deleteIfExistsAndAddRedirectAlias(Page $page): void
626    {
627
628        if ($this->page != null) {
629            $page->getDatabasePage()->deleteIfExist();
630            $this->addRedirectAliasWhileBuildingRow($page);
631        }
632
633    }
634
635    public
636    function getCanonical()
637    {
638        return $this->getFromRow(Canonical::PROPERTY_NAME);
639    }
640
641    /**
642     * Set the field to their values
643     * @param $row
644     */
645    public
646    function setRow($row)
647    {
648        if ($row === null) {
649            LogUtility::msg("A row should not be null");
650            return;
651        }
652        if (!is_array($row)) {
653            LogUtility::msg("A row should be an array");
654            return;
655        }
656
657        /**
658         * All get function lookup the row
659         */
660        $this->row = $row;
661
662
663    }
664
665    private
666    function buildInitObjectFields()
667    {
668        $this->row = null;
669
670    }
671
672    public
673    function rebuild(): DatabasePageRow
674    {
675
676        if ($this->page != null) {
677            $this->page->rebuild();
678            $row = $this->getDatabaseRowFromPage($this->page);
679            if ($row !== null) {
680                $this->setRow($row);
681            }
682        }
683        return $this;
684
685    }
686
687    /**
688     * @return array - an array of the fix page metadata (ie not derived)
689     * Therefore quick to insert/update
690     *
691     */
692    private
693    function getMetaRecord(): array
694    {
695        $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->page);
696        $targetStore = MetadataDbStore::getOrCreateFromResource($this->page);
697
698        $record = array(
699            Canonical::PROPERTY_NAME,
700            PagePath::PROPERTY_NAME,
701            ResourceName::PROPERTY_NAME,
702            PageTitle::TITLE,
703            PageH1::PROPERTY_NAME,
704            PageDescription::PROPERTY_NAME,
705            PageCreationDate::PROPERTY_NAME,
706            ModificationDate::PROPERTY_NAME,
707            PagePublicationDate::PROPERTY_NAME,
708            StartDate::PROPERTY_NAME,
709            EndDate::PROPERTY_NAME,
710            Region::PROPERTY_NAME,
711            Lang::PROPERTY_NAME,
712            PageType::PROPERTY_NAME,
713            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
714        );
715        $metaRecord = [];
716        foreach ($record as $name) {
717            $metadata = Metadata::getForName($name);
718            if ($metadata === null) {
719                throw new ExceptionComboRuntime("The metadata ($name) is unknown");
720            }
721            $metaRecord[$name] = $metadata
722                ->setResource($this->page)
723                ->setReadStore($sourceStore)
724                ->buildFromReadStore()
725                ->setWriteStore($targetStore)
726                ->toStoreValueOrDefault(); // used by the template, the value is or default
727        }
728
729        if ($this->page->getPageId() !== null) {
730            $this->addPageIdMeta($metaRecord);
731        }
732        return $metaRecord;
733    }
734
735    public
736    function deleteIfExist(): DatabasePageRow
737    {
738        if ($this->exists()) {
739            $this->delete();
740        }
741        return $this;
742    }
743
744    public
745    function getRowId()
746    {
747        return $this->getFromRow(self::ROWID);
748    }
749
750    private
751    function getDatabaseRowFromPageId(string $pageId)
752    {
753
754        if ($this->sqlite === null) {
755            return null;
756        }
757
758        $pageIdAttribute = PageId::PROPERTY_NAME;
759        $query = $this->getParametrizedLookupQuery($pageIdAttribute);
760        $request = Sqlite::createOrGetSqlite()
761            ->createRequest()
762            ->setQueryParametrized($query, [$pageId]);
763        $rows = [];
764        try {
765            $rows = $request
766                ->execute()
767                ->getRows();
768        } catch (ExceptionCombo $e) {
769            LogUtility::msg($e->getMessage(), LogUtility::LVL_MSG_ERROR, $e->getCanonical());
770            return null;
771        } finally {
772            $request->close();
773        }
774
775        switch (sizeof($rows)) {
776            case 0:
777                return null;
778            case 1:
779                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
780                /**
781                 * Page Id Collision detection
782                 */
783                if ($this->page != null && $id !== $this->page->getDokuwikiId()) {
784                    $duplicatePage = Page::createPageFromId($id);
785                    if (!$duplicatePage->exists()) {
786                        // Move
787                        LogUtility::msg("The non-existing duplicate page ($id) has been added as redirect alias for the page ($this->page)", LogUtility::LVL_MSG_INFO);
788                        $this->addRedirectAliasWhileBuildingRow($duplicatePage);
789                    } else {
790                        // This can happens if two page were created not on the same website
791                        // of if the sqlite database was deleted and rebuilt.
792                        // The chance is really, really low
793                        $errorMessage = "The page ($this->page) and the page ($id) have the same page id ($pageId)";
794                        LogUtility::msg($errorMessage, LogUtility::LVL_MSG_ERROR, self::CANONICAL);
795                        // What to do ?
796                        // The database does not allow two page id with the same value
797                        // If it happens, ugh, ugh, ..., a replication process between website may be.
798                        return null;
799                    }
800                }
801                return $rows[0];
802            default:
803                $existingPages = implode(", ", $rows);
804                LogUtility::msg("The pages ($existingPages) have all the same page id ($pageId)", LogUtility::LVL_MSG_ERROR);
805                return null;
806        }
807
808    }
809
810
811    private
812    function getParametrizedLookupQuery(string $pageIdAttribute): string
813    {
814        $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES);
815        return "$select where $pageIdAttribute = ?";
816    }
817
818
819    public function setPage(Page $page)
820    {
821        $this->page = $page;
822        return $this;
823    }
824
825    private
826    function getDatabaseRowFromCanonical($canonical)
827    {
828        $query = $this->getParametrizedLookupQuery(Canonical::PROPERTY_NAME);
829        $request = $this->sqlite
830            ->createRequest()
831            ->setQueryParametrized($query, [$canonical]);
832        $rows = [];
833        try {
834            $rows = $request
835                ->execute()
836                ->getRows();
837        } catch (ExceptionCombo $e) {
838            LogUtility::msg("An exception has occurred with the page search from CANONICAL. " . $e->getMessage());
839            return null;
840        } finally {
841            $request->close();
842        }
843
844        switch (sizeof($rows)) {
845            case 0:
846                return null;
847            case 1:
848                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
849                if ($this->page !== null && $id !== $this->page->getDokuwikiId()) {
850                    $duplicatePage = Page::createPageFromId($id);
851                    if (!$duplicatePage->exists()) {
852                        $this->addRedirectAliasWhileBuildingRow($duplicatePage);
853                        LogUtility::msg("The non-existing duplicate page ($id) has been added as redirect alias for the page ($this->page)", LogUtility::LVL_MSG_INFO);
854                    } else {
855                        LogUtility::msg("The page ($this->page) and the page ($id) have the same canonical ($canonical)", LogUtility::LVL_MSG_ERROR);
856                    }
857                }
858                return $rows[0];
859            default:
860                $existingPages = [];
861                foreach ($rows as $row) {
862                    $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
863                    $duplicatePage = Page::createPageFromId($id);
864                    if (!$duplicatePage->exists()) {
865
866                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
867
868                    } else {
869
870                        /**
871                         * Check if the error may come from the auto-canonical
872                         * (Never ever save generated data)
873                         */
874                        $canonicalLastNamesCount = PluginUtility::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0);
875                        if ($canonicalLastNamesCount > 0) {
876                            $this->page->unsetMetadata(Canonical::PROPERTY_NAME);
877                            $duplicatePage->unsetMetadata(Canonical::PROPERTY_NAME);
878                        }
879
880                        $existingPages[] = $row;
881                    }
882                }
883                if (sizeof($existingPages) === 1) {
884                    return $existingPages[0];
885                } else {
886                    $existingPages = implode(", ", $existingPages);
887                    LogUtility::msg("The existing pages ($existingPages) have all the same canonical ($canonical)", LogUtility::LVL_MSG_ERROR);
888                    return null;
889                }
890        }
891    }
892
893    private
894    function getDatabaseRowFromPath(string $path): ?array
895    {
896        DokuPath::addRootSeparatorIfNotPresent($path);
897        return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path);
898    }
899
900    private
901    function getDatabaseRowFromDokuWikiId(string $id): ?array
902    {
903        return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
904    }
905
906    public
907    function getDatabaseRowFromAttribute(string $attribute, string $value)
908    {
909        $query = $this->getParametrizedLookupQuery($attribute);
910        $request = $this->sqlite
911            ->createRequest()
912            ->setQueryParametrized($query, [$value]);
913        $rows = [];
914        try {
915            $rows = $request
916                ->execute()
917                ->getRows();
918        } catch (ExceptionCombo $e) {
919            LogUtility::msg("An exception has occurred with the page search from a PATH: " . $e->getMessage());
920            return null;
921        } finally {
922            $request->close();
923        }
924
925        switch (sizeof($rows)) {
926            case 0:
927                return null;
928            case 1:
929                $value = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
930                if ($this->page != null && $value !== $this->page->getDokuwikiId()) {
931                    $duplicatePage = Page::createPageFromId($value);
932                    if (!$duplicatePage->exists()) {
933                        $this->addRedirectAliasWhileBuildingRow($duplicatePage);
934                    } else {
935                        LogUtility::msg("The page ($this->page) and the page ($value) have the same $attribute ($value)", LogUtility::LVL_MSG_ERROR);
936                    }
937                }
938                return $rows[0];
939            default:
940                $existingPages = [];
941                foreach ($rows as $row) {
942                    $value = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
943                    $duplicatePage = Page::createPageFromId($value);
944                    if (!$duplicatePage->exists()) {
945
946                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
947
948                    } else {
949                        $existingPages[] = $row;
950                    }
951                }
952                if (sizeof($existingPages) === 1) {
953                    return $existingPages[0];
954                } else {
955                    $existingPages = implode(", ", $existingPages);
956                    LogUtility::msg("The existing pages ($existingPages) have all the same $attribute ($value)", LogUtility::LVL_MSG_ERROR);
957                    return null;
958                }
959        }
960    }
961
962    public
963    function getPage(): ?Page
964    {
965        if (
966            $this->page === null
967            && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null
968        ) {
969            $this->page = Page::createPageFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]);
970        }
971        return $this->page;
972    }
973
974    private
975    function getDatabaseRowFromAlias($alias): ?array
976    {
977
978        $pageIdAttribute = PageId::PROPERTY_NAME;
979        $buildFields = self::PAGE_BUILD_ATTRIBUTES;
980        $fields = array_reduce($buildFields, function ($carry, $element) {
981            if ($carry !== null) {
982                return "$carry, p.{$element}";
983            } else {
984                return "p.{$element}";
985            }
986        }, null);
987        $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? ";
988        $request = $this->sqlite
989            ->createRequest()
990            ->setQueryParametrized($query, [$alias]);
991        $rows = [];
992        try {
993            $rows = $request
994                ->execute()
995                ->getRows();
996        } catch (ExceptionCombo $e) {
997            LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}");
998            return null;
999        } finally {
1000            $request->close();
1001        }
1002        switch (sizeof($rows)) {
1003            case 0:
1004                return null;
1005            case 1:
1006                return $rows[0];
1007            default:
1008                $id = $rows[0]['ID'];
1009                $pages = implode(",",
1010                    array_map(
1011                        function ($row) {
1012                            return $row['ID'];
1013                        },
1014                        $rows
1015                    )
1016                );
1017                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);
1018                return $rows[0];
1019        }
1020    }
1021
1022
1023    /**
1024     * Utility function
1025     * @param Page $pageAlias
1026     */
1027    private
1028    function addRedirectAliasWhileBuildingRow(Page $pageAlias)
1029    {
1030
1031        $aliasPath = $pageAlias->getPath()->toString();
1032        try {
1033            Aliases::createForPage($this->page)
1034                ->addAlias($aliasPath)
1035                ->sendToWriteStore();
1036        } catch (ExceptionCombo $e) {
1037            // we don't throw while getting
1038            LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->page)");
1039        }
1040
1041    }
1042
1043    private
1044    function addPageIdAttributeIfNeeded(array &$values)
1045    {
1046        if (!isset($values[PageId::getPersistentName()])) {
1047            $values[PageId::getPersistentName()] = $this->page->getPageId();
1048        }
1049        if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) {
1050            $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->page->getPageIdAbbr();
1051        }
1052    }
1053
1054    public
1055    function getFromRow(string $attribute)
1056    {
1057        if ($this->row === null) {
1058            return null;
1059        }
1060
1061        if (!array_key_exists($attribute, $this->row)) {
1062            /**
1063             * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES}
1064             * or in the table
1065             */
1066            throw new ExceptionComboRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical());
1067        }
1068
1069        $value = $this->row[$attribute];
1070
1071        if ($value !== null) {
1072            return $value;
1073        }
1074
1075        // don't know why but the sqlite plugin returns them uppercase
1076        // rowid is returned lowercase from the sqlite plugin
1077        $upperAttribute = strtoupper($attribute);
1078        return $this->row[$upperAttribute];
1079
1080    }
1081
1082
1083    /**
1084     * @throws ExceptionCombo
1085     */
1086    public function replicateAnalytics()
1087    {
1088
1089        try {
1090            $analyticsJson = $this->page->getAnalyticsDocument()->getOrProcessJson();
1091        } catch (ExceptionCombo $e) {
1092            throw new ExceptionCombo("Unable to get the analytics document", self::CANONICAL, 0, $e);
1093        }
1094
1095        /**
1096         * Replication Date
1097         */
1098        $replicationDateMeta = \ReplicationDate::createFromPage($this->page)
1099            ->setWriteStore(MetadataDbStore::class)
1100            ->setValue(new DateTime());
1101
1102        /**
1103         * Analytics
1104         */
1105        $analyticsJsonAsString = $analyticsJson->toPrettyJsonString();
1106        $analyticsJsonAsArray = $analyticsJson->toArray();
1107
1108        /**
1109         * Record
1110         */
1111        $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString;
1112        $record['IS_LOW_QUALITY'] = ($this->page->isLowQualityPage() === true ? 1 : 0);
1113        $record['WORD_COUNT'] = $analyticsJsonAsArray[AnalyticsDocument::STATISTICS][AnalyticsDocument::WORD_COUNT];
1114        $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[AnalyticsDocument::STATISTICS][BacklinkCount::getPersistentName()];
1115        $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue();
1116        $this->upsertAttributes($record);
1117    }
1118
1119
1120}
1121