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