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