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