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