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) && $oldValue !== null) {
228            $oldValue = DataType::toBoolean($oldValue);
229        }
230        if ($oldValue !== $value) {
231
232            /**
233             * Metadata in Dokuwiki is fucked up.
234             *
235             * You can't remove a metadata,
236             * You need to know if this is a rendering or not
237             *
238             * See just how fucked {@link p_set_metadata()} is
239             *
240             * Also don't change the type of the value to a string
241             * otherwise dokuwiki will not see a change
242             * between true and a string and will not persist the value
243             *
244             * By default, the value is copied in the current and persistent array
245             * and there is no render
246             */
247            $wikiId = $this->getWikiId();
248            if (self::isRendering($wikiId)) {
249
250                /**
251                 * It seems that {@link p_set_metadata()} uses it also
252                 * but we show it here
253                 */
254                global $METADATA_RENDERERS;
255                $METADATA_RENDERERS[$wikiId][self::CURRENT_METADATA][$name] = $value;
256                $METADATA_RENDERERS[$wikiId][self::PERSISTENT_DOKUWIKI_KEY][$name] = $value;
257
258            } else {
259
260                p_set_metadata($wikiId,
261                    [
262                        $name => $value
263                    ]
264                );
265
266            }
267            $this->setGlobalCacheIfAny($name, $value);
268        }
269        return $this;
270    }
271
272    public function getData(): array
273    {
274        /**
275         * We return only the current data.
276         *
277         * Why ?
278         * To be consistent with {@link p_get_metadata()} that retrieves only from the `current` array
279         * Therefore the `persistent` array values should always be duplicated in the `current` array
280         *
281         * (the only diff is that the persistent value are still available during a {@link p_render_metadata() metadata render})
282         *
283         * Note that Dokuwiki load them also for the requested path
284         * at `global $INFO, $info['meta']` with {@link pageinfo()}
285         * and is synced in {@link p_save_metadata()}
286         *
287         */
288        return $this->getDataCurrentAndPersistent()[self::CURRENT_METADATA];
289
290    }
291
292    private function getWikiId(): string
293    {
294        try {
295            return $this->getResource()->getPathObject()->toWikiPath()->getWikiId();
296        } catch (ExceptionCast $e) {
297            throw new ExceptionRuntimeInternal("Should not happen", $e);
298        }
299    }
300
301
302    /**
303     *
304     * @param $name
305     * @return mixed|null
306     * @deprecated - the data should always be replicated in current use {@link self::getFromName()}
307     */
308    public
309    function getCurrentFromName($name)
310    {
311        return $this->getFromName($name);
312    }
313
314    /**
315     * @return MetadataDokuWikiStore
316     * @deprecated should use a fetcher markup ?
317     */
318    public function renderAndPersist(): MetadataDokuWikiStore
319    {
320        /**
321         * Read/render the metadata from the file
322         * with parsing
323         */
324        $wikiPage = $this->getResource();
325        if (!$wikiPage instanceof WikiPath) {
326            LogUtility::errorIfDevOrTest("The resource is not a wiki path");
327            return $this;
328        }
329        try {
330            FetcherMarkup::confRoot()
331                ->setRequestedExecutingPath($wikiPage)
332                ->setRequestedContextPath($wikiPage)
333                ->setRequestedMimeToMetadata()
334                ->build()
335                ->processMetadataIfNotYetDone();
336        } catch (ExceptionNotExists $e) {
337            LogUtility::error("Metadata Build Error", self::CANONICAL, $e);
338        }
339
340        return $this;
341    }
342
343
344    public function isHierarchicalTextBased(): bool
345    {
346        return true;
347    }
348
349    /**
350     * @return Path - the full path to the meta file
351     */
352    public
353    function getMetaFilePath(): ?Path
354    {
355        $dokuwikiId = $this->getWikiId();
356        return LocalPath::createFromPathString(metaFN($dokuwikiId, '.meta'));
357    }
358
359    public function __toString()
360    {
361        return "DokuMeta ({$this->getWikiId()}";
362    }
363
364
365    public function deleteAndFlush()
366    {
367        $emptyMeta = [MetadataDokuWikiStore::CURRENT_METADATA => [], self::PERSISTENT_DOKUWIKI_KEY => []];
368        $dokuwikiId = $this->getWikiId();
369        p_save_metadata($dokuwikiId, $emptyMeta);
370    }
371
372
373    public function reset()
374    {
375        self::unsetGlobalVariables();
376    }
377
378    /**
379     * In {@link p_read_metadata()}, there is a global cache
380     * @param string $name
381     * @param mixed $value
382     */
383    private function setGlobalCacheIfAny(string $name, $value)
384    {
385        global $cache_metadata;
386
387        $id = $this->getWikiId();
388        if (isset($cache_metadata[$id])) {
389            $cache_metadata[$id]['persistent'][$name] = $value;
390            $cache_metadata[$id]['current'][$name] = $value;
391        }
392
393    }
394
395    /**
396     * @return array -the full array only needed by the rendering process
397     * You should use {@link self::getData()} otherwise
398     */
399    public function getDataCurrentAndPersistent(): array
400    {
401
402        $id = $this->getWikiId();
403        $data = p_read_metadata($id, true);
404        if (empty($data)) {
405            LogUtility::internalError("The metadata cache was empty");
406            $data = p_read_metadata($id);
407        }
408        return $data;
409    }
410}
411