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 $row = $databasePage->getDatabaseRowFromDokuWikiId($id); 301 $databasePage->setRow($row); 302 return $databasePage; 303 } 304 305 public 306 function getPageId() 307 { 308 return $this->getFromRow(PageId::PROPERTY_NAME); 309 } 310 311 312 public 313 function shouldReplicate(): bool 314 { 315 316 317 $dateReplication = $this->getReplicationDate(); 318 if ($dateReplication === null) { 319 return true; 320 } 321 322 /** 323 * When the replication date is older than the actual document 324 */ 325 try { 326 $modifiedTime = FileSystems::getModifiedTime($this->markupPath->getPathObject()); 327 if ($modifiedTime > $dateReplication) { 328 return true; 329 } 330 } catch (ExceptionNotFound $e) { 331 return false; 332 } 333 334 335 $path = $this->markupPath->fetchAnalyticsPath(); 336 337 /** 338 * When the file does not exist 339 */ 340 $exist = FileSystems::exists($path); 341 if (!$exist) { 342 return true; 343 } 344 345 /** 346 * When the analytics document is older 347 */ 348 try { 349 350 $modifiedTime = FileSystems::getModifiedTime($path); 351 if ($modifiedTime > $dateReplication) { 352 return true; 353 } 354 } catch (ExceptionNotFound $e) { 355 // 356 } 357 358 359 /** 360 * When the database version file is higher 361 */ 362 $version = LocalPath::createFromPathString(__DIR__ . "/../db/latest.version"); 363 try { 364 $versionModifiedTime = FileSystems::getModifiedTime($version); 365 } catch (ExceptionNotFound $e) { 366 return false; 367 } 368 if ($versionModifiedTime > $dateReplication) { 369 return true; 370 } 371 372 /** 373 * When the class date time is higher 374 */ 375 $code = LocalPath::createFromPathString(__DIR__ . "/DatabasePageRow.php"); 376 try { 377 $codeModified = FileSystems::getModifiedTime($code); 378 } catch (ExceptionNotFound $e) { 379 throw new ExceptionRuntime("The database file does not exist"); 380 } 381 if ($codeModified > $dateReplication) { 382 return true; 383 } 384 385 return false; 386 387 } 388 389 public 390 function delete() 391 { 392 393 $request = $this->sqlite 394 ->createRequest() 395 ->setQueryParametrized('delete from pages where id = ?', [$this->markupPath->getWikiId()]); 396 try { 397 $request->execute(); 398 } catch (ExceptionCompile $e) { 399 LogUtility::msg("Something went wrong when deleting the page ({$this->markupPath}) from the database"); 400 } finally { 401 $request->close(); 402 } 403 $this->buildInitObjectFields(); 404 405 } 406 407 /** 408 * @return Json the analytics array or null if not in db 409 */ 410 public 411 function getAnalyticsData(): Json 412 { 413 414 $jsonString = $this->getFromRow(self::ANALYTICS_ATTRIBUTE); 415 if ($jsonString === null) { 416 // we put an empty json to not get any problem with the json database function 417 // on an empty string / null (for sqlite) 418 return Json::createEmpty(); 419 } 420 try { 421 return Json::createFromString($jsonString); 422 } catch (ExceptionCompile $e) { 423 throw ExceptionRuntimeInternal::withMessageAndError("Error while building back the analytics JSON object. {$e->getMessage()}", $e); 424 } 425 426 } 427 428 /** 429 * Return the database row 430 * 431 * 432 * @throws ExceptionNotExists - if the row does not exists 433 */ 434 public 435 function getDatabaseRowFromPage(MarkupPath $markupPath): array 436 { 437 438 $this->setMarkupPath($markupPath); 439 440 /** 441 * Generated identifier 442 */ 443 try { 444 $pageId = $markupPath->getPageId(); 445 return $this->getDatabaseRowFromPageId($pageId); 446 } catch (ExceptionNotFound $e) { 447 // no page id 448 } 449 450 /** 451 * Named identifier: path 452 */ 453 try { 454 $path = $markupPath->getPathObject(); 455 return $this->getDatabaseRowFromPath($path); 456 } catch (ExceptionNotFound $e) { 457 // not found 458 } 459 460 /** 461 * Named identifier: id (ie path) 462 */ 463 try { 464 $id = $markupPath->getPathObject()->toWikiPath()->getWikiId(); 465 return $this->getDatabaseRowFromDokuWikiId($id); 466 } catch (ExceptionCast|ExceptionNotFound $e) { 467 } 468 469 /** 470 * Named identifier: canonical 471 * (Note that canonical should become a soft link and therefore a path) 472 */ 473 try { 474 $canonical = Canonical::createForPage($markupPath)->getValue(); 475 return $this->getDatabaseRowFromCanonical($canonical->toAbsoluteId()); 476 } catch (ExceptionNotFound $e) { 477 // no canonical 478 } 479 480 // we send a not exist 481 throw new ExceptionNotExists("No row could be found"); 482 483 484 } 485 486 487 /** 488 * @return DateTime|null 489 */ 490 public 491 function getReplicationDate(): ?DateTime 492 { 493 $dateString = $this->getFromRow(ReplicationDate::getPersistentName()); 494 if ($dateString === null) { 495 return null; 496 } 497 try { 498 return Iso8601Date::createFromString($dateString)->getDateTime(); 499 } catch (ExceptionCompile $e) { 500 LogUtility::msg("Error while reading the replication date in the database. {$e->getMessage()}"); 501 return null; 502 } 503 504 } 505 506 /** 507 * 508 * @throws ExceptionBadState 509 * @throws ExceptionSqliteNotAvailable 510 */ 511 public 512 function replicatePage(): void 513 { 514 515 if (!FileSystems::exists($this->markupPath)) { 516 throw new ExceptionBadState("You can't replicate the page ($this->markupPath) because it does not exists."); 517 } 518 519 /** 520 * Replication Date 521 */ 522 $replicationDate = ReplicationDate::createFromPage($this->markupPath) 523 ->setWriteStore(MetadataDbStore::class) 524 ->setValue(new DateTime()); 525 526 /** 527 * Same data as {@link MarkupPath::getMetadataForRendering()} 528 */ 529 $record = $this->getMetaRecord(); 530 $record[$replicationDate::getPersistentName()] = $replicationDate->toStoreValue(); 531 $this->upsertAttributes($record); 532 533 } 534 535 536 /** 537 * 538 * 539 * Attribute that are scalar / modifiable in the database 540 * (not aliases or json data for instance) 541 * 542 * @throws ExceptionBadState 543 * @throws ExceptionSqliteNotAvailable 544 */ 545 public 546 function replicateMetaAttributes(): void 547 { 548 549 $this->upsertAttributes($this->getMetaRecord()); 550 551 } 552 553 /** 554 * @throws ExceptionBadState - if the array is empty 555 */ 556 public 557 function upsertAttributes(array $attributes): void 558 { 559 560 if (empty($attributes)) { 561 throw new ExceptionBadState("The page database attribute passed should not be empty"); 562 } 563 564 $values = []; 565 $columnClauses = []; 566 foreach ($attributes as $key => $value) { 567 if (is_array($value)) { 568 throw new ExceptionRuntime("The attribute ($key) has value that is an array (" . implode(", ", $value) . ")"); 569 } 570 $columnClauses[] = "$key = ?"; 571 $values[$key] = $value; 572 } 573 574 /** 575 * Primary key has moved during the time 576 * It should be the UUID but not for older version 577 * 578 * If the primary key is null, no record was found 579 */ 580 $rowId = $this->getRowId(); 581 if ($rowId !== null) { 582 /** 583 * We just add the primary key 584 * otherwise as this is a associative 585 * array, we will miss a value for the update statement 586 */ 587 $values[] = $rowId; 588 589 $updateStatement = "update PAGES SET " . implode(", ", $columnClauses) . " where ROWID = ?"; 590 $request = $this->sqlite 591 ->createRequest() 592 ->setQueryParametrized($updateStatement, $values); 593 $countChanges = 0; 594 try { 595 $countChanges = $request 596 ->execute() 597 ->getChangeCount(); 598 } catch (ExceptionCompile $e) { 599 throw new ExceptionBadState("There was a problem during the page attribute updates. : {$e->getMessage()}"); 600 } finally { 601 $request->close(); 602 } 603 if ($countChanges !== 1) { 604 // internal error 605 LogUtility::error("The database replication has not updated exactly 1 record but ($countChanges) record", \action_plugin_combo_indexer::CANONICAL); 606 } 607 608 } else { 609 610 /** 611 * Creation 612 */ 613 if ($this->markupPath === null) { 614 throw new ExceptionBadState("The page should be defined to create a page database row"); 615 } 616 617 /** 618 * If the user copy a frontmatter with the same page id abbr, we got a problem 619 */ 620 $pageIdAbbr = $values[PageId::PAGE_ID_ABBR_ATTRIBUTE]; 621 if ($pageIdAbbr == null) { 622 $pageId = $values[PageId::getPersistentName()]; 623 if ($pageId === null) { 624 throw new ExceptionBadState("You can't insert a page in the database without a page id"); 625 } 626 $pageIdAbbr = PageId::getAbbreviated($pageId); 627 $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $pageIdAbbr; 628 } 629 630 $databasePage = DatabasePageRow::createFromPageIdAbbr($pageIdAbbr); 631 if ($databasePage->exists()) { 632 $duplicatePage = $databasePage->getMarkupPath(); 633 if ($duplicatePage->getPathObject()->toUriString() === $this->markupPath->toUriString()) { 634 $message = "The page ($this->markupPath) is already in the database with the uid ($pageIdAbbr)."; 635 } else { 636 $message = "The page ($this->markupPath) cannot be replicated to the database because it has the same page id abbreviation ($pageIdAbbr) than the page ($duplicatePage)"; 637 } 638 throw new ExceptionBadState($message); 639 } 640 641 $values[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] = $this->markupPath->getPathObject()->getWikiId(); 642 $values[PagePath::PROPERTY_NAME] = $this->markupPath->getPathObject()->toAbsolutePath()->toAbsoluteId(); 643 /** 644 * Default implements the auto-canonical feature 645 */ 646 try { 647 $values[Canonical::PROPERTY_NAME] = $this->markupPath->getCanonicalOrDefault(); 648 } catch (ExceptionNotFound $e) { 649 $values[Canonical::PROPERTY_NAME] = null; 650 } 651 652 /** 653 * Analytics 654 */ 655 if (!isset($values[self::ANALYTICS_ATTRIBUTE])) { 656 // otherwise we get an empty string 657 // and a json function will not work 658 $values[self::ANALYTICS_ATTRIBUTE] = Json::createEmpty()->toPrettyJsonString(); 659 } 660 661 /** 662 * Page Id / Abbr are mandatory for url redirection 663 */ 664 $this->addPageIdAttributeIfNeeded($values); 665 666 $request = $this->sqlite 667 ->createRequest() 668 ->setTableRow('PAGES', $values); 669 try { 670 /** 671 * rowid is used in {@link DatabasePageRow::exists()} 672 * to check if the page exists in the database 673 * We update it 674 */ 675 $this->row[self::ROWID] = $request 676 ->execute() 677 ->getInsertId(); 678 $this->row = array_merge($values, $this->row); 679 } catch (ExceptionCompile $e) { 680 throw new ExceptionBadState("There was a problem during the updateAttributes insert. : {$e->getMessage()}"); 681 } finally { 682 $request->close(); 683 } 684 685 } 686 687 } 688 689 public 690 function getDescription() 691 { 692 return $this->getFromRow(PageDescription::DESCRIPTION_PROPERTY); 693 } 694 695 696 public 697 function getPageName() 698 { 699 return $this->getFromRow(ResourceName::PROPERTY_NAME); 700 } 701 702 public 703 function exists(): bool 704 { 705 return $this->getFromRow(self::ROWID) !== null; 706 } 707 708 /** 709 * Called when a page is moved 710 * @param $targetId 711 */ 712 public 713 function updatePathAndDokuwikiId($targetId) 714 { 715 if (!$this->exists()) { 716 LogUtility::error("The `database` page ($this) does not exist and cannot be moved to ($targetId)"); 717 } 718 719 $path = $targetId; 720 WikiPath::addRootSeparatorIfNotPresent($path); 721 $attributes = [ 722 DokuwikiId::DOKUWIKI_ID_ATTRIBUTE => $targetId, 723 PagePath::PROPERTY_NAME => $path 724 ]; 725 726 $this->upsertAttributes($attributes); 727 728 } 729 730 public 731 function __toString() 732 { 733 return $this->markupPath->__toString(); 734 } 735 736 737 /** 738 * Redirect are now added during a move 739 * Not when a duplicate is found. 740 * With the advent of the page id, it should never occurs anymore 741 * @param MarkupPath $page 742 * @deprecated 2012-10-28 743 */ 744 private 745 function deleteIfExistsAndAddRedirectAlias(MarkupPath $page): void 746 { 747 748 if ($this->markupPath != null) { 749 $page->getDatabasePage()->deleteIfExist(); 750 $this->addRedirectAliasWhileBuildingRow($page); 751 } 752 753 } 754 755 public 756 function getCanonical() 757 { 758 return $this->getFromRow(Canonical::PROPERTY_NAME); 759 } 760 761 /** 762 * Set the field to their values 763 * @param $row 764 */ 765 public 766 function setRow($row) 767 { 768 if ($row === null) { 769 LogUtility::msg("A row should not be null"); 770 return; 771 } 772 if (!is_array($row)) { 773 LogUtility::msg("A row should be an array"); 774 return; 775 } 776 777 /** 778 * All get function lookup the row 779 */ 780 $this->row = $row; 781 782 783 } 784 785 private 786 function buildInitObjectFields() 787 { 788 $this->row = null; 789 790 } 791 792 public 793 function rebuild(): DatabasePageRow 794 { 795 796 if ($this->markupPath != null) { 797 $this->markupPath->rebuild(); 798 try { 799 $row = $this->getDatabaseRowFromPage($this->markupPath); 800 $this->setRow($row); 801 } catch (ExceptionNotExists $e) { 802 // ok 803 } 804 } 805 return $this; 806 807 } 808 809 /** 810 * @return array - an array of the fix page metadata (ie not derived) 811 * Therefore quick to insert/update 812 * 813 */ 814 private 815 function getMetaRecord(): array 816 { 817 $sourceStore = MetadataDokuWikiStore::getOrCreateFromResource($this->markupPath); 818 $targetStore = MetadataDbStore::getOrCreateFromResource($this->markupPath); 819 820 $record = array( 821 Canonical::PROPERTY_NAME, 822 PagePath::PROPERTY_NAME, 823 ResourceName::PROPERTY_NAME, 824 PageTitle::TITLE, 825 PageH1::PROPERTY_NAME, 826 PageDescription::PROPERTY_NAME, 827 CreationDate::PROPERTY_NAME, 828 ModificationDate::PROPERTY_NAME, 829 PagePublicationDate::PROPERTY_NAME, 830 StartDate::PROPERTY_NAME, 831 EndDate::PROPERTY_NAME, 832 Region::PROPERTY_NAME, 833 Lang::PROPERTY_NAME, 834 PageType::PROPERTY_NAME, 835 DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, 836 PageLevel::PROPERTY_NAME 837 ); 838 $metaRecord = []; 839 foreach ($record as $name) { 840 try { 841 $metadata = Meta\Api\MetadataSystem::getForName($name); 842 } catch (ExceptionNotFound $e) { 843 LogUtility::internalError("The metadata ($name) is unknown", self::CANONICAL); 844 continue; 845 } 846 $metaRecord[$name] = $metadata 847 ->setResource($this->markupPath) 848 ->setReadStore($sourceStore) 849 ->buildFromReadStore() 850 ->setWriteStore($targetStore) 851 ->toStoreValueOrDefault(); // used by the template, the value is or default 852 853 } 854 855 try { 856 $this->addPageIdMeta($metaRecord); 857 } catch (ExceptionNotExists $e) { 858 // no page id for non-existent page ok 859 } 860 861 // Is index 862 $metaRecord[self::IS_INDEX_COLUMN] = ($this->markupPath->isIndexPage() === true ? 1 : 0); 863 864 return $metaRecord; 865 } 866 867 public 868 function deleteIfExist(): DatabasePageRow 869 { 870 if ($this->exists()) { 871 $this->delete(); 872 } 873 return $this; 874 } 875 876 public 877 function getRowId() 878 { 879 return $this->getFromRow(self::ROWID); 880 } 881 882 /** 883 * @throws ExceptionNotFound 884 */ 885 private 886 function getDatabaseRowFromPageId(string $pageIdValue) 887 { 888 889 $pageIdAttribute = PageId::PROPERTY_NAME; 890 $query = $this->getParametrizedLookupQuery($pageIdAttribute); 891 $request = Sqlite::createOrGetSqlite() 892 ->createRequest() 893 ->setQueryParametrized($query, [$pageIdValue]); 894 $rows = []; 895 try { 896 $rows = $request 897 ->execute() 898 ->getRows(); 899 } catch (ExceptionCompile $e) { 900 throw new ExceptionRuntimeInternal("Error while retrieving the object by id", self::CANONICAL, 1, $e); 901 } finally { 902 $request->close(); 903 } 904 905 switch (sizeof($rows)) { 906 case 0: 907 throw new ExceptionNotFound("No object by page id"); 908 case 1: 909 /** 910 * Page Id Collision detection 911 */ 912 $rowId = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]; 913 $this->checkCollision($rowId, $pageIdAttribute, $pageIdValue); 914 return $rows[0]; 915 default: 916 $existingPages = implode(", ", $rows); 917 $message = "The pages ($existingPages) have all the same page id ($pageIdValue)"; 918 throw new ExceptionRuntimeInternal($message, self::CANONICAL); 919 } 920 921 } 922 923 924 private 925 function getParametrizedLookupQuery(string $attributeName): string 926 { 927 $select = Sqlite::createSelectFromTableAndColumns("pages", self::PAGE_BUILD_ATTRIBUTES); 928 return "$select where $attributeName = ?"; 929 } 930 931 932 public 933 function setMarkupPath(MarkupPath $page) 934 { 935 $this->markupPath = $page; 936 return $this; 937 } 938 939 /** 940 * @throws ExceptionNotFound 941 */ 942 private 943 function getDatabaseRowFromCanonical($canonicalValue) 944 { 945 $canoncialName = Canonical::PROPERTY_NAME; 946 $query = $this->getParametrizedLookupQuery($canoncialName); 947 $request = $this->sqlite 948 ->createRequest() 949 ->setQueryParametrized($query, [$canonicalValue]); 950 $rows = []; 951 try { 952 $rows = $request 953 ->execute() 954 ->getRows(); 955 } catch (ExceptionCompile $e) { 956 throw new ExceptionRuntime("An exception has occurred with the page search from CANONICAL. " . $e->getMessage()); 957 } finally { 958 $request->close(); 959 } 960 961 switch (sizeof($rows)) { 962 case 0: 963 throw new ExceptionNotFound("No canonical row was found"); 964 case 1: 965 $id = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]; 966 $this->checkCollision($id, $canoncialName, $canonicalValue); 967 return $rows[0]; 968 default: 969 $existingPages = []; 970 foreach ($rows as $row) { 971 $id = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]; 972 $duplicatePage = MarkupPath::createMarkupFromId($id); 973 if (!$duplicatePage->exists()) { 974 975 $this->deleteIfExistsAndAddRedirectAlias($duplicatePage); 976 977 } else { 978 979 /** 980 * Check if the error may come from the auto-canonical 981 * (Never ever save generated data) 982 */ 983 $canonicalLastNamesCount = SiteConfig::getConfValue(Canonical::CONF_CANONICAL_LAST_NAMES_COUNT, 0); 984 if ($canonicalLastNamesCount > 0) { 985 $this->markupPath->unsetMetadata($canoncialName); 986 $duplicatePage->unsetMetadata($canoncialName); 987 } 988 989 $existingPages[] = $row; 990 } 991 } 992 if (sizeof($existingPages) > 1) { 993 $existingPages = implode(", ", $existingPages); 994 $message = "The existing pages ($existingPages) have all the same canonical ($canonicalValue), return the first one"; 995 LogUtility::error($message, self::CANONICAL); 996 } 997 return $existingPages[0]; 998 } 999 } 1000 1001 /** 1002 * @throws ExceptionNotFound 1003 */ 1004 private 1005 function getDatabaseRowFromPath(string $path): ?array 1006 { 1007 WikiPath::addRootSeparatorIfNotPresent($path); 1008 return $this->getDatabaseRowFromAttribute(PagePath::PROPERTY_NAME, $path); 1009 } 1010 1011 /** 1012 * @throws ExceptionNotFound 1013 */ 1014 private 1015 function getDatabaseRowFromDokuWikiId(string $id): array 1016 { 1017 return $this->getDatabaseRowFromAttribute(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id); 1018 } 1019 1020 /** 1021 * @throws ExceptionNotFound 1022 */ 1023 public 1024 function getDatabaseRowFromAttribute(string $attribute, string $value) 1025 { 1026 $query = $this->getParametrizedLookupQuery($attribute); 1027 $request = $this->sqlite 1028 ->createRequest() 1029 ->setQueryParametrized($query, [$value]); 1030 $rows = []; 1031 try { 1032 $rows = $request 1033 ->execute() 1034 ->getRows(); 1035 } catch (ExceptionCompile $e) { 1036 $message = "Internal Error: An exception has occurred with the page search from a PATH: " . $e->getMessage(); 1037 LogUtility::log2file($message); 1038 throw new ExceptionNotFound($message); 1039 } finally { 1040 $request->close(); 1041 } 1042 1043 switch (sizeof($rows)) { 1044 case 0: 1045 throw new ExceptionNotFound("No database row found for the page"); 1046 case 1: 1047 $value = $rows[0][DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]; 1048 if ($this->markupPath != null && $value !== $this->markupPath->getWikiId()) { 1049 $duplicatePage = MarkupPath::createMarkupFromId($value); 1050 if (!$duplicatePage->exists()) { 1051 $this->addRedirectAliasWhileBuildingRow($duplicatePage); 1052 } else { 1053 LogUtility::msg("The page ($this->markupPath) and the page ($value) have the same $attribute ($value)", LogUtility::LVL_MSG_ERROR); 1054 } 1055 } 1056 return $rows[0]; 1057 default: 1058 $existingPages = []; 1059 foreach ($rows as $row) { 1060 $value = $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]; 1061 $duplicatePage = MarkupPath::createMarkupFromId($value); 1062 if (!$duplicatePage->exists()) { 1063 1064 $this->deleteIfExistsAndAddRedirectAlias($duplicatePage); 1065 1066 } else { 1067 $existingPages[] = $row; 1068 } 1069 } 1070 if (sizeof($existingPages) === 1) { 1071 return $existingPages[0]; 1072 } else { 1073 $existingPageIds = array_map( 1074 function ($row) { 1075 return $row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]; 1076 }, 1077 $existingPages); 1078 $existingPages = implode(", ", $existingPageIds); 1079 throw new ExceptionNotFound("The existing pages ($existingPages) have all the same attribute $attribute with the value ($value)", LogUtility::LVL_MSG_ERROR); 1080 } 1081 } 1082 } 1083 1084 public 1085 function getMarkupPath(): ?MarkupPath 1086 { 1087 if ( 1088 $this->markupPath === null 1089 && $this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE] !== null 1090 ) { 1091 $this->markupPath = MarkupPath::createMarkupFromId($this->row[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE]); 1092 } 1093 return $this->markupPath; 1094 } 1095 1096 private 1097 function getDatabaseRowFromAlias($alias): ?array 1098 { 1099 1100 $pageIdAttribute = PageId::PROPERTY_NAME; 1101 $buildFields = self::PAGE_BUILD_ATTRIBUTES; 1102 $fields = array_reduce($buildFields, function ($carry, $element) { 1103 if ($carry !== null) { 1104 return "$carry, p.{$element}"; 1105 } else { 1106 return "p.{$element}"; 1107 } 1108 }, null); 1109 /** @noinspection SqlResolve */ 1110 $query = "select {$fields} from PAGES p, PAGE_ALIASES pa where p.{$pageIdAttribute} = pa.{$pageIdAttribute} and pa.PATH = ? "; 1111 $request = $this->sqlite 1112 ->createRequest() 1113 ->setQueryParametrized($query, [$alias]); 1114 $rows = []; 1115 try { 1116 $rows = $request 1117 ->execute() 1118 ->getRows(); 1119 } catch (ExceptionCompile $e) { 1120 LogUtility::msg("An exception has occurred with the alias selection query. {$e->getMessage()}"); 1121 return null; 1122 } finally { 1123 $request->close(); 1124 } 1125 switch (sizeof($rows)) { 1126 case 0: 1127 return null; 1128 case 1: 1129 return $rows[0]; 1130 default: 1131 $id = $rows[0]['ID']; 1132 $pages = implode(",", 1133 array_map( 1134 function ($row) { 1135 return $row['ID']; 1136 }, 1137 $rows 1138 ) 1139 ); 1140 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); 1141 return $rows[0]; 1142 } 1143 } 1144 1145 1146 /** 1147 * Utility function 1148 * @param MarkupPath $pageAlias 1149 */ 1150 private 1151 function addRedirectAliasWhileBuildingRow(MarkupPath $pageAlias) 1152 { 1153 1154 $aliasPath = $pageAlias->getPathObject()->toAbsoluteId(); 1155 try { 1156 Aliases::createForPage($this->markupPath) 1157 ->addAlias($aliasPath) 1158 ->sendToWriteStore(); 1159 } catch (ExceptionCompile $e) { 1160 // we don't throw while getting 1161 LogUtility::msg("Unable to add the alias ($aliasPath) for the page ($this->markupPath)"); 1162 } 1163 1164 } 1165 1166 private 1167 function addPageIdAttributeIfNeeded(array &$values) 1168 { 1169 if (!isset($values[PageId::getPersistentName()])) { 1170 $values[PageId::getPersistentName()] = $this->markupPath->getPageId(); 1171 } 1172 if (!isset($values[PageId::PAGE_ID_ABBR_ATTRIBUTE])) { 1173 $values[PageId::PAGE_ID_ABBR_ATTRIBUTE] = $this->markupPath->getPageIdAbbr(); 1174 } 1175 } 1176 1177 public 1178 function getFromRow(string $attribute) 1179 { 1180 if ($this->row === null) { 1181 return null; 1182 } 1183 1184 if (!array_key_exists($attribute, $this->row)) { 1185 /** 1186 * An attribute should be added to {@link DatabasePageRow::PAGE_BUILD_ATTRIBUTES} 1187 * or in the table 1188 */ 1189 throw new ExceptionRuntime("The metadata ($attribute) was not found in the returned database row.", $this->getCanonical()); 1190 } 1191 1192 $value = $this->row[$attribute]; 1193 1194 if ($value !== null) { 1195 return $value; 1196 } 1197 1198 // don't know why but the sqlite plugin returns them uppercase 1199 // rowid is returned lowercase from the sqlite plugin 1200 $upperAttribute = strtoupper($attribute); 1201 return $this->row[$upperAttribute]; 1202 1203 } 1204 1205 1206 /** 1207 * @throws ExceptionCompile 1208 */ 1209 public 1210 function replicateAnalytics() 1211 { 1212 1213 try { 1214 $fetchPath = $this->markupPath->fetchAnalyticsPath(); 1215 $analyticsJson = Json::createFromPath($fetchPath); 1216 } catch (ExceptionCompile $e) { 1217 if (PluginUtility::isDevOrTest()) { 1218 throw $e; 1219 } 1220 throw new ExceptionCompile("Unable to get the analytics document", self::CANONICAL, 0, $e); 1221 } 1222 1223 /** 1224 * Replication Date 1225 */ 1226 $replicationDateMeta = ReplicationDate::createFromPage($this->markupPath) 1227 ->setWriteStore(MetadataDbStore::class) 1228 ->setValue(new DateTime()); 1229 1230 /** 1231 * Analytics 1232 */ 1233 $analyticsJsonAsString = $analyticsJson->toPrettyJsonString(); 1234 $analyticsJsonAsArray = $analyticsJson->toArray(); 1235 1236 /** 1237 * Record 1238 */ 1239 $record[self::ANALYTICS_ATTRIBUTE] = $analyticsJsonAsString; 1240 $record['IS_LOW_QUALITY'] = ($this->markupPath->isLowQualityPage() === true ? 1 : 0); 1241 $record['WORD_COUNT'] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][renderer_plugin_combo_analytics::WORD_COUNT]; 1242 $record[BacklinkCount::getPersistentName()] = $analyticsJsonAsArray[renderer_plugin_combo_analytics::STATISTICS][BacklinkCount::getPersistentName()]; 1243 $record[$replicationDateMeta::getPersistentName()] = $replicationDateMeta->toStoreValue(); 1244 $this->upsertAttributes($record); 1245 } 1246 1247 private 1248 function checkCollision($wikiIdInDatabase, $attribute, $value) 1249 { 1250 if ($this->markupPath === null) { 1251 return; 1252 } 1253 try { 1254 $markupWikiId = $this->markupPath->toWikiPath()->getWikiId(); 1255 } catch (ExceptionCast $e) { 1256 return; 1257 } 1258 if ($wikiIdInDatabase !== $markupWikiId) { 1259 $duplicatePage = MarkupPath::createMarkupFromId($wikiIdInDatabase); 1260 if (!FileSystems::exists($duplicatePage)) { 1261 // Move 1262 LogUtility::info("The non-existing duplicate page ($wikiIdInDatabase) has been added as redirect alias for the page ($this->markupPath)", self::CANONICAL); 1263 $this->addRedirectAliasWhileBuildingRow($duplicatePage); 1264 } else { 1265 // This can happens if two page were created not on the same website 1266 // of if the sqlite database was deleted and rebuilt. 1267 // The chance is really, really low 1268 $errorMessage = "The page ($this->markupPath) and the page ($wikiIdInDatabase) have the same $attribute value ($value)"; 1269 throw new ExceptionRuntimeInternal($errorMessage, self::CANONICAL); 1270 // What to do ? 1271 // The database does not allow two page id with the same value 1272 // If it happens, ugh, ugh, ..., a replication process between website may be. 1273 } 1274 } 1275 } 1276 1277 1278} 1279