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