1 <?php
2 
3 
4 namespace ComboStrap\Meta\Api;
5 
6 
7 use action_plugin_combo_metadescription;
8 use ComboStrap\CacheExpirationDate;
9 use ComboStrap\CacheExpirationFrequency;
10 use ComboStrap\Canonical;
11 use ComboStrap\DataType;
12 use ComboStrap\ExceptionRuntimeInternal;
13 use ComboStrap\ExecutionContext;
14 use ComboStrap\Label;
15 use ComboStrap\DisqusIdentifier;
16 use ComboStrap\DokuwikiId;
17 use ComboStrap\EndDate;
18 use ComboStrap\ExceptionBadArgument;
19 use ComboStrap\ExceptionCompile;
20 use ComboStrap\ExceptionNotFound;
21 use ComboStrap\ExceptionRuntime;
22 use ComboStrap\FirstImage;
23 use ComboStrap\FirstRasterImage;
24 use ComboStrap\FirstSvgIllustration;
25 use ComboStrap\FeaturedIcon;
26 use ComboStrap\Lang;
27 use ComboStrap\LdJson;
28 use ComboStrap\Lead;
29 use ComboStrap\Locale;
30 use ComboStrap\LogUtility;
31 use ComboStrap\LowQualityCalculatedIndicator;
32 use ComboStrap\LowQualityPageOverwrite;
33 use ComboStrap\Meta\Field\Aliases;
34 use ComboStrap\Meta\Field\AliasPath;
35 use ComboStrap\Meta\Field\AliasType;
36 use ComboStrap\Meta\Field\AncestorImage;
37 use ComboStrap\Meta\Field\FacebookImage;
38 use ComboStrap\Meta\Field\FeaturedImage;
39 use ComboStrap\Meta\Field\SocialCardImage;
40 use ComboStrap\Meta\Field\FeaturedRasterImage;
41 use ComboStrap\Meta\Field\FeaturedSvgImage;
42 use ComboStrap\Meta\Field\PageH1;
43 use ComboStrap\Meta\Field\PageTemplateName;
44 use ComboStrap\Meta\Field\TwitterImage;
45 use ComboStrap\Meta\Store\MetadataDokuWikiStore;
46 use ComboStrap\MetadataMutation;
47 use ComboStrap\ModificationDate;
48 use ComboStrap\CreationDate;
49 use ComboStrap\PageDescription;
50 use ComboStrap\PageId;
51 use ComboStrap\Meta\Field\PageImagePath;
52 use ComboStrap\Meta\Field\PageImages;
53 use ComboStrap\PageImageUsage;
54 use ComboStrap\PageKeywords;
55 use ComboStrap\PageLevel;
56 use ComboStrap\PagePath;
57 use ComboStrap\PagePublicationDate;
58 use ComboStrap\PageTitle;
59 use ComboStrap\PageType;
60 use ComboStrap\PageUrlPath;
61 use ComboStrap\PluginUtility;
62 use ComboStrap\QualityDynamicMonitoringOverwrite;
63 use ComboStrap\References;
64 use ComboStrap\Meta\Field\Region;
65 use ComboStrap\ReplicationDate;
66 use ComboStrap\ResourceCombo;
67 use ComboStrap\ResourceName;
68 use ComboStrap\Slug;
69 use ComboStrap\StartDate;
70 use ComboStrap\Web\UrlEndpoint;
71 
72 abstract 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