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 if ($INFO['rev']) { 245 throw new ExceptionBadState("Internal Error: No edit button can be added to a revision page"); 246 } 247 } 248 249 250 /** 251 * Request based because the button are added only for a user that can write 252 */ 253 $snippetManager = PluginUtility::getSnippetManager(); 254 $snippetManager->attachCssInternalStylesheet(self::SNIPPET_ID); 255 $snippetManager->attachJavascriptFromComponentId(self::SNIPPET_ID); 256 257 /** 258 * The callback function on all edit comment 259 * @param $matches 260 * @return string 261 */ 262 $editFormCallBack = function ($matches) { 263 $json = Html::decode($matches[1]); 264 $data = json_decode($json, true); 265 266 $target = $data[self::TARGET_ATTRIBUTE_NAME]; 267 268 $message = $data[self::EDIT_MESSAGE]; 269 unset($data[self::EDIT_MESSAGE]); 270 if ($message === null || trim($message) === "") { 271 $message = "Edit {$target}"; 272 } 273 274 if ($data === NULL) { 275 LogUtility::internalError("No data found in the edit comment", self::CANONICAL); 276 return ""; 277 } 278 $wikiId = $data[self::WIKI_ID]; 279 unset($data[self::WIKI_ID]); 280 if ($wikiId === null) { 281 try { 282 $page = MarkupPath::createPageFromExecutingId(); 283 } catch (ExceptionNotFound $e) { 284 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); 285 return ""; 286 } 287 } else { 288 $page = MarkupPath::createMarkupFromId($wikiId); 289 } 290 $formId = $data[self::FORM_ID]; 291 unset($data[self::FORM_ID]); 292 $data["summary"] = $message; 293 try { 294 $data['rev'] = $page->getPathObject()->getRevisionOrDefault(); 295 } catch (ExceptionNotFound $e) { 296 //LogUtility::internalError("The file ({$page->getPathObject()}) does not exist, we cannot set the last modified time on the edit buttons.", self::CANONICAL); 297 } 298 $hiddenInputs = ""; 299 foreach ($data as $key => $val) { 300 $inputAttributes = TagAttributes::createEmpty() 301 ->addOutputAttributeValue("name", $key) 302 ->addOutputAttributeValue("value", $val) 303 ->addOutputAttributeValue("type", "hidden"); 304 $hiddenInputs .= $inputAttributes->toHtmlEmptyTag("input"); 305 } 306 $url = $page->getUrl() 307 ->withoutRewrite() 308 ->toHtmlString(); 309 $classPageEdit = StyleAttribute::addComboStrapSuffix(self::CLASS_SUFFIX); 310 311 /** 312 * Important Note: the first div and the public class is mandatory for the edittable plugin 313 * See {@link editbutton.js file} 314 */ 315 $editTableClass = "editbutton_{$target}"; 316 return <<<EOF 317<div class="$classPageEdit $editTableClass"> 318 <form id="$formId" method="post" action="{$url}"> 319 $hiddenInputs 320 <input name="do" type="hidden" value="edit"/> 321 <button type="submit" title="$message"> 322 </button> 323 </form> 324</div> 325EOF; 326 }; 327 328 /** 329 * The replacement 330 */ 331 return preg_replace_callback(self::SEC_EDIT_PATTERN, $editFormCallBack, $html); 332 } 333 334 335 public function setWikiId(string $id): EditButton 336 { 337 $this->wikiId = $id; 338 return $this; 339 } 340 341 /** 342 * Page / Section edit 343 * (This is known as the target for dokuwiki) 344 * @param string $target 345 * @return $this 346 * 347 */ 348 public function setTarget(string $target): EditButton 349 { 350 $this->target = $target; 351 return $this; 352 } 353 354 public function setStartPosition(int $startPosition): EditButton 355 { 356 $this->startPosition = $startPosition; 357 return $this; 358 } 359 360 public function setEndPosition(?int $endPosition): EditButton 361 { 362 $this->endPosition = $endPosition; 363 return $this; 364 } 365 366 /** 367 * @return string the file character position range of the section to edit 368 */ 369 private function getRange(): string 370 { 371 $range = ""; 372 if (isset($this->startPosition)) { 373 $range = $this->startPosition; 374 } 375 $range = "$range-"; 376 if (isset($this->endPosition)) { 377 $range = "$range{$this->endPosition}"; 378 } 379 return $range; 380 381 } 382 383 public function toComboCallComboFormat(): Call 384 { 385 return $this->toComboCall(self::COMBO_FORMAT); 386 } 387 388 public function toComboCall($format): Call 389 { 390 return Call::createComboCall( 391 \syntax_plugin_combo_edit::TAG, 392 DOKU_LEXER_SPECIAL, 393 [ 394 \syntax_plugin_combo_edit::START_POSITION => $this->startPosition, 395 \syntax_plugin_combo_edit::END_POSITION => $this->endPosition, 396 \syntax_plugin_combo_edit::LABEL => $this->label, 397 \syntax_plugin_combo_edit::FORMAT => $format, 398 \syntax_plugin_combo_edit::HEADING_ID => $this->getHeadingId(), 399 \syntax_plugin_combo_edit::SECTION_ID => $this->getSectionId(), 400 TagAttributes::WIKI_ID => $this->getWikiId() 401 ] 402 ); 403 } 404 405 406 /** 407 * 408 */ 409 private function getWikiId(): string 410 { 411 412 $wikiId = $this->wikiId; 413 if ($wikiId !== null) { 414 return $wikiId; 415 } 416 417 return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath()->getWikiId(); 418 419 420 } 421 422 423 public function toComboCallDokuWikiForm(): Call 424 { 425 return $this->toComboCall(self::DOKUWIKI_FORMAT); 426 } 427 428 /** @noinspection PhpReturnValueOfMethodIsNeverUsedInspection */ 429 private function setFormat($format): EditButton 430 { 431 432 if (!in_array($format, [self::DOKUWIKI_FORMAT, self::COMBO_FORMAT])) { 433 LogUtility::internalError("The tag format ($format) is not valid", self::CANONICAL); 434 return $this; 435 } 436 $this->format = $format; 437 return $this; 438 } 439 440 public function setOutlineHeadingId($id): EditButton 441 { 442 $this->outlineHeadingId = $id; 443 return $this; 444 } 445 446 /** 447 * @return string|null 448 */ 449 private function getHeadingId(): ?string 450 { 451 return $this->outlineHeadingId; 452 } 453 454 private function getSectionId(): ?int 455 { 456 return $this->outlineSectionId; 457 } 458 459 public function setOutlineSectionId(int $sectionSequenceId): EditButton 460 { 461 $this->outlineSectionId = $sectionSequenceId; 462 return $this; 463 } 464 465} 466