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