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