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