1<?php 2 3 4namespace ComboStrap; 5 6use ComboStrap\TagAttribute\StyleAttribute; 7 8/** 9 * Class SectionEdit 10 * @package ComboStrap 11 * Manage the edit button 12 * (ie add HTML comment that are parsed into forms 13 * for editor user) 14 */ 15class EditButton 16{ 17 18 19 const SEC_EDIT_PATTERN = "/" . self::ENTER_HTML_COMMENT . "\s*" . self::EDIT_BUTTON_PREFIX . "({.*?})\s*" . self::CLOSE_HTML_COMMENT . "/"; 20 const EDIT_BUTTON_PREFIX = "EDIT"; 21 const WIKI_ID = "wiki-id"; 22 23 const FORM_ID = "hid"; // id to be dokuwiki conform 24 const EDIT_MESSAGE = "name"; // name to be dokuwiki conform 25 26 const CANONICAL = "edit-button"; 27 const ENTER_HTML_COMMENT = "<!--"; 28 const CLOSE_HTML_COMMENT = "-->"; 29 const SNIPPET_ID = "edit-button"; 30 31 32 /** 33 * The target drive the type of editor 34 * As of today, there is two type 35 * section and table 36 */ 37 const TARGET_ATTRIBUTE_NAME = "target"; 38 const TARGET_SECTION_VALUE = "section"; 39 /** 40 * The table does not have an edit form at all 41 * It's created by {@link \Doku_Renderer_xhtml::table_close()} 42 * They are not printed by default via CSS. Edittable show them by default via Javascript 43 */ 44 const TARGET_TABLE_VALUE = "table"; 45 public const EDIT_SECTION_TARGET = 'section'; 46 const RANGE = "range"; 47 const DOKUWIKI_FORMAT = "dokuwiki"; 48 const COMBO_FORMAT = "combo"; 49 const TAG = "edit-button"; 50 const CLASS_SUFFIX = "edit-button"; 51 52 53 private $label; 54 /** 55 * @var string 56 */ 57 private $wikiId; 58 59 /** 60 * Edit type 61 * @var string 62 * This is the default 63 */ 64 private string $target = self::TARGET_SECTION_VALUE; 65 /** 66 * @var int 67 */ 68 private int $startPosition; 69 70 private ?int $endPosition; 71 /** 72 * @var string $format - to conform or not to dokuwiki format 73 */ 74 private string $format = self::COMBO_FORMAT; 75 76 /** 77 * the id of the heading, ie the id of the section 78 * Not really needed, just to be conform with Dokuwiki 79 * When the edit button is not for an outline may be null 80 */ 81 private ?string $outlineHeadingId = null; 82 /** 83 * @var ?int $sectionid - sequence id of the section used only by dokuwiki 84 * When the edit button is not for an outline may be null 85 */ 86 private ?int $outlineSectionId = null; 87 88 89 /** 90 * Section constructor. 91 */ 92 public function __construct($label) 93 { 94 $this->label = $label; 95 } 96 97 98 public static function create($label): EditButton 99 { 100 return new EditButton($label); 101 } 102 103 public static function createFromCallStackArray($attributes): EditButton 104 { 105 $label = $attributes[\syntax_plugin_combo_edit::LABEL]; 106 $startPosition = $attributes[\syntax_plugin_combo_edit::START_POSITION]; 107 $endPosition = $attributes[\syntax_plugin_combo_edit::END_POSITION]; 108 $wikiId = $attributes[TagAttributes::WIKI_ID]; 109 $editButton = EditButton::create($label) 110 ->setStartPosition($startPosition) 111 ->setEndPosition($endPosition) 112 ->setWikiId($wikiId); 113 $headingId = $attributes[\syntax_plugin_combo_edit::HEADING_ID]; 114 if ($headingId !== null) { 115 $editButton->setOutlineHeadingId($headingId); 116 } 117 $sectionId = $attributes[\syntax_plugin_combo_edit::SECTION_ID]; 118 if ($sectionId !== null) { 119 $editButton->setOutlineSectionId($sectionId); 120 } 121 $format = $attributes[\syntax_plugin_combo_edit::FORMAT]; 122 if ($format !== null) { 123 $editButton->setFormat($format); 124 } 125 return $editButton; 126 127 128 } 129 130 public static function deleteAll(string $html) 131 { 132 // Dokuwiki way is to delete 133 // but because they are comment, they are not shown 134 // We delete to serve clean page to search engine 135 return preg_replace(SEC_EDIT_PATTERN, '', $html); 136 } 137 138 public static function replaceOrDeleteAll(string $html_output) 139 { 140 try { 141 return EditButton::replaceAll($html_output); 142 } catch (ExceptionNotAuthorized|ExceptionBadState $e) { 143 return EditButton::deleteAll($html_output); 144 } 145 } 146 147 /** 148 * See {@link \Doku_Renderer_xhtml::finishSectionEdit()} 149 */ 150 public function toTag(): string 151 { 152 153 /** 154 * The following data are mandatory from: 155 * {@link html_secedit_get_button} 156 */ 157 $wikiId = $this->getWikiId(); 158 159 160 /** 161 * We follow the order of Dokuwiki for compatibility purpose 162 */ 163 $data[self::TARGET_ATTRIBUTE_NAME] = $this->target; 164 165 if ($this->format === self::COMBO_FORMAT) { 166 /** 167 * In the combo edit format, we had the dokuwiki id 168 * because the edit button may also be on the secondary slot 169 */ 170 $data[self::WIKI_ID] = $wikiId; 171 } 172 $data[self::EDIT_MESSAGE] = $this->label; 173 if ($this->format === self::COMBO_FORMAT) { 174 /** 175 * In the combo edit format, we had the dokuwiki id as form id 176 * to make it unique on the whole page 177 * because the edit button may also be on the secondary slot 178 */ 179 $slotPath = WikiPath::createMarkupPathFromId($wikiId); 180 $formId = ExecutionContext::getActualOrCreateFromEnv() 181 ->getIdManager() 182 ->generateNewHtmlIdForComponent(self::CANONICAL, $slotPath); 183 $data[self::FORM_ID] = $formId; 184 185 186 } else { 187 $data[self::FORM_ID] = $this->getHeadingId(); 188 $data["codeblockOffset"] = 0; // what is that ? 189 $data["secid"] = $this->getSectionId(); 190 } 191 $data[self::RANGE] = $this->getRange(); 192 193 194 return self::EDIT_BUTTON_PREFIX . Html::encode(json_encode($data)); 195 } 196 197 /** 198 * 199 * @throws ExceptionBadArgument - if the wiki id could not be found 200 * @throws ExceptionNotEnabled 201 */ 202 public function toHtmlComment(): string 203 { 204 global $ACT; 205 if ($ACT === FetcherMarkup::MARKUP_DYNAMIC_EXECUTION_NAME) { 206 // ie weblog, they are generated via dynamic markup 207 // meaning that there is no button to edit the file 208 if (!PluginUtility::isTest()) { 209 return ""; 210 } 211 } 212 /** 213 * We don't encode there is only internal information 214 * and this is easier to see / debug the output 215 */ 216 return self::ENTER_HTML_COMMENT . " " . $this->toTag() . " " . self::CLOSE_HTML_COMMENT; 217 } 218 219 public function __toString() 220 { 221 return "Section Edit $this->label"; 222 } 223 224 225 /** 226 * @throws ExceptionNotAuthorized - if the user cannot modify the page 227 * @throws ExceptionBadState - if the page is a revision page or the HTML is not the output of a page 228 */ 229 public static function replaceAll($html) 230 { 231 232 if (!Identity::isWriter()) { 233 throw new ExceptionNotAuthorized("Page is not writable by the user"); 234 } 235 /** 236 * Delete the edit comment 237 * * if not writable 238 * * or an old revision 239 * Original: {@link html_secedit()} {@link html_secedit_get_button()} 240 */ 241 global $INFO; 242 if (isset($INFO)) { 243 // the page is a revision page 244 $rev = $INFO['rev'] ?? 0; 245 if ($rev !== 0) { 246 throw new ExceptionBadState("Internal Error: No edit button can be added to a revision page"); 247 } 248 } 249 250 251 /** 252 * Request based because the button are added only for a user that can write 253 */ 254 $snippetManager = PluginUtility::getSnippetManager(); 255 $snippetManager->attachCssInternalStylesheet(self::SNIPPET_ID); 256 $snippetManager->attachJavascriptFromComponentId(self::SNIPPET_ID); 257 258 /** 259 * The callback function on all edit comment 260 * @param $matches 261 * @return string 262 */ 263 $editFormCallBack = function ($matches) { 264 $json = Html::decode($matches[1]); 265 $data = json_decode($json, true); 266 267 $target = $data[self::TARGET_ATTRIBUTE_NAME]; 268 269 $message = $data[self::EDIT_MESSAGE]; 270 unset($data[self::EDIT_MESSAGE]); 271 if ($message === null || trim($message) === "") { 272 $message = "Edit {$target}"; 273 } 274 275 if ($data === NULL) { 276 LogUtility::internalError("No data found in the edit comment", self::CANONICAL); 277 return ""; 278 } 279 $wikiId = $data[self::WIKI_ID] ?? null; 280 unset($data[self::WIKI_ID]); 281 if ($wikiId === null) { 282 try { 283 $page = MarkupPath::createPageFromExecutingId(); 284 } catch (ExceptionNotFound $e) { 285 LogUtility::internalError("A page id is mandatory for a edit button (no wiki id, no global ID were found). No edit buttons was created then.", self::CANONICAL); 286 return ""; 287 } 288 } else { 289 $page = MarkupPath::createMarkupFromId($wikiId); 290 } 291 $formId = $data[self::FORM_ID]; 292 unset($data[self::FORM_ID]); 293 $data["summary"] = $message; 294 try { 295 $data['rev'] = $page->getPathObject()->getRevisionOrDefault(); 296 } catch (ExceptionNotFound $e) { 297 //LogUtility::internalError("The file ({$page->getPathObject()}) does not exist, we cannot set the last modified time on the edit buttons.", self::CANONICAL); 298 } 299 $hiddenInputs = ""; 300 foreach ($data as $key => $val) { 301 $inputAttributes = TagAttributes::createEmpty() 302 ->addOutputAttributeValue("name", $key) 303 ->addOutputAttributeValue("value", $val) 304 ->addOutputAttributeValue("type", "hidden"); 305 $hiddenInputs .= $inputAttributes->toHtmlEmptyTag("input"); 306 } 307 $url = $page->getUrl() 308 ->withoutRewrite() 309 ->toHtmlString(); 310 $classPageEdit = StyleAttribute::addComboStrapSuffix(self::CLASS_SUFFIX); 311 312 /** 313 * Important Note: the first div and the public class is mandatory for the edittable plugin 314 * See {@link editbutton.js file} 315 */ 316 $editTableClass = "editbutton_{$target}"; 317 return <<<EOF 318<div class="$classPageEdit $editTableClass"> 319 <form id="$formId" method="post" action="{$url}"> 320 $hiddenInputs 321 <input name="do" type="hidden" value="edit"/> 322 <button type="submit" title="$message"> 323 </button> 324 </form> 325</div> 326EOF; 327 }; 328 329 /** 330 * The replacement 331 */ 332 return preg_replace_callback(self::SEC_EDIT_PATTERN, $editFormCallBack, $html); 333 } 334 335 336 public function setWikiId(string $id): EditButton 337 { 338 $this->wikiId = $id; 339 return $this; 340 } 341 342 /** 343 * Page / Section edit 344 * (This is known as the target for dokuwiki) 345 * @param string $target 346 * @return $this 347 * 348 */ 349 public function setTarget(string $target): EditButton 350 { 351 $this->target = $target; 352 return $this; 353 } 354 355 public function setStartPosition(int $startPosition): EditButton 356 { 357 $this->startPosition = $startPosition; 358 return $this; 359 } 360 361 public function setEndPosition(?int $endPosition): EditButton 362 { 363 $this->endPosition = $endPosition; 364 return $this; 365 } 366 367 /** 368 * @return string the file character position range of the section to edit 369 */ 370 private function getRange(): string 371 { 372 $range = ""; 373 if (isset($this->startPosition)) { 374 $range = $this->startPosition; 375 } 376 $range = "$range-"; 377 if (isset($this->endPosition)) { 378 $range = "$range{$this->endPosition}"; 379 } 380 return $range; 381 382 } 383 384 public function toComboCallComboFormat(): Call 385 { 386 return $this->toComboCall(self::COMBO_FORMAT); 387 } 388 389 public function toComboCall($format): Call 390 { 391 return Call::createComboCall( 392 \syntax_plugin_combo_edit::TAG, 393 DOKU_LEXER_SPECIAL, 394 [ 395 \syntax_plugin_combo_edit::START_POSITION => $this->startPosition, 396 \syntax_plugin_combo_edit::END_POSITION => $this->endPosition, 397 \syntax_plugin_combo_edit::LABEL => $this->label, 398 \syntax_plugin_combo_edit::FORMAT => $format, 399 \syntax_plugin_combo_edit::HEADING_ID => $this->getHeadingId(), 400 \syntax_plugin_combo_edit::SECTION_ID => $this->getSectionId(), 401 TagAttributes::WIKI_ID => $this->getWikiId() 402 ] 403 ); 404 } 405 406 407 /** 408 * 409 */ 410 private function getWikiId(): string 411 { 412 413 $wikiId = $this->wikiId; 414 if ($wikiId !== null) { 415 return $wikiId; 416 } 417 418 return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath()->getWikiId(); 419 420 421 } 422 423 424 public function toComboCallDokuWikiForm(): Call 425 { 426 return $this->toComboCall(self::DOKUWIKI_FORMAT); 427 } 428 429 /** @noinspection PhpReturnValueOfMethodIsNeverUsedInspection */ 430 private function setFormat($format): EditButton 431 { 432 433 if (!in_array($format, [self::DOKUWIKI_FORMAT, self::COMBO_FORMAT])) { 434 LogUtility::internalError("The tag format ($format) is not valid", self::CANONICAL); 435 return $this; 436 } 437 $this->format = $format; 438 return $this; 439 } 440 441 public function setOutlineHeadingId($id): EditButton 442 { 443 $this->outlineHeadingId = $id; 444 return $this; 445 } 446 447 /** 448 * @return string|null 449 */ 450 private function getHeadingId(): ?string 451 { 452 return $this->outlineHeadingId; 453 } 454 455 private function getSectionId(): ?int 456 { 457 return $this->outlineSectionId; 458 } 459 460 public function setOutlineSectionId(int $sectionSequenceId): EditButton 461 { 462 $this->outlineSectionId = $sectionSequenceId; 463 return $this; 464 } 465 466} 467