xref: /plugin/combo/ComboStrap/Meta/Store/MetadataDokuWikiStore.php (revision 3e1693237c254d9ee82f94fb9f9128467b8c10e4)
1<?php
2
3
4namespace ComboStrap\Meta\Store;
5
6use ComboStrap\DataType;
7use ComboStrap\ExceptionBadState;
8use ComboStrap\ExceptionCast;
9use ComboStrap\ExceptionNotExists;
10use ComboStrap\ExceptionNotFound;
11use ComboStrap\ExceptionRuntime;
12use ComboStrap\ExceptionRuntimeInternal;
13use ComboStrap\ExecutionContext;
14use ComboStrap\FetcherMarkup;
15use ComboStrap\LocalPath;
16use ComboStrap\LogUtility;
17use ComboStrap\MarkupPath;
18use ComboStrap\Meta\Api\Metadata;
19use ComboStrap\Meta\Api\MetadataStore;
20use ComboStrap\Meta\Api\MetadataStoreAbs;
21use ComboStrap\Path;
22use ComboStrap\ResourceCombo;
23use ComboStrap\WikiPath;
24
25/**
26 * Class MetadataFileSystemStore
27 * @package ComboStrap
28 *
29 * A wrapper around the dokuwiki meta file system store.
30 *
31 */
32class MetadataDokuWikiStore extends MetadataStoreAbs
33{
34
35    /**
36     * Current metadata / runtime metadata / calculated metadata
37     * This metadata can only be set when  {@link Syntax::render() rendering}
38     * The data may be deleted
39     * https://www.dokuwiki.org/devel:metadata#metadata_persistence
40     *
41     * This is generally where the default data is located
42     * if not found in the persistent
43     */
44    public const CURRENT_METADATA = "current";
45
46
47    const CANONICAL = Metadata::CANONICAL;
48    /**
49     * Persistent metadata (data that should be in a backup)
50     *
51     * They are used as the default of the current metadata
52     * and is never cleaned
53     *
54     * https://www.dokuwiki.org/devel:metadata#metadata_persistence
55     *
56     * Because the current is only usable in rendering, all
57     * metadata are persistent inside dokuwiki
58     */
59    public const PERSISTENT_DOKUWIKI_KEY = "persistent";
60
61
62    /**
63     * @return MetadataDokuWikiStore
64     * We don't use a global static variable
65     * because we are working with php as cgi script
66     * and there is no notion of request
67     * to be able to flush the data on the disk
68     *
69     * The scope of the data will be then the store
70     */
71    public static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore
72    {
73
74        $context = ExecutionContext::getActualOrCreateFromEnv();
75
76        try {
77            $executionCachedStores = &$context->getRuntimeObject(MetadataDokuWikiStore::class);
78        } catch (ExceptionNotFound $e) {
79            $executionCachedStores = [];
80            $context->setRuntimeObject(MetadataDokuWikiStore::class, $executionCachedStores);
81        }
82        $path = $resourceCombo->getPathObject()->toAbsoluteId();
83        if (isset($executionCachedStores[$path])) {
84            return $executionCachedStores[$path];
85        }
86
87        $metadataStore = new MetadataDokuWikiStore($resourceCombo);
88        $executionCachedStores[$path] = $metadataStore;
89        return $metadataStore;
90
91    }
92
93    /**
94     *
95     * In a rendering, you should not use the {@link p_set_metadata()}
96     * but use {@link \Doku_Renderer_metadata::meta} and {@link \Doku_Renderer_metadata::$persistent}
97     * to set the metadata
98     *
99     * Why ?
100     * The metadata are set in $METADATA_RENDERERS (A global cache variable where the persistent data is set/exist
101     * only during metadata rendering with the function {@link p_render_metadata()}) and then
102     * saved at the end
103     *
104     */
105    private static function isRendering(string $wikiId): bool
106    {
107        global $METADATA_RENDERERS;
108        if (isset($METADATA_RENDERERS[$wikiId])) {
109            return true;
110        }
111        return false;
112    }
113
114
115    /**
116     * Delete the globals
117     */
118    public static function unsetGlobalVariables()
119    {
120        /**
121         * {@link p_read_metadata() global cache}
122         */
123        unset($GLOBALS['cache_metadata']);
124
125        /**
126         * {@link p_render_metadata()} temporary render cache
127         * global $METADATA_RENDERERS;
128         */
129        unset($GLOBALS['METADATA_RENDERERS']);
130
131    }
132
133
134    /**
135     * @throws ExceptionBadState - if for any reason, it's not possible to store the data
136     */
137    public function set(Metadata $metadata)
138    {
139
140        $name = $metadata->getName();
141        $persistentValue = $metadata->toStoreValue();
142        $defaultValue = $metadata->toStoreDefaultValue();
143        $resource = $metadata->getResource();
144        $this->checkResource($resource);
145        if ($resource === null) {
146            throw new ExceptionBadState("A resource is mandatory", self::CANONICAL);
147        }
148        if (!($resource instanceof MarkupPath)) {
149            throw new ExceptionBadState("The DokuWiki metadata store is only for page resource", self::CANONICAL);
150        }
151        $this->setFromPersistentName($name, $persistentValue, $defaultValue);
152    }
153
154    /**
155     * @param Metadata $metadata
156     * @param null $default
157     * @return mixed|null
158     */
159    public function get(Metadata $metadata, $default = null)
160    {
161
162        $resource = $metadata->getResource();
163        $this->checkResource($resource);
164        if ($resource === null) {
165            throw new ExceptionRuntime("A resource is mandatory", self::CANONICAL);
166        }
167        if (!($resource instanceof MarkupPath)) {
168            throw new ExceptionRuntime("The DokuWiki metadata store is only for page resource", self::CANONICAL);
169        }
170        return $this->getFromName($metadata->getName(), $default);
171
172
173    }
174
175    /**
176     *
177     * Getting a metadata for a resource via its name
178     * when we don't want to create a class
179     *
180     * This function is used primarily by derived / process metadata
181     *
182     * @param string $name
183     * @param null $default
184     * @return mixed
185     */
186    public function getFromName(string $name, $default = null)
187    {
188        /**
189         * We don't use {@link p_get_metadata()}
190         * because it will trigger a {@link p_render_metadata()}
191         * But we may just want to check if there is a {@link PageId}
192         * before rendering
193         */
194        $data = $this->getData();
195        $value = $data[$name] ?? null;
196
197        /**
198         * Empty string return null
199         * because Dokuwiki does not allow to delete keys
200         * {@link p_set_metadata()}
201         */
202        if ($value !== null && $value !== "") {
203            return $value;
204        }
205        return $default;
206    }
207
208
209    public function persist()
210    {
211
212        /**
213         * Done on set via the dokuwiki function
214         */
215
216    }
217
218    /**
219     * @param string $name
220     * @param string|array $value
221     * @param null $default
222     * @return MetadataDokuWikiStore
223     */
224    public function setFromPersistentName(string $name, $value, $default = null): MetadataDokuWikiStore
225    {
226        $oldValue = $this->getFromName($name);
227        if (is_bool($value)) {
228            if ($oldValue === null) {
229                $oldValue = $default;
230            } else {
231                $oldValue = DataType::toBoolean($oldValue);
232            }
233        }
234        if ($oldValue !== $value) {
235
236            /**
237             * Metadata in Dokuwiki is fucked up.
238             *
239             * You can't remove a metadata,
240             * You need to know if this is a rendering or not
241             *
242             * See just how fucked {@link p_set_metadata()} is
243             *
244             * Also don't change the type of the value to a string
245             * otherwise dokuwiki will not see a change
246             * between true and a string and will not persist the value
247             *
248             * By default, the value is copied in the current and persistent array
249             * and there is no render
250             */
251            $wikiId = $this->getWikiId();
252            if (self::isRendering($wikiId)) {
253
254                /**
255                 * It seems that {@link p_set_metadata()} uses it also
256                 * but we show it here
257                 */
258                global $METADATA_RENDERERS;
259                $METADATA_RENDERERS[$wikiId][self::CURRENT_METADATA][$name] = $value;
260                $METADATA_RENDERERS[$wikiId][self::PERSISTENT_DOKUWIKI_KEY][$name] = $value;
261
262            } else {
263
264                p_set_metadata($wikiId,
265                    [
266                        $name => $value
267                    ]
268                );
269
270            }
271            $this->setGlobalCacheIfAny($name, $value);
272        }
273        return $this;
274    }
275
276    public function getData(): array
277    {
278        /**
279         * We return only the current data.
280         *
281         * Why ?
282         * To be consistent with {@link p_get_metadata()} that retrieves only from the `current` array
283         * Therefore the `persistent` array values should always be duplicated in the `current` array
284         *
285         * (the only diff is that the persistent value are still available during a {@link p_render_metadata() metadata render})
286         *
287         * Note that Dokuwiki load them also for the requested path
288         * at `global $INFO, $info['meta']` with {@link pageinfo()}
289         * and is synced in {@link p_save_metadata()}
290         *
291         */
292        return $this->getDataCurrentAndPersistent()[self::CURRENT_METADATA];
293    }
294
295    private function getWikiId(): string
296    {
297        try {
298            return $this->getResource()->getPathObject()->toWikiPath()->getWikiId();
299        } catch (ExceptionCast $e) {
300            throw new ExceptionRuntimeInternal("Should not happen", $e);
301        }
302    }
303
304
305    /**
306     *
307     * @param $name
308     * @return mixed|null
309     * @deprecated - the data should always be replicated in current use {@link self::getFromName()}
310     */
311    public
312    function getCurrentFromName($name)
313    {
314        return $this->getFromName($name);
315    }
316
317    /**
318     * @return MetadataDokuWikiStore
319     * @deprecated should use a fetcher markup ?
320     */
321    public function renderAndPersist(): MetadataDokuWikiStore
322    {
323        /**
324         * Read/render the metadata from the file
325         * with parsing
326         */
327        $wikiPage = $this->getResource();
328        if (!$wikiPage instanceof WikiPath) {
329            LogUtility::errorIfDevOrTest("The resource is not a wiki path");
330            return $this;
331        }
332        try {
333            FetcherMarkup::confRoot()
334                ->setRequestedExecutingPath($wikiPage)
335                ->setRequestedContextPath($wikiPage)
336                ->setRequestedMimeToMetadata()
337                ->build()
338                ->processMetadataIfNotYetDone();
339        } catch (ExceptionNotExists $e) {
340            LogUtility::error("Metadata Build Error", self::CANONICAL, $e);
341        }
342
343        return $this;
344    }
345
346
347    public function isHierarchicalTextBased(): bool
348    {
349        return true;
350    }
351
352    /**
353     * @return Path - the full path to the meta file
354     */
355    public
356    function getMetaFilePath(): ?Path
357    {
358        $dokuwikiId = $this->getWikiId();
359        return LocalPath::createFromPathString(metaFN($dokuwikiId, '.meta'));
360    }
361
362    public function __toString()
363    {
364        return "DokuMeta ({$this->getWikiId()}";
365    }
366
367
368    public function deleteAndFlush()
369    {
370        $emptyMeta = [MetadataDokuWikiStore::CURRENT_METADATA => [], self::PERSISTENT_DOKUWIKI_KEY => []];
371        $dokuwikiId = $this->getWikiId();
372        p_save_metadata($dokuwikiId, $emptyMeta);
373    }
374
375
376    public function reset()
377    {
378        self::unsetGlobalVariables();
379    }
380
381    /**
382     * In {@link p_read_metadata()}, there is a global cache
383     * @param string $name
384     * @param mixed $value
385     */
386    private function setGlobalCacheIfAny(string $name, $value)
387    {
388        global $cache_metadata;
389
390        $id = $this->getWikiId();
391        if (isset($cache_metadata[$id])) {
392            $cache_metadata[$id]['persistent'][$name] = $value;
393            $cache_metadata[$id]['current'][$name] = $value;
394        }
395
396    }
397
398    /**
399     * @return array -the full array only needed by the rendering process
400     * You should use {@link self::getData()} otherwise
401     */
402    public function getDataCurrentAndPersistent(): array
403    {
404
405        $id = $this->getWikiId();
406        $data = p_read_metadata($id, true);
407        if (empty($data)) {
408            LogUtility::internalError("The metadata cache was empty");
409            $data = p_read_metadata($id);
410        }
411        return $data;
412    }
413}
414