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) && $oldValue !== null) { 228 $oldValue = DataType::toBoolean($oldValue); 229 } 230 if ($oldValue !== $value) { 231 232 /** 233 * Metadata in Dokuwiki is fucked up. 234 * 235 * You can't remove a metadata, 236 * You need to know if this is a rendering or not 237 * 238 * See just how fucked {@link p_set_metadata()} is 239 * 240 * Also don't change the type of the value to a string 241 * otherwise dokuwiki will not see a change 242 * between true and a string and will not persist the value 243 * 244 * By default, the value is copied in the current and persistent array 245 * and there is no render 246 */ 247 $wikiId = $this->getWikiId(); 248 if (self::isRendering($wikiId)) { 249 250 /** 251 * It seems that {@link p_set_metadata()} uses it also 252 * but we show it here 253 */ 254 global $METADATA_RENDERERS; 255 $METADATA_RENDERERS[$wikiId][self::CURRENT_METADATA][$name] = $value; 256 $METADATA_RENDERERS[$wikiId][self::PERSISTENT_DOKUWIKI_KEY][$name] = $value; 257 258 } else { 259 260 p_set_metadata($wikiId, 261 [ 262 $name => $value 263 ] 264 ); 265 266 } 267 $this->setGlobalCacheIfAny($name, $value); 268 } 269 return $this; 270 } 271 272 public function getData(): array 273 { 274 /** 275 * We return only the current data. 276 * 277 * Why ? 278 * To be consistent with {@link p_get_metadata()} that retrieves only from the `current` array 279 * Therefore the `persistent` array values should always be duplicated in the `current` array 280 * 281 * (the only diff is that the persistent value are still available during a {@link p_render_metadata() metadata render}) 282 * 283 * Note that Dokuwiki load them also for the requested path 284 * at `global $INFO, $info['meta']` with {@link pageinfo()} 285 * and is synced in {@link p_save_metadata()} 286 * 287 */ 288 return $this->getDataCurrentAndPersistent()[self::CURRENT_METADATA]; 289 290 } 291 292 private function getWikiId(): string 293 { 294 try { 295 return $this->getResource()->getPathObject()->toWikiPath()->getWikiId(); 296 } catch (ExceptionCast $e) { 297 throw new ExceptionRuntimeInternal("Should not happen", $e); 298 } 299 } 300 301 302 /** 303 * 304 * @param $name 305 * @return mixed|null 306 * @deprecated - the data should always be replicated in current use {@link self::getFromName()} 307 */ 308 public 309 function getCurrentFromName($name) 310 { 311 return $this->getFromName($name); 312 } 313 314 /** 315 * @return MetadataDokuWikiStore 316 * @deprecated should use a fetcher markup ? 317 */ 318 public function renderAndPersist(): MetadataDokuWikiStore 319 { 320 /** 321 * Read/render the metadata from the file 322 * with parsing 323 */ 324 $wikiPage = $this->getResource(); 325 if (!$wikiPage instanceof WikiPath) { 326 LogUtility::errorIfDevOrTest("The resource is not a wiki path"); 327 return $this; 328 } 329 try { 330 FetcherMarkup::confRoot() 331 ->setRequestedExecutingPath($wikiPage) 332 ->setRequestedContextPath($wikiPage) 333 ->setRequestedMimeToMetadata() 334 ->build() 335 ->processMetadataIfNotYetDone(); 336 } catch (ExceptionNotExists $e) { 337 LogUtility::error("Metadata Build Error", self::CANONICAL, $e); 338 } 339 340 return $this; 341 } 342 343 344 public function isHierarchicalTextBased(): bool 345 { 346 return true; 347 } 348 349 /** 350 * @return Path - the full path to the meta file 351 */ 352 public 353 function getMetaFilePath(): ?Path 354 { 355 $dokuwikiId = $this->getWikiId(); 356 return LocalPath::createFromPathString(metaFN($dokuwikiId, '.meta')); 357 } 358 359 public function __toString() 360 { 361 return "DokuMeta ({$this->getWikiId()}"; 362 } 363 364 365 public function deleteAndFlush() 366 { 367 $emptyMeta = [MetadataDokuWikiStore::CURRENT_METADATA => [], self::PERSISTENT_DOKUWIKI_KEY => []]; 368 $dokuwikiId = $this->getWikiId(); 369 p_save_metadata($dokuwikiId, $emptyMeta); 370 } 371 372 373 public function reset() 374 { 375 self::unsetGlobalVariables(); 376 } 377 378 /** 379 * In {@link p_read_metadata()}, there is a global cache 380 * @param string $name 381 * @param mixed $value 382 */ 383 private function setGlobalCacheIfAny(string $name, $value) 384 { 385 global $cache_metadata; 386 387 $id = $this->getWikiId(); 388 if (isset($cache_metadata[$id])) { 389 $cache_metadata[$id]['persistent'][$name] = $value; 390 $cache_metadata[$id]['current'][$name] = $value; 391 } 392 393 } 394 395 /** 396 * @return array -the full array only needed by the rendering process 397 * You should use {@link self::getData()} otherwise 398 */ 399 public function getDataCurrentAndPersistent(): array 400 { 401 402 $id = $this->getWikiId(); 403 $data = p_read_metadata($id, true); 404 if (empty($data)) { 405 LogUtility::internalError("The metadata cache was empty"); 406 $data = p_read_metadata($id); 407 } 408 return $data; 409 } 410} 411