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 $_POST = Json::createFromString($jsonString)->toArray(); 155 } 156 157 if ($call === self::META_MANAGER_CALL_ID) { 158 $this->handleManagerPost($event, $page, $_POST); 159 } else { 160 $this->handleViewerPost($event, $page, $_POST); 161 } 162 163 return; 164 case "GET": 165 166 if ($call === self::META_MANAGER_CALL_ID) { 167 $this->handleManagerGet($event, $page); 168 } else { 169 $this->handleViewerGet($event, $page); 170 } 171 return; 172 173 } 174 175 } 176 177 public function handle_rail_bar(Doku_Event $event, $param) 178 { 179 180 if (!Identity::isWriter()) { 181 return; 182 } 183 184 /** 185 * The `view` property defines the menu that is currently built 186 * https://www.dokuwiki.org/devel:menus 187 * If this is not the page menu, return 188 */ 189 if ($event->data['view'] != 'page') return; 190 191 global $INFO; 192 if (!$INFO['exists']) { 193 return; 194 } 195 array_splice($event->data['items'], -1, 0, array(new MetaManagerMenuItem())); 196 197 } 198 199 /** 200 * @param $event 201 * @param Page $page 202 * @param array $post 203 */ 204 private function handleManagerPost($event, Page $page, array $post) 205 { 206 207 $formStore = MetadataFormDataStore::getOrCreateFromResource($page, $post); 208 $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($page); 209 210 /** 211 * Boolean form field (default values) 212 * are not send back by the HTML form 213 */ 214 $defaultBooleanMetadata = [ 215 LowQualityPageOverwrite::PROPERTY_NAME, 216 QualityDynamicMonitoringOverwrite::PROPERTY_NAME 217 ]; 218 $defaultBoolean = []; 219 foreach ($defaultBooleanMetadata as $booleanMeta) { 220 $metadata = Metadata::getForName($booleanMeta) 221 ->setResource($page) 222 ->setReadStore($formStore) 223 ->setWriteStore($targetStore); 224 $defaultBoolean[$metadata::getName()] = $metadata->toStoreDefaultValue(); 225 } 226 $post = array_merge($defaultBoolean, $post); 227 228 /** 229 * Processing 230 */ 231 $transfer = MetadataStoreTransfer::createForPage($page) 232 ->fromStore($formStore) 233 ->toStore($targetStore) 234 ->process($post); 235 $processingMessages = $transfer->getMessages(); 236 237 238 $responseMessages = []; 239 $responseStatus = HttpResponse::STATUS_ALL_GOOD; 240 foreach ($processingMessages as $upsertMessages) { 241 $responseMessage = [ucfirst($upsertMessages->getType())]; 242 $documentationHyperlink = $upsertMessages->getDocumentationHyperLink(); 243 if ($documentationHyperlink !== null) { 244 $responseMessage[] = $documentationHyperlink; 245 } 246 $responseMessage[] = $upsertMessages->getContent(Mime::PLAIN_TEXT); 247 $responseMessages[] = implode(" - ", $responseMessage); 248 if ($upsertMessages->getType() === Message::TYPE_ERROR && $responseStatus !== HttpResponse::STATUS_BAD_REQUEST) { 249 $responseStatus = HttpResponse::STATUS_BAD_REQUEST; 250 } 251 } 252 253 if (sizeof($responseMessages) === 0) { 254 $responseMessages[] = self::SUCCESS_MESSAGE; 255 } 256 257 try { 258 $frontMatterMessage = MetadataFrontmatterStore::createFromPage($page) 259 ->sync(); 260 $responseMessages[] = $frontMatterMessage->getPlainTextContent(); 261 } catch (ExceptionCombo $e) { 262 $responseMessages[] = $e->getMessage(); 263 } 264 265 266 /** 267 * Response 268 */ 269 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 270 ->setEvent($event) 271 ->sendMessage($responseMessages); 272 273 274 } 275 276 /** 277 * @param Doku_Event $event 278 * @param Page $page 279 * @throws ExceptionCombo 280 */ 281 private 282 function handleManagerGet(Doku_Event $event, Page $page) 283 { 284 $formMeta = MetaManagerForm::createForPage($page)->toFormMeta(); 285 $payload = json_encode($formMeta->toAssociativeArray()); 286 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 287 ->setEvent($event) 288 ->send($payload, Mime::JSON); 289 } 290 291 /** 292 * @param Doku_Event $event 293 * @param Page $page 294 */ 295 private function handleViewerGet(Doku_Event $event, Page $page) 296 { 297 if (!Identity::isManager()) { 298 HttpResponse::create(HttpResponse::STATUS_NOT_AUTHORIZED) 299 ->setEvent($event) 300 ->setCanonical(self::CANONICAL) 301 ->sendMessage("Not Authorized (managers only)"); 302 return; 303 } 304 $metadata = MetadataDokuWikiStore::getOrCreateFromResource($page)->getData(); 305 $persistent = $metadata[MetadataDokuWikiStore::PERSISTENT_METADATA]; 306 ksort($persistent); 307 $current = $metadata[MetadataDokuWikiStore::CURRENT_METADATA]; 308 ksort($current); 309 $form = FormMeta::create("raw_metadata") 310 ->addField( 311 FormMetaField::create(MetadataDokuWikiStore::PERSISTENT_METADATA) 312 ->setLabel("Persistent Metadata (User Metadata)") 313 ->setTab("persistent") 314 ->setDescription("The persistent metadata contains raw values. They contains the values set by the user and the fixed values such as page id.") 315 ->addValue(json_encode($persistent)) 316 ->setType(DataType::JSON_TYPE_VALUE) 317 ) 318 ->addField(FormMetaField::create(MetadataDokuWikiStore::CURRENT_METADATA) 319 ->setLabel("Current (Derived) Metadata") 320 ->setTab("current") 321 ->setDescription("The current metadata are the derived / calculated / runtime metadata values (extended with the persistent metadata).") 322 ->addValue(json_encode($current)) 323 ->setType(DataType::JSON_TYPE_VALUE) 324 ->setMutable(false) 325 ) 326 ->toAssociativeArray(); 327 328 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 329 ->setEvent($event) 330 ->setCanonical(self::CANONICAL) 331 ->send(json_encode($form), Mime::JSON); 332 333 } 334 335 private function handleViewerPost(Doku_Event $event, Page $page, array $post) 336 { 337 338 $metadataStore = MetadataDokuWikiStore::getOrCreateFromResource($page); 339 $metaData = $metadataStore->getData(); 340 341 /** 342 * @var Message[] 343 */ 344 $messages = []; 345 /** 346 * Only Persistent, current cannot be modified 347 */ 348 $persistentMetadataType = MetadataDokuWikiStore::PERSISTENT_METADATA; 349 $postMeta = json_decode($post[$persistentMetadataType], true); 350 if ($postMeta === null) { 351 HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST) 352 ->setEvent($event) 353 ->sendMessage("The metadata $persistentMetadataType should be in json format"); 354 return; 355 } 356 $persistentPageMeta = &$metaData[$persistentMetadataType]; 357 358 359 $managedMetaMessageSuffix = "is a managed metadata, you need to use the metadata manager to delete it"; 360 361 /** 362 * Process the actual attribute 363 */ 364 foreach ($persistentPageMeta as $key => $value) { 365 $postMetaValue = null; 366 if (isset($postMeta[$key])) { 367 $postMetaValue = $postMeta[$key]; 368 unset($postMeta[$key]); 369 } 370 371 if ($postMetaValue === null) { 372 if (in_array($key, Metadata::MUTABLE_METADATA)) { 373 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix"); 374 continue; 375 } 376 if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) { 377 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't delete it"); 378 continue; 379 } 380 unset($persistentPageMeta[$key]); 381 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) with the value ($value) was deleted"); 382 } else { 383 if ($value !== $postMetaValue) { 384 if (in_array($key, Metadata::MUTABLE_METADATA)) { 385 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix"); 386 continue; 387 } 388 if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) { 389 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it"); 390 continue; 391 } 392 $persistentPageMeta[$key] = $postMetaValue; 393 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was updated to the value ($postMetaValue) - Old value ($value)"); 394 } 395 } 396 } 397 /** 398 * Process the new attribute 399 */ 400 foreach ($postMeta as $key => $value) { 401 if (in_array($key, Metadata::MUTABLE_METADATA)) { 402 // This meta should be modified via the form 403 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) can only be added via the meta manager"); 404 continue; 405 } 406 if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) { 407 // this meta are not modifiable 408 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it"); 409 continue; 410 } 411 $persistentPageMeta[$key] = $value; 412 $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was created with the value ($value)"); 413 } 414 415 /** 416 * Delete the runtime if present 417 * (They were saved in persistent) 418 */ 419 Metadata::deleteIfPresent($persistentPageMeta, Metadata::RUNTIME_META); 420 421 p_save_metadata($page->getDokuwikiId(), $metaData); 422 $metadataStore->setData($metaData); 423 424 if (sizeof($messages) !== 0) { 425 $messagesToSend = []; 426 foreach ($messages as $message) { 427 $messagesToSend[] = $message->getPlainTextContent(); 428 } 429 } else { 430 $messagesToSend = "No metadata has been changed."; 431 } 432 HttpResponse::create(HttpResponse::STATUS_ALL_GOOD) 433 ->setEvent($event) 434 ->sendMessage($messagesToSend); 435 436 } 437 438 439} 440