1<?php 2 3require_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 4 5use ComboStrap\DataType; 6use ComboStrap\ExceptionCombo; 7use ComboStrap\FormMeta; 8use ComboStrap\FormMetaField; 9use ComboStrap\HttpResponse; 10use ComboStrap\Identity; 11use ComboStrap\Json; 12use ComboStrap\LowQualityPageOverwrite; 13use ComboStrap\Message; 14use ComboStrap\Metadata; 15use ComboStrap\MetadataDokuWikiStore; 16use ComboStrap\MetadataFormDataStore; 17use ComboStrap\MetadataFrontmatterStore; 18use ComboStrap\MetadataStoreTransfer; 19use ComboStrap\MetaManagerForm; 20use ComboStrap\MetaManagerMenuItem; 21use ComboStrap\Mime; 22use ComboStrap\Page; 23use ComboStrap\PluginUtility; 24use ComboStrap\QualityDynamicMonitoringOverwrite; 25 26if (!defined('DOKU_INC')) die(); 27 28/** 29 * 30 * Save metadata that were send by ajax 31 */ 32class action_plugin_combo_metamanager extends DokuWiki_Action_Plugin 33{ 34 35 36 const META_MANAGER_CALL_ID = "combo-meta-manager"; 37 const META_VIEWER_CALL_ID = "combo-meta-viewer"; 38 39 const CANONICAL = "meta-manager"; 40 41 42 43 /** 44 * The canonical for the metadata page 45 */ 46 const METADATA_CANONICAL = "metadata"; 47 48 const SUCCESS_MESSAGE = "The data were updated without errors."; 49 50 51 public function register(Doku_Event_Handler $controller) 52 { 53 54 /** 55 * The ajax api to return data 56 */ 57 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_ajax_call'); 58 59 /** 60 * Add a icon in the page tools menu 61 * https://www.dokuwiki.org/devel:event:menu_items_assembly 62 */ 63 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'handle_rail_bar'); 64 } 65 66 /** 67 * Handle Metadata HTTP ajax requests 68 * @param $event Doku_Event 69 * 70 * 71 * https://www.dokuwiki.org/devel:plugin_programming_tips#handle_json_ajax_request 72 * 73 * CSRF checks are only for logged in users 74 * This is public ({@link getSecurityToken()} 75 */ 76 function _ajax_call(Doku_Event &$event): void 77 { 78 79 $call = $event->data; 80 if (!in_array($call, [self::META_MANAGER_CALL_ID, self::META_VIEWER_CALL_ID])) { 81 return; 82 } 83 //no other ajax call handlers needed 84 $event->stopPropagation(); 85 $event->preventDefault(); 86 87 /** 88 * Shared check between post and get HTTP method 89 */ 90 $id = $_GET["id"]; 91 if ($id === null) { 92 /** 93 * With {@link TestRequest} 94 * for instance 95 */ 96 $id = $_REQUEST["id"]; 97 } 98 99 if (empty($id)) { 100 HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST) 101 ->setEvent($event) 102 ->setCanonical(self::CANONICAL) 103 ->sendMessage("The page path (id form) is empty"); 104 return; 105 } 106 $page = Page::createPageFromId($id); 107 if (!$page->exists()) { 108 HttpResponse::create(HttpResponse::STATUS_DOES_NOT_EXIST) 109 ->setEvent($event) 110 ->setCanonical(self::CANONICAL) 111 ->sendMessage("The page ($id) does not exist"); 112 return; 113 } 114 115 /** 116 * Security 117 */ 118 if (!$page->canBeUpdatedByCurrentUser()) { 119 $user = Identity::getUser(); 120 HttpResponse::create(HttpResponse::STATUS_NOT_AUTHORIZED) 121 ->setEvent($event) 122 ->setCanonical(self::CANONICAL) 123 ->sendMessage("Not Authorized: The user ($user) has not the `write` permission for the page (:$id)."); 124 return; 125 } 126 127 /** 128 * Functional code 129 */ 130 131 $requestMethod = $_SERVER['REQUEST_METHOD']; 132 switch ($requestMethod) { 133 case 'POST': 134 135 if ($_SERVER["CONTENT_TYPE"] !== "application/json") { 136 /** 137 * We can't set the mime content in a {@link TestRequest} 138 */ 139 if (!PluginUtility::isTest()) { 140 HttpResponse::create(HttpResponse::STATUS_UNSUPPORTED_MEDIA_TYPE) 141 ->setEvent($event) 142 ->setCanonical(self::CANONICAL) 143 ->sendMessage("The post content should be in json format"); 144 return; 145 } 146 } 147 148 /** 149 * We can't simulate a php://input in a {@link TestRequest} 150 * We set therefore the post 151 */ 152 if (!PluginUtility::isTest()) { 153 $jsonString = file_get_contents('php://input'); 154 try { 155 $_POST = Json::createFromString($jsonString)->toArray(); 156 } catch (ExceptionCombo $e) { 157 HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST) 158 ->setEvent($event) 159 ->setCanonical(self::CANONICAL) 160 ->sendMessage("The json payload could not decoded. Error: {$e->getMessage()}"); 161 return; 162 } 163 } 164 165 if ($call === self::META_MANAGER_CALL_ID) { 166 $this->handleManagerPost($event, $page, $_POST); 167 } else { 168 $this->handleViewerPost($event, $page, $_POST); 169 } 170 171 return; 172 case "GET": 173 174 if ($call === self::META_MANAGER_CALL_ID) { 175 $this->handleManagerGet($event, $page); 176 } else { 177 $this->handleViewerGet($event, $page); 178 } 179 return; 180 181 } 182 183 } 184 185 public function handle_rail_bar(Doku_Event $event, $param) 186 { 187 188 if (!Identity::isWriter()) { 189 return; 190 } 191 192 /** 193 * The `view` property defines the menu that is currently built 194 * https://www.dokuwiki.org/devel:menus 195 * If this is not the page menu, return 196 */ 197 if ($event->data['view'] != 'page') return; 198 199 global $INFO; 200 if (!$INFO['exists']) { 201 return; 202 } 203 array_splice($event->data['items'], -1, 0, array(new MetaManagerMenuItem())); 204 205 } 206 207 /** 208 * @param $event 209 * @param Page $page 210 * @param array $post 211 */ 212 private function handleManagerPost($event, Page $page, array $post) 213 { 214 215 $formStore = MetadataFormDataStore::getOrCreateFromResource($page, $post); 216 $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($page); 217 218 /** 219 * Boolean form field (default values) 220 * are not send back by the HTML form 221 */ 222 $defaultBooleanMetadata = [ 223 LowQualityPageOverwrite::PROPERTY_NAME, 224 QualityDynamicMonitoringOverwrite::PROPERTY_NAME 225 ]; 226 $defaultBoolean = []; 227 foreach ($defaultBooleanMetadata as $booleanMeta) { 228 $metadata = Metadata::getForName($booleanMeta) 229 ->setResource($page) 230 ->setReadStore($formStore) 231 ->setWriteStore($targetStore); 232 $defaultBoolean[$metadata::getName()] = $metadata->toStoreDefaultValue(); 233 } 234 $post = array_merge($defaultBoolean, $post); 235 236 /** 237 * Processing 238 */ 239 $transfer = MetadataStoreTransfer::createForPage($page) 240 ->fromStore($formStore) 241 ->toStore($targetStore) 242 ->process($post); 243 $processingMessages = $transfer->getMessages(); 244 245 246 $responseMessages = []; 247 $responseStatus = HttpResponse::STATUS_ALL_GOOD; 248 foreach ($processingMessages as $upsertMessages) { 249 $responseMessage = [ucfirst($upsertMessages->getType())]; 250 $documentationHyperlink = $upsertMessages->getDocumentationHyperLink(); 251 if ($documentationHyperlink !== null) { 252 $responseMessage[] = $documentationHyperlink; 253 } 254 $responseMessage[] = $upsertMessages->getContent(Mime::PLAIN_TEXT); 255 $responseMessages[] = implode(" - ", $responseMessage); 256 if ($upsertMessages->getType() === Message::TYPE_ERROR && $responseStatus !== HttpResponse::STATUS_BAD_REQUEST) { 257 $responseStatus = HttpResponse::STATUS_BAD_REQUEST; 258 } 259 } 260 261 if (sizeof($responseMessages) === 0) { 262 $responseMessages[] = self::SUCCESS_MESSAGE; 263 } 264 265 try { 266 $frontMatterMessage = MetadataFrontmatterStore::createFromPage($page) 267 ->sync(); 268 $responseMessages[] = $frontMatterMessage->getPlainTextContent(); 269 } catch (ExceptionCombo $e) { 270 $responseMessages[] = $e->getMessage(); 271 } 272 273 274 /** 275 * Response 276 */ 277 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 278 ->setEvent($event) 279 ->sendMessage($responseMessages); 280 281 282 } 283 284 /** 285 * @param Doku_Event $event 286 * @param Page $page 287 */ 288 private 289 function handleManagerGet(Doku_Event $event, Page $page) 290 { 291 $formMeta = MetaManagerForm::createForPage($page)->toFormMeta(); 292 $payload = json_encode($formMeta->toAssociativeArray()); 293 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 294 ->setEvent($event) 295 ->send($payload, Mime::JSON); 296 } 297 298 /** 299 * @param Doku_Event $event 300 * @param Page $page 301 */ 302 private function handleViewerGet(Doku_Event $event, Page $page) 303 { 304 if (!Identity::isManager()) { 305 HttpResponse::create(HttpResponse::STATUS_NOT_AUTHORIZED) 306 ->setEvent($event) 307 ->setCanonical(self::CANONICAL) 308 ->sendMessage("Not Authorized (managers only)"); 309 return; 310 } 311 $metadata = MetadataDokuWikiStore::getOrCreateFromResource($page)->getData(); 312 $persistent = $metadata[MetadataDokuWikiStore::PERSISTENT_METADATA]; 313 ksort($persistent); 314 $current = $metadata[MetadataDokuWikiStore::CURRENT_METADATA]; 315 ksort($current); 316 $form = FormMeta::create("raw_metadata") 317 ->addField( 318 FormMetaField::create(MetadataDokuWikiStore::PERSISTENT_METADATA) 319 ->setLabel("Persistent Metadata (User Metadata)") 320 ->setTab("persistent") 321 ->setDescription("The persistent metadata contains raw values. They contains the values set by the user and the fixed values such as page id.") 322 ->addValue(json_encode($persistent)) 323 ->setType(DataType::JSON_TYPE_VALUE) 324 ) 325 ->addField(FormMetaField::create(MetadataDokuWikiStore::CURRENT_METADATA) 326 ->setLabel("Current (Derived) Metadata") 327 ->setTab("current") 328 ->setDescription("The current metadata are the derived / calculated / runtime metadata values (extended with the persistent metadata).") 329 ->addValue(json_encode($current)) 330 ->setType(DataType::JSON_TYPE_VALUE) 331 ->setMutable(false) 332 ) 333 ->toAssociativeArray(); 334 335 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 336 ->setEvent($event) 337 ->setCanonical(self::CANONICAL) 338 ->send(json_encode($form), Mime::JSON); 339 340 } 341 342 private function handleViewerPost(Doku_Event $event, Page $page, array $post) 343 { 344 345 $metadataStore = MetadataDokuWikiStore::getOrCreateFromResource($page); 346 $metaData = $metadataStore->getData(); 347 348 /** 349 * @var Message[] 350 */ 351 $messages = []; 352 /** 353 * Only Persistent, current cannot be modified 354 */ 355 $persistentMetadataType = MetadataDokuWikiStore::PERSISTENT_METADATA; 356 $postMeta = json_decode($post[$persistentMetadataType], true); 357 if ($postMeta === null) { 358 HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST) 359 ->setEvent($event) 360 ->sendMessage("The metadata $persistentMetadataType should be in json format"); 361 return; 362 } 363 $persistentPageMeta = &$metaData[$persistentMetadataType]; 364 365 366 $managedMetaMessageSuffix = "is a managed metadata, you need to use the metadata manager to delete it"; 367 368 /** 369 * Process the actual attribute 370 */ 371 foreach ($persistentPageMeta as $key => $value) { 372 $postMetaValue = null; 373 if (isset($postMeta[$key])) { 374 $postMetaValue = $postMeta[$key]; 375 unset($postMeta[$key]); 376 } 377 378 if ($postMetaValue === null) { 379 if (in_array($key, Metadata::MUTABLE_METADATA)) { 380 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix"); 381 continue; 382 } 383 if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) { 384 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't delete it"); 385 continue; 386 } 387 unset($persistentPageMeta[$key]); 388 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) with the value ($value) was deleted"); 389 } else { 390 if ($value !== $postMetaValue) { 391 if (in_array($key, Metadata::MUTABLE_METADATA)) { 392 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix"); 393 continue; 394 } 395 if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) { 396 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it"); 397 continue; 398 } 399 $persistentPageMeta[$key] = $postMetaValue; 400 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was updated to the value ($postMetaValue) - Old value ($value)"); 401 } 402 } 403 } 404 /** 405 * Process the new attribute 406 */ 407 foreach ($postMeta as $key => $value) { 408 if (in_array($key, Metadata::MUTABLE_METADATA)) { 409 // This meta should be modified via the form 410 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) can only be added via the meta manager"); 411 continue; 412 } 413 if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) { 414 // this meta are not modifiable 415 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it"); 416 continue; 417 } 418 $persistentPageMeta[$key] = $value; 419 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was created with the value ($value)"); 420 } 421 422 /** 423 * Delete the runtime if present 424 * (They were saved in persistent) 425 */ 426 Metadata::deleteIfPresent($persistentPageMeta, Metadata::RUNTIME_META); 427 428 p_save_metadata($page->getDokuwikiId(), $metaData); 429 $metadataStore->setData($metaData); 430 431 if (sizeof($messages) !== 0) { 432 $messagesToSend = []; 433 foreach ($messages as $message) { 434 $messagesToSend[] = $message->getPlainTextContent(); 435 } 436 } else { 437 $messagesToSend = "No metadata has been changed."; 438 } 439 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 440 ->setEvent($event) 441 ->sendMessage($messagesToSend); 442 443 } 444 445 446} 447