1<?php
2
3
4namespace ComboStrap;
5
6use dokuwiki\Extension\Event;
7
8/**
9 * Class MetadataFileSystemStore
10 * @package ComboStrap
11 *
12 * The meta file system store.
13 *
14 * It mimics an in-memory store where data are
15 *      * read at the store creation
16 *      * refreshed when the metadata render runs (See {@link  \action_plugin_combo_metasync}) (Ie dokuwiki modifies the metadata files this way) {@link MetadataDokuWikiStore::renderAndPersist()}
17 *      * written immediately on the disk (few write) with the {@link p_set_metadata()}
18 *
19 * Why ?
20 * Php is a CGI script meaning that it starts and end for each request
21 * on the server.
22 * But in test, this is not the case, as the script starts for the first test
23 * and end with the last test.
24 *
25 * If the data store is local scoped, we get then a lot of inconsistency
26 *   - the data for one page is not the same than another
27 *   - the metadata object {@link PageId} (from {@link ResourceCombo::getUidObject()} may be null while it was created with another {@link Metadata} creating it twice
28 *
29 * This implementation has a cache object
30 *
31 */
32class MetadataDokuWikiStore extends MetadataSingleArrayStore
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     * Persistent metadata (data that should be in a backup)
47     *
48     * They are used as the default of the current metadata
49     * and is never cleaned
50     *
51     * https://www.dokuwiki.org/devel:metadata#metadata_persistence
52     *
53     * Because the current is only usable in rendering, all
54     * metadata are persistent inside dokuwiki
55     */
56    public const PERSISTENT_METADATA = "persistent";
57
58
59    const CANONICAL = Metadata::CANONICAL;
60    /**
61     * When the value of a metadata has changed
62     */
63    public const PAGE_METADATA_MUTATION_EVENT = "PAGE_METADATA_MUTATION_EVENT";
64    const NEW_VALUE_ATTRIBUTE = "new_value";
65
66    /**
67     *
68     * @var MetadataDokuWikiStore[] a cache of store
69     */
70    private static $storesByRequestedPage;
71
72
73    /**
74     * @return MetadataDokuWikiStore
75     * We don't use a global static variable
76     * because we are working with php as cgi script
77     * and there is no notion of request
78     * to be able to flush the data on the disk
79     *
80     * The scope of the data will be then the store
81     */
82    public static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore
83    {
84
85        $requestedId = PluginUtility::getRequestedWikiId();
86        if ($requestedId === null) {
87            if ($resourceCombo instanceof Page) {
88                $requestedId = $resourceCombo->getDokuwikiId();
89            } else {
90                $requestedId = "not-a-page";
91            }
92        }
93        $storesByRequestedId = &self::$storesByRequestedPage[$requestedId];
94        if ($storesByRequestedId === null) {
95            // delete all previous stores by requested page id
96            self::$storesByRequestedPage = null;
97            self::$storesByRequestedPage[$requestedId] = [];
98            $storesByRequestedId = &self::$storesByRequestedPage[$requestedId];
99        }
100        $path = $resourceCombo->getPath()->toString();
101        if (isset($storesByRequestedId[$path])) {
102            return $storesByRequestedId[$path];
103        }
104
105        if (!($resourceCombo instanceof Page)) {
106            LogUtility::msg("The resource is not a page. File System store supports only page resources");
107            $data = null;
108        } else {
109            $data = p_read_metadata($resourceCombo->getDokuwikiId());
110        }
111
112        $metadataStore = new MetadataDokuWikiStore($resourceCombo, $data);
113        $storesByRequestedId[$path] = $metadataStore;
114        return $metadataStore;
115
116    }
117
118
119    /**
120     * @return MetadataDokuWikiStore[]
121     */
122    public static function getStores(): array
123    {
124        return self::$storesByRequestedPage;
125    }
126
127    /**
128     * Delete the in-memory data store
129     */
130    public static function resetAll()
131    {
132        self::$storesByRequestedPage = [];
133    }
134
135    public function set(Metadata $metadata)
136    {
137
138        $name = $metadata->getName();
139        $persistentValue = $metadata->toStoreValue();
140        $defaultValue = $metadata->toStoreDefaultValue();
141        $resource = $metadata->getResource();
142        $this->checkResource($resource);
143        if ($resource === null) {
144            throw new ExceptionComboRuntime("A resource is mandatory", self::CANONICAL);
145        }
146        if (!($resource instanceof Page)) {
147            throw new ExceptionComboRuntime("The DokuWiki metadata store is only for page resource", self::CANONICAL);
148        }
149        $dokuwikiId = $resource->getDokuwikiId();
150        $this->setFromWikiId($dokuwikiId, $name, $persistentValue, $defaultValue);
151    }
152
153    /**
154     * @param Metadata $metadata
155     * @param null $default
156     * @return mixed|null
157     *
158     *
159     */
160    public function get(Metadata $metadata, $default = null)
161    {
162
163        $resource = $metadata->getResource();
164        $this->checkResource($resource);
165        if ($resource === null) {
166            throw new ExceptionComboRuntime("A resource is mandatory", self::CANONICAL);
167        }
168        if (!($resource instanceof Page)) {
169            throw new ExceptionComboRuntime("The DokuWiki metadata store is only for page resource", self::CANONICAL);
170        }
171        return $this->getFromWikiId($resource->getDokuwikiId(), $metadata->getName(), $default);
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 getFromPersistentName(string $name, $default = null)
187    {
188        $wikiId = $this->getResource()->getDokuwikiId();
189        return $this->getFromWikiId($wikiId, $name, $default);
190    }
191
192
193    public function persist()
194    {
195
196        /**
197         * Done on set via the dokuwiki function
198         */
199
200    }
201
202    /**
203     * @param string $name
204     * @param string|array $value
205     * @return MetadataDokuWikiStore
206     */
207    public function setFromPersistentName(string $name, $value): MetadataDokuWikiStore
208    {
209        $this->setFromWikiId($this->getResource()->getDokuwikiId(), $name, $value);
210        return $this;
211    }
212
213    public function getData(): ?array
214    {
215        if (
216            $this->data === null
217            || sizeof($this->data[self::PERSISTENT_METADATA]) === 0 // move
218        ) {
219            $this->data = p_read_metadata($this->getResource()->getDokuwikiId());
220        }
221        return parent::getData();
222    }
223
224
225    /**
226     * @param $name
227     * @return mixed|null
228     */
229    private function getPersistentMetadata($name)
230    {
231        $value = $this->getData()[self::PERSISTENT_METADATA][$name];
232        /**
233         * Empty string return null
234         * because Dokuwiki does not allow to delete keys
235         * {@link p_set_metadata()}
236         */
237        if ($value === "") {
238            return null;
239        }
240        return $value;
241
242    }
243
244
245    /**
246     * @param $dokuWikiId
247     * @param $name
248     * @return mixed|null
249     */
250    public
251    function getCurrentFromName($name)
252    {
253        $value = $this->getData()[self::CURRENT_METADATA][$name];
254        /**
255         * Empty string return null
256         * because Dokuwiki does not allow to delete keys
257         * {@link p_set_metadata()}
258         */
259        if ($value === "") {
260            return null;
261        }
262        return $value;
263    }
264
265    /**
266     * @return MetadataDokuWikiStore
267     */
268    public function renderAndPersist(): MetadataDokuWikiStore
269    {
270        /**
271         * Read/render the metadata from the file
272         * with parsing
273         */
274        $dokuwikiId = $this->getResource()->getDokuwikiId();
275        $actualMeta = $this->getData();
276        global $ID;
277        $keep = $ID;
278        try {
279            $ID = $dokuwikiId;
280            $newMetadata = p_render_metadata($dokuwikiId, $actualMeta);
281            p_save_metadata($dokuwikiId, $newMetadata);
282            $this->data = $newMetadata;
283        } finally {
284            $ID = $keep;
285        }
286        return $this;
287    }
288
289
290    /**
291     * Change a meta on file
292     * and triggers the {@link self::PAGE_METADATA_MUTATION_EVENT} event
293     *
294     * @param $wikiId
295     * @param $key
296     * @param $value
297     * @param null $default - use in case of boolean
298     */
299    private function setFromWikiId($wikiId, $key, $value, $default = null)
300    {
301
302        $oldValue = $this->getFromWikiId($wikiId, $key);
303        if (is_bool($value)) {
304            if ($oldValue === null) {
305                $oldValue = $default;
306            } else {
307                $oldValue = Boolean::toBoolean($oldValue);
308            }
309        }
310        if ($oldValue !== $value) {
311
312
313            $this->data[self::PERSISTENT_METADATA][$key] = $value;
314            /**
315             * Metadata in Dokuwiki is fucked up.
316             *
317             * You can't remove a metadata,
318             * You need to know if this is a rendering or not
319             *
320             * See just how fucked {@link p_set_metadata()} is
321             *
322             * Also don't change the type of the value to a string
323             * otherwise dokuwiki will not see a change
324             * between true and a string and will not persist the value
325             *
326             * A metadata is also not immediately flushed on disk
327             * in a test when rendering
328             * They are going into the global $METADATA_RENDERERS
329             *
330             * A current metadata is never stored if not set in the rendering process
331             * We persist therefore always
332             */
333            $persistent = true;
334            p_set_metadata($wikiId,
335                [
336                    $key => $value
337                ],
338                false,
339                $persistent
340            );
341            /**
342             * Event
343             */
344            $data = [
345                "name" => $key,
346                self::NEW_VALUE_ATTRIBUTE => $value,
347                "old_value" => $oldValue,
348                PagePath::getPersistentName() => ":$wikiId"
349            ];
350            Event::createAndTrigger(self::PAGE_METADATA_MUTATION_EVENT, $data);
351        }
352
353    }
354
355
356    public function isHierarchicalTextBased(): bool
357    {
358        return true;
359    }
360
361    /**
362     *
363     * @return Path - the full path to the meta file
364     */
365    public
366    function getMetaFilePath(): ?Path
367    {
368        $resource = $this->getResource();
369        if (!($resource instanceof Page)) {
370            LogUtility::msg("The resource type ({$resource->getType()}) meta file is unknown and can't be retrieved.");
371            return null;
372        }
373        $dokuwikiId = $resource->getPath()->getDokuWikiId();
374        return LocalPath::create(metaFN($dokuwikiId, '.meta'));
375    }
376
377    public function __toString()
378    {
379        return "DokuMeta";
380    }
381
382
383    private function getFromWikiId($dokuwikiId, string $name, $default = null)
384    {
385        /**
386         * Note that {@link p_get_metadata()} can trigger a rendering of the meta again
387         * and it has a fucking cache
388         *
389         * Due to the cache in {@link p_get_metadata()} we can't use {@link p_read_metadata}
390         * when testing a {@link \action_plugin_combo_imgmove move} otherwise
391         * the move meta is not seen and the tests are failing.
392         *
393         * $METADATA_RENDERERS: A global cache variable where the persistent data is set
394         * with {@link p_set_metadata()} and that you can't retrieve with {@link p_get_metadata()}
395         *
396         * This variable is unset at the end function of {@link p_render_metadata()}
397         */
398        global $METADATA_RENDERERS;
399        $value = $METADATA_RENDERERS[$dokuwikiId][MetadataDokuWikiStore::PERSISTENT_METADATA][$name];
400        if ($value !== null) {
401            return $value;
402        }
403
404        /**
405         * {@link p_get_metadata} flat out the metadata array and we loose the
406         * persistent and current information
407         * Because there may be already a metadata in current for instance title
408         * It will be returned, but we want only the persistent
409         */
410        $value = $this->getPersistentMetadata($name);
411
412        /**
413         * Empty string return the default (null)
414         * because Dokuwiki does not allow to delete keys
415         * {@link p_set_metadata()}
416         */
417        if ($value !== null && $value !== "") {
418            return $value;
419        }
420        return $default;
421    }
422
423
424    /**
425     * @param $dokuwikiId
426     * @return null|array
427     */
428    private function getFlatMetadatas($dokuwikiId): ?array
429    {
430        return p_get_metadata($dokuwikiId, '', METADATA_DONT_RENDER);
431    }
432
433    public function deleteAndFlush()
434    {
435        $emptyMeta = [MetadataDokuWikiStore::CURRENT_METADATA => [], MetadataDokuWikiStore::PERSISTENT_METADATA => []];
436        $dokuwikiId = $this->getResource()->getDokuwikiId();
437        p_save_metadata($dokuwikiId, $emptyMeta);
438        $this->data = $emptyMeta;
439
440    }
441
442
443}
444