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