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*37748cd8SNickeauuse ComboStrap\Analytics; 24*37748cd8SNickeauuse ComboStrap\CacheManager; 25*37748cd8SNickeauuse ComboStrap\Iso8601Date; 26007225e5Sgerardnicouse ComboStrap\LogUtility; 27*37748cd8SNickeauuse ComboStrap\MediaLink; 2871f916b9Sgerardnicouse ComboStrap\Page; 29a6bf47aaSNickeauuse ComboStrap\PluginUtility; 30*37748cd8SNickeauuse ComboStrap\Publication; 31007225e5Sgerardnico 32*37748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 33007225e5Sgerardnico 34007225e5Sgerardnicoif (!defined('DOKU_INC')) { 35007225e5Sgerardnico die(); 36007225e5Sgerardnico} 37007225e5Sgerardnico 38007225e5Sgerardnico/** 39007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism 40007225e5Sgerardnico * need to inherit from this class 41d5303bc5Sgerardnico * 42d5303bc5Sgerardnico * For a list of meta, see also https://ghost.org/docs/publishing/#api-data 43007225e5Sgerardnico */ 44007225e5Sgerardnicoclass syntax_plugin_combo_frontmatter extends DokuWiki_Syntax_Plugin 45007225e5Sgerardnico{ 46007225e5Sgerardnico const PARSING_STATE_EMPTY = "empty"; 47007225e5Sgerardnico const PARSING_STATE_ERROR = "error"; 48007225e5Sgerardnico const PARSING_STATE_SUCCESSFUL = "successful"; 495f891b7eSNickeau const STATUS = "status"; 505f891b7eSNickeau const CANONICAL = "frontmatter"; 5121913ab3SNickeau const CONF_ENABLE_SECTION_EDITING = 'enableFrontMatterSectionEditing'; 52007225e5Sgerardnico 53007225e5Sgerardnico /** 54*37748cd8SNickeau * Used in the move plugin 55*37748cd8SNickeau * !!! The two last word of the plugin class !!! 56*37748cd8SNickeau */ 57*37748cd8SNickeau const COMPONENT = 'combo_' . self::CANONICAL; 58*37748cd8SNickeau const START_TAG = '---json'; 59*37748cd8SNickeau const END_TAG = '---'; 60*37748cd8SNickeau const METADATA_IMAGE_CANONICAL = "metadata:image"; 61*37748cd8SNickeau 62*37748cd8SNickeau /** 63*37748cd8SNickeau * @param $match 64*37748cd8SNickeau * @return array|mixed - null if decodage problem, empty array if no json or an associative array 65*37748cd8SNickeau */ 66*37748cd8SNickeau public static function FrontMatterMatchToAssociativeArray($match) 67*37748cd8SNickeau { 68*37748cd8SNickeau // strip 69*37748cd8SNickeau // from start `---json` + eol = 8 70*37748cd8SNickeau // from end `---` + eol = 4 71*37748cd8SNickeau $jsonString = substr($match, 7, -3); 72*37748cd8SNickeau 73*37748cd8SNickeau // Empty front matter 74*37748cd8SNickeau if (trim($jsonString) == "") { 75*37748cd8SNickeau self::deleteKnownMetaThatAreNoMorePresent(); 76*37748cd8SNickeau return []; 77*37748cd8SNickeau } 78*37748cd8SNickeau 79*37748cd8SNickeau // Otherwise you get an object ie $arrayFormat-> syntax 80*37748cd8SNickeau $arrayFormat = true; 81*37748cd8SNickeau return json_decode($jsonString, $arrayFormat); 82*37748cd8SNickeau } 83*37748cd8SNickeau 84*37748cd8SNickeau /** 85007225e5Sgerardnico * Syntax Type. 86007225e5Sgerardnico * 87007225e5Sgerardnico * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 88007225e5Sgerardnico * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 89007225e5Sgerardnico * 90007225e5Sgerardnico * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 91007225e5Sgerardnico * 92007225e5Sgerardnico * baseonly - run only in the base 93007225e5Sgerardnico */ 94007225e5Sgerardnico function getType() 95007225e5Sgerardnico { 96007225e5Sgerardnico return 'baseonly'; 97007225e5Sgerardnico } 98007225e5Sgerardnico 99531e725cSNickeau public function getPType() 100531e725cSNickeau { 10185e82846SNickeau /** 10285e82846SNickeau * This element create a section 10385e82846SNickeau * element that is a div 10485e82846SNickeau * that should not be in paragraph 10585e82846SNickeau * 10685e82846SNickeau * We make it a block 10785e82846SNickeau */ 10885e82846SNickeau return "block"; 109531e725cSNickeau } 110531e725cSNickeau 111531e725cSNickeau 112007225e5Sgerardnico /** 113007225e5Sgerardnico * @see Doku_Parser_Mode::getSort() 114007225e5Sgerardnico * Higher number than the teaser-columns 115007225e5Sgerardnico * because the mode with the lowest sort number will win out 116007225e5Sgerardnico */ 117007225e5Sgerardnico function getSort() 118007225e5Sgerardnico { 119007225e5Sgerardnico return 99; 120007225e5Sgerardnico } 121007225e5Sgerardnico 122007225e5Sgerardnico /** 123007225e5Sgerardnico * Create a pattern that will called this plugin 124007225e5Sgerardnico * 125007225e5Sgerardnico * @param string $mode 126007225e5Sgerardnico * @see Doku_Parser_Mode::connectTo() 127007225e5Sgerardnico */ 128007225e5Sgerardnico function connectTo($mode) 129007225e5Sgerardnico { 130007225e5Sgerardnico if ($mode == "base") { 131007225e5Sgerardnico // only from the top 132*37748cd8SNickeau $this->Lexer->addSpecialPattern(self::START_TAG . '.*?' . self::END_TAG, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 133007225e5Sgerardnico } 134007225e5Sgerardnico } 135007225e5Sgerardnico 136007225e5Sgerardnico /** 137007225e5Sgerardnico * 138007225e5Sgerardnico * The handle function goal is to parse the matched syntax through the pattern function 139007225e5Sgerardnico * and to return the result for use in the renderer 140007225e5Sgerardnico * This result is always cached until the page is modified. 141007225e5Sgerardnico * @param string $match 142007225e5Sgerardnico * @param int $state 143007225e5Sgerardnico * @param int $pos 144007225e5Sgerardnico * @param Doku_Handler $handler 145007225e5Sgerardnico * @return array|bool 146007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::handle() 147007225e5Sgerardnico * 148007225e5Sgerardnico */ 149007225e5Sgerardnico function handle($match, $state, $pos, Doku_Handler $handler) 150007225e5Sgerardnico { 151007225e5Sgerardnico 152007225e5Sgerardnico if ($state == DOKU_LEXER_SPECIAL) { 153007225e5Sgerardnico 154007225e5Sgerardnico 155*37748cd8SNickeau $jsonArray = self::FrontMatterMatchToAssociativeArray($match); 156007225e5Sgerardnico 157007225e5Sgerardnico 158a6bf47aaSNickeau $result = []; 159007225e5Sgerardnico // Decodage problem 160531e725cSNickeau if ($jsonArray == null) { 161*37748cd8SNickeau 162a6bf47aaSNickeau $result[self::STATUS] = self::PARSING_STATE_ERROR; 163a6bf47aaSNickeau $result[PluginUtility::PAYLOAD] = $match; 164*37748cd8SNickeau 165a6bf47aaSNickeau } else { 166*37748cd8SNickeau 167*37748cd8SNickeau if (sizeof($jsonArray) === 0) { 168*37748cd8SNickeau return array(self::STATUS => self::PARSING_STATE_EMPTY); 169*37748cd8SNickeau } 170*37748cd8SNickeau 1715f891b7eSNickeau $result[self::STATUS] = self::PARSING_STATE_SUCCESSFUL; 172*37748cd8SNickeau /** 173*37748cd8SNickeau * Published is an alias for date published 174*37748cd8SNickeau */ 175*37748cd8SNickeau if (isset($jsonArray[Publication::OLD_META_KEY])) { 176*37748cd8SNickeau $jsonArray[Publication::DATE_PUBLISHED] = $jsonArray[Publication::OLD_META_KEY]; 177*37748cd8SNickeau unset($jsonArray[Publication::OLD_META_KEY]); 178*37748cd8SNickeau } 179*37748cd8SNickeau /** 180*37748cd8SNickeau * Add the time part if not present 181*37748cd8SNickeau */ 182*37748cd8SNickeau if (isset($jsonArray[Publication::DATE_PUBLISHED])) { 183*37748cd8SNickeau $datePublishedString = $jsonArray[Publication::DATE_PUBLISHED]; 184*37748cd8SNickeau $datePublished = Iso8601Date::create($datePublishedString); 185*37748cd8SNickeau if (!$datePublished->isValidDateEntry()) { 186*37748cd8SNickeau LogUtility::msg("The published date ($datePublishedString) is not a valid ISO date supported.", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 187*37748cd8SNickeau unset($jsonArray[Publication::DATE_PUBLISHED]); 188*37748cd8SNickeau } else { 189*37748cd8SNickeau $jsonArray[Publication::DATE_PUBLISHED] = "$datePublished"; 190*37748cd8SNickeau } 191*37748cd8SNickeau 192*37748cd8SNickeau } 193a6bf47aaSNickeau $result[PluginUtility::ATTRIBUTES] = $jsonArray; 194a6bf47aaSNickeau } 195531e725cSNickeau 196531e725cSNickeau /** 197531e725cSNickeau * End position is the length of the match + 1 for the newline 198531e725cSNickeau */ 199531e725cSNickeau $newLine = 1; 200531e725cSNickeau $endPosition = $pos + strlen($match) + $newLine; 201531e725cSNickeau $result[PluginUtility::POSITION] = [$pos, $endPosition]; 202007225e5Sgerardnico 203007225e5Sgerardnico return $result; 204007225e5Sgerardnico } 205007225e5Sgerardnico 206007225e5Sgerardnico return array(); 207007225e5Sgerardnico } 208007225e5Sgerardnico 209007225e5Sgerardnico /** 210007225e5Sgerardnico * Render the output 211007225e5Sgerardnico * @param string $format 212007225e5Sgerardnico * @param Doku_Renderer $renderer 213007225e5Sgerardnico * @param array $data - what the function handle() return'ed 214007225e5Sgerardnico * @return boolean - rendered correctly? (however, returned value is not used at the moment) 215007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::render() 216007225e5Sgerardnico * 217007225e5Sgerardnico * 218007225e5Sgerardnico */ 219007225e5Sgerardnico function render($format, Doku_Renderer $renderer, $data) 220007225e5Sgerardnico { 221007225e5Sgerardnico 222007225e5Sgerardnico switch ($format) { 223007225e5Sgerardnico case 'xhtml': 224007225e5Sgerardnico global $ID; 225007225e5Sgerardnico /** @var Doku_Renderer_xhtml $renderer */ 22621913ab3SNickeau 2275f891b7eSNickeau $state = $data[self::STATUS]; 228007225e5Sgerardnico if ($state == self::PARSING_STATE_ERROR) { 2295f891b7eSNickeau $json = $data[PluginUtility::PAYLOAD]; 2305f891b7eSNickeau 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); 231007225e5Sgerardnico } 23221913ab3SNickeau 23321913ab3SNickeau /** 23421913ab3SNickeau * Section 23521913ab3SNickeau */ 23621913ab3SNickeau list($startPosition, $endPosition) = $data[PluginUtility::POSITION]; 23721913ab3SNickeau if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) { 23821913ab3SNickeau $position = $startPosition; 23921913ab3SNickeau $name = self::CANONICAL; 24021913ab3SNickeau PluginUtility::startSection($renderer, $position, $name); 24121913ab3SNickeau $renderer->finishSectionEdit($endPosition); 24221913ab3SNickeau } 243007225e5Sgerardnico break; 244531e725cSNickeau case renderer_plugin_combo_analytics::RENDERER_FORMAT: 245a6bf47aaSNickeau 246a6bf47aaSNickeau if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) { 247a6bf47aaSNickeau return false; 248a6bf47aaSNickeau } 249a6bf47aaSNickeau 250*37748cd8SNickeau $notModifiableMeta = [ 251*37748cd8SNickeau Analytics::PATH, 252*37748cd8SNickeau Analytics::DATE_CREATED, 253*37748cd8SNickeau Analytics::DATE_MODIFIED 254*37748cd8SNickeau ]; 255*37748cd8SNickeau 256007225e5Sgerardnico /** @var renderer_plugin_combo_analytics $renderer */ 257a6bf47aaSNickeau $jsonArray = $data[PluginUtility::ATTRIBUTES]; 258*37748cd8SNickeau foreach ($jsonArray as $key => $value) { 259*37748cd8SNickeau if (!in_array($key, $notModifiableMeta)) { 260*37748cd8SNickeau 261*37748cd8SNickeau $renderer->setMeta($key, $value); 262*37748cd8SNickeau if ($key === Page::IMAGE_META_PROPERTY) { 263*37748cd8SNickeau $this->updateImageStatistics($value, $renderer); 264007225e5Sgerardnico } 265*37748cd8SNickeau 266*37748cd8SNickeau } else { 267*37748cd8SNickeau LogUtility::msg("The metadata ($key) cannot be set.", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 268007225e5Sgerardnico } 2699b9e6d1fSgerardnico } 270007225e5Sgerardnico break; 271*37748cd8SNickeau 272a6bf47aaSNickeau case "metadata": 273a6bf47aaSNickeau 274*37748cd8SNickeau /** @var Doku_Renderer_metadata $renderer */ 275a6bf47aaSNickeau if ($data[self::STATUS] != self::PARSING_STATE_SUCCESSFUL) { 276a6bf47aaSNickeau return false; 277a6bf47aaSNickeau } 278a6bf47aaSNickeau 279a6bf47aaSNickeau global $ID; 280a6bf47aaSNickeau $jsonArray = $data[PluginUtility::ATTRIBUTES]; 281a6bf47aaSNickeau 282a6bf47aaSNickeau 283a6bf47aaSNickeau $notModifiableMeta = [ 284a6bf47aaSNickeau "date", 285a6bf47aaSNickeau "user", 286a6bf47aaSNickeau "last_change", 287a6bf47aaSNickeau "creator", 288a6bf47aaSNickeau "contributor" 289a6bf47aaSNickeau ]; 290a6bf47aaSNickeau 291a6bf47aaSNickeau foreach ($jsonArray as $key => $value) { 292a6bf47aaSNickeau 293a6bf47aaSNickeau $lowerCaseKey = trim(strtolower($key)); 294a6bf47aaSNickeau 295a6bf47aaSNickeau // Not modifiable metadata 296a6bf47aaSNickeau if (in_array($lowerCaseKey, $notModifiableMeta)) { 297a6bf47aaSNickeau LogUtility::msg("Front Matter: The metadata ($lowerCaseKey) is a protected metadata and cannot be modified", LogUtility::LVL_MSG_WARNING); 298a6bf47aaSNickeau continue; 299a6bf47aaSNickeau } 300a6bf47aaSNickeau 301a6bf47aaSNickeau switch ($lowerCaseKey) { 302a6bf47aaSNickeau 303a6bf47aaSNickeau case Page::DESCRIPTION_PROPERTY: 304a6bf47aaSNickeau /** 305a6bf47aaSNickeau * Overwrite also the actual description 306a6bf47aaSNickeau */ 307a6bf47aaSNickeau p_set_metadata($ID, array(Page::DESCRIPTION_PROPERTY => array( 308a6bf47aaSNickeau "abstract" => $value, 309a6bf47aaSNickeau "origin" => syntax_plugin_combo_frontmatter::CANONICAL 310a6bf47aaSNickeau ))); 311a6bf47aaSNickeau /** 312a6bf47aaSNickeau * Continue because 313a6bf47aaSNickeau * the description value was already stored 314a6bf47aaSNickeau * We don't want to override it 315a6bf47aaSNickeau * And continue 2 because continue == break in a switch 316a6bf47aaSNickeau */ 317a6bf47aaSNickeau continue 2; 318a6bf47aaSNickeau 319a6bf47aaSNickeau 320a6bf47aaSNickeau // Canonical should be lowercase 321a6bf47aaSNickeau case Page::CANONICAL_PROPERTY: 322a6bf47aaSNickeau $value = strtolower($value); 323a6bf47aaSNickeau break; 324a6bf47aaSNickeau 325*37748cd8SNickeau case Page::IMAGE_META_PROPERTY: 326*37748cd8SNickeau 327*37748cd8SNickeau $imageValues = []; 328*37748cd8SNickeau $this->aggregateImageValues($imageValues, $value); 329*37748cd8SNickeau foreach ($imageValues as $imageValue) { 330*37748cd8SNickeau $media = MediaLink::createFromRenderMatch($imageValue); 331*37748cd8SNickeau $attributes = $media->toCallStackArray(); 332*37748cd8SNickeau syntax_plugin_combo_media::registerImageMeta($attributes, $renderer); 333*37748cd8SNickeau } 334*37748cd8SNickeau break; 335*37748cd8SNickeau 336a6bf47aaSNickeau } 337a6bf47aaSNickeau // Set the value persistently 338a6bf47aaSNickeau p_set_metadata($ID, array($lowerCaseKey => $value)); 339a6bf47aaSNickeau 340a6bf47aaSNickeau } 341a6bf47aaSNickeau 342a6bf47aaSNickeau $this->deleteKnownMetaThatAreNoMorePresent($jsonArray); 343a6bf47aaSNickeau 344a6bf47aaSNickeau break; 345007225e5Sgerardnico 346007225e5Sgerardnico } 347007225e5Sgerardnico return true; 348007225e5Sgerardnico } 349007225e5Sgerardnico 350007225e5Sgerardnico /** 351007225e5Sgerardnico * 352007225e5Sgerardnico * @param array $json - The Json 353007225e5Sgerardnico * Delete the controlled meta that are no more present if they exists 354007225e5Sgerardnico * @return bool 355007225e5Sgerardnico */ 356*37748cd8SNickeau static public 357a6bf47aaSNickeau function deleteKnownMetaThatAreNoMorePresent(array $json = array()) 358007225e5Sgerardnico { 359007225e5Sgerardnico global $ID; 360007225e5Sgerardnico 361007225e5Sgerardnico /** 362007225e5Sgerardnico * The managed meta with the exception of 363007225e5Sgerardnico * the {@link action_plugin_combo_metadescription::DESCRIPTION_META_KEY description} 364007225e5Sgerardnico * because it's already managed by dokuwiki in description['abstract'] 365007225e5Sgerardnico */ 366007225e5Sgerardnico $managedMeta = [ 36771f916b9Sgerardnico Page::CANONICAL_PROPERTY, 368*37748cd8SNickeau Page::TYPE_META_PROPERTY, 369*37748cd8SNickeau Page::IMAGE_META_PROPERTY, 370*37748cd8SNickeau Page::COUNTRY_META_PROPERTY, 371*37748cd8SNickeau Page::LANG_META_PROPERTY, 372*37748cd8SNickeau Analytics::TITLE, 373*37748cd8SNickeau syntax_plugin_combo_disqus::META_DISQUS_IDENTIFIER, 374*37748cd8SNickeau Publication::OLD_META_KEY, 375*37748cd8SNickeau Publication::DATE_PUBLISHED, 376*37748cd8SNickeau Analytics::NAME, 377*37748cd8SNickeau CacheManager::DATE_CACHE_EXPIRATION_META_KEY, 378*37748cd8SNickeau action_plugin_combo_metagoogle::JSON_LD_META_PROPERTY, 379*37748cd8SNickeau 380007225e5Sgerardnico ]; 381007225e5Sgerardnico $meta = p_read_metadata($ID); 382007225e5Sgerardnico foreach ($managedMeta as $metaKey) { 383007225e5Sgerardnico if (!array_key_exists($metaKey, $json)) { 384007225e5Sgerardnico if (isset($meta['persistent'][$metaKey])) { 385007225e5Sgerardnico unset($meta['persistent'][$metaKey]); 386007225e5Sgerardnico } 387007225e5Sgerardnico } 388007225e5Sgerardnico } 389007225e5Sgerardnico return p_save_metadata($ID, $meta); 390007225e5Sgerardnico } 391007225e5Sgerardnico 392*37748cd8SNickeau private function updateImageStatistics($value, $renderer) 393*37748cd8SNickeau { 394*37748cd8SNickeau if(is_array($value)){ 395*37748cd8SNickeau foreach($value as $subImage){ 396*37748cd8SNickeau $this->updateImageStatistics($subImage, $renderer); 397*37748cd8SNickeau } 398*37748cd8SNickeau } else { 399*37748cd8SNickeau $media = MediaLink::createFromRenderMatch($value); 400*37748cd8SNickeau $attributes = $media->toCallStackArray(); 401*37748cd8SNickeau syntax_plugin_combo_media::updateStatistics($attributes, $renderer); 402*37748cd8SNickeau } 403*37748cd8SNickeau } 404*37748cd8SNickeau 405*37748cd8SNickeau private function aggregateImageValues(array &$imageValues, $value) 406*37748cd8SNickeau { 407*37748cd8SNickeau if (is_array($value)) { 408*37748cd8SNickeau foreach ($value as $subImageValue) { 409*37748cd8SNickeau $this->aggregateImageValues($imageValues,$subImageValue); 410*37748cd8SNickeau } 411*37748cd8SNickeau } else { 412*37748cd8SNickeau $imageValues[] = $value; 413*37748cd8SNickeau } 414*37748cd8SNickeau } 415*37748cd8SNickeau 416007225e5Sgerardnico 417007225e5Sgerardnico} 418007225e5Sgerardnico 419