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