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