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