1<?php 2 3/** 4 * Plugin BatchEdit: User interface 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Mykola Ostrovskyy <dwpforge@gmail.com> 8 */ 9 10class BatcheditMessage implements Serializable { 11 const ERROR = 1; 12 const WARNING = 2; 13 14 public $type; 15 public $data; 16 17 /** 18 * 19 */ 20 public function getClass() { 21 switch ($this->type) { 22 case self::ERROR: 23 return 'error'; 24 case self::WARNING: 25 return 'notify'; 26 } 27 28 return ''; 29 } 30 31 /** 32 * 33 */ 34 public function getFormatId() { 35 switch ($this->type) { 36 case self::ERROR: 37 return 'msg_error'; 38 case self::WARNING: 39 return 'msg_warning'; 40 } 41 42 return ''; 43 } 44 45 /** 46 * 47 */ 48 public function getId() { 49 return $this->data[0]; 50 } 51 52 /** 53 * 54 */ 55 public function serialize() { 56 return serialize(array($this->type, $this->data)); 57 } 58 59 /** 60 * 61 */ 62 public function unserialize($data) { 63 list($this->type, $this->data) = unserialize($data); 64 } 65} 66 67class BatcheditErrorMessage extends BatcheditMessage { 68 69 /** 70 * Accepts message array that starts with message id followed by optional arguments. 71 */ 72 public function __construct($message) { 73 $this->type = self::ERROR; 74 $this->data = $message; 75 } 76} 77 78class BatcheditWarningMessage extends BatcheditMessage { 79 80 /** 81 * Accepts message array that starts with message id followed by optional arguments. 82 */ 83 public function __construct($message) { 84 $this->type = self::WARNING; 85 $this->data = $message; 86 } 87} 88 89class BatcheditInterface { 90 91 private $plugin; 92 private $indent; 93 private $svgCache; 94 95 /** 96 * 97 */ 98 public function __construct($plugin) { 99 $this->plugin = $plugin; 100 $this->indent = 0; 101 $this->svgCache = array(); 102 } 103 104 /** 105 * 106 */ 107 public function configure($config) { 108 foreach ($config->getConfig() as $id => $value) { 109 if (!empty($value) || $value === 0) { 110 $_REQUEST[$id] = $value; 111 } 112 else { 113 unset($_REQUEST[$id]); 114 } 115 } 116 } 117 118 /** 119 * 120 */ 121 public function printBeginning($sessionId) { 122 $this->ptln('<!-- batchedit -->'); 123 $this->ptln('<div id="batchedit">'); 124 125 $this->printJavascriptLang(); 126 127 $this->ptln('<form method="post">'); 128 $this->ptln('<input type="hidden" name="session" value="' . $sessionId . '" />'); 129 } 130 131 /** 132 * 133 */ 134 public function printEnding() { 135 $this->ptln('</form>'); 136 $this->ptln('</div>'); 137 $this->ptln('<!-- /batchedit -->'); 138 } 139 140 /** 141 * 142 */ 143 public function printMessages($messages) { 144 if (empty($messages)) { 145 return; 146 } 147 148 $this->ptln('<div id="be-messages">', +2); 149 150 foreach ($messages as $message) { 151 $this->ptln('<div class="' . $message->getClass() . '">', +2); 152 $this->ptln($this->getLang($message->getFormatId(), call_user_func_array(array($this, 'getLang'), $message->data))); 153 $this->ptln('</div>', -2); 154 } 155 156 $this->ptln('</div>', -2); 157 } 158 159 /** 160 * 161 */ 162 public function printTotalStats($command, $matchCount, $pageCount, $editCount) { 163 $matches = $this->getLangPlural('sts_matches', $matchCount); 164 $pages = $this->getLangPlural('sts_pages', $pageCount); 165 166 switch ($command) { 167 case BatcheditRequest::COMMAND_PREVIEW: 168 $stats = $this->getLang('sts_preview', $matches, $pages); 169 break; 170 171 case BatcheditRequest::COMMAND_APPLY: 172 $edits = $this->getLangPlural('sts_edits', $editCount); 173 $stats = $this->getLang('sts_apply', $matches, $pages, $edits); 174 break; 175 } 176 177 $this->ptln('<div id="be-totalstats"><div>', +2); 178 179 if ($editCount < $matchCount) { 180 $this->printApplyCheckBox('be-applyall', $stats, 'ttl_applyall'); 181 } 182 else { 183 $this->ptln($stats); 184 } 185 186 $this->ptln('</div></div>', -2); 187 } 188 189 /** 190 * 191 */ 192 public function printMatches($pages) { 193 foreach ($pages as $page) { 194 $this->ptln('<div class="be-file">', +2); 195 $this->printPageStats($page); 196 $this->printPageActions($page->getId()); 197 $this->printPageMatches($page); 198 $this->ptln('</div>', -2); 199 } 200 } 201 202 /** 203 * 204 */ 205 public function printMainForm($enableApply) { 206 $this->ptln('<div id="be-mainform">', +2); 207 208 $this->ptln('<div>', +2); 209 210 $this->ptln('<div id="be-editboxes">', +2); 211 $this->ptln('<table>', +2); 212 $this->printFormEdit('lbl_ns', 'namespace'); 213 $this->printFormEdit('lbl_search', 'search'); 214 $this->printFormEdit('lbl_replace', 'replace'); 215 $this->printFormEdit('lbl_summary', 'summary'); 216 $this->ptln('</table>', -2); 217 $this->ptln('</div>', -2); 218 219 $this->printOptions(); 220 221 $this->ptln('</div>', -2); 222 223 // Value for this hidden input is set before submit through jQuery, containing 224 // JSON-encoded list of all checked checkbox ids for single matches. 225 // Consolidating these inputs into a single string variable avoids problems for 226 // huge replacement sets exceeding `max_input_vars` in `php.ini`. 227 $this->ptln('<input type="hidden" name="apply" value="" />'); 228 229 $this->ptln('<div id="be-submitbar">', +2); 230 $this->printSubmitButton('cmd[preview]', 'btn_preview'); 231 $this->printSubmitButton('cmd[apply]', 'btn_apply', $enableApply); 232 $this->ptln('<div id="be-progressbar">', +2); 233 $this->ptln('<div id="be-progresswrap"><div id="be-progress"></div></div>'); 234 $this->printButton('cancel', 'btn_cancel'); 235 $this->ptln('</div>', -2); 236 $this->ptln('</div>', -2); 237 238 $this->ptln('</div>', -2); 239 } 240 241 /** 242 * 243 */ 244 private function printJavascriptLang() { 245 $this->ptln('<script type="text/javascript">'); 246 247 $langIds = array('hnt_textsearch', 'hnt_textreplace', 'hnt_regexpsearch', 'hnt_regexpreplace', 248 'hnt_advregexpsearch', 'war_nosummary'); 249 $lang = array(); 250 251 foreach ($langIds as $id) { 252 $lang[$id] = $this->getLang($id); 253 } 254 255 $this->ptln('var batcheditLang = ' . json_encode($lang) . ';'); 256 $this->ptln('</script>'); 257 } 258 259 /** 260 * 261 */ 262 private function printApplyCheckBox($id, $label, $title, $checked = FALSE) { 263 $checked = $checked ? ' checked="checked"' : ''; 264 265 $this->ptln('<span class="be-apply" title="' . $this->getLang($title) . '">', +2); 266 $this->ptln('<input type="checkbox" id="' . $id . '"' . $checked . ' />'); 267 $this->ptln('<label for="' . $id . '">' . $label . '</label>'); 268 $this->ptln('</span>', -2); 269 } 270 271 /** 272 * 273 */ 274 private function printPageStats($page) { 275 $stats = $this->getLang('sts_page', $page->getId(), $this->getLangPlural('sts_matches', count($page->getMatches()))); 276 277 $this->ptln('<div class="be-stats">', +2); 278 279 if ($page->hasUnappliedMatches()) { 280 $this->printApplyCheckBox($page->getId(), $stats, 'ttl_applyfile', !$page->hasUnmarkedMatches()); 281 } 282 else { 283 $this->ptln($stats); 284 } 285 286 $this->ptln('</div>', -2); 287 } 288 289 /** 290 * 291 */ 292 private function printPageActions($pageId) { 293 $link = wl($pageId); 294 295 $this->ptln('<div class="be-actions">', +2); 296 $this->printAction($link, 'ttl_view', 'file-document'); 297 $this->printAction($link . (strpos($link, '?') === FALSE ? '?' : '&') . 'do=edit', 'ttl_edit', 'pencil'); 298 $this->printAction('#be-mainform', 'ttl_mainform', 'arrow-down'); 299 $this->ptln('</div>', -2); 300 } 301 302 /** 303 * 304 */ 305 private function printAction($href, $titleId, $iconId) { 306 $action = '<a class="be-action" href="' . $href . '" title="' . $this->getLang($titleId) . '">'; 307 $action .= $this->getSvg($iconId); 308 $action .= '</a>'; 309 310 $this->ptln($action); 311 } 312 313 /** 314 * 315 */ 316 private function printPageMatches($page) { 317 foreach ($page->getMatches() as $match) { 318 $this->ptln('<div class="be-match">', +2); 319 $this->printMatchHeader($page->getId(), $match); 320 $this->printMatchTable($match); 321 $this->ptln('</div>', -2); 322 } 323 } 324 325 /** 326 * 327 */ 328 private function printMatchHeader($pageId, $match) { 329 $id = $pageId . '#' . $match->getPageOffset(); 330 331 $this->ptln('<div class="be-matchid">', +2); 332 333 if (!$match->isApplied()) { 334 $this->printApplyCheckBox($id, $id, 'ttl_applymatch', $match->isMarked()); 335 } 336 else { 337 // Add hidden checked checkbox to ensure that marked status is not lost on 338 // applied matches if application is performed in multiple rounds. This can 339 // be the case when one apply command is timed out and user issues a second 340 // one to apply the remaining matches. 341 $this->ptln('<input type="checkbox" id="' . $id . '" checked="checked" style="display:none;" />'); 342 $this->ptln($id); 343 } 344 345 $this->ptln('</div>', -2); 346 } 347 348 /** 349 * 350 */ 351 private function printMatchTable($match) { 352 $original = $this->prepareText($match->getOriginalText(), $match->isApplied() ? ' be-replaced' : 'be-preview'); 353 $replaced = $this->prepareText($match->getReplacedText(), $match->isApplied() ? ' be-applied' : 'be-preview'); 354 $before = $this->prepareText($match->getContextBefore()); 355 $after = $this->prepareText($match->getContextAfter()); 356 357 $this->ptln('<table><tr>', +2); 358 $this->ptln('<td class="be-text">' . $before . $original . $after . '</td>'); 359 $this->ptln('<td class="be-arrow">' . $this->getSvg('slide-arrow-right') . '</td>'); 360 $this->ptln('<td class="be-text">' . $before . $replaced . $after . '</td>'); 361 $this->ptln('</tr></table>', -2); 362 } 363 364 /** 365 * Prepare wiki text to be displayed as html 366 */ 367 private function prepareText($text, $highlight = '') { 368 $html = htmlspecialchars($text); 369 $html = str_replace("\n", '<br />', $html); 370 371 if ($highlight != '') { 372 $html = '<span class="' . $highlight . '">' . $html . '</span>'; 373 } 374 375 return $html; 376 } 377 378 /** 379 * 380 */ 381 private function printFormEdit($title, $name) { 382 $this->ptln('<tr>', +2); 383 $this->ptln('<td class="be-title">' . $this->getLang($title) . '</td>'); 384 $this->ptln('<td class="be-edit">', +2); 385 386 switch ($name) { 387 case 'namespace': 388 $this->printEditBox($name); 389 break; 390 391 case 'search': 392 case 'replace': 393 $multiline = isset($_REQUEST['multiline']); 394 $placeholder = $this->getLang($this->getPlaceholderId($name)); 395 396 $this->printEditBox($name, FALSE, TRUE, !$multiline, $placeholder); 397 $this->printTextArea($name, $multiline, $placeholder); 398 break; 399 400 case 'summary': 401 $this->printEditBox($name); 402 $this->printCheckBox('minor', 'lbl_minor'); 403 break; 404 } 405 406 $this->ptln('</td>', -2); 407 $this->ptln('</tr>', -2); 408 } 409 410 /** 411 * 412 */ 413 private function getPlaceholderId($editName) { 414 switch ($editName) { 415 case 'search': 416 switch ($_REQUEST['searchmode']) { 417 case 'text': 418 return 'hnt_textsearch'; 419 case 'regexp': 420 return isset($_REQUEST['advregexp']) ? 'hnt_advregexpsearch' : 'hnt_regexpsearch'; 421 } 422 case 'replace': 423 return 'hnt_' . $_REQUEST['searchmode'] . 'replace'; 424 } 425 426 return ''; 427 } 428 429 /** 430 * 431 */ 432 private function printOptions() { 433 $style = 'min-width: ' . $this->getLang('dim_options') . ';'; 434 435 $this->ptln('<div id="be-options" style="' . $style . '">', +2); 436 437 $this->ptln('<div class="be-radiogroup">', +2); 438 $this->ptln('<div>' . $this->getLang('lbl_searchmode') . '</div>'); 439 $this->printRadioButton('searchmode', 'text', 'lbl_searchtext'); 440 $this->printRadioButton('searchmode', 'regexp', 'lbl_searchregexp'); 441 $this->ptln('</div>', -2); 442 443 $this->printCheckBox('matchcase', 'lbl_matchcase'); 444 $this->printCheckBox('multiline', 'lbl_multiline'); 445 446 $this->ptln('</div>', -2); 447 448 $this->ptln('<div class="be-actions">', +2); 449 $this->printAction('javascript:openAdvancedOptions();', 'ttl_extoptions', 'settings'); 450 $this->ptln('</div>', -2); 451 452 $style = 'width: ' . $this->getLang('dim_extoptions') . ';'; 453 454 $this->ptln('<div id="be-extoptions" style="' . $style . '">', +2); 455 $this->ptln('<div class="be-actions">', +2); 456 $this->printAction('javascript:closeAdvancedOptions();', '', 'close'); 457 $this->ptln('</div>', -2); 458 459 $this->printCheckBox('advregexp', 'lbl_advregexp'); 460 $this->printCheckBox('matchctx', 'printMatchContextLabel'); 461 $this->printCheckBox('searchlimit', 'printSearchLimitLabel'); 462 $this->printCheckBox('keepmarks', 'printKeepMarksLabel'); 463 $this->printCheckBox('tplpatterns', 'lbl_tplpatterns'); 464 $this->printCheckBox('checksummary', 'lbl_checksummary'); 465 466 $this->ptln('</div>', -2); 467 } 468 469 /** 470 * 471 */ 472 private function printMatchContextLabel() { 473 $label = preg_split('/(\{\d\})/', $this->getLang('lbl_matchctx'), -1, PREG_SPLIT_DELIM_CAPTURE); 474 $edits = array('{1}' => 'ctxchars', '{2}' => 'ctxlines'); 475 476 $this->printLabel('matchctx', $label[0]); 477 $this->printEditBox($edits[$label[1]], TRUE, isset($_REQUEST['matchctx'])); 478 $this->printLabel('matchctx', $label[2]); 479 $this->printEditBox($edits[$label[3]], TRUE, isset($_REQUEST['matchctx'])); 480 $this->printLabel('matchctx', $label[4]); 481 } 482 483 /** 484 * 485 */ 486 private function printSearchLimitLabel() { 487 $label = explode('{1}', $this->getLang('lbl_searchlimit')); 488 489 $this->printLabel('searchlimit', $label[0]); 490 $this->printEditBox('searchmax', TRUE, isset($_REQUEST['searchlimit'])); 491 $this->printLabel('searchlimit', $label[1]); 492 } 493 494 /** 495 * 496 */ 497 private function printKeepMarksLabel() { 498 $label = explode('{1}', $this->getLang('lbl_keepmarks')); 499 $disabled = isset($_REQUEST['keepmarks']) ? '' : ' disabled="disabled"'; 500 501 $this->printLabel('keepmarks', $label[0]); 502 $this->ptln('<select name="markpolicy"' . $disabled . '>', +2); 503 504 for ($i = 1; $i <= 4; $i++) { 505 $selected = $_REQUEST['markpolicy'] == $i ? ' selected="selected"' : ''; 506 507 $this->ptln('<option value="' . $i . '"' . $selected . '>' . $this->getLang('lbl_keepmarks' . $i) . '</option>'); 508 } 509 510 $this->ptln('</select>', -2); 511 $this->printLabel('keepmarks', $label[1]); 512 } 513 514 /** 515 * 516 */ 517 private function printEditBox($name, $submitted = TRUE, $enabled = TRUE, $visible = TRUE, $placeholder = '') { 518 $html = '<input type="text" class="be-edit" id="be-' . $name . 'edit"'; 519 520 if ($submitted) { 521 $html .= ' name="' . $name . '"'; 522 } 523 524 if (!empty($placeholder)) { 525 $html .= ' placeholder="' . $placeholder . '"'; 526 } 527 528 if (($submitted || $visible) && isset($_REQUEST[$name])) { 529 $html .= ' value="' . htmlspecialchars($_REQUEST[$name]) . '"'; 530 } 531 532 if (!$enabled) { 533 $html .= ' disabled="disabled"'; 534 } 535 536 if (!$visible) { 537 $html .= ' style="display: none;"'; 538 } 539 540 $this->ptln($html . ' />'); 541 } 542 543 /** 544 * 545 */ 546 private function printTextArea($name, $visible = TRUE, $placeholder = '') { 547 $html = '<textarea class="be-edit" id="be-' . $name . 'area" name="' . $name . '"'; 548 549 if (!empty($placeholder)) { 550 $html .= ' placeholder="' . $placeholder . '"'; 551 } 552 553 $style = array(); 554 555 if (!$visible) { 556 $style[] = 'display: none;'; 557 } 558 559 if (isset($_REQUEST[$name . 'height'])) { 560 $style[] = 'height: ' . $_REQUEST[$name . 'height'] . 'px;'; 561 } 562 563 if (!empty($style)) { 564 $html .= ' style="' . join(' ', $style) . '"'; 565 } 566 567 $html .= '>'; 568 569 if (isset($_REQUEST[$name])) { 570 $value = $_REQUEST[$name]; 571 572 // HACK: It seems that even with "white-space: pre" textarea trims one leading 573 // empty line. To workaround this duplicate the empty line. 574 if (preg_match("/^(\r?\n)/", $value, $match) == 1) { 575 $value = $match[1] . $value; 576 } 577 578 $html .= htmlspecialchars($value); 579 } 580 581 $this->ptln($html . '</textarea>'); 582 } 583 584 /** 585 * 586 */ 587 private function printCheckBox($name, $label) { 588 $html = '<input type="checkbox" id="be-' . $name . '" name="' . $name . '" value="on"'; 589 590 if (isset($_REQUEST[$name])) { 591 $html .= ' checked="checked"'; 592 } 593 594 $this->ptln('<div class="be-checkbox">', +2); 595 $this->ptln($html . ' />'); 596 $this->printLabel($name, $label); 597 $this->ptln('</div>', -2); 598 } 599 600 /** 601 * 602 */ 603 private function printRadioButton($group, $name, $label) { 604 $id = $group . $name; 605 $html = '<input type="radio" id="be-' . $id . '" name="' . $group . '" value="' . $name . '"'; 606 607 if (isset($_REQUEST[$group]) && $_REQUEST[$group] == $name) { 608 $html .= ' checked="checked"'; 609 } 610 611 $this->ptln('<div class="be-radiobtn">', +2); 612 $this->ptln($html . ' />'); 613 $this->printLabel($id, $label); 614 $this->ptln('</div>', -2); 615 } 616 617 /** 618 * 619 */ 620 private function printLabel($name, $label) { 621 if (substr($label, 0, 5) == 'print') { 622 $this->$label(); 623 } 624 else { 625 if (substr($label, 0, 4) == 'lbl_') { 626 $label = $this->getLang($label); 627 } 628 else { 629 $label = trim($label); 630 } 631 632 if (!empty($label)) { 633 $this->ptln('<label for="be-' . $name . '">' . $label . '</label>'); 634 } 635 } 636 } 637 638 /** 639 * 640 */ 641 private function printSubmitButton($name, $label, $enabled = TRUE) { 642 $html = '<input type="submit" class="button be-button be-submit" name="' . $name . '" value="' . $this->getLang($label) . '"'; 643 644 if (!$enabled) { 645 $html .= ' disabled="disabled"'; 646 } 647 648 $this->ptln($html . ' />'); 649 } 650 651 /** 652 * 653 */ 654 private function printButton($name, $label) { 655 $this->ptln('<input type="button" class="button be-button" name="' . $name . '" value="' . $this->getLang($label) . '" />'); 656 } 657 658 /** 659 * 660 */ 661 private function getLang($id) { 662 $string = $this->plugin->getLang($id); 663 664 if (func_num_args() > 1) { 665 $search = array(); 666 $replace = array(); 667 668 for ($i = 1; $i < func_num_args(); $i++) { 669 $search[$i - 1] = '{' . $i . '}'; 670 $replace[$i - 1] = func_get_arg($i); 671 } 672 673 $string = str_replace($search, $replace, $string); 674 } 675 676 return $string; 677 } 678 679 /** 680 * 681 */ 682 private function getLangPlural($id, $quantity) { 683 $lang = $this->getLang($id . $this->getPluralForm($quantity), $quantity); 684 685 if (!empty($lang)) { 686 return $lang; 687 } 688 689 return $this->getLang($id . '#many', $quantity); 690 } 691 692 /** 693 * 694 */ 695 private function getPluralForm($quantity) { 696 global $conf; 697 698 if ($conf['lang'] == 'ru') { 699 $quantity %= 100; 700 701 if ($quantity >= 5 && $quantity <= 20) { 702 return '#many'; 703 } 704 705 $quantity %= 10; 706 707 if ($quantity >= 2 && $quantity <= 4) { 708 return '#few'; 709 } 710 } 711 712 if ($quantity == 1) { 713 return '#one'; 714 } 715 716 return '#many'; 717 } 718 719 /** 720 * 721 */ 722 private function ptln($string, $indentDelta = 0) { 723 if ($indentDelta < 0) { 724 $this->indent += $indentDelta; 725 } 726 727 ptln($string, $this->indent); 728 729 if ($indentDelta > 0) { 730 $this->indent += $indentDelta; 731 } 732 } 733 734 /** 735 * 736 */ 737 private function getSvg($id) { 738 if (!array_key_exists($id, $this->svgCache)) { 739 $this->svgCache[$id] = file_get_contents(DOKU_PLUGIN . 'batchedit/images/' . $id . '.svg'); 740 } 741 742 return $this->svgCache[$id]; 743 } 744} 745