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