1<?php 2 3 4namespace ComboStrap\Meta\Store; 5 6use ComboStrap\DataType; 7use ComboStrap\ExceptionBadState; 8use ComboStrap\ExceptionCast; 9use ComboStrap\ExceptionNotExists; 10use ComboStrap\ExceptionNotFound; 11use ComboStrap\ExceptionRuntime; 12use ComboStrap\ExceptionRuntimeInternal; 13use ComboStrap\ExecutionContext; 14use ComboStrap\FetcherMarkup; 15use ComboStrap\LocalPath; 16use ComboStrap\LogUtility; 17use ComboStrap\MarkupPath; 18use ComboStrap\Meta\Api\Metadata; 19use ComboStrap\Meta\Api\MetadataStore; 20use ComboStrap\Meta\Api\MetadataStoreAbs; 21use ComboStrap\Path; 22use ComboStrap\ResourceCombo; 23use ComboStrap\WikiPath; 24 25/** 26 * Class MetadataFileSystemStore 27 * @package ComboStrap 28 * 29 * A wrapper around the dokuwiki meta file system store. 30 * 31 */ 32class MetadataDokuWikiStore extends MetadataStoreAbs 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 47 const CANONICAL = Metadata::CANONICAL; 48 /** 49 * Persistent metadata (data that should be in a backup) 50 * 51 * They are used as the default of the current metadata 52 * and is never cleaned 53 * 54 * https://www.dokuwiki.org/devel:metadata#metadata_persistence 55 * 56 * Because the current is only usable in rendering, all 57 * metadata are persistent inside dokuwiki 58 */ 59 public const PERSISTENT_DOKUWIKI_KEY = "persistent"; 60 61 62 /** 63 * @return MetadataDokuWikiStore 64 * We don't use a global static variable 65 * because we are working with php as cgi script 66 * and there is no notion of request 67 * to be able to flush the data on the disk 68 * 69 * The scope of the data will be then the store 70 */ 71 public static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore 72 { 73 74 $context = ExecutionContext::getActualOrCreateFromEnv(); 75 76 try { 77 $executionCachedStores = &$context->getRuntimeObject(MetadataDokuWikiStore::class); 78 } catch (ExceptionNotFound $e) { 79 $executionCachedStores = []; 80 $context->setRuntimeObject(MetadataDokuWikiStore::class, $executionCachedStores); 81 } 82 $path = $resourceCombo->getPathObject()->toAbsoluteId(); 83 if (isset($executionCachedStores[$path])) { 84 return $executionCachedStores[$path]; 85 } 86 87 $metadataStore = new MetadataDokuWikiStore($resourceCombo); 88 $executionCachedStores[$path] = $metadataStore; 89 return $metadataStore; 90 91 } 92 93 /** 94 * 95 * In a rendering, you should not use the {@link p_set_metadata()} 96 * but use {@link \Doku_Renderer_metadata::meta} and {@link \Doku_Renderer_metadata::$persistent} 97 * to set the metadata 98 * 99 * Why ? 100 * The metadata are set in $METADATA_RENDERERS (A global cache variable where the persistent data is set/exist 101 * only during metadata rendering with the function {@link p_render_metadata()}) and then 102 * saved at the end 103 * 104 */ 105 private static function isRendering(string $wikiId): bool 106 { 107 global $METADATA_RENDERERS; 108 if (isset($METADATA_RENDERERS[$wikiId])) { 109 return true; 110 } 111 return false; 112 } 113 114 115 /** 116 * Delete the globals 117 */ 118 public static function unsetGlobalVariables() 119 { 120 /** 121 * {@link p_read_metadata() global cache} 122 */ 123 unset($GLOBALS['cache_metadata']); 124 125 /** 126 * {@link p_render_metadata()} temporary render cache 127 * global $METADATA_RENDERERS; 128 */ 129 unset($GLOBALS['METADATA_RENDERERS']); 130 131 } 132 133 134 /** 135 * @throws ExceptionBadState - if for any reason, it's not possible to store the data 136 */ 137 public function set(Metadata $metadata) 138 { 139 140 $name = $metadata->getName(); 141 $persistentValue = $metadata->toStoreValue(); 142 $defaultValue = $metadata->toStoreDefaultValue(); 143 $resource = $metadata->getResource(); 144 $this->checkResource($resource); 145 if ($resource === null) { 146 throw new ExceptionBadState("A resource is mandatory", self::CANONICAL); 147 } 148 if (!($resource instanceof MarkupPath)) { 149 throw new ExceptionBadState("The DokuWiki metadata store is only for page resource", self::CANONICAL); 150 } 151 $this->setFromPersistentName($name, $persistentValue, $defaultValue); 152 } 153 154 /** 155 * @param Metadata $metadata 156 * @param null $default 157 * @return mixed|null 158 */ 159 public function get(Metadata $metadata, $default = null) 160 { 161 162 $resource = $metadata->getResource(); 163 $this->checkResource($resource); 164 if ($resource === null) { 165 throw new ExceptionRuntime("A resource is mandatory", self::CANONICAL); 166 } 167 if (!($resource instanceof MarkupPath)) { 168 throw new ExceptionRuntime("The DokuWiki metadata store is only for page resource", self::CANONICAL); 169 } 170 return $this->getFromName($metadata->getName(), $default); 171 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 getFromName(string $name, $default = null) 187 { 188 /** 189 * We don't use {@link p_get_metadata()} 190 * because it will trigger a {@link p_render_metadata()} 191 * But we may just want to check if there is a {@link PageId} 192 * before rendering 193 */ 194 $data = $this->getData(); 195 $value = $data[$name] ?? null; 196 197 /** 198 * Empty string return null 199 * because Dokuwiki does not allow to delete keys 200 * {@link p_set_metadata()} 201 */ 202 if ($value !== null && $value !== "") { 203 return $value; 204 } 205 return $default; 206 } 207 208 209 public function persist() 210 { 211 212 /** 213 * Done on set via the dokuwiki function 214 */ 215 216 } 217 218 /** 219 * @param string $name 220 * @param string|array $value 221 * @param null $default 222 * @return MetadataDokuWikiStore 223 */ 224 public function setFromPersistentName(string $name, $value, $default = null): MetadataDokuWikiStore 225 { 226 $oldValue = $this->getFromName($name); 227 if (is_bool($value)) { 228 if ($oldValue === null) { 229 $oldValue = $default; 230 } else { 231 $oldValue = DataType::toBoolean($oldValue); 232 } 233 } 234 if ($oldValue !== $value) { 235 236 /** 237 * Metadata in Dokuwiki is fucked up. 238 * 239 * You can't remove a metadata, 240 * You need to know if this is a rendering or not 241 * 242 * See just how fucked {@link p_set_metadata()} is 243 * 244 * Also don't change the type of the value to a string 245 * otherwise dokuwiki will not see a change 246 * between true and a string and will not persist the value 247 * 248 * By default, the value is copied in the current and persistent array 249 * and there is no render 250 */ 251 $wikiId = $this->getWikiId(); 252 if (self::isRendering($wikiId)) { 253 254 /** 255 * It seems that {@link p_set_metadata()} uses it also 256 * but we show it here 257 */ 258 global $METADATA_RENDERERS; 259 $METADATA_RENDERERS[$wikiId][self::CURRENT_METADATA][$name] = $value; 260 $METADATA_RENDERERS[$wikiId][self::PERSISTENT_DOKUWIKI_KEY][$name] = $value; 261 262 } else { 263 264 p_set_metadata($wikiId, 265 [ 266 $name => $value 267 ] 268 ); 269 270 } 271 $this->setGlobalCacheIfAny($name, $value); 272 } 273 return $this; 274 } 275 276 public function getData(): array 277 { 278 /** 279 * We return only the current data. 280 * 281 * Why ? 282 * To be consistent with {@link p_get_metadata()} that retrieves only from the `current` array 283 * Therefore the `persistent` array values should always be duplicated in the `current` array 284 * 285 * (the only diff is that the persistent value are still available during a {@link p_render_metadata() metadata render}) 286 * 287 * Note that Dokuwiki load them also for the requested path 288 * at `global $INFO, $info['meta']` with {@link pageinfo()} 289 * and is synced in {@link p_save_metadata()} 290 * 291 */ 292 return $this->getDataCurrentAndPersistent()[self::CURRENT_METADATA]; 293 } 294 295 private function getWikiId(): string 296 { 297 try { 298 return $this->getResource()->getPathObject()->toWikiPath()->getWikiId(); 299 } catch (ExceptionCast $e) { 300 throw new ExceptionRuntimeInternal("Should not happen", $e); 301 } 302 } 303 304 305 /** 306 * 307 * @param $name 308 * @return mixed|null 309 * @deprecated - the data should always be replicated in current use {@link self::getFromName()} 310 */ 311 public 312 function getCurrentFromName($name) 313 { 314 return $this->getFromName($name); 315 } 316 317 /** 318 * @return MetadataDokuWikiStore 319 * @deprecated should use a fetcher markup ? 320 */ 321 public function renderAndPersist(): MetadataDokuWikiStore 322 { 323 /** 324 * Read/render the metadata from the file 325 * with parsing 326 */ 327 $wikiPage = $this->getResource(); 328 if (!$wikiPage instanceof WikiPath) { 329 LogUtility::errorIfDevOrTest("The resource is not a wiki path"); 330 return $this; 331 } 332 try { 333 FetcherMarkup::confRoot() 334 ->setRequestedExecutingPath($wikiPage) 335 ->setRequestedContextPath($wikiPage) 336 ->setRequestedMimeToMetadata() 337 ->build() 338 ->processMetadataIfNotYetDone(); 339 } catch (ExceptionNotExists $e) { 340 LogUtility::error("Metadata Build Error", self::CANONICAL, $e); 341 } 342 343 return $this; 344 } 345 346 347 public function isHierarchicalTextBased(): bool 348 { 349 return true; 350 } 351 352 /** 353 * @return Path - the full path to the meta file 354 */ 355 public 356 function getMetaFilePath(): ?Path 357 { 358 $dokuwikiId = $this->getWikiId(); 359 return LocalPath::createFromPathString(metaFN($dokuwikiId, '.meta')); 360 } 361 362 public function __toString() 363 { 364 return "DokuMeta ({$this->getWikiId()}"; 365 } 366 367 368 public function deleteAndFlush() 369 { 370 $emptyMeta = [MetadataDokuWikiStore::CURRENT_METADATA => [], self::PERSISTENT_DOKUWIKI_KEY => []]; 371 $dokuwikiId = $this->getWikiId(); 372 p_save_metadata($dokuwikiId, $emptyMeta); 373 } 374 375 376 public function reset() 377 { 378 self::unsetGlobalVariables(); 379 } 380 381 /** 382 * In {@link p_read_metadata()}, there is a global cache 383 * @param string $name 384 * @param mixed $value 385 */ 386 private function setGlobalCacheIfAny(string $name, $value) 387 { 388 global $cache_metadata; 389 390 $id = $this->getWikiId(); 391 if (isset($cache_metadata[$id])) { 392 $cache_metadata[$id]['persistent'][$name] = $value; 393 $cache_metadata[$id]['current'][$name] = $value; 394 } 395 396 } 397 398 /** 399 * @return array -the full array only needed by the rendering process 400 * You should use {@link self::getData()} otherwise 401 */ 402 public function getDataCurrentAndPersistent(): array 403 { 404 405 $id = $this->getWikiId(); 406 $data = p_read_metadata($id, true); 407 if (empty($data)) { 408 LogUtility::internalError("The metadata cache was empty"); 409 $data = p_read_metadata($id); 410 } 411 return $data; 412 } 413} 414