1<?php 2 3 4namespace ComboStrap\Meta\Api; 5 6 7use action_plugin_combo_metadescription; 8use ComboStrap\CacheExpirationDate; 9use ComboStrap\CacheExpirationFrequency; 10use ComboStrap\Canonical; 11use ComboStrap\DataType; 12use ComboStrap\ExceptionRuntimeInternal; 13use ComboStrap\ExecutionContext; 14use ComboStrap\Label; 15use ComboStrap\DisqusIdentifier; 16use ComboStrap\DokuwikiId; 17use ComboStrap\EndDate; 18use ComboStrap\ExceptionBadArgument; 19use ComboStrap\ExceptionCompile; 20use ComboStrap\ExceptionNotFound; 21use ComboStrap\ExceptionRuntime; 22use ComboStrap\FirstImage; 23use ComboStrap\FirstRasterImage; 24use ComboStrap\FirstSvgIllustration; 25use ComboStrap\FeaturedIcon; 26use ComboStrap\Lang; 27use ComboStrap\LdJson; 28use ComboStrap\Lead; 29use ComboStrap\Locale; 30use ComboStrap\LogUtility; 31use ComboStrap\LowQualityCalculatedIndicator; 32use ComboStrap\LowQualityPageOverwrite; 33use ComboStrap\Meta\Field\Aliases; 34use ComboStrap\Meta\Field\AliasPath; 35use ComboStrap\Meta\Field\AliasType; 36use ComboStrap\Meta\Field\AncestorImage; 37use ComboStrap\Meta\Field\FacebookImage; 38use ComboStrap\Meta\Field\FeaturedImage; 39use ComboStrap\Meta\Field\SocialCardImage; 40use ComboStrap\Meta\Field\FeaturedRasterImage; 41use ComboStrap\Meta\Field\FeaturedSvgImage; 42use ComboStrap\Meta\Field\PageH1; 43use ComboStrap\Meta\Field\PageTemplateName; 44use ComboStrap\Meta\Field\TwitterImage; 45use ComboStrap\Meta\Store\MetadataDokuWikiStore; 46use ComboStrap\MetadataMutation; 47use ComboStrap\ModificationDate; 48use ComboStrap\CreationDate; 49use ComboStrap\PageDescription; 50use ComboStrap\PageId; 51use ComboStrap\Meta\Field\PageImagePath; 52use ComboStrap\Meta\Field\PageImages; 53use ComboStrap\PageImageUsage; 54use ComboStrap\PageKeywords; 55use ComboStrap\PageLevel; 56use ComboStrap\PagePath; 57use ComboStrap\PagePublicationDate; 58use ComboStrap\PageTitle; 59use ComboStrap\PageType; 60use ComboStrap\PageUrlPath; 61use ComboStrap\PluginUtility; 62use ComboStrap\QualityDynamicMonitoringOverwrite; 63use ComboStrap\References; 64use ComboStrap\Meta\Field\Region; 65use ComboStrap\ReplicationDate; 66use ComboStrap\ResourceCombo; 67use ComboStrap\ResourceName; 68use ComboStrap\Slug; 69use ComboStrap\StartDate; 70use ComboStrap\Web\UrlEndpoint; 71 72abstract class Metadata 73{ 74 const CANONICAL = "metadata"; 75 76 public const NOT_MODIFIABLE_METAS = [ 77 "date", 78 "user", 79 "last_change", 80 "creator", 81 "contributor" 82 ]; 83 84 85 /** 86 * @var bool 87 */ 88 protected bool $wasBuild = false; 89 90 /** 91 * The metadata is for this resource 92 * @var ResourceCombo $resource 93 */ 94 private $resource; 95 96 /** 97 * @var MetadataStore|string - string is the class 98 */ 99 private $readStore; 100 101 /** 102 * @var MetadataStore|string 103 */ 104 private $writeStore; 105 /** 106 * @var Metadata 107 */ 108 private $uidObject; 109 110 /** 111 * @var Metadata[] - the runtime children 112 */ 113 private array $childrenObject; 114 115 /** 116 * @var Metadata|null - the parent with runtime metadata 117 */ 118 private ?Metadata $parent; 119 120 121 /** 122 * The metadata may be just not stored 123 * The page is just the scope 124 */ 125 public function __construct(Metadata $parent = null) 126 { 127 $this->parent = $parent; 128 } 129 130 /** 131 * @throws ExceptionNotFound 132 */ 133 public function getParent(): Metadata 134 { 135 if ($this->parent === null) { 136 throw new ExceptionNotFound("No parent"); 137 } 138 return $this->parent; 139 } 140 141 /** 142 * The class string of the child/columns metadata 143 * @return string[]; 144 */ 145 public static function getChildrenClass(): array 146 { 147 return []; 148 } 149 150 151 /** 152 * @return array|mixed|string|null 153 * Store value may returns null as they may be stored 154 * Be careful 155 */ 156 public 157 function toStoreValueOrDefault() 158 { 159 160 $value = $this->toStoreValue(); 161 if ($value !== null) { 162 return $value; 163 } 164 return $this->toStoreDefaultValue(); 165 166 } 167 168 /** 169 * @return Metadata[] 170 */ 171 public function getChildrenObject(): array 172 { 173 if (static::getChildrenClass() === []) { 174 return []; 175 } 176 if (isset($this->childrenObject)) { 177 return $this->childrenObject; 178 } 179 foreach (static::getChildrenClass() as $childrenClass) { 180 try { 181 $this->childrenObject[] = MetadataSystem::toMetadataObject($childrenClass) 182 ->setResource($this->getResource()); 183 } catch (ExceptionCompile $e) { 184 LogUtility::msg("Unable to build the metadata children object: " . $e->getMessage()); 185 } 186 } 187 return $this->childrenObject; 188 189 } 190 191 /** 192 * @return bool - true if single value, false if an array 193 */ 194 public 195 function isScalar(): bool 196 { 197 198 try { 199 $parent = $this->getParent(); 200 if ($parent::getDataType() === DataType::TABULAR_TYPE_VALUE) { 201 return false; 202 } 203 } catch (ExceptionNotFound $e) { 204 // no parent 205 } 206 return true; 207 208 } 209 210 211 212 213 /** 214 * @param $store 215 * @return $this 216 */ 217 public 218 function setReadStore($store): Metadata 219 { 220 if (isset($this->readStore)) { 221 LogUtility::msg("The read store was already set."); 222 } 223 if (is_string($store) && !is_subclass_of($store, MetadataStore::class)) { 224 throw new ExceptionRuntime("The store class ($store) is not a metadata store class"); 225 } 226 $this->readStore = $store; 227 return $this; 228 } 229 230 /** 231 * @param MetadataStore|string $store 232 * @return $this 233 */ 234 public 235 function setWriteStore($store): Metadata 236 { 237 $this->writeStore = $store; 238 return $this; 239 } 240 241 /** 242 * @param mixed $value 243 * @return Metadata 244 */ 245 public 246 247 abstract function setValue($value): Metadata; 248 249 /** 250 * @return bool 251 * Used in the {@link Metadata::buildCheck()} function 252 * If the value is null, the {@link Metadata::buildFromReadStore()} will be performed 253 * otherwise, it will not. 254 * Why ? because the value may have been set before building. 255 */ 256 public abstract function valueIsNotNull(): bool; 257 258 /** 259 * If the {@link Metadata::getValue()} is null and if the object was not already build 260 * this function will call the function {@link Metadata::buildFromReadStore()} 261 */ 262 protected function buildCheck() 263 { 264 if (!$this->wasBuild && !$this->valueIsNotNull()) { 265 $this->wasBuild = true; 266 $this->buildFromReadStore(); 267 } 268 } 269 270 /** 271 * Return the store for this metadata 272 * By default, this is the {@link ResourceCombo::getReadStoreOrDefault() default resource metadata store} 273 * 274 * @return MetadataStore 275 */ 276 public function getReadStore(): ?MetadataStore 277 { 278 if (!isset($this->readStore)) { 279 return $this->getResource()->getReadStoreOrDefault(); 280 } 281 if (!$this->readStore instanceof MetadataStore) { 282 $this->readStore = MetadataStoreAbs::toMetadataStore($this->readStore, $this->getResource()); 283 } 284 return $this->readStore; 285 } 286 287 public static function getTab(): ?string 288 { 289 return static::getName(); 290 } 291 292 /** 293 * This function sends the object value to the memory {@link Metadata::getWriteStore() store} 294 * 295 * This function should be used at the end of each setter/adder function 296 * 297 * To persist or commit on disk, you use the {@link MetadataStore::persist()} 298 * Because the metadata is stored by resource, the persist function is 299 * also made available on the resource level 300 * 301 * @throws ExceptionBadArgument - if the value cannot be persisted 302 */ 303 public function sendToWriteStore(): Metadata 304 { 305 $this->getWriteStore()->set($this); 306 return $this; 307 } 308 309 /** 310 * @return string - the name to lookup the value 311 * This is the column name in a database or the property name in a key value store 312 * It should be unique over all metadata 313 */ 314 public function __toString() 315 { 316 return $this->getName(); 317 } 318 319 /** @noinspection PhpMissingReturnTypeInspection */ 320 public function buildFromReadStore() 321 { 322 $this->wasBuild = true; 323 $metadataStore = $this->getReadStore(); 324 if ($metadataStore === null) { 325 LogUtility::msg("The metadata store is unknown. You need to define a resource or a store to build from it"); 326 return $this; 327 } 328 $value = $metadataStore->get($this); 329 $this->setFromStoreValueWithoutException($value); 330 return $this; 331 } 332 333 334 /** 335 * @return string - the data type 336 * used: 337 * * to store the data in the database 338 * * to select the type of input in a HTML form 339 */ 340 public static abstract function getDataType(): string; 341 342 /** 343 * @return string - the description (used in tooltip) 344 */ 345 public static abstract function getDescription(): string; 346 347 /** 348 * @return string - the label used in a form or log 349 */ 350 public static abstract function getLabel(): string; 351 352 /** 353 * @return string - the short link name 354 */ 355 public static function getCanonical(): string 356 { 357 return static::getName(); 358 } 359 360 /** 361 * @return ResourceCombo - The resource 362 */ 363 public function getResource(): ?ResourceCombo 364 { 365 if ($this->resource !== null) { 366 return $this->resource; 367 } 368 if ($this->parent !== null) { 369 return $this->parent->getResource(); 370 } 371 return null; 372 } 373 374 /** 375 * For which resources is the metadata for 376 * @param ResourceCombo $resource 377 * @return $this 378 */ 379 public function setResource(ResourceCombo $resource): Metadata 380 { 381 $this->resource = $resource; 382 return $this; 383 } 384 385 /** 386 * @return string - the storage name use in the store 387 * 388 * For instance, a {@link PageImagePath} has a unique name of `page-image-path` 389 * but when we store it hierarchically, the prefix `page-image` is not needed 390 * and becomes simple `path` 391 * The metadata is stored in a table `page-image` with the column `path`. 392 */ 393 public static function getPersistentName(): string 394 { 395 return static::getName(); 396 } 397 398 /** 399 * @return string the unique name of the metadata (property) 400 * 401 * 402 * It's the hierachical representation of the {@link self::getPersistentName()} 403 * 404 */ 405 public static abstract function getName(): string; 406 407 408 /** 409 * @return null|string|array the value to be persisted to the {@link self::setWriteStore()} 410 * the reverse action is {@link Metadata::setFromStoreValue()} 411 * 412 * Null may be returned (no exception is thrown) 413 * as this is a possible storage value 414 */ 415 public function toStoreValue() 416 { 417 try { 418 return $this->getValue(); 419 } catch (ExceptionNotFound $e) { 420 /** 421 * The only case when we return null 422 * and not throw an exception 423 * because it may be stored 424 */ 425 return null; 426 } 427 } 428 429 430 /** 431 * @return mixed 432 * The store default value is used to 433 * see if the value set is the same than the default one 434 * It this is the case, the data is not stored 435 */ 436 public function toStoreDefaultValue() 437 { 438 try { 439 return $this->getDefaultValue(); 440 } catch (ExceptionNotFound $e) { 441 /** 442 * We don't throw an null exception here because 443 * null may be stored 444 */ 445 return null; 446 } 447 } 448 449 /** 450 * Data that should persist (this data should be in a backup) 451 */ 452 public const PERSISTENT_METADATA = "persistent"; 453 /** 454 * Data that are derived from other and stored 455 */ 456 public const DERIVED_METADATA = "derived"; 457 /** 458 * A runtime metadata is created for the purpose of a process 459 * Example {@link CacheRuntimeDependencies2} 460 */ 461 const RUNTIME_METADATA = "runtime"; 462 463 464 /** 465 * 466 * Return the type of metadata. 467 * * {@link Metadata::PERSISTENT_METADATA} 468 * * {@link Metadata::DERIVED_METADATA} 469 * * {@link Metadata::RUNTIME_METADATA} 470 * * 471 * 472 * Backup: Only the {@link Metadata::PERSISTENT_METADATA} got a backup 473 * 474 * @return string 475 * 476 * Unfortunately, Dokuwiki makes this distinction only in rendering 477 * https://forum.dokuwiki.org/d/19764-how-to-test-a-current-metadata-setting 478 * Therefore all metadata are persistent 479 * 480 * Ie a {@link MetadataDokuWikiStore::CURRENT_METADATA} is only derived 481 * in a rendering context. A {@link MetadataDokuWikiStore::PERSISTENT_DOKUWIKI_KEY} is always stored. 482 * 483 * 484 * 485 */ 486 public abstract static function getPersistenceType(): string; 487 488 489 /** 490 * The user can't delete this metadata 491 * in the persistent metadata 492 */ 493 const NOT_MODIFIABLE_PERSISTENT_METADATA = [ 494 PagePath::PROPERTY_NAME, 495 CreationDate::PROPERTY_NAME, 496 ModificationDate::PROPERTY_NAME, 497 PageId::PROPERTY_NAME, 498 "user", 499 "contributor", 500 "creator", 501 "date", 502 action_plugin_combo_metadescription::DESCRIPTION_META_KEY, // Dokuwiki implements it as an array (you can't modify it directly) 503 "last_change" // not sure why it's in the persistent data 504 ]; 505 506 /** 507 * Metadata that we can lose 508 * because they are generated 509 * 510 * They still needs to be saved in as persistent metadata 511 * otherwise they are just not persisted 512 * https://forum.dokuwiki.org/d/19764-how-to-test-a-current-metadata-setting 513 */ 514 const RUNTIME_META = [ 515 "format", 516 "internal", // toc, cache, ... 517 "relation", 518 PageH1::H1_PARSED, 519 LowQualityCalculatedIndicator::PROPERTY_NAME 520 ]; 521 522 523 /** 524 * Delete the managed metadata 525 * @param $metadataArray - a metadata array 526 * @return array - the metadata array without the managed metadata 527 */ 528 public static function deleteMutableMetadata($metadataArray): array 529 { 530 if (sizeof($metadataArray) === 0) { 531 return $metadataArray; 532 } 533 $cleanedMetadata = []; 534 foreach ($metadataArray as $key => $value) { 535 try { 536 if (!MetadataSystem::getForName($key)->isMutable()) { 537 $cleanedMetadata[$key] = $value; 538 } 539 } catch (ExceptionNotFound $e) { 540 continue; 541 } 542 } 543 return $cleanedMetadata; 544 } 545 546 /** 547 * This function will upsert the meta array 548 * with a unique property 549 * @param $metaArray 550 * @param string $uniqueAttribute 551 * @param array $attributes 552 */ 553 public static function upsertMetaOnUniqueAttribute(&$metaArray, string $uniqueAttribute, array $attributes) 554 { 555 556 foreach ($metaArray as $key => $meta) { 557 if (!is_numeric($key)) { 558 LogUtility::msg("The passed array is not a meta array because the index are not numeric. Unable to update it."); 559 return; 560 } 561 if (isset($meta[$uniqueAttribute])) { 562 $value = $meta[$uniqueAttribute]; 563 if ($value === $attributes[$uniqueAttribute]) { 564 $metaArray[$key] = $attributes; 565 return; 566 } 567 } 568 } 569 $metaArray[] = $attributes; 570 571 } 572 573 /** 574 * Delete the runtime if present 575 * (They were saved in persistent) 576 */ 577 public static function deleteIfPresent(array &$persistentPageMeta, array $attributeToDeletes): bool 578 { 579 $unsetWasPerformed = false; 580 foreach ($attributeToDeletes as $runtimeMeta) { 581 if (isset($persistentPageMeta[$runtimeMeta])) { 582 unset($persistentPageMeta[$runtimeMeta]); 583 $unsetWasPerformed = true; 584 } 585 } 586 return $unsetWasPerformed; 587 } 588 589 /** 590 * @return bool can the user change the value 591 * 592 * 593 * The meta that are modifiable by the user (in the form, ...) 594 * 595 * Usage: 596 * * In a form, the field will be disabled 597 * * Replication: this meta could be replicated 598 * * in the {@link \syntax_plugin_combo_frontmatter} 599 * * or in the database 600 * 601 * If you deprecate a metadata, you should set this value to false 602 * and set the replacement to true (where the replacement takes the value of the deprecated metadata) 603 */ 604 public abstract static function isMutable(): bool; 605 606 /** 607 * @return bool if true the metadata will be shown on the meta manager form 608 * Note that a non-mutable meta may also be shown for information purpose 609 */ 610 public static function isOnForm(): bool 611 { 612 return false; 613 } 614 615 616 /** 617 * @return array|null The possible values that can take this metadata 618 * If null, all, no constraints 619 */ 620 public function getPossibleValues(): ?array 621 { 622 return null; 623 } 624 625 /** 626 * An utility function to {@link Metadata::sendToWriteStore()} 627 * and {@link MetadataStore::persist()} at the same time in the {@link Metadata::getWriteStore() write store} 628 * 629 * @throws ExceptionBadArgument - if the value can not be persisted 630 */ 631 public function persist(): Metadata 632 { 633 634 $oldValue = $this->getWriteStore()->get($this); 635 $this->sendToWriteStore(); 636 $this->getWriteStore()->persist(); 637 $actualValue = $this->toStoreValue(); 638 $attribute = $this->getName(); 639 640 MetadataMutation::notifyMetadataMutation($attribute, $oldValue, $actualValue, $this->getResource()->getPathObject()); 641 642 return $this; 643 } 644 645 /** 646 * Build the object from the store value 647 * 648 * The inverse function is {@link Metadata::toStoreValue()} 649 * 650 * The function used by {@link Metadata::buildFromReadStore()} 651 * to build the value from the {@link MetadataStore::get()} 652 * function. 653 * 654 * The difference between the {@link Metadata::setFromStoreValue()} 655 * is that this function should not make any validity check 656 * or throw any compile exception. 657 * 658 * It's a commodity function that should be use by the developer 659 * when it sets a known value and therefore does not expect a quality error 660 * 661 * @param $value 662 * @return mixed 663 */ 664 public abstract function setFromStoreValueWithoutException($value): Metadata; 665 666 /** 667 * Set a value from the {@link self::getReadStore()} 668 * 669 * If you have quality problem to throw, you can use this function 670 * instead of {@link Metadata::setFromStoreValueWithoutException()} 671 * 672 * @param $value 673 * @return Metadata 674 */ 675 public function setFromStoreValue($value): Metadata 676 { 677 return $this->setFromStoreValueWithoutException($value); 678 } 679 680 681 /** 682 * The class of an entity metadata (ie if the metadata has children / is a {@link Metadata::$parent} 683 * 684 * One id value = one row = one entity 685 * 686 * @return string|null 687 */ 688 public function getUidClass(): ?string 689 { 690 if (count(static::getChildrenClass()) >= 1) { 691 LogUtility::msg("An entity metadata should define a metadata that store the unique value"); 692 } 693 return null; 694 } 695 696 /** 697 * The width on a scale of 12 for the form field 698 * @return null 699 */ 700 public static function getFormControlWidth() 701 { 702 return null; 703 } 704 705 /** 706 * @return string[] - the old name if any 707 */ 708 public static function getOldPersistentNames(): array 709 { 710 return []; 711 } 712 713 /** 714 * @return mixed - the memory value 715 * @throws ExceptionNotFound - if the value is null 716 */ 717 public abstract function getValue(); 718 719 /** 720 * @return mixed 721 * @throws ExceptionNotFound 722 */ 723 public abstract function getDefaultValue(); 724 725 /** 726 * @return mixed - set the memory value from the store and return ut 727 * @deprecated use the {@link self::toStoreValue()} instead 728 */ 729 public function getValueFromStore() 730 { 731 return $this->toStoreValue(); 732 } 733 734 735 /** 736 * @throws ExceptionNotFound 737 */ 738 public function getValueFromStoreOrDefault() 739 { 740 $this->setFromStoreValueWithoutException($this->getReadStore()->get($this)); 741 return $this->getValueOrDefault(); 742 } 743 744 /** 745 * @throws ExceptionNotFound 746 */ 747 public function getValueOrDefault() 748 { 749 750 try { 751 $value = $this->getValue(); 752 if ($value === "") { 753 return $this->getDefaultValue(); 754 } 755 return $value; 756 } catch (ExceptionNotFound $e) { 757 return $this->getDefaultValue(); 758 } 759 760 } 761 762 763 /** 764 * @return MetadataStore - the store where the metadata are persist (by default, the {@link Metadata::getReadStore()} 765 */ 766 public function getWriteStore(): MetadataStore 767 { 768 if ($this->writeStore === null) { 769 return $this->getReadStore(); 770 } 771 /** 772 * WriteStore may be just the string class name 773 */ 774 if (!($this->writeStore instanceof MetadataStore)) { 775 $this->writeStore = MetadataStoreAbs::toMetadataStore($this->writeStore, $this->getResource()); 776 } 777 return $this->writeStore; 778 } 779 780 /** 781 * @throws ExceptionBadArgument - if the class string of the children are not good 782 */ 783 public function getUidObject(): Metadata 784 { 785 if ($this->uidObject === null) { 786 787 $this->uidObject = MetadataSystem::toMetadataObject($this->getUidClass()) 788 ->setResource($this->getResource()); 789 790 } 791 return $this->uidObject; 792 } 793 794} 795