MediaWiki REL1_34
SpecialReplaceText.php
Go to the documentation of this file.
1<?php
22
24 private $target;
25 private $replacement;
26 private $use_regex;
27 private $category;
28 private $prefix;
29 private $edit_pages;
30 private $move_pages;
32 private $doAnnounce;
33
34 public function __construct() {
35 parent::__construct( 'ReplaceText', 'replacetext' );
36 }
37
41 public function doesWrites() {
42 return true;
43 }
44
48 function execute( $query ) {
50
51 if ( !$this->getUser()->isAllowed( 'replacetext' ) ) {
52 throw new PermissionsError( 'replacetext' );
53 }
54
55 $out = $this->getOutput();
56 // Replace Text can't be run with certain settings, due to the
57 // changes they make to the DB storage setup.
58 if ( $wgCompressRevisions ) {
59 $errorMsg = "Error: text replacements cannot be run if \$wgCompressRevisions is set to true.";
60 $out->addWikiTextAsContent( "<div class=\"errorbox\">$errorMsg</div>" );
61 return;
62 }
63 if ( !empty( $wgExternalStores ) ) {
64 $errorMsg = "Error: text replacements cannot be run if \$wgExternalStores is non-empty.";
65 $out->addWikiTextAsContent( "<div class=\"errorbox\">$errorMsg</div>" );
66 return;
67 }
68
69 $this->setHeaders();
70 if ( !is_null( $out->getResourceLoader()->getModule( 'mediawiki.special' ) ) ) {
71 $out->addModuleStyles( 'mediawiki.special' );
72 }
73 $this->doSpecialReplaceText();
74 }
75
80 if ( class_exists( MediaWikiServices::class ) ) {
81 // MW 1.27+
82 $all_namespaces = MediaWikiServices::getInstance()->getSearchEngineConfig()
83 ->searchableNamespaces();
84 } else {
86 $all_namespaces = SearchEngine::searchableNamespaces();
87 }
89 foreach ( $all_namespaces as $ns => $name ) {
90 if ( $this->getRequest()->getCheck( 'ns' . $ns ) ) {
92 }
93 }
95 }
96
101 $out = $this->getOutput();
102 $request = $this->getRequest();
103
104 $this->target = $request->getText( 'target' );
105 $this->replacement = $request->getText( 'replacement' );
106 $this->use_regex = $request->getBool( 'use_regex' );
107 $this->category = $request->getText( 'category' );
108 $this->prefix = $request->getText( 'prefix' );
109 $this->edit_pages = $request->getBool( 'edit_pages' );
110 $this->move_pages = $request->getBool( 'move_pages' );
111 $this->doAnnounce = $request->getBool( 'doAnnounce' );
112 $this->selected_namespaces = $this->getSelectedNamespaces();
113
114 if ( $request->getCheck( 'continue' ) && $this->target === '' ) {
115 $this->showForm( 'replacetext_givetarget' );
116 return;
117 }
118
119 if ( $request->getCheck( 'replace' ) ) {
120
121 // check for CSRF
122 $user = $this->getUser();
123 if ( !$user->matchEditToken( $request->getVal( 'token' ) ) ) {
124 $out->addWikiMsg( 'sessionfailure' );
125 return;
126 }
127
128 $jobs = $this->createJobsForTextReplacements();
129 JobQueueGroup::singleton()->push( $jobs );
130
131 $count = $this->getLanguage()->formatNum( count( $jobs ) );
132 $out->addWikiMsg(
133 'replacetext_success',
134 "<code><nowiki>{$this->target}</nowiki></code>",
135 "<code><nowiki>{$this->replacement}</nowiki></code>",
136 $count
137 );
138
139 // Link back
140 $out->addHTML(
142 $this->getPageTitle(),
143 $this->msg( 'replacetext_return' )->text()
144 )
145 );
146 return;
147 }
148
149 if ( $request->getCheck( 'target' ) ) {
150 // check for CSRF
151 $user = $this->getUser();
152 if ( !$user->matchEditToken( $request->getVal( 'token' ) ) ) {
153 $out->addWikiMsg( 'sessionfailure' );
154 return;
155 }
156
157 // first, check that at least one namespace has been
158 // picked, and that either editing or moving pages
159 // has been selected
160 if ( count( $this->selected_namespaces ) == 0 ) {
161 $this->showForm( 'replacetext_nonamespace' );
162 return;
163 }
164 if ( !$this->edit_pages && !$this->move_pages ) {
165 $this->showForm( 'replacetext_editormove' );
166 return;
167 }
168
169 // If user is replacing text within pages...
170 $titles_for_edit = $titles_for_move = $unmoveable_titles = [];
171 if ( $this->edit_pages ) {
172 $titles_for_edit = $this->getTitlesForEditingWithContext();
173 }
174 if ( $this->move_pages ) {
175 list( $titles_for_move, $unmoveable_titles ) = $this->getTitlesForMoveAndUnmoveableTitles();
176 }
177
178 // If no results were found, check to see if a bad
179 // category name was entered.
180 if ( count( $titles_for_edit ) == 0 && count( $titles_for_move ) == 0 ) {
181 $bad_cat_name = false;
182
183 if ( !empty( $this->category ) ) {
184 $category_title = Title::makeTitleSafe( NS_CATEGORY, $this->category );
185 if ( !$category_title->exists() ) {
186 $bad_cat_name = true;
187 }
188 }
189
190 if ( $bad_cat_name ) {
192 $category_title,
193 ucfirst( $this->category )
194 );
195 $out->addHTML(
196 $this->msg( 'replacetext_nosuchcategory' )->rawParams( $link )->escaped()
197 );
198 } else {
199 if ( $this->edit_pages ) {
200 $out->addWikiMsg(
201 'replacetext_noreplacement', "<code><nowiki>{$this->target}</nowiki></code>"
202 );
203 }
204
205 if ( $this->move_pages ) {
206 $out->addWikiMsg( 'replacetext_nomove', "<code><nowiki>{$this->target}</nowiki></code>" );
207 }
208 }
209 // link back to starting form
210 $out->addHTML(
211 '<p>' .
213 $this->getPageTitle(),
214 $this->msg( 'replacetext_return' )->text()
215 )
216 . '</p>'
217 );
218 } else {
219 $warning_msg = $this->getAnyWarningMessageBeforeReplace( $titles_for_edit, $titles_for_move );
220 if ( !is_null( $warning_msg ) ) {
221 $out->addWikiTextAsContent(
222 "<div class=\"errorbox\">$warning_msg</div><br clear=\"both\" />"
223 );
224 }
225
226 $this->pageListForm( $titles_for_edit, $titles_for_move, $unmoveable_titles );
227 }
228 return;
229 }
230
231 // If we're still here, show the starting form.
232 $this->showForm();
233 }
234
241 global $wgReplaceTextUser;
242
243 $replacement_params = [];
244 if ( $wgReplaceTextUser != null ) {
245 $user = User::newFromName( $wgReplaceTextUser );
246 } else {
247 $user = $this->getUser();
248 }
249
250 $replacement_params['user_id'] = $user->getId();
251 $replacement_params['target_str'] = $this->target;
252 $replacement_params['replacement_str'] = $this->replacement;
253 $replacement_params['use_regex'] = $this->use_regex;
254 $replacement_params['edit_summary'] = $this->msg(
255 'replacetext_editsummary',
256 $this->target, $this->replacement
257 )->inContentLanguage()->plain();
258 $replacement_params['create_redirect'] = false;
259 $replacement_params['watch_page'] = false;
260 $replacement_params['doAnnounce'] = $this->doAnnounce;
261
262 $request = $this->getRequest();
263 foreach ( $request->getValues() as $key => $value ) {
264 if ( $key == 'create-redirect' && $value == '1' ) {
265 $replacement_params['create_redirect'] = true;
266 } elseif ( $key == 'watch-pages' && $value == '1' ) {
267 $replacement_params['watch_page'] = true;
268 }
269 }
270
271 $jobs = [];
272 foreach ( $request->getValues() as $key => $value ) {
273 if ( $value == '1' && $key !== 'replace' && $key !== 'use_regex' ) {
274 if ( strpos( $key, 'move-' ) !== false ) {
275 $title = Title::newFromID( substr( $key, 5 ) );
276 $replacement_params['move_page'] = true;
277 } else {
278 $title = Title::newFromID( $key );
279 }
280 if ( $title !== null ) {
281 $jobs[] = new ReplaceTextJob( $title, $replacement_params );
282 }
283 }
284 }
285
286 return $jobs;
287 }
288
296 $titles_for_edit = [];
297
299 $this->target,
300 $this->selected_namespaces,
301 $this->category,
302 $this->prefix,
303 $this->use_regex
304 );
305
306 foreach ( $res as $row ) {
307 $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
308 if ( $title == null ) {
309 continue;
310 }
311 $context = $this->extractContext( $row->old_text, $this->target, $this->use_regex );
312 $titles_for_edit[] = [ $title, $context ];
313 }
314
315 return $titles_for_edit;
316 }
317
327 $titles_for_move = [];
328 $unmoveable_titles = [];
329
331 $this->target,
332 $this->selected_namespaces,
333 $this->category,
334 $this->prefix,
335 $this->use_regex
336 );
337
338 foreach ( $res as $row ) {
339 $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
340 if ( $title == null ) {
341 continue;
342 }
343
345 $title,
346 $this->target,
347 $this->replacement,
348 $this->use_regex
349 );
350
351 $mvPage = new MovePage( $title, $new_title );
352 $moveStatus = $mvPage->isValidMove();
353 $permissionStatus = $mvPage->checkPermissions( $this->getUser(), null );
354
355 if ( $permissionStatus->isOK() && $moveStatus->isOK() ) {
356 $titles_for_move[] = $title;
357 } else {
358 $unmoveable_titles[] = $title;
359 }
360 }
361
362 return [ $titles_for_move, $unmoveable_titles ];
363 }
364
374 function getAnyWarningMessageBeforeReplace( $titles_for_edit, $titles_for_move ) {
375 if ( $this->replacement === '' ) {
376 return $this->msg( 'replacetext_blankwarning' )->text();
377 } elseif ( $this->use_regex ) {
378 // If it's a regex, don't bother checking for existing
379 // pages - if the replacement string includes wildcards,
380 // it's a meaningless check.
381 return null;
382 } elseif ( count( $titles_for_edit ) > 0 ) {
384 $this->replacement,
385 $this->selected_namespaces,
386 $this->category,
387 $this->prefix,
388 $this->use_regex
389 );
390 $count = $res->numRows();
391 if ( $count > 0 ) {
392 return $this->msg( 'replacetext_warning' )->numParams( $count )
393 ->params( "<code><nowiki>{$this->replacement}</nowiki></code>" )->text();
394 }
395 } elseif ( count( $titles_for_move ) > 0 ) {
397 $this->replacement,
398 $this->selected_namespaces,
399 $this->category,
400 $this->prefix,
401 $this->use_regex
402 );
403 $count = $res->numRows();
404 if ( $count > 0 ) {
405 return $this->msg( 'replacetext_warning' )->numParams( $count )
406 ->params( $this->replacement )->text();
407 }
408 }
409
410 return null;
411 }
412
416 function showForm( $warning_msg = null ) {
417 $out = $this->getOutput();
418
419 $out->addHTML(
420 Xml::openElement(
421 'form',
422 [
423 'id' => 'powersearch',
424 'action' => $this->getPageTitle()->getFullURL(),
425 'method' => 'post'
426 ]
427 ) . "\n" .
428 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
429 Html::hidden( 'continue', 1 ) .
430 Html::hidden( 'token', $out->getUser()->getEditToken() )
431 );
432 if ( is_null( $warning_msg ) ) {
433 $out->addWikiMsg( 'replacetext_docu' );
434 } else {
435 $out->wrapWikiMsg(
436 "<div class=\"errorbox\">\n$1\n</div><br clear=\"both\" />",
437 $warning_msg
438 );
439 }
440
441 $out->addHTML( '<table><tr><td style="vertical-align: top;">' );
442 $out->addWikiMsg( 'replacetext_originaltext' );
443 $out->addHTML( '</td><td>' );
444 // 'width: auto' style is needed to override MediaWiki's
445 // normal 'width: 100%', which causes the textarea to get
446 // zero width in IE
447 $out->addHTML(
448 Xml::textarea( 'target', $this->target, 100, 5, [ 'style' => 'width: auto;' ] )
449 );
450 $out->addHTML( '</td></tr><tr><td style="vertical-align: top;">' );
451 $out->addWikiMsg( 'replacetext_replacementtext' );
452 $out->addHTML( '</td><td>' );
453 $out->addHTML(
454 Xml::textarea( 'replacement', $this->replacement, 100, 5, [ 'style' => 'width: auto;' ] )
455 );
456 $out->addHTML( '</td></tr></table>' );
457
458 // MSSQL/SQLServer and SQLite unfortunately lack a REGEXP
459 // function or operator by default, so disable regex(p)
460 // searches for both these DB types.
462 if ( $dbr->getType() != 'sqlite' && $dbr->getType() != 'mssql' ) {
463 $out->addHTML( Xml::tags( 'p', null,
464 Xml::checkLabel(
465 $this->msg( 'replacetext_useregex' )->text(),
466 'use_regex', 'use_regex'
467 )
468 ) . "\n" .
469 Xml::element( 'p',
470 [ 'style' => 'font-style: italic' ],
471 $this->msg( 'replacetext_regexdocu' )->text()
472 )
473 );
474 }
475
476 // The interface is heavily based on the one in Special:Search.
477 if ( class_exists( MediaWikiServices::class ) ) {
478 // MW 1.27+
479 $namespaces = MediaWikiServices::getInstance()->getSearchEngineConfig()
480 ->searchableNamespaces();
481 } else {
483 $namespaces = SearchEngine::searchableNamespaces();
484 }
485 $tables = $this->namespaceTables( $namespaces );
486 $out->addHTML(
487 "<div class=\"mw-search-formheader\"></div>\n" .
488 "<fieldset id=\"mw-searchoptions\">\n" .
489 Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() )
490 );
491 // The ability to select/unselect groups of namespaces in the
492 // search interface exists only in some skins, like Vector -
493 // check for the presence of the 'powersearch-togglelabel'
494 // message to see if we can use this functionality here.
495 if ( $this->msg( 'powersearch-togglelabel' )->isDisabled() ) {
496 // do nothing
497 } else {
498 $out->addHTML(
499 Html::element(
500 'div',
501 [ 'id' => 'mw-search-togglebox' ]
502 )
503 );
504 }
505 $out->addHTML(
506 Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
507 "$tables\n</fieldset>"
508 );
509 // @todo FIXME: raw html messages
510 $category_search_label = $this->msg( 'replacetext_categorysearch' )->escaped();
511 $prefix_search_label = $this->msg( 'replacetext_prefixsearch' )->escaped();
512 $rcPage = SpecialPage::getTitleFor( 'Recentchanges' );
513 $rcPageName = $rcPage->getPrefixedText();
514 $out->addHTML(
515 "<fieldset id=\"mw-searchoptions\">\n" .
516 Xml::tags( 'h4', null, $this->msg( 'replacetext_optionalfilters' )->parse() ) .
517 Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
518 "<p>$category_search_label\n" .
519 Xml::input( 'category', 20, $this->category, [ 'type' => 'text' ] ) . '</p>' .
520 "<p>$prefix_search_label\n" .
521 Xml::input( 'prefix', 20, $this->prefix, [ 'type' => 'text' ] ) . '</p>' .
522 "</fieldset>\n" .
523 "<p>\n" .
524 Xml::checkLabel(
525 $this->msg( 'replacetext_editpages' )->text(), 'edit_pages', 'edit_pages', true
526 ) . '<br />' .
527 Xml::checkLabel(
528 $this->msg( 'replacetext_movepages' )->text(), 'move_pages', 'move_pages'
529 ) . '<br />' .
530 Xml::checkLabel(
531 $this->msg( 'replacetext_announce', $rcPageName )->text(), 'doAnnounce', 'doAnnounce', true
532 ) .
533 "</p>\n" .
534 Xml::submitButton( $this->msg( 'replacetext_continue' )->text() ) .
535 Xml::closeElement( 'form' )
536 );
537 $out->addModules( 'ext.ReplaceText' );
538 }
539
547 function namespaceTables( $namespaces, $rowsPerTable = 3 ) {
548 global $wgContLang;
549 // Group namespaces into rows according to subject.
550 // Try not to make too many assumptions about namespace numbering.
551 $rows = [];
552 $tables = "";
553 foreach ( $namespaces as $ns => $name ) {
554 $subj = MWNamespace::getSubject( $ns );
555 if ( !array_key_exists( $subj, $rows ) ) {
556 $rows[$subj] = "";
557 }
558 $name = str_replace( '_', ' ', $name );
559 if ( '' == $name ) {
560 $name = $this->msg( 'blanknamespace' )->text();
561 }
562 $rows[$subj] .= Xml::openElement( 'td', [ 'style' => 'white-space: nowrap' ] ) .
563 Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $namespaces ) ) .
564 Xml::closeElement( 'td' ) . "\n";
565 }
566 $rows = array_values( $rows );
567 $numRows = count( $rows );
568 // Lay out namespaces in multiple floating two-column tables so they'll
569 // be arranged nicely while still accommodating different screen widths
570 // Float to the right on RTL wikis
571 $tableStyle = $wgContLang->isRTL() ?
572 'float: right; margin: 0 0 0em 1em' : 'float: left; margin: 0 1em 0em 0';
573 // Build the final HTML table...
574 for ( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
575 $tables .= Xml::openElement( 'table', [ 'style' => $tableStyle ] );
576 for ( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
577 $tables .= "<tr>\n" . $rows[$j] . "</tr>";
578 }
579 $tables .= Xml::closeElement( 'table' ) . "\n";
580 }
581 return $tables;
582 }
583
589 function pageListForm( $titles_for_edit, $titles_for_move, $unmoveable_titles ) {
590 global $wgLang;
591
592 $out = $this->getOutput();
593
594 $formOpts = [
595 'id' => 'choose_pages',
596 'method' => 'post',
597 'action' => $this->getPageTitle()->getFullUrl()
598 ];
599 $out->addHTML(
600 Xml::openElement( 'form', $formOpts ) . "\n" .
601 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
602 Html::hidden( 'target', $this->target ) .
603 Html::hidden( 'replacement', $this->replacement ) .
604 Html::hidden( 'use_regex', $this->use_regex ) .
605 Html::hidden( 'move_pages', $this->move_pages ) .
606 Html::hidden( 'edit_pages', $this->edit_pages ) .
607 Html::hidden( 'doAnnounce', $this->doAnnounce ) .
608 Html::hidden( 'replace', 1 ) .
609 Html::hidden( 'token', $out->getUser()->getEditToken() )
610 );
611
612 foreach ( $this->selected_namespaces as $ns ) {
613 $out->addHTML( Html::hidden( 'ns' . $ns, 1 ) );
614 }
615
616 $out->addModules( "ext.ReplaceText" );
617 $out->addModuleStyles( "ext.ReplaceTextStyles" );
618 // Needed for bolding of search term.
619 $out->addModuleStyles( "mediawiki.special.search.styles" );
620
621 if ( count( $titles_for_edit ) > 0 ) {
622 $out->addWikiMsg(
623 'replacetext_choosepagesforedit',
624 "<code><nowiki>{$this->target}</nowiki></code>",
625 "<code><nowiki>{$this->replacement}</nowiki></code>",
626 $wgLang->formatNum( count( $titles_for_edit ) )
627 );
628
629 foreach ( $titles_for_edit as $title_and_context ) {
633 list( $title, $context ) = $title_and_context;
634 $out->addHTML(
635 Xml::check( $title->getArticleID(), true ) .
637 " - <small>$context</small><br />\n"
638 );
639 }
640 $out->addHTML( '<br />' );
641 }
642
643 if ( count( $titles_for_move ) > 0 ) {
644 $out->addWikiMsg(
645 'replacetext_choosepagesformove',
646 $this->target, $this->replacement, $wgLang->formatNum( count( $titles_for_move ) )
647 );
648 foreach ( $titles_for_move as $title ) {
649 $out->addHTML(
650 Xml::check( 'move-' . $title->getArticleID(), true ) .
651 ReplaceTextUtils::link( $title ) . "<br />\n"
652 );
653 }
654 $out->addHTML( '<br />' );
655 $out->addWikiMsg( 'replacetext_formovedpages' );
656 $rcPage = SpecialPage::getTitleFor( 'Recentchanges' );
657 $rcPageName = $rcPage->getPrefixedText();
658 $out->addHTML(
659 Xml::checkLabel(
660 $this->msg( 'replacetext_savemovedpages' )->text(),
661 'create-redirect', 'create-redirect', true ) . "<br />\n" .
662 Xml::checkLabel(
663 $this->msg( 'replacetext_watchmovedpages' )->text(),
664 'watch-pages', 'watch-pages', false ) . '<br />'
665 );
666 $out->addHTML( '<br />' );
667 }
668
669 $out->addHTML(
670 "<br />\n" .
671 Xml::submitButton( $this->msg( 'replacetext_replace' )->text() ) . "\n"
672 );
673
674 // Only show "invert selections" link if there are more than
675 // five pages.
676 if ( count( $titles_for_edit ) + count( $titles_for_move ) > 5 ) {
677 $buttonOpts = [
678 'type' => 'button',
679 'value' => $this->msg( 'replacetext_invertselections' )->text(),
680 'disabled' => true,
681 'id' => 'replacetext-invert',
682 'class' => 'mw-replacetext-invert'
683 ];
684
685 $out->addHTML(
686 Xml::element( 'input', $buttonOpts )
687 );
688 }
689
690 $out->addHTML( '</form>' );
691
692 if ( count( $unmoveable_titles ) > 0 ) {
693 $out->addWikiMsg( 'replacetext_cannotmove', $wgLang->formatNum( count( $unmoveable_titles ) ) );
694 $text = "<ul>\n";
695 foreach ( $unmoveable_titles as $title ) {
696 $text .= "<li>" . ReplaceTextUtils::link( $title ) . "<br />\n";
697 }
698 $text .= "</ul>\n";
699 $out->addHTML( $text );
700 }
701 }
702
712 function extractContext( $text, $target, $use_regex = false ) {
713 global $wgLang;
714
715 $cw = $this->getUser()->getOption( 'contextchars', 40 );
716
717 // Get all indexes
718 if ( $use_regex ) {
719 preg_match_all( "/$target/Uu", $text, $matches, PREG_OFFSET_CAPTURE );
720 } else {
721 $targetq = preg_quote( $target, '/' );
722 preg_match_all( "/$targetq/", $text, $matches, PREG_OFFSET_CAPTURE );
723 }
724
725 $poss = [];
726 foreach ( $matches[0] as $_ ) {
727 $poss[] = $_[1];
728 }
729
730 $cuts = [];
731 // @codingStandardsIgnoreStart
732 for ( $i = 0; $i < count( $poss ); $i++ ) {
733 // @codingStandardsIgnoreEnd
734 $index = $poss[$i];
735 $len = strlen( $target );
736
737 // Merge to the next if possible
738 while ( isset( $poss[$i + 1] ) ) {
739 if ( $poss[$i + 1] < $index + $len + $cw * 2 ) {
740 $len += $poss[$i + 1] - $poss[$i];
741 $i++;
742 } else {
743 // Can't merge, exit the inner loop
744 break;
745 }
746 }
747 $cuts[] = [ $index, $len ];
748 }
749
750 $context = '';
751 foreach ( $cuts as $_ ) {
752 list( $index, $len, ) = $_;
753 $contextBefore = substr( $text, 0, $index );
754 $contextAfter = substr( $text, $index + $len );
755 if ( !is_callable( [ $wgLang, 'truncateForDatabase' ] ) ) {
756 // Backwards compatibility code; remove once MW 1.30 is
757 // no longer supported.
758 $contextBefore =
759 $wgLang->truncate( $contextBefore, - $cw, '...', false );
760 $contextAfter =
761 $wgLang->truncate( $contextAfter, $cw, '...', false );
762 } else {
763 $contextBefore =
764 $wgLang->truncateForDatabase( $contextBefore, - $cw, '...', false );
765 $contextAfter =
766 $wgLang->truncateForDatabase( $contextAfter, $cw, '...', false );
767 }
768 $context .= $this->convertWhiteSpaceToHTML( $contextBefore );
769 $snippet = $this->convertWhiteSpaceToHTML( substr( $text, $index, $len ) );
770 if ( $use_regex ) {
771 $targetStr = "/$target/Uu";
772 } else {
773 $targetq = preg_quote( $this->convertWhiteSpaceToHTML( $target ), '/' );
774 $targetStr = "/$targetq/i";
775 }
776 $context .= preg_replace( $targetStr, '<span class="searchmatch">\0</span>', $snippet );
777
778 $context .= $this->convertWhiteSpaceToHTML( $contextAfter );
779 }
780 return $context;
781 }
782
783 private function convertWhiteSpaceToHTML( $message ) {
784 $msg = htmlspecialchars( $message );
785 $msg = preg_replace( '/^ /m', '&#160; ', $msg );
786 $msg = preg_replace( '/ $/m', ' &#160;', $msg );
787 $msg = preg_replace( '/ /', '&#160; ', $msg );
788 # $msg = str_replace( "\n", '<br />', $msg );
789 return $msg;
790 }
791
795 protected function getGroupName() {
796 return 'wiki';
797 }
798}
$wgCompressRevisions
We can also compress text stored in the 'text' table.
$wgExternalStores
External stores allow including content from non database sources following URL links.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$wgReplaceTextUser
$wgContLang
Definition Setup.php:800
$wgLang
Definition Setup.php:880
static getSubject( $index)
Get the subject namespace index for a given namespace Special namespaces (NS_MEDIA,...
MediaWikiServices is the service locator for the application scope of MediaWiki.
Handles the backend logic of moving a page from one title to another.
Definition MovePage.php:36
Show an error when a user tries to do something they do not have the necessary permissions for.
Background job to replace text in a given page.
static doSearchQuery( $search, $namespaces, $category, $prefix, $use_regex=false)
static getReplacedTitle(Title $title, $search, $replacement, $regex)
Do a replacement on a title.
static getMatchingTitles( $str, $namespaces, $category, $prefix, $use_regex=false)
static link(Title $title, $text=null)
Shim for compatibility.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
getTitlesForMoveAndUnmoveableTitles()
Returns two lists: the set of titles that would be moved/renamed by the current text replacement,...
getTitlesForEditingWithContext()
Returns the set of Titles whose contents would be modified by this replacement, along with the "searc...
namespaceTables( $namespaces, $rowsPerTable=3)
Copied almost exactly from MediaWiki's SpecialSearch class, i.e.
doSpecialReplaceText()
Do the actual display and logic of Special:ReplaceText.
createJobsForTextReplacements()
Returns the set of MediaWiki jobs that will do all the actual replacements.
getAnyWarningMessageBeforeReplace( $titles_for_edit, $titles_for_move)
Get the warning message if the replacement string is either blank or found elsewhere on the wiki (sin...
showForm( $warning_msg=null)
pageListForm( $titles_for_edit, $titles_for_move, $unmoveable_titles)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
extractContext( $text, $target, $use_regex=false)
Extract context and highlights search text.
doesWrites()
Indicates whether this special page may perform database writes.bool 1.27
const NS_CATEGORY
Definition Defines.php:83
$context
Definition load.php:45
const DB_REPLICA
Definition defines.php:25