xref: /plugin/combo/syntax/frontmatter.php (revision 37748cd8654635afbeca80942126742f0f4cc346)
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\Analytics;
24use ComboStrap\CacheManager;
25use ComboStrap\Iso8601Date;
26use ComboStrap\LogUtility;
27use ComboStrap\MediaLink;
28use ComboStrap\Page;
29use ComboStrap\PluginUtility;
30use ComboStrap\Publication;
31
32require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
33
34if (!defined('DOKU_INC')) {
35    die();
36}
37
38/**
39 * All DokuWiki plugins to extend the parser/rendering mechanism
40 * need to inherit from this class
41 *
42 * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
43 */
44class syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
45{
46    const PARSING_STATE_EMPTY = "empty";
47    const PARSING_STATE_ERROR = "error";
48    const PARSING_STATE_SUCCESSFUL = "successful";
49    const STATUS = "status";
50    const CANONICAL = "frontmatter";
51    const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing';
52
53    /**
54     * Used in the move plugin
55     * !!! The two last word of the plugin class !!!
56     */
57    const COMPONENT = 'combo_' . self::CANONICAL;
58    const START_TAG = '---json';
59    const END_TAG = '---';
60    const METADATA_IMAGE_CANONICAL = "metadata:image";
61
62    /**
63     * @param $match
64     * @return array|mixed - null if decodage problem, empty array if no json or an associative array
65     */
66    public static function FrontMatterMatchToAssociativeArray($match)
67    {
68        // strip
69        //   from start `---json` + eol = 8
70        //   from end   `---` + eol = 4
71        $jsonString = substr($match, 7, -3);
72
73        // Empty front matter
74        if (trim($jsonString) == "") {
75            self::deleteKnownMetaThatAreNoMorePresent();
76            return [];
77        }
78
79        // Otherwise you get an object ie $arrayFormat-> syntax
80        $arrayFormat = true;
81        return json_decode($jsonString, $arrayFormat);
82    }
83
84    /**
85     * Syntax Type.
86     *
87     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
88     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
89     *
90     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
91     *
92     * baseonly - run only in the base
93     */
94    function getType()
95    {
96        return 'baseonly';
97    }
98
99    public function getPType()
100    {
101        /**
102         * This element create a section
103         * element that is a div
104         * that should not be in paragraph
105         *
106         * We make it a block
107         */
108        return "block";
109    }
110
111
112    /**
113     * @see Doku_Parser_Mode::getSort()
114     * Higher number than the teaser-columns
115     * because the mode with the lowest sort number will win out
116     */
117    function getSort()
118    {
119        return 99;
120    }
121
122    /**
123     * Create a pattern that will called this plugin
124     *
125     * @param string $mode
126     * @see Doku_Parser_Mode::connectTo()
127     */
128    function connectTo($mode)
129    {
130        if ($mode == "base") {
131            // only from the top
132            $this->Lexer->addSpecialPattern(self::START_TAG . '.*?' . self::END_TAG, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
133        }
134    }
135
136    /**
137     *
138     * The handle function goal is to parse the matched syntax through the pattern function
139     * and to return the result for use in the renderer
140     * This result is always cached until the page is modified.
141     * @param string $match
142     * @param int $state
143     * @param int $pos
144     * @param Doku_Handler $handler
145     * @return array|bool
146     * @see DokuWiki_Syntax_Plugin::handle()
147     *
148     */
149    function handle($match, $state, $pos, Doku_Handler $handler)
150    {
151
152        if ($state == DOKU_LEXER_SPECIAL) {
153
154
155            $jsonArray = self::FrontMatterMatchToAssociativeArray($match);
156
157
158            $result = [];
159            // Decodage problem
160            if ($jsonArray == null) {
161
162                $result[self::STATUS] = self::PARSING_STATE_ERROR;
163                $result[PluginUtility::PAYLOAD] = $match;
164
165            } else {
166
167                if (sizeof($jsonArray) === 0) {
168                    return array(self::STATUS => self::PARSING_STATE_EMPTY);
169                }
170
171                $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
172                /**
173                 * Published is an alias for date published
174                 */
175                if (isset($jsonArray[Publication::OLD_META_KEY])) {
176                    $jsonArray[Publication::DATE_PUBLISHED] = $jsonArray[Publication::OLD_META_KEY];
177                    unset($jsonArray[Publication::OLD_META_KEY]);
178                }
179                /**
180                 * Add the time part if not present
181                 */
182                if (isset($jsonArray[Publication::DATE_PUBLISHED])) {
183                    $datePublishedString = $jsonArray[Publication::DATE_PUBLISHED];
184                    $datePublished = Iso8601Date::create($datePublishedString);
185                    if (!$datePublished->isValidDateEntry()) {
186                        LogUtility::msg("The published date ($datePublishedString) is not a valid ISO date supported.", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
187                        unset($jsonArray[Publication::DATE_PUBLISHED]);
188                    } else {
189                        $jsonArray[Publication::DATE_PUBLISHED] = "$datePublished";
190                    }
191
192                }
193                $result[PluginUtility::ATTRIBUTES] = $jsonArray;
194            }
195
196            /**
197             * End position is the length of the match + 1 for the newline
198             */
199            $newLine = 1;
200            $endPosition = $pos + strlen($match) + $newLine;
201            $result[PluginUtility::POSITION] = [$pos, $endPosition];
202
203            return $result;
204        }
205
206        return array();
207    }
208
209    /**
210     * Render the output
211     * @param string $format
212     * @param Doku_Renderer $renderer
213     * @param array $data - what the function handle() return'ed
214     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
215     * @see DokuWiki_Syntax_Plugin::render()
216     *
217     *
218     */
219    function render($format, Doku_Renderer $renderer, $data)
220    {
221
222        switch ($format) {
223            case 'xhtml':
224                global $ID;
225                /** @var Doku_Renderer_xhtml $renderer */
226
227                $state = $data[self::STATUS];
228                if ($state == self::PARSING_STATE_ERROR) {
229                    $json = $data[PluginUtility::PAYLOAD];
230                    LogUtility::msg("Front Matter: The json object for the page ($ID) is not valid. See the errors it by clicking on <a href=\"https://jsonformatter.curiousconcept.com/?data=" . urlencode($json) . "\">this link</a>.", LogUtility::LVL_MSG_ERROR);
231                }
232
233                /**
234                 * Section
235                 */
236                list($startPosition, $endPosition) = $data[PluginUtility::POSITION];
237                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
238                    $position = $startPosition;
239                    $name = self::CANONICAL;
240                    PluginUtility::startSection($renderer, $position, $name);
241                    $renderer->finishSectionEdit($endPosition);
242                }
243                break;
244            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
245
246                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
247                    return false;
248                }
249
250                $notModifiableMeta = [
251                    Analytics::PATH,
252                    Analytics::DATE_CREATED,
253                    Analytics::DATE_MODIFIED
254                ];
255
256                /** @var renderer_plugin_combo_analytics $renderer */
257                $jsonArray = $data[PluginUtility::ATTRIBUTES];
258                foreach ($jsonArray as $key => $value) {
259                    if (!in_array($key, $notModifiableMeta)) {
260
261                        $renderer->setMeta($key, $value);
262                        if ($key === Page::IMAGE_META_PROPERTY) {
263                            $this->updateImageStatistics($value, $renderer);
264                        }
265
266                    } else {
267                        LogUtility::msg("The metadata ($key) cannot be set.", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
268                    }
269                }
270                break;
271
272            case "metadata":
273
274                /** @var Doku_Renderer_metadata $renderer */
275                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
276                    return false;
277                }
278
279                global $ID;
280                $jsonArray = $data[PluginUtility::ATTRIBUTES];
281
282
283                $notModifiableMeta = [
284                    "date",
285                    "user",
286                    "last_change",
287                    "creator",
288                    "contributor"
289                ];
290
291                foreach ($jsonArray as $key => $value) {
292
293                    $lowerCaseKey = trim(strtolower($key));
294
295                    // Not modifiable metadata
296                    if (in_array($lowerCaseKey, $notModifiableMeta)) {
297                        LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING);
298                        continue;
299                    }
300
301                    switch ($lowerCaseKey) {
302
303                        case Page::DESCRIPTION_PROPERTY:
304                            /**
305                             * Overwrite also the actual description
306                             */
307                            p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array(
308                                "abstract" => $value,
309                                "origin" => syntax_plugin_combo_frontmatter::CANONICAL
310                            )));
311                            /**
312                             * Continue because
313                             * the description value was already stored
314                             * We don't want to override it
315                             * And continue 2 because continue == break in a switch
316                             */
317                            continue 2;
318
319
320                        // Canonical should be lowercase
321                        case Page::CANONICAL_PROPERTY:
322                            $value = strtolower($value);
323                            break;
324
325                        case Page::IMAGE_META_PROPERTY:
326
327                            $imageValues = [];
328                            $this->aggregateImageValues($imageValues, $value);
329                            foreach ($imageValues as $imageValue) {
330                                $media = MediaLink::createFromRenderMatch($imageValue);
331                                $attributes = $media->toCallStackArray();
332                                syntax_plugin_combo_media::registerImageMeta($attributes, $renderer);
333                            }
334                            break;
335
336                    }
337                    // Set the value persistently
338                    p_set_metadata($ID, array($lowerCaseKey => $value));
339
340                }
341
342                $this->deleteKnownMetaThatAreNoMorePresent($jsonArray);
343
344                break;
345
346        }
347        return true;
348    }
349
350    /**
351     *
352     * @param array $json - The Json
353     * Delete the controlled meta that are no more present if they exists
354     * @return bool
355     */
356    static public
357    function deleteKnownMetaThatAreNoMorePresent(array $json = array())
358    {
359        global $ID;
360
361        /**
362         * The managed meta with the exception of
363         * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description}
364         * because it's already managed by dokuwiki in description['abstract']
365         */
366        $managedMeta = [
367            Page::CANONICAL_PROPERTY,
368            Page::TYPE_META_PROPERTY,
369            Page::IMAGE_META_PROPERTY,
370            Page::COUNTRY_META_PROPERTY,
371            Page::LANG_META_PROPERTY,
372            Analytics::TITLE,
373            syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER,
374            Publication::OLD_META_KEY,
375            Publication::DATE_PUBLISHED,
376            Analytics::NAME,
377            CacheManager::DATE_CACHE_EXPIRATION_META_KEY,
378            action_plugin_combo_metagoogle::JSON_LD_META_PROPERTY,
379
380        ];
381        $meta = p_read_metadata($ID);
382        foreach ($managedMeta as $metaKey) {
383            if (!array_key_exists($metaKey, $json)) {
384                if (isset($meta['persistent'][$metaKey])) {
385                    unset($meta['persistent'][$metaKey]);
386                }
387            }
388        }
389        return p_save_metadata($ID, $meta);
390    }
391
392    private function updateImageStatistics($value, $renderer)
393    {
394        if(is_array($value)){
395            foreach($value as $subImage){
396                $this->updateImageStatistics($subImage, $renderer);
397            }
398        } else {
399            $media = MediaLink::createFromRenderMatch($value);
400            $attributes = $media->toCallStackArray();
401            syntax_plugin_combo_media::updateStatistics($attributes, $renderer);
402        }
403    }
404
405    private function aggregateImageValues(array &$imageValues, $value)
406    {
407        if (is_array($value)) {
408            foreach ($value as $subImageValue) {
409                $this->aggregateImageValues($imageValues,$subImageValue);
410            }
411        } else {
412            $imageValues[] = $value;
413        }
414    }
415
416
417}
418
419