1<?php
2/**
3 * Front Matter implementation to add metadata
4 *
5 *
6 * that enhance the metadata dokuwiki system
7 * https://www.dokuwiki.org/metadata
8 * that use the Dublin Core Standard
9 * http://dublincore.org/
10 * by adding the front matter markup specification
11 * https://gerardnico.com/markup/front-matter
12 *
13 * Inspiration
14 * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php
15 * https://www.dokuwiki.org/plugin:semantic
16 *
17 * See also structured plugin
18 * https://www.dokuwiki.org/plugin:data
19 * https://www.dokuwiki.org/plugin:struct
20 *
21 */
22
23use ComboStrap\ExceptionBadArgument;
24use ComboStrap\ExceptionBadSyntax;
25use ComboStrap\ExceptionCompile;
26use ComboStrap\ExceptionNotFound;
27use ComboStrap\ExceptionRuntime;
28use ComboStrap\ExecutionContext;
29use ComboStrap\LogUtility;
30use ComboStrap\MarkupPath;
31use ComboStrap\MarkupRef;
32use ComboStrap\MediaMarkup;
33use ComboStrap\Meta\Api\Metadata;
34use ComboStrap\Meta\Api\MetadataSystem;
35use ComboStrap\Meta\Store\MetadataDokuWikiStore;
36use ComboStrap\MetadataFrontmatterStore;
37use ComboStrap\MetadataStoreTransfer;
38use ComboStrap\PageDescription;
39use ComboStrap\Meta\Field\PageImagePath;
40use ComboStrap\Meta\Field\PageImages;
41use ComboStrap\PluginUtility;
42
43require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
44
45
46/**
47 * All DokuWiki plugins to extend the parser/rendering mechanism
48 * need to inherit from this class
49 *
50 * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
51 */
52class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
53{
54
55    const PARSING_STATE_ERROR = 1;
56    const PARSING_STATE_SUCCESSFUL = 0;
57
58    const CANONICAL = "frontmatter";
59    const TAG = "frontmatter";
60    const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT = 0;
61
62    /**
63     * Used in the move plugin
64     * !!! The two last word of the plugin class !!!
65     */
66    const COMPONENT = 'combo_' . self::CANONICAL;
67    const START_TAG = '---json';
68    const END_TAG = '---';
69    const METADATA_IMAGE_CANONICAL = "metadata:image";
70    const PATTERN = self::START_TAG . '.*?' . self::END_TAG;
71
72    /**
73     * The update status for the update of the frontmatter
74     */
75    const UPDATE_EXIT_CODE_DONE = 000;
76    const UPDATE_EXIT_CODE_NOT_ENABLED = 100;
77    const UPDATE_EXIT_CODE_NOT_CHANGED = 200;
78    const UPDATE_EXIT_CODE_ERROR = 500;
79
80
81    /**
82     * Syntax Type.
83     *
84     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
85     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
86     *
87     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
88     *
89     * baseonly - run only in the base
90     */
91    function getType(): string
92    {
93        return 'baseonly';
94    }
95
96    public function getPType(): string
97    {
98        /**
99         * This element create a section
100         * element that is a div
101         * that should not be in paragraph
102         *
103         * We make it a block
104         */
105        return "block";
106    }
107
108
109    /**
110     * @see Doku_Parser_Mode::getSort()
111     * Higher number than the teaser-columns
112     * because the mode with the lowest sort number will win out
113     */
114    function getSort()
115    {
116        return 99;
117    }
118
119    /**
120     * Create a pattern that will called this plugin
121     *
122     * @param string $mode
123     * @see Doku_Parser_Mode::connectTo()
124     */
125    function connectTo($mode)
126    {
127        if ($mode == "base") {
128            // only from the top
129            $this->Lexer->addSpecialPattern(self::PATTERN, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
130        }
131    }
132
133    /**
134     *
135     * The handle function goal is to parse the matched syntax through the pattern function
136     * and to return the result for use in the renderer
137     * This result is always cached until the page is modified.
138     * @param string $match
139     * @param int $state
140     * @param int $pos
141     * @param Doku_Handler $handler
142     * @return array
143     * @see DokuWiki_Syntax_Plugin::handle()
144     *
145     */
146    function handle($match, $state, $pos, Doku_Handler $handler): array
147    {
148
149        $result = [];
150
151        try {
152            $wikiPath = ExecutionContext::getActualOrCreateFromEnv()->getExecutingWikiPath();
153            $parsedPage = MarkupPath::createPageFromPathObject($wikiPath);
154        } catch (ExceptionCompile $e) {
155            LogUtility::error("The global ID is unknown, we couldn't get the requested page", self::CANONICAL);
156            return [];
157        }
158        try {
159
160            $frontMatterStore = MetadataFrontmatterStore::createFromFrontmatterString($parsedPage, $match);
161            $result[PluginUtility::EXIT_CODE] = self::PARSING_STATE_SUCCESSFUL;
162        } catch (ExceptionCompile $e) {
163            // Decode problem
164            $result[PluginUtility::EXIT_CODE] = self::PARSING_STATE_ERROR;
165            $result[PluginUtility::EXIT_MESSAGE] = $match;
166            return $result;
167        }
168
169
170        $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($parsedPage);
171        $frontMatterData = $frontMatterStore->getData();
172
173        $transfer = MetadataStoreTransfer::createForPage($parsedPage)
174            ->fromStore($frontMatterStore)
175            ->toStore($targetStore)
176            ->setMetadatas($frontMatterData)
177            ->validate();
178
179        $messages = $transfer->getMessages();
180        $validatedMetadatas = $transfer->getValidatedMetadatas();
181        $renderMetadata = [];
182        foreach ($validatedMetadatas as $metadataObject) {
183            $renderMetadata[$metadataObject::getPersistentName()] = $metadataObject->toStoreValue();
184        }
185
186        foreach ($messages as $message) {
187            $message->sendToLogUtility();
188        }
189
190        /**
191         * Return them for metadata rendering
192         */
193        $result[PluginUtility::ATTRIBUTES] = $renderMetadata;
194        return $result;
195
196    }
197
198    /**
199     * Render the output
200     * @param string $format
201     * @param Doku_Renderer $renderer
202     * @param array $data - what the function handle() return'ed
203     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
204     * @see DokuWiki_Syntax_Plugin::render()
205     *
206     *
207     */
208    function render($format, Doku_Renderer $renderer, $data): bool
209    {
210
211        try {
212            $executingPath = ExecutionContext::getActualOrCreateFromEnv()
213                ->getExecutingWikiPath();
214        } catch (ExceptionNotFound $e) {
215            // markup string rendering
216            return false;
217        }
218        switch ($format) {
219            case 'xhtml':
220
221                /** @var Doku_Renderer_xhtml $renderer */
222                $exitCode = $data[PluginUtility::EXIT_CODE];
223                if ($exitCode == self::PARSING_STATE_ERROR) {
224                    $json = MetadataFrontmatterStore::stripFrontmatterTag($data[PluginUtility::EXIT_MESSAGE]);
225                    LogUtility::error("Front Matter: The json object for the page ($executingPath) is not valid. " . \ComboStrap\Json::getValidationLink($json), self::CANONICAL);
226                }
227                return true;
228
229            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
230
231                if ($data[PluginUtility::EXIT_CODE] !== self::PARSING_STATE_SUCCESSFUL) {
232                    return true;
233                }
234
235                /** @var renderer_plugin_combo_analytics $renderer */
236                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
237                foreach ($frontMatterJsonArray as $key => $value) {
238
239                    /**
240                     * Hack while metadata and analtyics stats are not together
241                     */
242                    if ($key === PageDescription::DESCRIPTION_PROPERTY) {
243                        $value = $value['abstract'];
244                    }
245                    $renderer->setAnalyticsMetaForReporting($key, $value);
246                    if ($key === PageImages::PROPERTY_NAME) {
247                        $this->updateImageStatistics($value, $renderer);
248                    }
249
250                }
251                return true;
252
253            case "metadata":
254
255
256                /** @var Doku_Renderer_metadata $renderer */
257                if ($data[PluginUtility::EXIT_CODE] === self::PARSING_STATE_ERROR) {
258                    if (PluginUtility::isTest()) {
259                        // fail if test
260                        throw new ExceptionRuntime("Front Matter: The json object for the page () is not valid.", LogUtility::LVL_MSG_ERROR);
261                    }
262                    return false;
263                }
264
265                /**
266                 * Empty string
267                 * Rare case, we delete all mutable meta if present
268                 */
269                $frontmatterData = $data[PluginUtility::ATTRIBUTES];
270                if (sizeof($frontmatterData) === 0) {
271                    foreach (MetadataSystem::getMutableMetadata() as $metaData) {
272                        $metaKey = $metaData::getName();
273                        if ($metaKey === PageDescription::PROPERTY_NAME) {
274                            // array
275                            continue;
276                        }
277                        // runtime
278                        if ($renderer->meta[$metaKey]) {
279                            unset($renderer->meta[$metaKey]);
280                        }
281                        // persistent
282                        if ($renderer->persistent[$metaKey]) {
283                            unset($renderer->persistent[$metaKey]);
284                        }
285                    }
286                    return true;
287                }
288
289                /**
290                 * Meta update
291                 * (The {@link p_get_metadata()} starts {@link p_render_metadata()}
292                 * and stores them if there is any diff
293                 */
294                foreach ($frontmatterData as $metaKey => $metaValue) {
295
296                    $renderer->meta[$metaKey] = $metaValue;
297
298                    /**
299                     * Persistence is just a duplicate of the meta (ie current)
300                     *
301                     * Why from https://www.dokuwiki.org/devel:metadata#metadata_persistence
302                     * The persistent array holds ****duplicates****
303                     * as the {@link p_get_metadata()} returns only `current` data
304                     * which should not be cleared during the rendering process.
305                     */
306                    $renderer->persistent[$metaKey] = $metaValue;
307
308                }
309
310
311                /**
312                 * Register media in index
313                 */
314                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
315                if (isset($frontMatterJsonArray[PageImages::getPersistentName()])) {
316                    $value = $frontMatterJsonArray[PageImages::getPersistentName()];
317
318                    /**
319                     * @var PageImages $pageImages
320                     */
321                    $page = MarkupPath::createPageFromPathObject($executingPath);
322                    $pageImages = PageImages::createForPage($page)
323                        ->setFromStoreValueWithoutException($value);
324                    $pageImagesObject = $pageImages->getValueAsPageImages();
325                    foreach ($pageImagesObject as $imageValue) {
326                        $dokuwikiId = $imageValue->getImagePath()->getWikiId();
327                        $attributes = [MarkupRef::REF_ATTRIBUTE => ":$dokuwikiId"];
328                        try {
329                            syntax_plugin_combo_media::registerImageMeta($attributes, $renderer);
330                        } catch (\Exception $e) {
331                            LogUtility::internalError("The image registration did not work. Error: {$e->getMessage()}");
332                        }
333                    }
334                }
335                break;
336
337        }
338        return true;
339    }
340
341
342    private function updateImageStatistics($value, $renderer)
343    {
344        if (is_array($value) && sizeof($value) > 0) {
345            $firstKey = array_keys($value)[0];
346            if (is_numeric($firstKey)) {
347                foreach ($value as $subImage) {
348                    $this->updateImageStatistics($subImage, $renderer);
349                }
350                return;
351            }
352        }
353
354        /**
355         * Code below is fucked up
356         */
357        $path = $value;
358        if (is_array($value) && isset($value[PageImagePath::getPersistentName()])) {
359            $path = $value[PageImagePath::getPersistentName()];
360        }
361        try {
362            $media = MediaMarkup::createFromRef($path);
363        } catch (ExceptionBadArgument|ExceptionNotFound|ExceptionBadSyntax $e) {
364            LogUtility::internalError("The media image statistics could not be created. The media markup could not be instantiated with the path ($path). Error:{$e->getMessage()}");
365            return;
366        }
367
368        $attributes = $media->toCallStackArray();
369        syntax_plugin_combo_media::updateStatistics($attributes, $renderer);
370
371    }
372
373
374}
375
376