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