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\Aliases;
24use ComboStrap\CacheExpirationFrequency;
25use ComboStrap\Canonical;
26use ComboStrap\EndDate;
27use ComboStrap\ExceptionCombo;
28use ComboStrap\ExceptionComboRuntime;
29use ComboStrap\FileSystems;
30use ComboStrap\Lang;
31use ComboStrap\LdJson;
32use ComboStrap\LogUtility;
33use ComboStrap\LowQualityPageOverwrite;
34use ComboStrap\MediaLink;
35use ComboStrap\Message;
36use ComboStrap\Metadata;
37use ComboStrap\MetadataDokuWikiStore;
38use ComboStrap\MetadataFrontmatterStore;
39use ComboStrap\MetadataStoreTransfer;
40use ComboStrap\Page;
41use ComboStrap\PageH1;
42use ComboStrap\PageId;
43use ComboStrap\PageImagePath;
44use ComboStrap\PageImages;
45use ComboStrap\PageKeywords;
46use ComboStrap\PageLayout;
47use ComboStrap\PagePath;
48use ComboStrap\PagePublicationDate;
49use ComboStrap\PageTitle;
50use ComboStrap\PageType;
51use ComboStrap\PluginUtility;
52use ComboStrap\QualityDynamicMonitoringOverwrite;
53use ComboStrap\Region;
54use ComboStrap\ResourceName;
55use ComboStrap\StartDate;
56
57require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
58
59
60/**
61 * All DokuWiki plugins to extend the parser/rendering mechanism
62 * need to inherit from this class
63 *
64 * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
65 */
66class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
67{
68    const PARSING_STATE_EMPTY = "empty";
69    const PARSING_STATE_ERROR = "error";
70    const PARSING_STATE_SUCCESSFUL = "successful";
71    const STATUS = "status";
72    const CANONICAL = "frontmatter";
73    const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing';
74    const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT = "enableFrontMatterOnSubmit";
75    const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT = 0;
76
77    /**
78     * Used in the move plugin
79     * !!! The two last word of the plugin class !!!
80     */
81    const COMPONENT = 'combo_' . self::CANONICAL;
82    const START_TAG = '---json';
83    const END_TAG = '---';
84    const METADATA_IMAGE_CANONICAL = "metadata:image";
85    const PATTERN = self::START_TAG . '.*?' . self::END_TAG;
86
87    /**
88     * The update status for the update of the frontmatter
89     */
90    const UPDATE_EXIT_CODE_DONE = 000;
91    const UPDATE_EXIT_CODE_NOT_ENABLED = 100;
92    const UPDATE_EXIT_CODE_NOT_CHANGED = 200;
93    const UPDATE_EXIT_CODE_ERROR = 500;
94
95
96    /**
97     * Syntax Type.
98     *
99     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
100     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
101     *
102     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
103     *
104     * baseonly - run only in the base
105     */
106    function getType(): string
107    {
108        return 'baseonly';
109    }
110
111    public function getPType()
112    {
113        /**
114         * This element create a section
115         * element that is a div
116         * that should not be in paragraph
117         *
118         * We make it a block
119         */
120        return "block";
121    }
122
123
124    /**
125     * @see Doku_Parser_Mode::getSort()
126     * Higher number than the teaser-columns
127     * because the mode with the lowest sort number will win out
128     */
129    function getSort()
130    {
131        return 99;
132    }
133
134    /**
135     * Create a pattern that will called this plugin
136     *
137     * @param string $mode
138     * @see Doku_Parser_Mode::connectTo()
139     */
140    function connectTo($mode)
141    {
142        if ($mode == "base") {
143            // only from the top
144            $this->Lexer->addSpecialPattern(self::PATTERN, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
145        }
146    }
147
148    /**
149     *
150     * The handle function goal is to parse the matched syntax through the pattern function
151     * and to return the result for use in the renderer
152     * This result is always cached until the page is modified.
153     * @param string $match
154     * @param int $state
155     * @param int $pos
156     * @param Doku_Handler $handler
157     * @return array|bool
158     * @see DokuWiki_Syntax_Plugin::handle()
159     *
160     */
161    function handle($match, $state, $pos, Doku_Handler $handler)
162    {
163
164        if ($state == DOKU_LEXER_SPECIAL) {
165
166            $result = [];
167            $page = Page::createPageFromGlobalDokuwikiId();
168            try {
169                $frontMatterStore = MetadataFrontmatterStore::createFromFrontmatterString($page, $match);
170                $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
171            } catch (ExceptionCombo $e) {
172                // Decode problem
173                $result[self::STATUS] = self::PARSING_STATE_ERROR;
174                $result[PluginUtility::PAYLOAD] = $match;
175                return $result;
176            }
177
178            /**
179             * Empty string
180             * Rare case, we delete all mutable meta if present
181             */
182            $frontmatterData = $frontMatterStore->getData();
183            if ($frontmatterData === null) {
184                global $ID;
185                $meta = p_read_metadata($ID);
186                foreach (Metadata::MUTABLE_METADATA as $metaKey) {
187                    if (isset($meta['persistent'][$metaKey])) {
188                        unset($meta['persistent'][$metaKey]);
189                    }
190                }
191                p_save_metadata($ID, $meta);
192                return array(self::STATUS => self::PARSING_STATE_EMPTY);
193            }
194
195
196            /**
197             * Sync
198             */
199            $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($page);
200            $transfer = MetadataStoreTransfer::createForPage($page)
201                ->fromStore($frontMatterStore)
202                ->toStore($targetStore)
203                ->process($frontmatterData);
204
205            $messages = $transfer->getMessages();
206            $dataForRenderer = $transfer->getNormalizedDataArray();
207
208
209            /**
210             * Database update
211             */
212            try {
213                $databasePage = $page->getDatabasePage();
214                $databasePage->replicateMetaAttributes();
215            } catch (Exception $e) {
216                $message = Message::createErrorMessage($e->getMessage());
217                if ($e instanceof ExceptionCombo) {
218                    $message->setCanonical($e->getCanonical());
219                }
220                $messages[] = $message;
221            }
222
223
224            foreach ($messages as $message) {
225                $message->sendLogMsg();
226            }
227
228            /**
229             * Return them for metadata rendering
230             */
231            $result[PluginUtility::ATTRIBUTES] = $dataForRenderer;
232
233        }
234
235
236        /**
237         * End position is the length of the match + 1 for the newline
238         */
239        $newLine = 1;
240        $endPosition = $pos + strlen($match) + $newLine;
241        $result[PluginUtility::POSITION] = [$pos, $endPosition];
242
243        return $result;
244
245    }
246
247    /**
248     * Render the output
249     * @param string $format
250     * @param Doku_Renderer $renderer
251     * @param array $data - what the function handle() return'ed
252     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
253     * @see DokuWiki_Syntax_Plugin::render()
254     *
255     *
256     */
257    function render($format, Doku_Renderer $renderer, $data): bool
258    {
259
260        switch ($format) {
261            case 'xhtml':
262                global $ID;
263                /** @var Doku_Renderer_xhtml $renderer */
264
265                $state = $data[self::STATUS];
266                if ($state == self::PARSING_STATE_ERROR) {
267                    $json = MetadataFrontmatterStore::stripFrontmatterTag($data[PluginUtility::PAYLOAD]);
268                    LogUtility::msg("Front Matter: The json object for the page ($ID) is not valid. " . \ComboStrap\Json::getValidationLink($json), LogUtility::LVL_MSG_ERROR);
269                }
270
271                /**
272                 * Section
273                 */
274                list($startPosition, $endPosition) = $data[PluginUtility::POSITION];
275                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
276                    $position = $startPosition;
277                    $name = self::CANONICAL;
278                    PluginUtility::startSection($renderer, $position, $name);
279                    $renderer->finishSectionEdit($endPosition);
280                }
281                break;
282
283            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
284
285                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
286                    return false;
287                }
288
289
290                /** @var renderer_plugin_combo_analytics $renderer */
291                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
292                foreach ($frontMatterJsonArray as $key => $value) {
293
294                    $renderer->setAnalyticsMetaForReporting($key, $value);
295                    if ($key === PageImages::PROPERTY_NAME) {
296                        $this->updateImageStatistics($value, $renderer);
297                    }
298
299                }
300                break;
301
302            case "metadata":
303
304                global $ID;
305                /** @var Doku_Renderer_metadata $renderer */
306                if ($data[self::STATUS] === self::PARSING_STATE_ERROR) {
307                    if (PluginUtility::isDevOrTest()) {
308                        // fail if test
309                        throw new ExceptionComboRuntime("Front Matter: The json object for the page ($ID) is not valid.", LogUtility::LVL_MSG_ERROR);
310                    }
311                    return false;
312                }
313
314                /**
315                 * Register media in index
316                 */
317                $page = Page::createPageFromId($ID);
318                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
319                if (isset($frontMatterJsonArray[PageImages::getPersistentName()])) {
320                    $value = $frontMatterJsonArray[PageImages::getPersistentName()];
321
322                    /**
323                     * @var PageImages $pageImages
324                     */
325                    $pageImages = PageImages::createForPage($page)
326                        ->buildFromStoreValue($value);
327                    $pageImagesObject = $pageImages->getValueAsPageImages();
328                    foreach ($pageImagesObject as $imageValue) {
329                        $imagePath = $imageValue->getImage()->getPath()->toAbsolutePath()->toString();
330                        $attributes = [PagePath::PROPERTY_NAME => $imagePath];
331                        if (media_isexternal($imagePath)) {
332                            $attributes[MediaLink::MEDIA_DOKUWIKI_TYPE] = MediaLink::EXTERNAL_MEDIA_CALL_NAME;
333                        } else {
334                            $attributes[MediaLink::MEDIA_DOKUWIKI_TYPE] = MediaLink::INTERNAL_MEDIA_CALL_NAME;
335                        }
336                        syntax_plugin_combo_media::registerImageMeta($attributes, $renderer);
337                    }
338
339                }
340
341                break;
342
343        }
344        return true;
345    }
346
347
348    private function updateImageStatistics($value, $renderer)
349    {
350        if (is_array($value) && sizeof($value) > 0) {
351            $firstKey = array_keys($value)[0];
352            if (is_numeric($firstKey)) {
353                foreach ($value as $subImage) {
354                    $this->updateImageStatistics($subImage, $renderer);
355                }
356                return;
357            }
358        }
359
360        /**
361         * Code below is fucked up
362         */
363        $path = $value;
364        if (is_array($value) && isset($value[PageImagePath::getPersistentName()])) {
365            $path = $value[PageImagePath::getPersistentName()];
366        }
367        $media = MediaLink::createFromRenderMatch($path);
368        $attributes = $media->toCallStackArray();
369        syntax_plugin_combo_media::updateStatistics($attributes, $renderer);
370
371    }
372
373
374}
375
376