1*c3437056SNickeau<?php 2*c3437056SNickeau 3*c3437056SNickeau 4*c3437056SNickeaunamespace ComboStrap; 5*c3437056SNickeau 6*c3437056SNickeau 7*c3437056SNickeauuse syntax_plugin_combo_frontmatter; 8*c3437056SNickeau 9*c3437056SNickeauclass MetadataFrontmatterStore extends MetadataSingleArrayStore 10*c3437056SNickeau{ 11*c3437056SNickeau 12*c3437056SNickeau const NAME = "frontmatter"; 13*c3437056SNickeau const CANONICAL = self::NAME; 14*c3437056SNickeau 15*c3437056SNickeau /** 16*c3437056SNickeau * @var bool Do we have a frontmatter on the page 17*c3437056SNickeau */ 18*c3437056SNickeau private $isPresent = false; 19*c3437056SNickeau /** 20*c3437056SNickeau * @var string 21*c3437056SNickeau */ 22*c3437056SNickeau private $contentWithoutFrontMatter; 23*c3437056SNickeau 24*c3437056SNickeau /** 25*c3437056SNickeau * @throws ExceptionCombo 26*c3437056SNickeau */ 27*c3437056SNickeau private function syncData() 28*c3437056SNickeau { 29*c3437056SNickeau 30*c3437056SNickeau /** 31*c3437056SNickeau * @var Page $resourceCombo 32*c3437056SNickeau */ 33*c3437056SNickeau $resourceCombo = $this->getResource(); 34*c3437056SNickeau 35*c3437056SNickeau /** 36*c3437056SNickeau * Resource Id special 37*c3437056SNickeau */ 38*c3437056SNickeau $guidObject = $resourceCombo->getUidObject(); 39*c3437056SNickeau if ( 40*c3437056SNickeau !$this->hasProperty($guidObject::getPersistentName()) 41*c3437056SNickeau && 42*c3437056SNickeau $guidObject->getValue() !== null 43*c3437056SNickeau ) { 44*c3437056SNickeau $this->setFromPersistentName($guidObject::getPersistentName(), $guidObject->getValue()); 45*c3437056SNickeau } 46*c3437056SNickeau 47*c3437056SNickeau /** 48*c3437056SNickeau * Read store 49*c3437056SNickeau */ 50*c3437056SNickeau $dokuwikiStore = MetadataDokuWikiStore::getOrCreateFromResource($resourceCombo); 51*c3437056SNickeau $metaFilePath = $dokuwikiStore->getMetaFilePath(); 52*c3437056SNickeau if ($metaFilePath !== null) { 53*c3437056SNickeau $metaModifiedTime = FileSystems::getModifiedTime($metaFilePath); 54*c3437056SNickeau $pageModifiedTime = FileSystems::getModifiedTime($resourceCombo->getPath()); 55*c3437056SNickeau $diff = $pageModifiedTime->diff($metaModifiedTime); 56*c3437056SNickeau if ($diff === false) { 57*c3437056SNickeau throw new ExceptionCombo("Unable to calculate the diff between the page and metadata file"); 58*c3437056SNickeau } 59*c3437056SNickeau $secondDiff = intval($diff->format('%s')); 60*c3437056SNickeau if ($secondDiff > 0) { 61*c3437056SNickeau $resourceCombo->renderMetadataAndFlush(); 62*c3437056SNickeau } 63*c3437056SNickeau } 64*c3437056SNickeau /** 65*c3437056SNickeau * Update the mutable data 66*c3437056SNickeau * (ie delete insert) 67*c3437056SNickeau */ 68*c3437056SNickeau foreach (Metadata::MUTABLE_METADATA as $metaKey) { 69*c3437056SNickeau $metadata = Metadata::getForName($metaKey); 70*c3437056SNickeau if ($metadata === null) { 71*c3437056SNickeau $msg = "The metadata $metaKey should be defined"; 72*c3437056SNickeau if (PluginUtility::isDevOrTest()) { 73*c3437056SNickeau throw new ExceptionCombo($msg); 74*c3437056SNickeau } else { 75*c3437056SNickeau LogUtility::msg($msg); 76*c3437056SNickeau } 77*c3437056SNickeau } 78*c3437056SNickeau $metadata 79*c3437056SNickeau ->setResource($resourceCombo) 80*c3437056SNickeau ->setReadStore($dokuwikiStore) 81*c3437056SNickeau ->setWriteStore($this); 82*c3437056SNickeau 83*c3437056SNickeau $sourceValue = $this->get($metadata); 84*c3437056SNickeau $targetValue = $metadata->getValue(); 85*c3437056SNickeau $defaultValue = $metadata->getDefaultValue(); 86*c3437056SNickeau /** 87*c3437056SNickeau * Strict because otherwise the comparison `false = null` is true 88*c3437056SNickeau */ 89*c3437056SNickeau $targetValueShouldBeStore = !in_array($targetValue, [$defaultValue, null], true); 90*c3437056SNickeau if ($targetValueShouldBeStore) { 91*c3437056SNickeau if ($sourceValue !== $targetValue) { 92*c3437056SNickeau $this->set($metadata); 93*c3437056SNickeau } 94*c3437056SNickeau } else { 95*c3437056SNickeau if ($sourceValue !== null) { 96*c3437056SNickeau $this->remove($metadata); 97*c3437056SNickeau } 98*c3437056SNickeau } 99*c3437056SNickeau } 100*c3437056SNickeau } 101*c3437056SNickeau 102*c3437056SNickeau /** 103*c3437056SNickeau * Update the frontmatter with the managed metadata 104*c3437056SNickeau * Used after a submit from the form 105*c3437056SNickeau * @return Message 106*c3437056SNickeau */ 107*c3437056SNickeau public function sync(): Message 108*c3437056SNickeau { 109*c3437056SNickeau 110*c3437056SNickeau /** 111*c3437056SNickeau * Default update value for the frontmatter 112*c3437056SNickeau */ 113*c3437056SNickeau $updateFrontMatter = PluginUtility::getConfValue(syntax_plugin_combo_frontmatter::CONF_ENABLE_FRONT_MATTER_ON_SUBMIT, syntax_plugin_combo_frontmatter::CONF_ENABLE_FRONT_MATTER_ON_SUBMIT_DEFAULT); 114*c3437056SNickeau 115*c3437056SNickeau 116*c3437056SNickeau if ($this->isPresent()) { 117*c3437056SNickeau $updateFrontMatter = 1; 118*c3437056SNickeau } 119*c3437056SNickeau 120*c3437056SNickeau 121*c3437056SNickeau if ($updateFrontMatter === 0) { 122*c3437056SNickeau return Message::createInfoMessage("The frontmatter is not enabled") 123*c3437056SNickeau ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_NOT_ENABLED); 124*c3437056SNickeau } 125*c3437056SNickeau 126*c3437056SNickeau try { 127*c3437056SNickeau $this->syncData(); 128*c3437056SNickeau } catch (ExceptionCombo $e) { 129*c3437056SNickeau return Message::createInfoMessage($e->getMessage()) 130*c3437056SNickeau ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_ERROR); 131*c3437056SNickeau } 132*c3437056SNickeau 133*c3437056SNickeau 134*c3437056SNickeau /** 135*c3437056SNickeau * Same ? 136*c3437056SNickeau */ 137*c3437056SNickeau if (!$this->hasStateChanged()) { 138*c3437056SNickeau return Message::createInfoMessage("The frontmatter are the same (no update)") 139*c3437056SNickeau ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_NOT_CHANGED); 140*c3437056SNickeau } 141*c3437056SNickeau 142*c3437056SNickeau $this->persist(); 143*c3437056SNickeau 144*c3437056SNickeau return Message::createInfoMessage("The frontmatter was changed") 145*c3437056SNickeau ->setStatus(syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_DONE); 146*c3437056SNickeau 147*c3437056SNickeau } 148*c3437056SNickeau 149*c3437056SNickeau 150*c3437056SNickeau public function isPresent(): bool 151*c3437056SNickeau { 152*c3437056SNickeau return $this->isPresent; 153*c3437056SNickeau } 154*c3437056SNickeau 155*c3437056SNickeau /** 156*c3437056SNickeau * MetadataFrontmatterStore constructor. 157*c3437056SNickeau * @param ResourceCombo $page 158*c3437056SNickeau * @param array|null $data 159*c3437056SNickeau */ 160*c3437056SNickeau public function __construct(ResourceCombo $page, array $data = null) 161*c3437056SNickeau { 162*c3437056SNickeau parent::__construct($page, $data); 163*c3437056SNickeau } 164*c3437056SNickeau 165*c3437056SNickeau /** 166*c3437056SNickeau * @param $match 167*c3437056SNickeau * @return array|null - null if decodage problem, empty array if no json or an associative array 168*c3437056SNickeau * @deprecated used {@link MetadataFrontmatterStore::loadAsString()} instead 169*c3437056SNickeau */ 170*c3437056SNickeau public static function frontMatterMatchToAssociativeArray($match): ?array 171*c3437056SNickeau { 172*c3437056SNickeau $jsonString = self::stripFrontmatterTag($match); 173*c3437056SNickeau 174*c3437056SNickeau // Empty front matter 175*c3437056SNickeau if (trim($jsonString) == "") { 176*c3437056SNickeau return []; 177*c3437056SNickeau } 178*c3437056SNickeau 179*c3437056SNickeau // Otherwise you get an object ie $arrayFormat-> syntax 180*c3437056SNickeau $arrayFormat = true; 181*c3437056SNickeau return json_decode($jsonString, $arrayFormat); 182*c3437056SNickeau } 183*c3437056SNickeau 184*c3437056SNickeau public static function stripFrontmatterTag($match) 185*c3437056SNickeau { 186*c3437056SNickeau // strip 187*c3437056SNickeau // from start `---json` + eol = 8 188*c3437056SNickeau // from end `---` + eol = 4 189*c3437056SNickeau return substr($match, 7, -3); 190*c3437056SNickeau } 191*c3437056SNickeau 192*c3437056SNickeau 193*c3437056SNickeau public static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore 194*c3437056SNickeau { 195*c3437056SNickeau return new MetadataFrontmatterStore($resourceCombo, null); 196*c3437056SNickeau } 197*c3437056SNickeau 198*c3437056SNickeau public static function createFromArray(ResourceCombo $page, array $jsonArray): MetadataFrontmatterStore 199*c3437056SNickeau { 200*c3437056SNickeau return new MetadataFrontmatterStore($page, $jsonArray); 201*c3437056SNickeau } 202*c3437056SNickeau 203*c3437056SNickeau /** 204*c3437056SNickeau * @throws ExceptionCombo 205*c3437056SNickeau */ 206*c3437056SNickeau public static function createFromFrontmatterString($page, $frontmatter = null): MetadataFrontmatterStore 207*c3437056SNickeau { 208*c3437056SNickeau if ($frontmatter === null) { 209*c3437056SNickeau return new MetadataFrontmatterStore($page, []); 210*c3437056SNickeau } 211*c3437056SNickeau $jsonArray = self::frontMatterMatchToAssociativeArray($frontmatter); 212*c3437056SNickeau if ($jsonArray === null) { 213*c3437056SNickeau throw new ExceptionCombo("The frontmatter is not valid"); 214*c3437056SNickeau } 215*c3437056SNickeau return new MetadataFrontmatterStore($page, $jsonArray); 216*c3437056SNickeau } 217*c3437056SNickeau 218*c3437056SNickeau /** 219*c3437056SNickeau * @throws ExceptionCombo 220*c3437056SNickeau */ 221*c3437056SNickeau public static function createFromPage(Page $page): MetadataFrontmatterStore 222*c3437056SNickeau { 223*c3437056SNickeau $content = FileSystems::getContent($page->getPath()); 224*c3437056SNickeau $frontMatterStartTag = syntax_plugin_combo_frontmatter::START_TAG; 225*c3437056SNickeau if (strpos($content, $frontMatterStartTag) === 0) { 226*c3437056SNickeau 227*c3437056SNickeau /** 228*c3437056SNickeau * Extract the actual values 229*c3437056SNickeau */ 230*c3437056SNickeau $pattern = syntax_plugin_combo_frontmatter::PATTERN; 231*c3437056SNickeau $split = preg_split("/($pattern)/ms", $content, 2, PREG_SPLIT_DELIM_CAPTURE); 232*c3437056SNickeau 233*c3437056SNickeau /** 234*c3437056SNickeau * The split normally returns an array 235*c3437056SNickeau * where the first element is empty followed by the frontmatter 236*c3437056SNickeau */ 237*c3437056SNickeau $emptyString = array_shift($split); 238*c3437056SNickeau if (!empty($emptyString)) { 239*c3437056SNickeau throw new ExceptionCombo("The frontmatter is not the first element"); 240*c3437056SNickeau } 241*c3437056SNickeau 242*c3437056SNickeau $frontMatterMatch = array_shift($split); 243*c3437056SNickeau /** 244*c3437056SNickeau * Building the document again 245*c3437056SNickeau */ 246*c3437056SNickeau $contentWithoutFrontMatter = ""; 247*c3437056SNickeau while (($element = array_shift($split)) != null) { 248*c3437056SNickeau $contentWithoutFrontMatter .= $element; 249*c3437056SNickeau } 250*c3437056SNickeau 251*c3437056SNickeau return MetadataFrontmatterStore::createFromFrontmatterString($page, $frontMatterMatch) 252*c3437056SNickeau ->setIsPresent(true) 253*c3437056SNickeau ->setContentWithoutFrontMatter($contentWithoutFrontMatter); 254*c3437056SNickeau 255*c3437056SNickeau } 256*c3437056SNickeau return (new MetadataFrontmatterStore($page)) 257*c3437056SNickeau ->setIsPresent(false) 258*c3437056SNickeau ->setContentWithoutFrontMatter($content); 259*c3437056SNickeau 260*c3437056SNickeau } 261*c3437056SNickeau 262*c3437056SNickeau 263*c3437056SNickeau public function __toString() 264*c3437056SNickeau { 265*c3437056SNickeau return self::NAME; 266*c3437056SNickeau } 267*c3437056SNickeau 268*c3437056SNickeau 269*c3437056SNickeau public function getJsonString(): string 270*c3437056SNickeau { 271*c3437056SNickeau 272*c3437056SNickeau $jsonArray = $this->getData(); 273*c3437056SNickeau ksort($jsonArray); 274*c3437056SNickeau return self::toFrontmatterJsonString($jsonArray); 275*c3437056SNickeau 276*c3437056SNickeau } 277*c3437056SNickeau 278*c3437056SNickeau /** 279*c3437056SNickeau * This formatting make the object on one line for a list of object 280*c3437056SNickeau * making the frontmatter compacter (one line, one meta) 281*c3437056SNickeau * @param $jsonArray 282*c3437056SNickeau * @return string 283*c3437056SNickeau */ 284*c3437056SNickeau public static function toFrontmatterJsonString($jsonArray): string 285*c3437056SNickeau { 286*c3437056SNickeau 287*c3437056SNickeau if (sizeof($jsonArray) === 0) { 288*c3437056SNickeau return "{}"; 289*c3437056SNickeau } 290*c3437056SNickeau $jsonString = ""; 291*c3437056SNickeau self::jsonFlatRecursiveEncoding($jsonArray, $jsonString); 292*c3437056SNickeau 293*c3437056SNickeau /** 294*c3437056SNickeau * Double Guard (frontmatter should be quick enough) 295*c3437056SNickeau * to support this overhead 296*c3437056SNickeau */ 297*c3437056SNickeau $decoding = json_decode($jsonString); 298*c3437056SNickeau if ($decoding === null) { 299*c3437056SNickeau throw new ExceptionComboRuntime("The generated frontmatter json is no a valid json"); 300*c3437056SNickeau } 301*c3437056SNickeau return $jsonString; 302*c3437056SNickeau } 303*c3437056SNickeau 304*c3437056SNickeau private static function jsonFlatRecursiveEncoding(array $jsonProperty, &$jsonString, $level = 0, $endOfFieldCharacter = DOKU_LF, $type = Json::TYPE_OBJECT, $parentType = Json::TYPE_OBJECT) 305*c3437056SNickeau { 306*c3437056SNickeau /** 307*c3437056SNickeau * Open the root object 308*c3437056SNickeau */ 309*c3437056SNickeau if ($type === Json::TYPE_OBJECT) { 310*c3437056SNickeau $jsonString .= "{"; 311*c3437056SNickeau } else { 312*c3437056SNickeau $jsonString .= "["; 313*c3437056SNickeau } 314*c3437056SNickeau 315*c3437056SNickeau /** 316*c3437056SNickeau * Level indentation 317*c3437056SNickeau */ 318*c3437056SNickeau $levelSpaceIndentation = str_repeat(" ", ($level + 1) * Json::TAB_SPACES_COUNTER); 319*c3437056SNickeau 320*c3437056SNickeau /** 321*c3437056SNickeau * Loop 322*c3437056SNickeau */ 323*c3437056SNickeau $elementCounter = 0; 324*c3437056SNickeau foreach ($jsonProperty as $key => $value) { 325*c3437056SNickeau 326*c3437056SNickeau $elementCounter++; 327*c3437056SNickeau 328*c3437056SNickeau /** 329*c3437056SNickeau * Close the previous property 330*c3437056SNickeau */ 331*c3437056SNickeau $isFirstProperty = $elementCounter === 1; 332*c3437056SNickeau if ($isFirstProperty && $parentType !== Json::PARENT_TYPE_ARRAY) { 333*c3437056SNickeau // go the line if this is not a list of object 334*c3437056SNickeau $jsonString .= DOKU_LF; 335*c3437056SNickeau } 336*c3437056SNickeau if (!$isFirstProperty) { 337*c3437056SNickeau $jsonString .= ",$endOfFieldCharacter"; 338*c3437056SNickeau } 339*c3437056SNickeau if ($endOfFieldCharacter === DOKU_LF) { 340*c3437056SNickeau $tab = $levelSpaceIndentation; 341*c3437056SNickeau } else { 342*c3437056SNickeau $tab = " "; 343*c3437056SNickeau } 344*c3437056SNickeau $jsonString .= $tab; 345*c3437056SNickeau 346*c3437056SNickeau /** 347*c3437056SNickeau * Recurse 348*c3437056SNickeau */ 349*c3437056SNickeau $jsonEncodedKey = json_encode($key); 350*c3437056SNickeau if (is_array($value)) { 351*c3437056SNickeau $childLevel = $level + 1; 352*c3437056SNickeau if (is_numeric($key)) { 353*c3437056SNickeau /** 354*c3437056SNickeau * List of object 355*c3437056SNickeau */ 356*c3437056SNickeau $childType = Json::TYPE_OBJECT; 357*c3437056SNickeau $childEndOField = ""; 358*c3437056SNickeau } else { 359*c3437056SNickeau /** 360*c3437056SNickeau * Array 361*c3437056SNickeau */ 362*c3437056SNickeau $jsonString .= "$jsonEncodedKey: "; 363*c3437056SNickeau $childType = Json::TYPE_OBJECT; 364*c3437056SNickeau if ($value[0] !== null) { 365*c3437056SNickeau $childType = Json::PARENT_TYPE_ARRAY; 366*c3437056SNickeau } 367*c3437056SNickeau $childEndOField = $endOfFieldCharacter; 368*c3437056SNickeau } 369*c3437056SNickeau self::jsonFlatRecursiveEncoding($value, $jsonString, $childLevel, $childEndOField, $childType, $type); 370*c3437056SNickeau 371*c3437056SNickeau } else { 372*c3437056SNickeau /** 373*c3437056SNickeau * Single property 374*c3437056SNickeau */ 375*c3437056SNickeau $jsonEncodedValue = json_encode($value); 376*c3437056SNickeau $jsonString .= "$jsonEncodedKey: $jsonEncodedValue"; 377*c3437056SNickeau 378*c3437056SNickeau } 379*c3437056SNickeau 380*c3437056SNickeau } 381*c3437056SNickeau 382*c3437056SNickeau /** 383*c3437056SNickeau * Close the object or array 384*c3437056SNickeau */ 385*c3437056SNickeau $closingLevelSpaceIndentation = str_repeat(" ", $level * Json::TAB_SPACES_COUNTER); 386*c3437056SNickeau if ($type === Json::TYPE_OBJECT) { 387*c3437056SNickeau if ($parentType !== Json::PARENT_TYPE_ARRAY) { 388*c3437056SNickeau $jsonString .= DOKU_LF . $closingLevelSpaceIndentation; 389*c3437056SNickeau } else { 390*c3437056SNickeau $jsonString .= " "; 391*c3437056SNickeau } 392*c3437056SNickeau $jsonString .= "}"; 393*c3437056SNickeau } else { 394*c3437056SNickeau /** 395*c3437056SNickeau * The array is not going one level back 396*c3437056SNickeau */ 397*c3437056SNickeau $jsonString .= DOKU_LF . $closingLevelSpaceIndentation . "]"; 398*c3437056SNickeau } 399*c3437056SNickeau } 400*c3437056SNickeau 401*c3437056SNickeau 402*c3437056SNickeau public function toFrontmatterString(): string 403*c3437056SNickeau { 404*c3437056SNickeau $frontmatterStartTag = syntax_plugin_combo_frontmatter::START_TAG; 405*c3437056SNickeau $frontmatterEndTag = syntax_plugin_combo_frontmatter::END_TAG; 406*c3437056SNickeau $jsonArray = $this->getData(); 407*c3437056SNickeau ksort($jsonArray); 408*c3437056SNickeau $jsonEncode = self::toFrontmatterJsonString($jsonArray); 409*c3437056SNickeau 410*c3437056SNickeau return <<<EOF 411*c3437056SNickeau$frontmatterStartTag 412*c3437056SNickeau$jsonEncode 413*c3437056SNickeau$frontmatterEndTag 414*c3437056SNickeauEOF; 415*c3437056SNickeau 416*c3437056SNickeau 417*c3437056SNickeau } 418*c3437056SNickeau 419*c3437056SNickeau private function setIsPresent(bool $bool): MetadataFrontmatterStore 420*c3437056SNickeau { 421*c3437056SNickeau $this->isPresent = $bool; 422*c3437056SNickeau return $this; 423*c3437056SNickeau } 424*c3437056SNickeau 425*c3437056SNickeau public function persist() 426*c3437056SNickeau { 427*c3437056SNickeau if ($this->contentWithoutFrontMatter === null) { 428*c3437056SNickeau LogUtility::msg("The content without frontmatter should have been set. Did you you use the createFromPage constructor"); 429*c3437056SNickeau return $this; 430*c3437056SNickeau } 431*c3437056SNickeau $targetFrontMatterJsonString = $this->toFrontmatterString(); 432*c3437056SNickeau 433*c3437056SNickeau /** 434*c3437056SNickeau * EOL for the first frontmatter 435*c3437056SNickeau */ 436*c3437056SNickeau $sep = ""; 437*c3437056SNickeau if (strlen($this->contentWithoutFrontMatter) > 0) { 438*c3437056SNickeau $firstChar = $this->contentWithoutFrontMatter[0]; 439*c3437056SNickeau if (!in_array($firstChar, ["\n", "\r"])) { 440*c3437056SNickeau $sep = "\n"; 441*c3437056SNickeau } 442*c3437056SNickeau } 443*c3437056SNickeau 444*c3437056SNickeau /** 445*c3437056SNickeau * Build the new document 446*c3437056SNickeau */ 447*c3437056SNickeau $newPageContent = <<<EOF 448*c3437056SNickeau$targetFrontMatterJsonString$sep$this->contentWithoutFrontMatter 449*c3437056SNickeauEOF; 450*c3437056SNickeau $resourceCombo = $this->getResource(); 451*c3437056SNickeau if ($resourceCombo instanceof Page) { 452*c3437056SNickeau $resourceCombo->upsertContent($newPageContent, "Metadata frontmatter store upsert"); 453*c3437056SNickeau } 454*c3437056SNickeau return $this; 455*c3437056SNickeau } 456*c3437056SNickeau 457*c3437056SNickeau private function setContentWithoutFrontMatter(string $contentWithoutFrontMatter): MetadataFrontmatterStore 458*c3437056SNickeau { 459*c3437056SNickeau $this->contentWithoutFrontMatter = $contentWithoutFrontMatter; 460*c3437056SNickeau return $this; 461*c3437056SNickeau } 462*c3437056SNickeau 463*c3437056SNickeau 464*c3437056SNickeau} 465