xref: /plugin/combo/syntax/frontmatter.php (revision c3437056399326d621a01da73b649707fbb0ae69)
1007225e5Sgerardnico<?php
2007225e5Sgerardnico/**
3007225e5Sgerardnico * Front Matter implementation to add metadata
4007225e5Sgerardnico *
5007225e5Sgerardnico *
6007225e5Sgerardnico * that enhance the metadata dokuwiki system
7007225e5Sgerardnico * https://www.dokuwiki.org/metadata
8007225e5Sgerardnico * that use the Dublin Core Standard
9007225e5Sgerardnico * http://dublincore.org/
10007225e5Sgerardnico * by adding the front matter markup specification
11007225e5Sgerardnico * https://gerardnico.com/markup/front-matter
12007225e5Sgerardnico *
13007225e5Sgerardnico * Inspiration
14007225e5Sgerardnico * https://github.com/dokufreaks/plugin-meta/blob/master/syntax.php
15007225e5Sgerardnico * https://www.dokuwiki.org/plugin:semantic
16007225e5Sgerardnico *
17007225e5Sgerardnico * See also structured plugin
18007225e5Sgerardnico * https://www.dokuwiki.org/plugin:data
19007225e5Sgerardnico * https://www.dokuwiki.org/plugin:struct
20007225e5Sgerardnico *
21007225e5Sgerardnico */
22007225e5Sgerardnico
23*c3437056SNickeauuse ComboStrap\Aliases;
24*c3437056SNickeauuse ComboStrap\CacheExpirationFrequency;
25*c3437056SNickeauuse ComboStrap\Canonical;
26*c3437056SNickeauuse ComboStrap\EndDate;
27*c3437056SNickeauuse ComboStrap\ExceptionCombo;
28*c3437056SNickeauuse ComboStrap\ExceptionComboRuntime;
29*c3437056SNickeauuse ComboStrap\FileSystems;
30*c3437056SNickeauuse ComboStrap\Lang;
31*c3437056SNickeauuse ComboStrap\LdJson;
32007225e5Sgerardnicouse ComboStrap\LogUtility;
33*c3437056SNickeauuse ComboStrap\LowQualityPageOverwrite;
3437748cd8SNickeauuse ComboStrap\MediaLink;
35*c3437056SNickeauuse ComboStrap\Message;
36*c3437056SNickeauuse ComboStrap\Metadata;
37*c3437056SNickeauuse ComboStrap\MetadataDokuWikiStore;
38*c3437056SNickeauuse ComboStrap\MetadataFrontmatterStore;
39*c3437056SNickeauuse ComboStrap\MetadataStoreTransfer;
4071f916b9Sgerardnicouse ComboStrap\Page;
41*c3437056SNickeauuse ComboStrap\PageH1;
42*c3437056SNickeauuse ComboStrap\PageId;
43*c3437056SNickeauuse ComboStrap\PageImagePath;
44*c3437056SNickeauuse ComboStrap\PageImages;
45*c3437056SNickeauuse ComboStrap\PageKeywords;
46*c3437056SNickeauuse ComboStrap\PageLayout;
47*c3437056SNickeauuse ComboStrap\PagePath;
48*c3437056SNickeauuse ComboStrap\PagePublicationDate;
49*c3437056SNickeauuse ComboStrap\PageTitle;
50*c3437056SNickeauuse ComboStrap\PageType;
51a6bf47aaSNickeauuse ComboStrap\PluginUtility;
52*c3437056SNickeauuse ComboStrap\QualityDynamicMonitoringOverwrite;
53*c3437056SNickeauuse ComboStrap\Region;
54*c3437056SNickeauuse ComboStrap\ResourceName;
55*c3437056SNickeauuse ComboStrap\StartDate;
56007225e5Sgerardnico
5737748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
58007225e5Sgerardnico
59007225e5Sgerardnico
60007225e5Sgerardnico/**
61007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
62007225e5Sgerardnico * need to inherit from this class
63d5303bc5Sgerardnico *
64d5303bc5Sgerardnico * For a list of meta, see also https://ghost.org/docs/publishing/#api-data
65007225e5Sgerardnico */
66007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin
67007225e5Sgerardnico{
68007225e5Sgerardnico    const PARSING_STATE_EMPTY = "empty";
69007225e5Sgerardnico    const PARSING_STATE_ERROR = "error";
70007225e5Sgerardnico    const PARSING_STATE_SUCCESSFUL = "successful";
715f891b7eSNickeau    const STATUS = "status";
725f891b7eSNickeau    const CANONICAL = "frontmatter";
7321913ab3SNickeau    const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing';
74*c3437056SNickeau    const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT = "enableFrontMatterOnSubmit";
75*c3437056SNickeau    const CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT = 0;
76007225e5Sgerardnico
77007225e5Sgerardnico    /**
7837748cd8SNickeau     * Used in the move plugin
7937748cd8SNickeau     * !!! The two last word of the plugin class !!!
8037748cd8SNickeau     */
8137748cd8SNickeau    const COMPONENT = 'combo_' . self::CANONICAL;
8237748cd8SNickeau    const START_TAG = '---json';
8337748cd8SNickeau    const END_TAG = '---';
8437748cd8SNickeau    const METADATA_IMAGE_CANONICAL = "metadata:image";
85*c3437056SNickeau    const PATTERN = self::START_TAG . '.*?' . self::END_TAG;
8637748cd8SNickeau
8737748cd8SNickeau    /**
88*c3437056SNickeau     * The update status for the update of the frontmatter
8937748cd8SNickeau     */
90*c3437056SNickeau    const UPDATE_EXIT_CODE_DONE = 000;
91*c3437056SNickeau    const UPDATE_EXIT_CODE_NOT_ENABLED = 100;
92*c3437056SNickeau    const UPDATE_EXIT_CODE_NOT_CHANGED = 200;
93*c3437056SNickeau    const UPDATE_EXIT_CODE_ERROR = 500;
9437748cd8SNickeau
9537748cd8SNickeau
9637748cd8SNickeau    /**
97007225e5Sgerardnico     * Syntax Type.
98007225e5Sgerardnico     *
99007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
100007225e5Sgerardnico     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
101007225e5Sgerardnico     *
102007225e5Sgerardnico     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
103007225e5Sgerardnico     *
104007225e5Sgerardnico     * baseonly - run only in the base
105007225e5Sgerardnico     */
106*c3437056SNickeau    function getType(): string
107007225e5Sgerardnico    {
108007225e5Sgerardnico        return 'baseonly';
109007225e5Sgerardnico    }
110007225e5Sgerardnico
111531e725cSNickeau    public function getPType()
112531e725cSNickeau    {
11385e82846SNickeau        /**
11485e82846SNickeau         * This element create a section
11585e82846SNickeau         * element that is a div
11685e82846SNickeau         * that should not be in paragraph
11785e82846SNickeau         *
11885e82846SNickeau         * We make it a block
11985e82846SNickeau         */
12085e82846SNickeau        return "block";
121531e725cSNickeau    }
122531e725cSNickeau
123531e725cSNickeau
124007225e5Sgerardnico    /**
125007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
126007225e5Sgerardnico     * Higher number than the teaser-columns
127007225e5Sgerardnico     * because the mode with the lowest sort number will win out
128007225e5Sgerardnico     */
129007225e5Sgerardnico    function getSort()
130007225e5Sgerardnico    {
131007225e5Sgerardnico        return 99;
132007225e5Sgerardnico    }
133007225e5Sgerardnico
134007225e5Sgerardnico    /**
135007225e5Sgerardnico     * Create a pattern that will called this plugin
136007225e5Sgerardnico     *
137007225e5Sgerardnico     * @param string $mode
138007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
139007225e5Sgerardnico     */
140007225e5Sgerardnico    function connectTo($mode)
141007225e5Sgerardnico    {
142007225e5Sgerardnico        if ($mode == "base") {
143007225e5Sgerardnico            // only from the top
144*c3437056SNickeau            $this->Lexer->addSpecialPattern(self::PATTERN, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
145007225e5Sgerardnico        }
146007225e5Sgerardnico    }
147007225e5Sgerardnico
148007225e5Sgerardnico    /**
149007225e5Sgerardnico     *
150007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
151007225e5Sgerardnico     * and to return the result for use in the renderer
152007225e5Sgerardnico     * This result is always cached until the page is modified.
153007225e5Sgerardnico     * @param string $match
154007225e5Sgerardnico     * @param int $state
155007225e5Sgerardnico     * @param int $pos
156007225e5Sgerardnico     * @param Doku_Handler $handler
157007225e5Sgerardnico     * @return array|bool
158007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
159007225e5Sgerardnico     *
160007225e5Sgerardnico     */
161007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
162007225e5Sgerardnico    {
163007225e5Sgerardnico
164007225e5Sgerardnico        if ($state == DOKU_LEXER_SPECIAL) {
165007225e5Sgerardnico
166a6bf47aaSNickeau            $result = [];
167*c3437056SNickeau            $page = Page::createPageFromGlobalDokuwikiId();
168*c3437056SNickeau            try {
169*c3437056SNickeau                $frontMatterStore = MetadataFrontmatterStore::createFromFrontmatterString($page, $match);
170*c3437056SNickeau                $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL;
171*c3437056SNickeau            } catch (ExceptionCombo $e) {
172*c3437056SNickeau                // Decode problem
173a6bf47aaSNickeau                $result[self::STATUS] = self::PARSING_STATE_ERROR;
174a6bf47aaSNickeau                $result[PluginUtility::PAYLOAD] = $match;
175*c3437056SNickeau                return $result;
176*c3437056SNickeau            }
17737748cd8SNickeau
178*c3437056SNickeau            /**
179*c3437056SNickeau             * Empty string
180*c3437056SNickeau             * Rare case, we delete all mutable meta if present
181*c3437056SNickeau             */
182*c3437056SNickeau            $frontmatterData = $frontMatterStore->getData();
183*c3437056SNickeau            if ($frontmatterData === null) {
184*c3437056SNickeau                global $ID;
185*c3437056SNickeau                $meta = p_read_metadata($ID);
186*c3437056SNickeau                foreach (Metadata::MUTABLE_METADATA as $metaKey) {
187*c3437056SNickeau                    if (isset($meta['persistent'][$metaKey])) {
188*c3437056SNickeau                        unset($meta['persistent'][$metaKey]);
189*c3437056SNickeau                    }
190*c3437056SNickeau                }
191*c3437056SNickeau                p_save_metadata($ID, $meta);
19237748cd8SNickeau                return array(self::STATUS => self::PARSING_STATE_EMPTY);
19337748cd8SNickeau            }
19437748cd8SNickeau
195*c3437056SNickeau
19637748cd8SNickeau            /**
197*c3437056SNickeau             * Sync
19837748cd8SNickeau             */
199*c3437056SNickeau            $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($page);
200*c3437056SNickeau            $transfer = MetadataStoreTransfer::createForPage($page)
201*c3437056SNickeau                ->fromStore($frontMatterStore)
202*c3437056SNickeau                ->toStore($targetStore)
203*c3437056SNickeau                ->process($frontmatterData);
204*c3437056SNickeau
205*c3437056SNickeau            $messages = $transfer->getMessages();
206*c3437056SNickeau            $dataForRenderer = $transfer->getNormalizedDataArray();
207*c3437056SNickeau
208*c3437056SNickeau
20937748cd8SNickeau            /**
210*c3437056SNickeau             * Database update
21137748cd8SNickeau             */
212*c3437056SNickeau            try {
213*c3437056SNickeau                $databasePage = $page->getDatabasePage();
214*c3437056SNickeau                $databasePage->replicateMetaAttributes();
215*c3437056SNickeau            } catch (Exception $e) {
216*c3437056SNickeau                $message = Message::createErrorMessage($e->getMessage());
217*c3437056SNickeau                if ($e instanceof ExceptionCombo) {
218*c3437056SNickeau                    $message->setCanonical($e->getCanonical());
2191fa8c418SNickeau                }
220*c3437056SNickeau                $messages[] = $message;
22137748cd8SNickeau            }
22237748cd8SNickeau
223*c3437056SNickeau
224*c3437056SNickeau            foreach ($messages as $message) {
225*c3437056SNickeau                $message->sendLogMsg();
2261fa8c418SNickeau            }
2271fa8c418SNickeau
228*c3437056SNickeau            /**
229*c3437056SNickeau             * Return them for metadata rendering
230*c3437056SNickeau             */
231*c3437056SNickeau            $result[PluginUtility::ATTRIBUTES] = $dataForRenderer;
232*c3437056SNickeau
2331fa8c418SNickeau        }
2341fa8c418SNickeau
235531e725cSNickeau
236531e725cSNickeau        /**
237531e725cSNickeau         * End position is the length of the match + 1 for the newline
238531e725cSNickeau         */
239531e725cSNickeau        $newLine = 1;
240531e725cSNickeau        $endPosition = $pos + strlen($match) + $newLine;
241531e725cSNickeau        $result[PluginUtility::POSITION] = [$pos, $endPosition];
242007225e5Sgerardnico
243007225e5Sgerardnico        return $result;
244007225e5Sgerardnico
245007225e5Sgerardnico    }
246007225e5Sgerardnico
247007225e5Sgerardnico    /**
248007225e5Sgerardnico     * Render the output
249007225e5Sgerardnico     * @param string $format
250007225e5Sgerardnico     * @param Doku_Renderer $renderer
251007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
252007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
253007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
254007225e5Sgerardnico     *
255007225e5Sgerardnico     *
256007225e5Sgerardnico     */
257*c3437056SNickeau    function render($format, Doku_Renderer $renderer, $data): bool
258007225e5Sgerardnico    {
259007225e5Sgerardnico
260007225e5Sgerardnico        switch ($format) {
261007225e5Sgerardnico            case 'xhtml':
262007225e5Sgerardnico                global $ID;
263007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
26421913ab3SNickeau
2655f891b7eSNickeau                $state = $data[self::STATUS];
266007225e5Sgerardnico                if ($state == self::PARSING_STATE_ERROR) {
267*c3437056SNickeau                    $json = MetadataFrontmatterStore::stripFrontmatterTag($data[PluginUtility::PAYLOAD]);
268*c3437056SNickeau                    LogUtility::msg("Front Matter: The json object for the page ($ID) is not valid. " . \ComboStrap\Json::getValidationLink($json), LogUtility::LVL_MSG_ERROR);
269007225e5Sgerardnico                }
27021913ab3SNickeau
27121913ab3SNickeau                /**
27221913ab3SNickeau                 * Section
27321913ab3SNickeau                 */
27421913ab3SNickeau                list($startPosition, $endPosition) = $data[PluginUtility::POSITION];
27521913ab3SNickeau                if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
27621913ab3SNickeau                    $position = $startPosition;
27721913ab3SNickeau                    $name = self::CANONICAL;
27821913ab3SNickeau                    PluginUtility::startSection($renderer, $position, $name);
27921913ab3SNickeau                    $renderer->finishSectionEdit($endPosition);
28021913ab3SNickeau                }
281007225e5Sgerardnico                break;
282*c3437056SNickeau
283531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
284a6bf47aaSNickeau
285a6bf47aaSNickeau                if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) {
286a6bf47aaSNickeau                    return false;
287a6bf47aaSNickeau                }
288a6bf47aaSNickeau
28937748cd8SNickeau
290007225e5Sgerardnico                /** @var renderer_plugin_combo_analytics $renderer */
291*c3437056SNickeau                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
292*c3437056SNickeau                foreach ($frontMatterJsonArray as $key => $value) {
29337748cd8SNickeau
294*c3437056SNickeau                    $renderer->setAnalyticsMetaForReporting($key, $value);
295*c3437056SNickeau                    if ($key === PageImages::PROPERTY_NAME) {
29637748cd8SNickeau                        $this->updateImageStatistics($value, $renderer);
297007225e5Sgerardnico                    }
29837748cd8SNickeau
2999b9e6d1fSgerardnico                }
300007225e5Sgerardnico                break;
30137748cd8SNickeau
302a6bf47aaSNickeau            case "metadata":
303a6bf47aaSNickeau
304*c3437056SNickeau                global $ID;
30537748cd8SNickeau                /** @var Doku_Renderer_metadata $renderer */
306*c3437056SNickeau                if ($data[self::STATUS] === self::PARSING_STATE_ERROR) {
307*c3437056SNickeau                    if (PluginUtility::isDevOrTest()) {
308*c3437056SNickeau                        // fail if test
309*c3437056SNickeau                        throw new ExceptionComboRuntime("Front Matter: The json object for the page ($ID) is not valid.", LogUtility::LVL_MSG_ERROR);
310*c3437056SNickeau                    }
311a6bf47aaSNickeau                    return false;
312a6bf47aaSNickeau                }
313a6bf47aaSNickeau
314*c3437056SNickeau                /**
315*c3437056SNickeau                 * Register media in index
316*c3437056SNickeau                 */
317*c3437056SNickeau                $page = Page::createPageFromId($ID);
318*c3437056SNickeau                $frontMatterJsonArray = $data[PluginUtility::ATTRIBUTES];
319*c3437056SNickeau                if (isset($frontMatterJsonArray[PageImages::getPersistentName()])) {
320*c3437056SNickeau                    $value = $frontMatterJsonArray[PageImages::getPersistentName()];
321a6bf47aaSNickeau
322*c3437056SNickeau                    /**
323*c3437056SNickeau                     * @var PageImages $pageImages
324*c3437056SNickeau                     */
325*c3437056SNickeau                    $pageImages = PageImages::createForPage($page)
326*c3437056SNickeau                        ->buildFromStoreValue($value);
327*c3437056SNickeau                    foreach ($pageImages->getValueAsPageImages() as $imageValue) {
328*c3437056SNickeau                        $imagePath = $imageValue->getImage()->getPath()->toAbsolutePath()->toString();
329*c3437056SNickeau                        $attributes = [PagePath::PROPERTY_NAME => $imagePath];
330*c3437056SNickeau                        if (media_isexternal($imagePath)) {
331*c3437056SNickeau                            $attributes[MediaLink::MEDIA_DOKUWIKI_TYPE] = MediaLink::EXTERNAL_MEDIA_CALL_NAME;
332*c3437056SNickeau                        } else {
333*c3437056SNickeau                            $attributes[MediaLink::MEDIA_DOKUWIKI_TYPE] = MediaLink::INTERNAL_MEDIA_CALL_NAME;
334a6bf47aaSNickeau                        }
33537748cd8SNickeau                        syntax_plugin_combo_media::registerImageMeta($attributes, $renderer);
33637748cd8SNickeau                    }
33737748cd8SNickeau
338a6bf47aaSNickeau                }
339a6bf47aaSNickeau
340a6bf47aaSNickeau                break;
341007225e5Sgerardnico
342007225e5Sgerardnico        }
343007225e5Sgerardnico        return true;
344007225e5Sgerardnico    }
345007225e5Sgerardnico
346007225e5Sgerardnico
34737748cd8SNickeau    private function updateImageStatistics($value, $renderer)
34837748cd8SNickeau    {
349*c3437056SNickeau        if (is_array($value) && sizeof($value) > 0) {
350*c3437056SNickeau            $firstKey = array_keys($value)[0];
351*c3437056SNickeau            if (is_numeric($firstKey)) {
35237748cd8SNickeau                foreach ($value as $subImage) {
35337748cd8SNickeau                    $this->updateImageStatistics($subImage, $renderer);
35437748cd8SNickeau                }
355*c3437056SNickeau                return;
35637748cd8SNickeau            }
35737748cd8SNickeau        }
35837748cd8SNickeau
359*c3437056SNickeau        /**
360*c3437056SNickeau         * Code below is fucked up
361*c3437056SNickeau         */
362*c3437056SNickeau        $path = $value;
363*c3437056SNickeau        if (is_array($value) && isset($value[PageImagePath::getPersistentName()])) {
364*c3437056SNickeau            $path = $value[PageImagePath::getPersistentName()];
36537748cd8SNickeau        }
366*c3437056SNickeau        $media = MediaLink::createFromRenderMatch($path);
367*c3437056SNickeau        $attributes = $media->toCallStackArray();
368*c3437056SNickeau        syntax_plugin_combo_media::updateStatistics($attributes, $renderer);
369*c3437056SNickeau
37037748cd8SNickeau    }
37137748cd8SNickeau
372007225e5Sgerardnico
373007225e5Sgerardnico}
374007225e5Sgerardnico
375