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