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