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