1<?php 2 3 4namespace ComboStrap; 5 6use dokuwiki\Extension\Event; 7 8/** 9 * Class MetadataFileSystemStore 10 * @package ComboStrap 11 * 12 * The meta file system store. 13 * 14 * It mimics an in-memory store where data are 15 * * read at the store creation 16 * * refreshed when the metadata render runs (See {@link \action_plugin_combo_metasync}) (Ie dokuwiki modifies the metadata files this way) {@link MetadataDokuWikiStore::renderAndPersist()} 17 * * written immediately on the disk (few write) with the {@link p_set_metadata()} 18 * 19 * Why ? 20 * Php is a CGI script meaning that it starts and end for each request 21 * on the server. 22 * But in test, this is not the case, as the script starts for the first test 23 * and end with the last test. 24 * 25 * If the data store is local scoped, we get then a lot of inconsistency 26 * - the data for one page is not the same than another 27 * - the metadata object {@link PageId} (from {@link ResourceCombo::getUidObject()} may be null while it was created with another {@link Metadata} creating it twice 28 * 29 * This implementation has a cache object 30 * 31 */ 32class MetadataDokuWikiStore extends MetadataSingleArrayStore 33{ 34 35 /** 36 * Current metadata / runtime metadata / calculated metadata 37 * This metadata can only be set when {@link Syntax::render() rendering} 38 * The data may be deleted 39 * https://www.dokuwiki.org/devel:metadata#metadata_persistence 40 * 41 * This is generally where the default data is located 42 * if not found in the persistent 43 */ 44 public const CURRENT_METADATA = "current"; 45 /** 46 * Persistent metadata (data that should be in a backup) 47 * 48 * They are used as the default of the current metadata 49 * and is never cleaned 50 * 51 * https://www.dokuwiki.org/devel:metadata#metadata_persistence 52 * 53 * Because the current is only usable in rendering, all 54 * metadata are persistent inside dokuwiki 55 */ 56 public const PERSISTENT_METADATA = "persistent"; 57 58 59 const CANONICAL = Metadata::CANONICAL; 60 /** 61 * When the value of a metadata has changed 62 */ 63 public const PAGE_METADATA_MUTATION_EVENT = "PAGE_METADATA_MUTATION_EVENT"; 64 const NEW_VALUE_ATTRIBUTE = "new_value"; 65 66 /** 67 * 68 * @var MetadataDokuWikiStore[] a cache of store 69 */ 70 private static $storesByRequestedPage; 71 72 73 /** 74 * @return MetadataDokuWikiStore 75 * We don't use a global static variable 76 * because we are working with php as cgi script 77 * and there is no notion of request 78 * to be able to flush the data on the disk 79 * 80 * The scope of the data will be then the store 81 */ 82 public static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore 83 { 84 85 $requestedId = PluginUtility::getRequestedWikiId(); 86 if ($requestedId === null) { 87 if ($resourceCombo instanceof Page) { 88 $requestedId = $resourceCombo->getDokuwikiId(); 89 } else { 90 $requestedId = "not-a-page"; 91 } 92 } 93 $storesByRequestedId = &self::$storesByRequestedPage[$requestedId]; 94 if ($storesByRequestedId === null) { 95 // delete all previous stores by requested page id 96 self::$storesByRequestedPage = null; 97 self::$storesByRequestedPage[$requestedId] = []; 98 $storesByRequestedId = &self::$storesByRequestedPage[$requestedId]; 99 } 100 $path = $resourceCombo->getPath()->toString(); 101 if (isset($storesByRequestedId[$path])) { 102 return $storesByRequestedId[$path]; 103 } 104 105 if (!($resourceCombo instanceof Page)) { 106 LogUtility::msg("The resource is not a page. File System store supports only page resources"); 107 $data = null; 108 } else { 109 $data = p_read_metadata($resourceCombo->getDokuwikiId()); 110 } 111 112 $metadataStore = new MetadataDokuWikiStore($resourceCombo, $data); 113 $storesByRequestedId[$path] = $metadataStore; 114 return $metadataStore; 115 116 } 117 118 119 /** 120 * @return MetadataDokuWikiStore[] 121 */ 122 public static function getStores(): array 123 { 124 return self::$storesByRequestedPage; 125 } 126 127 /** 128 * Delete the in-memory data store 129 */ 130 public static function resetAll() 131 { 132 self::$storesByRequestedPage = []; 133 } 134 135 public function set(Metadata $metadata) 136 { 137 138 $name = $metadata->getName(); 139 $persistentValue = $metadata->toStoreValue(); 140 $defaultValue = $metadata->toStoreDefaultValue(); 141 $resource = $metadata->getResource(); 142 $this->checkResource($resource); 143 if ($resource === null) { 144 throw new ExceptionComboRuntime("A resource is mandatory", self::CANONICAL); 145 } 146 if (!($resource instanceof Page)) { 147 throw new ExceptionComboRuntime("The DokuWiki metadata store is only for page resource", self::CANONICAL); 148 } 149 $dokuwikiId = $resource->getDokuwikiId(); 150 $this->setFromWikiId($dokuwikiId, $name, $persistentValue, $defaultValue); 151 } 152 153 /** 154 * @param Metadata $metadata 155 * @param null $default 156 * @return mixed|null 157 * 158 * 159 */ 160 public function get(Metadata $metadata, $default = null) 161 { 162 163 $resource = $metadata->getResource(); 164 $this->checkResource($resource); 165 if ($resource === null) { 166 throw new ExceptionComboRuntime("A resource is mandatory", self::CANONICAL); 167 } 168 if (!($resource instanceof Page)) { 169 throw new ExceptionComboRuntime("The DokuWiki metadata store is only for page resource", self::CANONICAL); 170 } 171 return $this->getFromWikiId($resource->getDokuwikiId(), $metadata->getName(), $default); 172 173 174 } 175 176 /** 177 * Getting a metadata for a resource via its name 178 * when we don't want to create a class 179 * 180 * This function is used primarily by derived / process metadata 181 * 182 * @param string $name 183 * @param null $default 184 * @return mixed 185 */ 186 public function getFromPersistentName(string $name, $default = null) 187 { 188 $wikiId = $this->getResource()->getDokuwikiId(); 189 return $this->getFromWikiId($wikiId, $name, $default); 190 } 191 192 193 public function persist() 194 { 195 196 /** 197 * Done on set via the dokuwiki function 198 */ 199 200 } 201 202 /** 203 * @param string $name 204 * @param string|array $value 205 * @return MetadataDokuWikiStore 206 */ 207 public function setFromPersistentName(string $name, $value): MetadataDokuWikiStore 208 { 209 $this->setFromWikiId($this->getResource()->getDokuwikiId(), $name, $value); 210 return $this; 211 } 212 213 public function getData(): ?array 214 { 215 if ( 216 $this->data === null 217 || sizeof($this->data[self::PERSISTENT_METADATA]) === 0 // move 218 ) { 219 $this->data = p_read_metadata($this->getResource()->getDokuwikiId()); 220 } 221 return parent::getData(); 222 } 223 224 225 /** 226 * @param $name 227 * @return mixed|null 228 */ 229 private function getPersistentMetadata($name) 230 { 231 $value = $this->getData()[self::PERSISTENT_METADATA][$name]; 232 /** 233 * Empty string return null 234 * because Dokuwiki does not allow to delete keys 235 * {@link p_set_metadata()} 236 */ 237 if ($value === "") { 238 return null; 239 } 240 return $value; 241 242 } 243 244 245 /** 246 * @param $dokuWikiId 247 * @param $name 248 * @return mixed|null 249 */ 250 public 251 function getCurrentFromName($name) 252 { 253 $value = $this->getData()[self::CURRENT_METADATA][$name]; 254 /** 255 * Empty string return null 256 * because Dokuwiki does not allow to delete keys 257 * {@link p_set_metadata()} 258 */ 259 if ($value === "") { 260 return null; 261 } 262 return $value; 263 } 264 265 /** 266 * @return MetadataDokuWikiStore 267 */ 268 public function renderAndPersist(): MetadataDokuWikiStore 269 { 270 /** 271 * Read/render the metadata from the file 272 * with parsing 273 */ 274 $dokuwikiId = $this->getResource()->getDokuwikiId(); 275 $actualMeta = $this->getData(); 276 global $ID; 277 $keep = $ID; 278 try { 279 $ID = $dokuwikiId; 280 $newMetadata = p_render_metadata($dokuwikiId, $actualMeta); 281 p_save_metadata($dokuwikiId, $newMetadata); 282 $this->data = $newMetadata; 283 } finally { 284 $ID = $keep; 285 } 286 return $this; 287 } 288 289 290 /** 291 * Change a meta on file 292 * and triggers the {@link self::PAGE_METADATA_MUTATION_EVENT} event 293 * 294 * @param $wikiId 295 * @param $key 296 * @param $value 297 * @param null $default - use in case of boolean 298 */ 299 private function setFromWikiId($wikiId, $key, $value, $default = null) 300 { 301 302 $oldValue = $this->getFromWikiId($wikiId, $key); 303 if (is_bool($value)) { 304 if ($oldValue === null) { 305 $oldValue = $default; 306 } else { 307 $oldValue = Boolean::toBoolean($oldValue); 308 } 309 } 310 if ($oldValue !== $value) { 311 312 313 $this->data[self::PERSISTENT_METADATA][$key] = $value; 314 /** 315 * Metadata in Dokuwiki is fucked up. 316 * 317 * You can't remove a metadata, 318 * You need to know if this is a rendering or not 319 * 320 * See just how fucked {@link p_set_metadata()} is 321 * 322 * Also don't change the type of the value to a string 323 * otherwise dokuwiki will not see a change 324 * between true and a string and will not persist the value 325 * 326 * A metadata is also not immediately flushed on disk 327 * in a test when rendering 328 * They are going into the global $METADATA_RENDERERS 329 * 330 * A current metadata is never stored if not set in the rendering process 331 * We persist therefore always 332 */ 333 $persistent = true; 334 p_set_metadata($wikiId, 335 [ 336 $key => $value 337 ], 338 false, 339 $persistent 340 ); 341 /** 342 * Event 343 */ 344 $data = [ 345 "name" => $key, 346 self::NEW_VALUE_ATTRIBUTE => $value, 347 "old_value" => $oldValue, 348 PagePath::getPersistentName() => ":$wikiId" 349 ]; 350 Event::createAndTrigger(self::PAGE_METADATA_MUTATION_EVENT, $data); 351 } 352 353 } 354 355 356 public function isHierarchicalTextBased(): bool 357 { 358 return true; 359 } 360 361 /** 362 * 363 * @return Path - the full path to the meta file 364 */ 365 public 366 function getMetaFilePath(): ?Path 367 { 368 $resource = $this->getResource(); 369 if (!($resource instanceof Page)) { 370 LogUtility::msg("The resource type ({$resource->getType()}) meta file is unknown and can't be retrieved."); 371 return null; 372 } 373 $dokuwikiId = $resource->getPath()->getDokuWikiId(); 374 return LocalPath::create(metaFN($dokuwikiId, '.meta')); 375 } 376 377 public function __toString() 378 { 379 return "DokuMeta"; 380 } 381 382 383 private function getFromWikiId($dokuwikiId, string $name, $default = null) 384 { 385 /** 386 * Note that {@link p_get_metadata()} can trigger a rendering of the meta again 387 * and it has a fucking cache 388 * 389 * Due to the cache in {@link p_get_metadata()} we can't use {@link p_read_metadata} 390 * when testing a {@link \action_plugin_combo_imgmove move} otherwise 391 * the move meta is not seen and the tests are failing. 392 * 393 * $METADATA_RENDERERS: A global cache variable where the persistent data is set 394 * with {@link p_set_metadata()} and that you can't retrieve with {@link p_get_metadata()} 395 * 396 * This variable is unset at the end function of {@link p_render_metadata()} 397 */ 398 global $METADATA_RENDERERS; 399 $value = $METADATA_RENDERERS[$dokuwikiId][MetadataDokuWikiStore::PERSISTENT_METADATA][$name]; 400 if ($value !== null) { 401 return $value; 402 } 403 404 /** 405 * {@link p_get_metadata} flat out the metadata array and we loose the 406 * persistent and current information 407 * Because there may be already a metadata in current for instance title 408 * It will be returned, but we want only the persistent 409 */ 410 $value = $this->getPersistentMetadata($name); 411 412 /** 413 * Empty string return the default (null) 414 * because Dokuwiki does not allow to delete keys 415 * {@link p_set_metadata()} 416 */ 417 if ($value !== null && $value !== "") { 418 return $value; 419 } 420 return $default; 421 } 422 423 424 /** 425 * @param $dokuwikiId 426 * @return null|array 427 */ 428 private function getFlatMetadatas($dokuwikiId): ?array 429 { 430 return p_get_metadata($dokuwikiId, '', METADATA_DONT_RENDER); 431 } 432 433 public function deleteAndFlush() 434 { 435 $emptyMeta = [MetadataDokuWikiStore::CURRENT_METADATA => [], MetadataDokuWikiStore::PERSISTENT_METADATA => []]; 436 $dokuwikiId = $this->getResource()->getDokuwikiId(); 437 p_save_metadata($dokuwikiId, $emptyMeta); 438 $this->data = $emptyMeta; 439 440 } 441 442 443} 444