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