xref: /plugin/combo/ComboStrap/DatabasePageRow.php (revision 4ebc325786ead9709aa582b2a0377b9dac4403d9)
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];
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            if (!isset($values[self::ANALYTICS_ATTRIBUTE])) {
657                // otherwise we get an empty string
658                // and a json function will not work
659                $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString();
660            }
661
662            /**
663             * Page Id / Abbr are mandatory for url redirection
664             */
665            $this->addPageIdAttributeIfNeeded($values);
666
667            $request = $this->sqlite
668                ->createRequest()
669                ->setTableRow('PAGES', $values);
670            try {
671                /**
672                 * rowid is used in {@link DatabasePageRow::exists()}
673                 * to check if the page exists in the database
674                 * We update it
675                 */
676                $this->row[self::ROWID] = $request
677                    ->execute()
678                    ->getInsertId();
679                $this->row = array_merge($values, $this->row);
680            } catch (ExceptionCompile $e) {
681                throw new ExceptionBadState("There was a problem during the updateAttributes insert. : {$e->getMessage()}");
682            } finally {
683                $request->close();
684            }
685
686        }
687
688    }
689
690    public
691    function getDescription()
692    {
693        return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY);
694    }
695
696
697    public
698    function getPageName()
699    {
700        return $this->getFromRow(ResourceName::PROPERTY_NAME);
701    }
702
703    public
704    function exists(): bool
705    {
706        return $this->getFromRow(self::ROWID) !== null;
707    }
708
709    /**
710     * Called when a page is moved
711     * @param $targetId
712     */
713    public
714    function updatePathAndDokuwikiId($targetId)
715    {
716        if (!$this->exists()) {
717            LogUtility::error("The `database` page ($this) does not exist and cannot be moved to ($targetId)");
718        }
719
720        $path = $targetId;
721        WikiPath::addRootSeparatorIfNotPresent($path);
722        $attributes = [
723            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId,
724            PagePath::PROPERTY_NAME => $path
725        ];
726
727        $this->upsertAttributes($attributes);
728
729    }
730
731    public
732    function __toString()
733    {
734        return $this->markupPath->__toString();
735    }
736
737
738    /**
739     * Redirect are now added during a move
740     * Not when a duplicate is found.
741     * With the advent of the page id, it should never occurs anymore
742     * @param MarkupPath $page
743     * @deprecated 2012-10-28
744     */
745    private
746    function deleteIfExistsAndAddRedirectAlias(MarkupPath $page): void
747    {
748
749        if ($this->markupPath != null) {
750            $page->getDatabasePage()->deleteIfExist();
751            $this->addRedirectAliasWhileBuildingRow($page);
752        }
753
754    }
755
756    public
757    function getCanonical()
758    {
759        return $this->getFromRow(Canonical::PROPERTY_NAME);
760    }
761
762    /**
763     * Set the field to their values
764     * @param $row
765     */
766    public
767    function setRow($row)
768    {
769        if ($row === null) {
770            LogUtility::msg("A row should not be null");
771            return;
772        }
773        if (!is_array($row)) {
774            LogUtility::msg("A row should be an array");
775            return;
776        }
777
778        /**
779         * All get function lookup the row
780         */
781        $this->row = $row;
782
783
784    }
785
786    private
787    function buildInitObjectFields()
788    {
789        $this->row = null;
790
791    }
792
793    public
794    function rebuild(): DatabasePageRow
795    {
796
797        if ($this->markupPath != null) {
798            $this->markupPath->rebuild();
799            try {
800                $row = $this->getDatabaseRowFromPage($this->markupPath);
801                $this->setRow($row);
802            } catch (ExceptionNotExists $e) {
803                // ok
804            }
805        }
806        return $this;
807
808    }
809
810    /**
811     * @return array - an array of the fix page metadata (ie not derived)
812     * Therefore quick to insert/update
813     *
814     */
815    private
816    function getMetaRecord(): array
817    {
818        $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath);
819        $targetStore = MetadataDbStore::getOrCreateFromResource($this->markupPath);
820
821        $record = array(
822            Canonical::PROPERTY_NAME,
823            PagePath::PROPERTY_NAME,
824            ResourceName::PROPERTY_NAME,
825            PageTitle::TITLE,
826            PageH1::PROPERTY_NAME,
827            PageDescription::PROPERTY_NAME,
828            CreationDate::PROPERTY_NAME,
829            ModificationDate::PROPERTY_NAME,
830            PagePublicationDate::PROPERTY_NAME,
831            StartDate::PROPERTY_NAME,
832            EndDate::PROPERTY_NAME,
833            Region::PROPERTY_NAME,
834            Lang::PROPERTY_NAME,
835            PageType::PROPERTY_NAME,
836            DokuwikiId::DOKUWIKI_ID_ATTRIBUTE,
837            PageLevel::PROPERTY_NAME
838        );
839        $metaRecord = [];
840        foreach ($record as $name) {
841            try {
842                $metadata = Meta\Api\MetadataSystem::getForName($name);
843            } catch (ExceptionNotFound $e) {
844                LogUtility::internalError("The metadata ($name) is unknown", self::CANONICAL);
845                continue;
846            }
847            $metaRecord[$name] = $metadata
848                ->setResource($this->markupPath)
849                ->setReadStore($sourceStore)
850                ->buildFromReadStore()
851                ->setWriteStore($targetStore)
852                ->toStoreValueOrDefault(); // used by the template, the value is or default
853
854        }
855
856        try {
857            $this->addPageIdMeta($metaRecord);
858        } catch (ExceptionNotExists $e) {
859            // no page id for non-existent page ok
860        }
861
862        // Is index
863        $metaRecord[self::IS_INDEX_COLUMN] = ($this->markupPath->isIndexPage() === true ? 1 : 0);
864
865        return $metaRecord;
866    }
867
868    public
869    function deleteIfExist(): DatabasePageRow
870    {
871        if ($this->exists()) {
872            $this->delete();
873        }
874        return $this;
875    }
876
877    public
878    function getRowId()
879    {
880        return $this->getFromRow(self::ROWID);
881    }
882
883    /**
884     * @throws ExceptionNotFound
885     */
886    private
887    function getDatabaseRowFromPageId(string $pageIdValue)
888    {
889
890        $pageIdAttribute = PageId::PROPERTY_NAME;
891        $query = $this->getParametrizedLookupQuery($pageIdAttribute);
892        $request = Sqlite::createOrGetSqlite()
893            ->createRequest()
894            ->setQueryParametrized($query, [$pageIdValue]);
895        $rows = [];
896        try {
897            $rows = $request
898                ->execute()
899                ->getRows();
900        } catch (ExceptionCompile $e) {
901            throw new ExceptionRuntimeInternal("Error while retrieving the object by id", self::CANONICAL, 1, $e);
902        } finally {
903            $request->close();
904        }
905
906        switch (sizeof($rows)) {
907            case 0:
908                throw new ExceptionNotFound("No object by page id");
909            case 1:
910                /**
911                 * Page Id Collision detection
912                 */
913                $rowId = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
914                $this->checkCollision($rowId, $pageIdAttribute, $pageIdValue);
915                return $rows[0];
916            default:
917                $existingPages = implode(", ", $rows);
918                $message = "The pages ($existingPages) have all the same page id ($pageIdValue)";
919                throw new ExceptionRuntimeInternal($message, self::CANONICAL);
920        }
921
922    }
923
924
925    private
926    function getParametrizedLookupQuery(string $attributeName): string
927    {
928        $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES);
929        return "$select where $attributeName = ?";
930    }
931
932
933    public
934    function setMarkupPath(MarkupPath $page)
935    {
936        $this->markupPath = $page;
937        return $this;
938    }
939
940    /**
941     * @throws ExceptionNotFound
942     */
943    private
944    function getDatabaseRowFromCanonical($canonicalValue)
945    {
946        $canoncialName = Canonical::PROPERTY_NAME;
947        $query = $this->getParametrizedLookupQuery($canoncialName);
948        $request = $this->sqlite
949            ->createRequest()
950            ->setQueryParametrized($query, [$canonicalValue]);
951        $rows = [];
952        try {
953            $rows = $request
954                ->execute()
955                ->getRows();
956        } catch (ExceptionCompile $e) {
957            throw new ExceptionRuntime("An exception has occurred with the page search from CANONICAL. " . $e->getMessage());
958        } finally {
959            $request->close();
960        }
961
962        switch (sizeof($rows)) {
963            case 0:
964                throw new ExceptionNotFound("No canonical row was found");
965            case 1:
966                $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
967                $this->checkCollision($id, $canoncialName, $canonicalValue);
968                return $rows[0];
969            default:
970                $existingPages = [];
971                foreach ($rows as $row) {
972                    $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
973                    $duplicatePage = MarkupPath::createMarkupFromId($id);
974                    if (!$duplicatePage->exists()) {
975
976                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
977
978                    } else {
979
980                        /**
981                         * Check if the error may come from the auto-canonical
982                         * (Never ever save generated data)
983                         */
984                        $canonicalLastNamesCount = SiteConfig::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0);
985                        if ($canonicalLastNamesCount > 0) {
986                            $this->markupPath->unsetMetadata($canoncialName);
987                            $duplicatePage->unsetMetadata($canoncialName);
988                        }
989
990                        $existingPages[] = $row;
991                    }
992                }
993                if (sizeof($existingPages) > 1) {
994                    $existingPages = implode(", ", $existingPages);
995                    $message = "The existing pages ($existingPages) have all the same canonical ($canonicalValue), return the first one";
996                    LogUtility::error($message, self::CANONICAL);
997                }
998                return $existingPages[0];
999        }
1000    }
1001
1002    /**
1003     * @throws ExceptionNotFound
1004     */
1005    private
1006    function getDatabaseRowFromPath(string $path): ?array
1007    {
1008        WikiPath::addRootSeparatorIfNotPresent($path);
1009        return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path);
1010    }
1011
1012    /**
1013     * @throws ExceptionNotFound
1014     */
1015    private
1016    function getDatabaseRowFromDokuWikiId(string $id): array
1017    {
1018        return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id);
1019    }
1020
1021    /**
1022     * @throws ExceptionNotFound
1023     */
1024    public
1025    function getDatabaseRowFromAttribute(string $attribute, string $value)
1026    {
1027        $query = $this->getParametrizedLookupQuery($attribute);
1028        $request = $this->sqlite
1029            ->createRequest()
1030            ->setQueryParametrized($query, [$value]);
1031        $rows = [];
1032        try {
1033            $rows = $request
1034                ->execute()
1035                ->getRows();
1036        } catch (ExceptionCompile $e) {
1037            $message = "Internal Error: An exception has occurred with the page search from a PATH: " . $e->getMessage();
1038            LogUtility::log2file($message);
1039            throw new ExceptionNotFound($message);
1040        } finally {
1041            $request->close();
1042        }
1043
1044        switch (sizeof($rows)) {
1045            case 0:
1046                throw new ExceptionNotFound("No database row found for the page");
1047            case 1:
1048                $value = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1049                if ($this->markupPath != null && $value !== $this->markupPath->getWikiId()) {
1050                    $duplicatePage = MarkupPath::createMarkupFromId($value);
1051                    if (!$duplicatePage->exists()) {
1052                        $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1053                    } else {
1054                        LogUtility::msg("The page ($this->markupPath) and the page ($value) have the same $attribute ($value)", LogUtility::LVL_MSG_ERROR);
1055                    }
1056                }
1057                return $rows[0];
1058            default:
1059                $existingPages = [];
1060                foreach ($rows as $row) {
1061                    $value = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1062                    $duplicatePage = MarkupPath::createMarkupFromId($value);
1063                    if (!$duplicatePage->exists()) {
1064
1065                        $this->deleteIfExistsAndAddRedirectAlias($duplicatePage);
1066
1067                    } else {
1068                        $existingPages[] = $row;
1069                    }
1070                }
1071                if (sizeof($existingPages) === 1) {
1072                    return $existingPages[0];
1073                } else {
1074                    $existingPageIds = array_map(
1075                        function ($row) {
1076                            return $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
1077                        },
1078                        $existingPages);
1079                    $existingPages = implode(", ", $existingPageIds);
1080                    throw new ExceptionNotFound("The existing pages ($existingPages) have all the same attribute $attribute with the value ($value)", LogUtility::LVL_MSG_ERROR);
1081                }
1082        }
1083    }
1084
1085    public
1086    function getMarkupPath(): ?MarkupPath
1087    {
1088        if (
1089            $this->markupPath === null
1090            && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null
1091        ) {
1092            $this->markupPath = MarkupPath::createMarkupFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]);
1093        }
1094        return $this->markupPath;
1095    }
1096
1097    private
1098    function getDatabaseRowFromAlias($alias): ?array
1099    {
1100
1101        $pageIdAttribute = PageId::PROPERTY_NAME;
1102        $buildFields = self::PAGE_BUILD_ATTRIBUTES;
1103        $fields = array_reduce($buildFields, function ($carry, $element) {
1104            if ($carry !== null) {
1105                return "$carry, p.{$element}";
1106            } else {
1107                return "p.{$element}";
1108            }
1109        }, null);
1110        /** @noinspection SqlResolve */
1111        $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? ";
1112        $request = $this->sqlite
1113            ->createRequest()
1114            ->setQueryParametrized($query, [$alias]);
1115        $rows = [];
1116        try {
1117            $rows = $request
1118                ->execute()
1119                ->getRows();
1120        } catch (ExceptionCompile $e) {
1121            LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}");
1122            return null;
1123        } finally {
1124            $request->close();
1125        }
1126        switch (sizeof($rows)) {
1127            case 0:
1128                return null;
1129            case 1:
1130                return $rows[0];
1131            default:
1132                $id = $rows[0]['ID'];
1133                $pages = implode(",",
1134                    array_map(
1135                        function ($row) {
1136                            return $row['ID'];
1137                        },
1138                        $rows
1139                    )
1140                );
1141                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);
1142                return $rows[0];
1143        }
1144    }
1145
1146
1147    /**
1148     * Utility function
1149     * @param MarkupPath $pageAlias
1150     */
1151    private
1152    function addRedirectAliasWhileBuildingRow(MarkupPath $pageAlias)
1153    {
1154
1155        $aliasPath = $pageAlias->getPathObject()->toAbsoluteId();
1156        try {
1157            Aliases::createForPage($this->markupPath)
1158                ->addAlias($aliasPath)
1159                ->sendToWriteStore();
1160        } catch (ExceptionCompile $e) {
1161            // we don't throw while getting
1162            LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->markupPath)");
1163        }
1164
1165    }
1166
1167    private
1168    function addPageIdAttributeIfNeeded(array &$values)
1169    {
1170        if (!isset($values[PageId::getPersistentName()])) {
1171            $values[PageId::getPersistentName()] = $this->markupPath->getPageId();
1172        }
1173        if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) {
1174            $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->markupPath->getPageIdAbbr();
1175        }
1176    }
1177
1178    public
1179    function getFromRow(string $attribute)
1180    {
1181        if ($this->row === null) {
1182            return null;
1183        }
1184
1185        if (!array_key_exists($attribute, $this->row)) {
1186            /**
1187             * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES}
1188             * or in the table
1189             */
1190            throw new ExceptionRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical());
1191        }
1192
1193        $value = $this->row[$attribute];
1194
1195        if ($value !== null) {
1196            return $value;
1197        }
1198
1199        // don't know why but the sqlite plugin returns them uppercase
1200        // rowid is returned lowercase from the sqlite plugin
1201        $upperAttribute = strtoupper($attribute);
1202        return $this->row[$upperAttribute];
1203
1204    }
1205
1206
1207    /**
1208     * @throws ExceptionCompile
1209     */
1210    public
1211    function replicateAnalytics()
1212    {
1213
1214        try {
1215            $fetchPath = $this->markupPath->fetchAnalyticsPath();
1216            $analyticsJson = Json::createFromPath($fetchPath);
1217        } catch (ExceptionCompile $e) {
1218            if (PluginUtility::isDevOrTest()) {
1219                throw $e;
1220            }
1221            throw new ExceptionCompile("Unable to get the analytics document", self::CANONICAL, 0, $e);
1222        }
1223
1224        /**
1225         * Replication Date
1226         */
1227        $replicationDateMeta = ReplicationDate::createFromPage($this->markupPath)
1228            ->setWriteStore(MetadataDbStore::class)
1229            ->setValue(new DateTime());
1230
1231        /**
1232         * Analytics
1233         */
1234        $analyticsJsonAsString = $analyticsJson->toPrettyJsonString();
1235        $analyticsJsonAsArray = $analyticsJson->toArray();
1236
1237        /**
1238         * Record
1239         */
1240        $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString;
1241        $record['IS_LOW_QUALITY'] = ($this->markupPath->isLowQualityPage() === true ? 1 : 0);
1242        $record['WORD_COUNT'] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][renderer_plugin_combo_analytics::WORD_COUNT];
1243        $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][BacklinkCount::getPersistentName()];
1244        $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue();
1245        $this->upsertAttributes($record);
1246    }
1247
1248    private
1249    function checkCollision($wikiIdInDatabase, $attribute, $value)
1250    {
1251        if ($this->markupPath === null) {
1252            return;
1253        }
1254        try {
1255            $markupWikiId = $this->markupPath->toWikiPath()->getWikiId();
1256        } catch (ExceptionCast $e) {
1257            return;
1258        }
1259        if ($wikiIdInDatabase !== $markupWikiId) {
1260            $duplicatePage = MarkupPath::createMarkupFromId($wikiIdInDatabase);
1261            if (!FileSystems::exists($duplicatePage)) {
1262                // Move
1263                LogUtility::info("The non-existing duplicate page ($wikiIdInDatabase) has been added as redirect alias for the page ($this->markupPath)", self::CANONICAL);
1264                $this->addRedirectAliasWhileBuildingRow($duplicatePage);
1265            } else {
1266                // This can happens if two page were created not on the same website
1267                // of if the sqlite database was deleted and rebuilt.
1268                // The chance is really, really low
1269                $errorMessage = "The page ($this->markupPath) and the page ($wikiIdInDatabase) have the same $attribute value ($value)";
1270                throw new ExceptionRuntimeInternal($errorMessage, self::CANONICAL);
1271                // What to do ?
1272                // The database does not allow two page id with the same value
1273                // If it happens, ugh, ugh, ..., a replication process between website may be.
1274            }
1275        }
1276    }
1277
1278
1279}
1280